diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index de563219a..000000000 --- a/.gitattributes +++ /dev/null @@ -1,3 +0,0 @@ -# See: https://github.com/kynan/nbstripout -*.ipynb filter=nbstripout -*.ipynb diff=ipynb diff --git a/.gitconfig b/.gitconfig deleted file mode 100644 index a4f6a7c3b..000000000 --- a/.gitconfig +++ /dev/null @@ -1,7 +0,0 @@ -# See: https://github.com/kynan/nbstripout -[diff "ipynb"] - textconv = nbstripout -t -[filter "nbstripout"] - clean = "f() { echo >&2 \"clean: nbstripout $1\"; nbstripout; }; f %f" - smudge = "f() { echo >&2 \"smudge: cat $1\"; cat; }; f %f" - required = true diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 8f99cd209..80a66bb93 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -17,16 +17,15 @@ A "[Minimal, Complete and Verifiable Example](http://matthewrocklin.com/blog/wor **Actual behavior**: [What actually happened] - ### Equivalent steps in matplotlib -Please make sure this bug is related to a specific proplot feature. If you're not sure, try to replicate it with the [native matplotlib API](https://matplotlib.org/3.1.1/api/index.html). Matplotlib bugs belong on the [matplotlib github page](https://github.com/matplotlib/matplotlib). +Please try to make sure this bug is related to a proplot-specific feature. If you're not sure, try to replicate it with the [native matplotlib API](https://matplotlib.org/3.1.1/api/index.html). Matplotlib bugs belong on the [matplotlib github page](https://github.com/matplotlib/matplotlib). ```python # your code here, if applicable +import matplotlib.pyplot as plt ``` - ### Proplot version -Paste the result of `import proplot; print(proplot.version)` here. +Paste the results of `import matplotlib; print(matplotlib.__version__); import proplot; print(proplot.version)` here. diff --git a/.gitignore b/.gitignore index 490c271fe..a74ebf9bb 100644 --- a/.gitignore +++ b/.gitignore @@ -3,9 +3,6 @@ .vimsession .*.sw[a-z] -# Ignore auto-generated files -proplotrc - # PyPi stuff build dist @@ -15,19 +12,15 @@ dist # Local docs builds docs/api docs/_build +docs/_static/proplotrc +docs/_static/rctable.rst -# Subfolders in data directories -**/cmaps/*/ -**/cycles/*/ -**/fonts/*/ - -# Folder of notebooks for testing and bugfixing -local - -# Notebook stuff -.ipynb_checkpoints +# Development subfolders +dev +sources # Python extras +.ipynb_checkpoints *.log *.pyc .*.pyc @@ -40,5 +33,6 @@ __pycache__ .Trashes # Old files +tmp trash garbage diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 26973e759..8a0eb377f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,13 +1,39 @@ # See: https://pre-commit.com/hooks.html +# Must put flake8 in separate group so 'black' is executed first +# WARNING: Make sure to keep flags in sync with ci/run-linter.sh repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.2.3 + rev: v4.1.0 hooks: - - id: no-commit-to-branch - id: double-quote-string-fixer - id: check-docstring-first - id: check-merge-conflict - id: end-of-file-fixer - id: trailing-whitespace + + - repo: https://github.com/pre-commit/mirrors-isort + rev: v5.10.1 + hooks: + - id: isort + args: ['--line-width=88', '--multi-line=3', '--force-grid-wrap=0', '--trailing-comma'] + exclude: '(^docs|__init__)' + + - repo: https://github.com/PyCQA/flake8 + rev: 4.0.1 + hooks: - id: flake8 - args: ["--ignore=W503"] + args: ['--max-line-length=88', '--ignore=W503,E402,E731,E741'] + + # apply once this handles long tables better + # - repo: https://github.com/PyCQA/doc8 + # rev: 0.10.1 + # hooks: + # - id: doc8 + # args: ['--max-line-length', '88', '--allow-long-titles'] + + # apply after function keyword args can be ignored + # - repo: https://github.com/ambv/black + # rev: 22.1.0 + # hooks: + # - id: black + # args: ['--line-length', '88', '--skip-string-normalization'] diff --git a/.readthedocs.yml b/.readthedocs.yml index bbb41cdee..22d107f91 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,17 +1,19 @@ -# .readthedocs.yml # Read the Docs configuration file +# https://docs.readthedocs.io/en/stable/config-file/v2.html version: 2 +# Build config +# https://docs.readthedocs.io/en/stable/guides/conda.html#making-builds-faster-with-mamba +build: + os: "ubuntu-20.04" + tools: + python: "mambaforge-4.10" + # Sphinx config sphinx: builder: html configuration: docs/conf.py -# Python config -build: - image: latest -python: - version: 3.6 - system_packages: true +# Environment config conda: environment: docs/environment.yml diff --git a/.travis.yml b/.travis.yml index b5048bfc0..2d089026f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,27 +1,24 @@ # Travis Continuos Integration # Currently only tests notebook files -# Based on http://conda.pydata.org/docs/travis.html -# Not sure why pip install . is necessary since conf.py adds it -# to the the path. Maybe nbsphinx executes in separate environment. +# Based on https://conda.pydata.org/docs/travis.html sudo: false # use container based build language: python +dist: focal notifications: email: false python: - - "3.6" + - "3.7" before_install: - | MODIFIED_FILES=$(git diff --name-only "$TRAVIS_COMMIT_RANGE" 2>/dev/null) - if [ $? -eq 0 ]; then - if ! echo "$MODIFIED_FILES" | grep -qvE '(.md)|(.rst)|(.yml)|(.html)' + if [ $? -eq 0 ] && ! echo "$MODIFIED_FILES" | grep -qvE '(.md)|(.rst)|(.html)|(.png)|(.ico)' then - echo "Only doc files were updated, not running the CI." - exit - fi + echo "Only doc files were updated, not running the CI." + exit fi - - wget http://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh + - wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh - bash miniconda.sh -b -p $HOME/miniconda - export PATH="$HOME/miniconda/bin:$PATH" - hash -r @@ -30,15 +27,16 @@ before_install: - conda info -a install: - - conda env create -n proplot-dev --file docs/environment.yml + - conda env create --file ci/environment.yml - source activate proplot-dev - - pip install flake8 - conda list - which conda - which python + - python setup.py sdist bdist_wheel + - pip install --user ./dist/*.whl script: - - flake8 proplot --ignore=W503 + - ci/run-linter.sh - pushd docs - make html - popd diff --git a/CHANGELOG.rst b/CHANGELOG.rst deleted file mode 100644 index 216f0e7ac..000000000 --- a/CHANGELOG.rst +++ /dev/null @@ -1,460 +0,0 @@ -.. - Valid subsections: - - Deprecated - - Features - - Bug fixes - - Internals - - Documentation - -================= -Changelog history -================= - -ProPlot v1.0.0 (2020-##-##) -=========================== -This will be published when some major refactoring tasks are completed, -and deprecation warnings will be removed. See :pr:`89`, :pr:`109`, :pr:`110`, -and :pr:`111`. - -ProPlot v0.6.0 (2020-##-##) -=========================== -.. rubric:: Deprecated - -- Deprecate `~proplot.axes.Axes.format` functions in favor of the - axes-artist `~matplotlib.artist.Artist.set` override (:pr:`89`). -- Rename `width` and `height` `~proplot.subplots.subplots` keyword args to `figwidth` and `figheight` (:pr:`###`). -- Rename `aspect`, `axwidth`, and `axheight` keyword args to `refaspect`, `refwidth`, and `refheight` (:pr:`###`). -- Rename :rcraw:`subplots.pad` and :rcraw:`subplots.axpad` to - :rcraw:`subplots.edgepad` and :rcraw:`subplots.subplotpad` (:pr:`###`). - -.. rubric:: Features - -- All features are now implemented with individual *setters*, like in matplotlib, - but we still encourage using the bulk ``set`` method through documentation - examples and by populating the ``set`` docstring (so valid arguments are no - longer implicit). -- Users can now use `~proplot.subplots.figure` with - `~proplot.subplots.Figure.add_subplot` - *or* `~proplot.subplots.subplots` (:pr:`110`). This is a major improvement! -- `~proplot.subplots.GridSpec` now accepts physical units, rather than having - `~proplot.subplots.subplots` handle the units (:pr:`110`). -- Allow "hanging" twin *x* and *y* axes as members of the `~proplot.subplots.EdgeStack` - container. Arbitrarily many siblings are now permitted. -- Use `~proplot.subplots.GeometrySolver` for calculating various automatic layout - stuff instead of having 1000 hidden `~proplot.subplots.Figure` methods (:pr:`110`). -- Use `~proplot.subplots.EdgeStack` class for handling - stacks of colorbars, legends, and text (:pr:`110`). - -.. rubric:: Internals - -- Assignments to `~proplot.rctools.rc_configurator` are now validated, and the - configurator is now a monkey patch of `~matplotlib.rcParams` (:pr:`109`). -- Plotting wrapper features (e.g. `~proplot.wrappers.standardize_1d`) are now - implemented and documented on the individual methods themselves - (e.g. `~proplot.axes.Axes.plot`; :pr:`111`). - This is much easier for new users. -- Handle all projection keyword arguments in `~proplot.subplots.Figure.add_subplot` - instead of `~proplot.subplots.subplots` (:pr:`110`). -- Panels, colorbars, and legends are now members of `~proplot.subplots.EdgeStack` - stacks rather than getting inserted directly into - the main `~proplot.subplots.GridSpec` (:pr:`110`). - -ProPlot v0.5.1 (2020-##-##) -=========================== -.. rubric:: Deprecated - -- Remove custom ProPlot cyclers, need to be more well thought out (:commit:`43f65d17`). -- Drop support for ``.xrgb`` and ``.xrgba`` files (:commit:`4fa72b0c`). - Not sure if any online sources produce these kinds of files. -- Drop support for ``.rgba`` files, but optionally read 4th opacity column from ``.rgb`` - and ``.txt`` files (:commit:`4fa72b0c`). - -.. rubric:: Bug fixes - -- Label cyclic Scientific colour maps as cyclic (:commit:`e10a3109`). -- Fix v0.4.0 regression where panel sharing no longer works (:commit:`289e5538`). -- Make axis label sharing more robust (:commit:`7b709db9`). -- Fix fatal `~proplot.axistools.AutoFormatter` bug that pops up in rare circumstances - with small negative numbers (:issue:`117`). - -.. rubric:: Internals - -- Colorbar axes are now instances of `~proplot.axes.XYAxes` (:commit:`fcfcb6a1`). - -ProPlot v0.5.0 (2020-02-10) -=========================== -.. rubric:: Deprecated - -- Remove `abcformat` from `~proplot.axes.Axes.format` (:commit:`2f295e18`). -- Rename `top` to `abovetop` in `~proplot.axes.Axes.format` (:commit:`500dd381`). -- Rename `abc.linewidth` and `title.linewidth` to ``borderwidth`` (:commit:`54eb4bee`). -- Rename `~proplot.wrappers.text_wrapper` `linewidth` and `invert` to - `borderwidth` and `borderinvert` (:commit:`54eb4bee`). - -.. rubric:: Features - -- Add back `Fabio Crameri's scientific colour maps `__ (:pr:`116`). -- Permit both e.g. `locator` and `xlocator` as keyword arguments to - `~proplot.axes.Axes.altx`, etc. (:commit:`57fab860`). -- Permit *descending* `~proplot.styletools.BinNorm` and `~proplot.styletools.LinearSegmentedNorm` - levels (:pr:`119`). -- Permit overriding the font weight, style, and stretch in the - `~proplot.styletools.show_fonts` table (:commit:`e8b9ee38`). -- Permit hiding "unknown" colormaps and color cycles in the - `~proplot.styletools.show_cmaps` and `~proplot.styletools.show_cycles` - tables (:commit:`cb206f19`). - -.. rubric:: Bug fixes - -- Fix issue where `~proplot.styletools.show_cmaps` - and `~proplot.styletools.show_cycles` colormap names were messed up - (:commit:`13045599`) -- Fix issue where `~proplot.styletools.show_cmaps` - and `~proplot.styletools.show_cycles` did not return figure instance - (:commit:`98209e87`). -- Fix issue where user `values` passed to `~proplot.wrappers.colorbar_wrapper` - were sometimes ignored (:commit:`fd4f8d5f`). -- Prevent formatting rightmost meridian label as ``1e-10`` - on cartopy map projections (:commit:`37fdd1eb]`). -- Support CF-time axes by fixing bug in `~proplot.wrappers.standardize_1d` - and `~proplot.wrappers.standardize_2d` (:issue:`103`, :pr:`121`). -- Redirect to the "default" location - when using ``legend=True`` and ``colorbar=True`` to generate on-the-fly legends - and colorbars (:commit:`c2c5c58d`). This feature was accidentally removed. -- Let `~proplot.wrappers.colorbar_wrapper` - accept lists of colors (:commit:`e5f11591`). This feature was accidentally removed. - -.. rubric:: Internals - -- Remove various unused keyword arguments (:commit:`33654a42`). -- Major improvements to the API controlling axes titles and a-b-c labels - (:commit:`1ef7e65e`). -- Always use full names ``left``, ``right``, ``top``, and ``bottom`` instead of ``l``, ``r``, - ``b``, and ``t``, for clarity (:commit:`1ef7e65e`). -- Improve ``GrayCycle`` colormap, is now much shorter and built from reflected - Fabio ``GrayC`` colormaps (:commit:`5b2c7eb7`). - - -ProPlot v0.4.3 (2020-01-21) -=========================== -.. rubric:: Deprecated - -- Remove `~proplot.rctools.ipython_autoreload`, - `~proplot.rctools.ipython_autosave`, and `~proplot.rctools.ipython_matplotlib` - (:issue:`112`, :pr:`113`). Move inline backend configuration to a hidden - method that gets called whenever the ``rc_configurator`` is initalized. - -.. rubric:: Features - -- Permit comments at the head of colormap and color files (:commit:`0ffc1d15`). -- Make `~proplot.axes.Axes.parametric` match ``plot`` autoscaling behavior - (:commit:`ecdcba82`). - -.. rubric:: Internals - -- Use `~proplot.axes.Axes.colorbar` instead of `~matplotlib.axes.Axes.imshow` - for `~proplot.styletools.show_cmaps` and `~proplot.styletools.show_cycles` - displays (:pr:`107`). - -ProPlot v0.4.2 (2020-01-09) -=========================== -.. rubric:: Features - -- Add ``family`` keyword arg to `~proplot.styletools.show_fonts` (:pr:`106`). -- Package the `TeX Gyre `__ - font series with ProPlot (:pr:`106`). Remove a couple other fonts. -- Put the TeX Gyre fonts at the head of the serif, sans-serif, monospace, - cursive, and fantasy ``rcParams`` font family lists (:issue:`104`, :pr:`106`). - -.. rubric:: Bug fixes - -- Fix issues with Fira Math weights unrecognized by matplotlib (:pr:`106`). - -ProPlot v0.4.1 (2020-01-08) -=========================== -.. rubric:: Deprecation - -- Change the default ``.proplotrc`` format from YAML to the ``.matplotlibrc`` - syntax (:pr:`101`). - -.. rubric:: Features - -- Comments (lines starting with ``#``) are now permitted in all RGB and HEX style - colormap and cycle files (:pr:`100`). -- Break down `~proplot.styletools.show_cycles` bars into categories, just - like `~proplot.styletools.show_cmaps` (:pr:`100`). - -.. rubric:: Bug fixes - -- Fix issue where `~proplot.styletools.show_cmaps` and `~proplot.styletools.show_cycles` - draw empty axes (:pr:`100`). -- Add back the :ref:`default .proplorc file ` to docs (:pr:`101`). - To do this, ``conf.py`` auto-generates a file in ``_static``. - -.. rubric:: Internals - -- Add ``geogrid.color/linewidth/etc`` and ``gridminor.color/linewidth/etc`` props - as *children* of ``grid.color/linewidth/etc`` (:pr:`101`). -- Various `~proplot.rctools.rc_configurator` improvements, remove outdated - global variables (:pr:`101`). -- Better error handling when loading colormap/cycle files, and calls to - `~proplot.styletools.Colormap` and `~proplot.styletools.Cycle` now raise errors while - calls to `~proplot.styletools.register_cmaps` and `~proplot.styletools.register_cycles` - still issue warnings (:pr:`100`). - -ProPlot v0.4.0 (2020-01-07) -=========================== -.. rubric:: Deprecated - -- Rename `basemap_defaults` to `~proplot.projs.basemap_kwargs` and `cartopy_projs` - to `~proplot.projs.cartopy_names` (:commit:`431a06ce`). -- Remove ``subplots.innerspace``, ``subplots.titlespace``, - ``subplots.xlabspace``, and ``subplots.ylabspace`` spacing arguments, - automatically calculate default non-tight spacing using `~proplot.subplots._get_space` - based on current tick lengths, label sizes, etc. -- Remove redundant `~proplot.rctools.use_fonts`, use ``rcParams['sans-serif']`` - precedence instead (:pr:`95`). -- `~proplot.axes.Axes.dualx` and `~proplot.axes.Axes.dualy` no longer accept - "scale-spec" arguments. - Must be a function, two functions, or an axis scale instance (:pr:`96`). -- Remove `~proplot.axes.Axes` ``share[x|y]``, ``span[x|y]``, and ``align[x|y]`` - kwargs (:pr:`99`). - These settings are now always figure-wide. -- Rename `~proplot.styletools.Cycle` ``samples`` to ``N``, rename - `~proplot.styletools.show_colors` ``nbreak`` to ``nhues`` (:pr:`98`). - -.. rubric:: Features - -- Add `~proplot.styletools.LinearSegmentedColormap.from_file` static methods (:pr:`98`). - You can now load files by passing a name to `~proplot.styletools.Colormap`. -- Add TeX Gyre Heros as open source Helvetica-alternative; this is the new default font. - Add Fira Math as DejaVu Sans-alternative; has complete set of math characters (:pr:`95`). -- Add `xlinewidth`, `ylinewidth`, `xgridcolor`, `ygridcolor` keyword - args to `~proplot.axes.XYAxes.format` (:pr:`95`). -- Add getters and setters for various `~proplot.subplots.Figure` settings - like ``share[x|y]``, ``span[x|y]``, and ``align[x|y]`` (:pr:`99`). -- Let `~proplot.axes.Axes.twinx`, `~proplot.axes.Axes.twiny`, - `~proplot.axes.Axes.altx`, and `~proplot.axes.Axes.alty` accept - `~proplot.axes.XYAxes.format` keyword args just like - `~proplot.axes.Axes.dualx` and `~proplot.axes.Axes.dualy` (:pr:`99`). -- Add `~proplot.subplots.Figure` ``fallback_to_cm`` kwarg. This is used by - `~proplot.styletools.show_fonts` to show dummy glyphs to clearly illustrate when fonts are - missing characters, but preserve graceful fallback for end user. -- Improve `~proplot.projs.Proj` constructor function. It now accepts - `~cartopy.crs.Projection` and `~mpl_toolkits.basemap.Basemap` instances, just like other - constructor functions, and returns only the projection instance (:pr:`92`). -- `~proplot.rctools.rc` `~proplot.rctools.rc_configurator.__getitem__` always - returns the setting. To get context block-restricted settings, you must explicitly pass - ``context=True`` to `~proplot.rctools.rc_configurator.get`, `~proplot.rctools.rc_configurator.fill`, - or `~proplot.rctools.rc_configurator.category` (:pr:`91`). - -.. rubric:: Bug fixes - -- Fix `~proplot.rctools.rc_configurator.context` bug (:issue:`80` and :pr:`91`). -- Fix issues with `~proplot.axes.Axes.dualx` and `~proplot.axes.Axes.dualy` - with non-linear parent scales (:pr:`96`). -- Ignore TTC fonts because they cannot be saved in EPS/PDF figures (:issue:`94` and :pr:`95`). -- Do not try to use Helvetica Neue because "thin" font style is read as regular (:issue:`94` and :pr:`95`). - -.. rubric:: Documentation - -- Use the imperative mood for docstring summaries (:pr:`92`). -- Fix `~proplot.styletools.show_cycles` bug (:pr:`90`) and show cycles using colorbars - rather than lines (:pr:`98`). - -.. rubric:: Internals - -- Define `~proplot.rctools.rc` default values with inline dictionaries rather than - with a default ``.proplotrc`` file, change the auto-generated user ``.proplotrc`` - (:pr:`91`). -- Remove useless `panel_kw` keyword arg from `~proplot.wrappers.legend_wrapper` and - `~proplot.wrappers.colorbar_wrapper` (:pr:`91`). Remove `wflush`, `hflush`, - and `flush` keyword args from `~proplot.subplots.subplots` that should have been - removed long ago. - -ProPlot v0.3.1 (2019-12-16) -=========================== -.. rubric:: Bug fixes - -- Fix issue where custom fonts were not synced (:commit:`a1b47b4c`). -- Fix issue with latest versions of matplotlib where ``%matplotlib inline`` - fails *silently* so the backend is not instantiated (:commit:`cc39dc56`). - -ProPlot v0.3.0 (2019-12-15) -=========================== -.. rubric:: Deprecated - -- Remove ``'Moisture'`` colormap (:commit:`cf8952b1`). - -.. rubric:: Features - -- Add `~proplot.styletools.use_font`, only sync Google Fonts fonts (:pr:`87`). -- New ``'DryWet'`` colormap is colorblind friendly (:commit:`0280e266`). -- Permit shifting arbitrary colormaps by ``180`` degrees by appending the - name with ``'_shifted'``, just like ``'_r'`` (:commit:`e2e2b2c7`). - -.. rubric:: Bug fixes - -- Add brute force workaround for saving colormaps with - *callable* segmentdata (:commit:`8201a806`). -- Fix issue with latest versions of matplotlib where ``%matplotlib inline`` - fails *silently* so the backend is not instantiated (:commit:`cc39dc56`). -- Fix `~proplot.styletools.LinearSegmentedColormap.shifted` when `shift` is - not ``180`` (:commit:`e2e2b2c7`). -- Save the ``cyclic`` and ``gamma`` attributes in JSON files too (:commit:`8201a806`). - -.. rubric:: Documentation - -- Cleanup notebooks, especially the colormaps demo (e.g. :commit:`952d4cb3`). - -.. rubric:: Internals - -- Change `~time.clock` to `~time.perf_counter` (:pr:`86`). - -ProPlot v0.2.7 (2019-12-09) -=========================== - -.. rubric:: Bug fixes - -- Fix issue where `~proplot.styletools.AutoFormatter` logarithmic scale - points are incorrect (:commit:`9b164733`). - -.. rubric:: Documentation - -- Improve :ref:`Configuring proplot` documentation (:commit:`9d50719b`). - -.. rubric:: Internals - -- Remove `prefix`, `suffix`, and `negpos` keyword args from - `~proplot.styletools.SimpleFormatter`, remove `precision` keyword arg from - `~proplot.styletools.AutoFormatter` (:commit:`8520e363`). -- Make ``'deglat'``, ``'deglon'``, ``'lat'``, ``'lon'``, and ``'deg'`` instances - of `~proplot.styletools.AutoFormatter` instead of `~proplot.styletools.SimpleFormatter` - (:commit:`8520e363`). The latter should just be used for contours. - -ProPlot v0.2.6 (2019-12-08) -=========================== -.. rubric:: Bug fixes - -- Fix issue where twin axes are drawn *twice* (:commit:`56145122`). - - -ProPlot v0.2.5 (2019-12-07) -=========================== -.. rubric:: Features - -- Much better `~proplot.axistools.CutoffScale` algorithm, permit arbitrary - cutoffs (:pr:`83`). - -ProPlot v0.2.4 (2019-12-07) -=========================== -.. rubric:: Deprecated - -- Rename `ColorCacheDict` to `~proplot.styletools.ColorDict` (:commit:`aee7d1be`). -- Rename `colors` to `~proplot.styletools.Colors` (:commit:`aee7d1be`) -- Remove `fonts_system` and `fonts_proplot`, rename `colordict` to - `~proplot.styletools.colors`, make top-level variables - more robust (:commit:`861583f8`). - -.. rubric:: Documentation - -- Params table for `~proplot.styletools.show_fonts` (:commit:`861583f8`). - -.. rubric:: Internals - -- Improvements to `~proplot.styletools.register_colors`. - -ProPlot v0.2.3 (2019-12-05) -=========================== -.. rubric:: Bug fixes - -- Fix issue with overlapping gridlines using monkey patches on gridliner - instances (:commit:`8960ebdc`). -- Fix issue where auto colorbar labels are not applied when - ``globe=True`` (:commit:`ecb3c899`). -- More sensible zorder for gridlines (:commit:`90d94e55`). -- Fix issue where customized super title settings are overridden when - new axes are created (:commit:`35cb21f2`). - -.. rubric:: Documentation - -- Organize ipython notebook documentation (:commit:`35cb21f2`). - -.. rubric:: Internals - -- Major cleanup of the `~proplot.wrappers.colorbar_wrapper` source code, handle - minor ticks using the builtin matplotlib API just like major ticks (:commit:`b9976220`). - -ProPlot v0.2.2 (2019-12-04) -=========================== -.. rubric:: Deprecated - -- Rename `~proplot.subplots.axes_grid` to `~proplot.subplots.subplot_grid` (:commit:`ac14e9dd`). - -.. rubric:: Bug fixes - -- Fix shared *x* and *y* axis bugs (:commit:`ac14e9dd`). - -.. rubric:: Documentation - -- Make notebook examples PEP8 compliant (:commit:`97f5ffd4`). Much more readable now. - -ProPlot v0.2.1 (2019-12-02) -=========================== -.. rubric:: Deprecated - -- Rename `autoreload_setup`, `autosave_setup`, and `matplotlib_setup` to - `~proplot.rctools.ipython_autoreload`, `~proplot.rctools.ipython_autosave`, and `~proplot.rctools.ipython_matplotlib`, respectively (:commit:`84e80c1e`). - -ProPlot v0.2.0 (2019-12-02) -=========================== -.. rubric:: Deprecated - -- Remove the ``nbsetup`` rc setting in favor of separate ``autosave``, ``autoreload``, - and ``matplotlib`` settings for triggering the respective ``%`` magic commands. - (:commit:`3a622887`; ``nbsetup`` is still accepted but no longer documented). -- Rename the ``format`` rc setting in favor of the ``inlinefmt`` setting - (:commit:`3a622887`; ``format`` is still accepted but no longer documented). -- Rename ``FlexibleGridSpec`` and ``FlexibleSubplotSpec`` to ``GridSpec`` - and ``SubplotSpec`` (:commit:`3a622887`; until :pr:`110` is merged it is impossible - to use these manually, so this won't bother anyone). - -.. rubric:: Features - -- Support manual resizing for all backends, including ``osx`` and ``qt`` (:commit:`3a622887`). - -.. rubric:: Bug fixes - -- Disable automatic resizing for the ``nbAgg`` interactive inline backend. Found no - suitable workaround (:commit:`3a622887`). - -.. rubric:: Internals - -- Organize the ``rc`` documentation and the default ``.proplotrc`` file (:commit:`3a622887`). -- Rename ``rcParamsCustom`` to ``rcParamsLong`` - (:commit:`3a622887`; this is inaccessible to the user). -- When calling ``fig.canvas.print_figure()`` on a stale figure, call ``fig.canvas.draw()`` - first. May be overkill for `~matplotlib.figure.Figure.savefig` but critical for - correctly displaying already-drawn notebook figures. - -ProPlot v0.1.0 (2019-12-01) -=========================== -.. rubric:: Internals - -- Include `flake8` in Travis CI testing (:commit:`8743b857`). -- Enforce source code PEP8 compliance (:commit:`78da51a7`). -- Use pre-commit for all future commits (:commit:`e14f6809`). -- Implement tight layout stuff with canvas monkey patches (:commit:`67221d10`). - ProPlot now works for arbitrary backends, not just inline and qt. - -.. rubric:: Documentation - -- Various `RTD bugfixes `__ (e.g. :commit:`37633a4c`). - -ProPlot v0.0.0 (2019-11-27) -=========================== - -The first version released on `PyPi `__. - -.. _`Luke Davis`: https://github.com/lukelbd -.. _`Riley X. Brady`: https://github.com/bradyrx diff --git a/CODEOFCONDUCT.md b/CODEOFCONDUCT.md new file mode 100644 index 000000000..c7b97f9e4 --- /dev/null +++ b/CODEOFCONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +lukelbd@gmail.com. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 000000000..abfd1636c --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,271 @@ +.. _contrib: + +================== +How to contribute? +================== + +Contributions of any size are greatly appreciated! You can +make a significant impact on proplot by just using it and +reporting `issues `__. + +The following sections cover some general guidelines +regarding proplot development for new contributors. Feel +free to suggest improvements or changes to this workflow. + +.. _contrib_features: + +Feature requests +================ + +We are eager to hear your requests for new features and +suggestions regarding the current API. You can submit these as +`issues `__ on Github. +Please make sure to explain in detail how the feature should work and keep the scope as +narrow as possible. This will make it easier to implement in small pull requests. + +If you are feeling inspired, feel free to add the feature yourself and +submit a pull request! + +.. _contrib_bugs: + +Report bugs +=========== + +Bugs should be reported using the Github +`issues `__ page. When reporting a +bug, please follow the template message and include copy-pasteable code that +reproduces the issue. This is critical for contributors to fix the bug quickly. + +If you can figure out how to fix the bug yourself, feel free to submit +a pull request. + +.. _contrib_tets: + +Write tests +=========== + +Most modern python packages have ``test_*.py`` scripts that are run by `pytest` +via continuous integration services like `Travis `__ +whenever commits are pushed to the repository. Currently, proplot's continuous +integration includes only the examples that appear on the website User Guide (see +`.travis.yml`), and `Luke Davis ` runs additional tests +manually. This approach leaves out many use cases and leaves the project more +vulnerable to bugs. Improving proplot's continuous integration using `pytest` +and `pytest-mpl` is a *critical* item on our to-do list. + +If you can think of a useful test for proplot, feel free to submit a pull request. +Your test will be used in the future. + +.. _contrib_docs: + +Write documentation +=================== + +Documentation can always be improved. For minor changes, you can edit docstrings and +documentation files directly in the GitHub web interface without using a local copy. + +* The docstrings are written in + `reStructuredText `__ + with `numpydoc `__ style headers. + They are embedded in the :ref:`API reference` section using a + `fork of sphinx-automodapi `__. +* Other sections are written using ``.rst`` files and ``.py`` files in the ``docs`` + folder. The ``.py`` files are translated to python notebooks via + `jupytext `__ then embedded in + the User Guide using `nbsphinx `__. +* The `default ReST role `__ + is ``py:obj``. Please include ``py:obj`` links whenever discussing particular + functions or classes -- for example, if you are discussing the + `~proplot.axes.Axes.format` method, please write + ```~proplot.axes.Axes.format``` rather than ``format``. Proplot also uses + `intersphinx `__ + so you can link to external packages like matplotlib and cartopy. + +To build the documentation locally, use the following commands: + +.. code:: bash + + cd docs + # Install dependencies to the base conda environment.. + conda env update -f environment.yml + # ...or create a new conda environment + # conda env create -n proplot-dev --file docs/environment.yml + # source activate proplot-dev + # Create HTML documentation + make html + +The built documentation should be available in ``docs/_build/html``. + +.. _contrib_pr: + +Preparing pull requests +======================= + +New features and bug fixes should be addressed using pull requests. +Here is a quick guide for submitting pull requests: + +#. Fork the + `proplot GitHub repository `__. It's + fine to keep "proplot" as the fork repository name because it will live + under your account. + +#. Clone your fork locally using `git `__, connect your + repository to the upstream (main project), and create a branch as follows: + + .. code-block:: bash + + git clone git@github.com:YOUR_GITHUB_USERNAME/proplot.git + cd proplot + git remote add upstream git@github.com:lukelbd/proplot.git + git checkout -b your-branch-name master + + If you need some help with git, follow the + `quick start guide `__. + +#. Make an editable install of proplot by running: + + .. code-block:: bash + + pip install -e . + + This way ``import proplot`` imports your local copy, + rather than the stable version you last downloaded from PyPi. + You can ``import proplot; print(proplot.__file__)`` to verify your + local copy has been imported. + +#. Install `pre-commit `__ and its hook on the + ``proplot`` repo as follows: + + .. code-block:: bash + + pip install --user pre-commit + pre-commit install + + Afterwards ``pre-commit`` will run whenever you commit. + `pre-commit `__ is a framework for managing and + maintaining multi-language pre-commit hooks to + ensure code-style and code formatting is consistent. + +#. You can now edit your local working copy as necessary. Please follow + the `PEP8 style guide `__. + and try to generally adhere to the + `black `__ subset of the PEP8 style + (we may automatically enforce the "black" style in the future). + When committing, ``pre-commit`` will modify the files as needed, + or will generally be clear about what you need to do to pass the pre-commit test. + + Please break your edits up into reasonably sized commits: + + + .. code-block:: bash + + git commit -a -m "" + git push -u + + The commit messages should be short, sweet, and use the imperative mood, + e.g. "Fix bug" instead of "Fixed bug". + + .. + #. Run all the tests. Now running tests is as simple as issuing this command: + .. code-block:: bash + coverage run --source proplot -m py.test + This command will run tests via the ``pytest`` tool against Python 3.7. + +#. If you intend to make changes or add examples to the user guide, you may want to + open the ``docs/*.py`` files as + `jupyter notebooks `__. + This can be done by + `installing jupytext `__, + starting a jupyter session, and opening the ``.py`` files from the ``Files`` page. + +#. When you're finished, create a new changelog entry in ``CHANGELOG.rst``. + The entry should be entered as: + + .. code-block:: + + * (:pr:``) by ``_. + + where ```` is the description of the PR related to the change, + ```` is the pull request number, and ```` is your first + and last name. Make sure to add yourself to the list of authors at the end of + ``CHANGELOG.rst`` and the list of contributors in ``docs/authors.rst``. + Also make sure to add the changelog entry under one of the valid + ``.. rubric:: `` headings listed at the top of ``CHANGELOG.rst``. + +#. Finally, submit a pull request through the GitHub website using this data: + + .. code-block:: + + head-fork: YOUR_GITHUB_USERNAME/proplot + compare: your-branch-name + + base-fork: lukelbd/proplot + base: master + +Note that you can create the pull request before you're finished with your +feature addition or bug fix. The PR will update as you add more commits. Proplot +developers and contributors can then review your code and offer suggestions. + +.. _contrib_release: + +Release procedure +================= + +Once version 1.0 is released, proplot will follow semantic versioning. That is, given +a version number ``X.Y.Z``, the major version ``X`` will be incremented when something +is deprecated, the minor version ``Y`` will be incremented when features are added, +and the patch number ``Z`` will be incremented when bugs are fixed. + +Currently, proplot's major version number is ``0``, reflecting the fact that the API +is new and subject to rapid changes. Similar to semantic versioning, the minor version +number is incremented when something is deprecated or the style is changed, and the +patch number is incremented only when features are added or bugs are fixed. + +For now, `Luke Davis `__ is the only one who can +publish releases on PyPi, but this will change in the future. Releases should +be carried out as follows: + +#. Create a new branch ``release-vX.Y.Z`` with the version for the release. + +#. Make sure to update ``CHANGELOG.rst`` and that all new changes are reflected + in the documentation: + + .. code-block:: bash + + git add CHANGELOG.rst + git commit -m 'Update changelog' + +#. Open a new pull request for this branch targeting ``master``. + +#. After all tests pass and the pull request has been approved, merge into + ``master``. + +#. Get the latest version of the master branch: + + .. code-block:: bash + + git checkout master + git pull + +#. Tag the current commit and push to github: + + .. code-block:: bash + + git tag -a vX.Y.Z -m "Version X.Y.Z" + git push origin master --tags + +#. Build and publish release on PyPI: + + .. code-block:: bash + + # Remove previous build products and build the package + rm -r dist build *.egg-info + python setup.py sdist bdist_wheel + # Check the source and upload to the test repository + twine check dist/* + twine upload --repository-url https://test.pypi.org/legacy/ dist/* + # Go to https://test.pypi.org/project/proplot/ and make sure everything looks ok + # Then make sure the package is installable + pip install --index-url https://test.pypi.org/simple/ proplot + # Register and push to pypi + twine upload dist/* diff --git a/HOWTOCONTRIBUTE.rst b/HOWTOCONTRIBUTE.rst deleted file mode 100644 index a473df712..000000000 --- a/HOWTOCONTRIBUTE.rst +++ /dev/null @@ -1,232 +0,0 @@ -================== -Contribution guide -================== - -Contributions are highly welcomed and appreciated. Every little bit helps, -so please do not hesitate! You can make a high impact on ProPlot just by using it and -reporting `issues `__. - -The following sections cover some general guidelines -regarding development in ProPlot for maintainers and contributors. -Feel free to suggest improvements or changes in the workflow. - -Feature requests and feedback -============================= - -We are eager to hear your requests for new features, suggestions regarding the current -API, and so on. You can submit these as -`issues `__ with the label -"feature." -Please make sure to explain in detail how the feature should work and keep the scope as -narrow as possible. This will make it easier to implement in small pull requests. - -If you are feeling inspired, feel free to add the feature yourself! - - -Report bugs -=========== - -Bugs should be reported in the `issue tracker `__ -with the label "bug". When reporting a bug, please include: - -* Your operating system name and version, your python version, and your proplot and matplotlib versions. -* If the bug also involves cartopy or basemap, please include these versions as well. -* An example that can be copied and pasted to reproduce the bug. - -If you can figure out how to fix the bug, feel free to submit a pull request. - -Write tests -=========== - -Many packages include ``.py`` scripts in a ``tests`` folder -and have the `Travis Continuous Integration `__ service -automatically run them. Currently, we do -not use the ``tests`` folder -- we just have Travis run the ``.ipynb`` notebook -examples in the ``docs`` folder (see `.travis.yml`). -However, this is a *major* item on our to-do list! - -If you can think of a useful test for ProPlot, feel free to submit a pull request. -Your test will be used in the future. - - -Write documentation -=================== - -Documentation can always be improved. For minor changes, you can edit docstrings and documentation files directly in the GitHub web interface without using a local copy. - -* The docstrings are written in `reStructuredText `__ with `numpydoc `__ style headers. They are embedded in the :ref:`API reference` section using a `fork of sphinx-automodapi `__. Other sections are written using ``.rst`` and ``.ipynb`` notebook files in the ``docs`` folder. The notebooks are embedded in the User Guide using `nbsphinx `__. -* The `default ReST role `__ is ``py:obj``. Please include ``py:obj`` links whenever discussing particular functions or classes -- for example, if you are discussing the `~proplot.axes.Axes.format` method, please write ```~proplot.axes.Axes.format``` rather than ``format``. ProPlot also uses `intersphinx `__ so you can link to external packages like matplotlib and cartopy. -* When editing the ``.ipynb`` notebook files, make sure to put your example descriptions inside reStructedText cells, not markdown cells. This lets us add sphinx directives and API links to the descriptions. See `this guide `__ for how to convert cells to ReST. - -To build the documentation locally, use the following commands: - -.. code:: bash - - cd docs - conda env update -f environment.yml - make html - -The built documentation should be available in ``docs/_build/html``. - -Preparing pull requests -======================= - -#. Fork the - `proplot GitHub repository `__. It's - fine to keep "proplot" as the fork repository name because it will live - under your account. - -#. Clone your fork locally using `git `__, connect your repository - to the upstream (main project), and create a branch: - - .. code-block:: bash - - git clone git@github.com:YOUR_GITHUB_USERNAME/proplot.git - cd proplot - git remote add upstream git@github.com:lukelbd/proplot.git - git checkout -b your-branch-name master - - If you need some help with git, follow the - `quick start guide `__. - -#. Make an editable install of ProPlot by running: - - .. code-block:: bash - - pip install -e . - - This way when you ``import proplot``, the - local copy is used, rather than the stable version you - downloaded from PyPi. You can print ``proplot.__file__`` to verify the - correct version has been imported. - -#. Install `pre-commit `__ and its hook on the ``proplot`` repo - - .. code-block:: bash - - pip install --user pre-commit - pre-commit install - - Afterwards ``pre-commit`` will run whenever you commit. https://pre-commit.com/ - is a framework for managing and maintaining multi-language pre-commit hooks to - ensure code-style and code formatting is consistent. - -#. If you intend to make changes or add examples to the ipython notebooks, - you need to install and configure - `nbstripout `__: - - .. code-block:: bash - - pip install --user nbstripout - git config --local include.path ../.gitconfig - - This adds the ``proplot/.gitconfig`` file (which is not recognized by git) - to the local ``proplot/.git/config`` configuration file, which - defines the filters declared in ``proplot/.gitattributes``. It is necessary - because git cannot sync repository-specific configuration files. - - After this is done, cell output will be "invisible" to git; the version control - system only ever "sees" the content written in each cell. - This makes - ``git diff``\ s much more legible, significantly reduces the repo size, and - lets us test notebook examples using - `nbsphinx `__. - -#. You can now edit your local working copy as necessary. Please follow - the `PEP-8 style guide `__. - When committing, ``nbstripout`` will ignore changes to notebook cell output - and ``pre-commit`` will modify the files as needed, or will generally be clear - about what you need to do to pass the pre-commit test. - - Please break your edits up into reasonably sized commits: - - - .. code-block:: bash - - git commit -a -m "" - git push -u - - The commit messages should be short, sweet, and use the imperative mood, - e.g. "Fix bug" instead of "Fixed bug". - - .. - #. Run all the tests. Now running tests is as simple as issuing this command: - .. code-block:: bash - coverage run --source proplot -m py.test - This command will run tests via the ``pytest`` tool against Python 3.7. - -#. Create a new changelog entry in ``CHANGELOG.rst``. The entry should be entered as: - - .. code-block:: - - (:pr:``) ``_ - - where ```` is the description of the PR related to the change, ```` is the pull request number, and ```` is your first and last name. Add yourself to list of authors at the end of ``CHANGELOG.rst`` if not there, in alphabetical order. - - Make sure to add the changelog entry under one of the valid ``.. rubric:: `` headings listed at the top of ``CHANGELOG.rst``. - -#. Finally, submit a pull request through the GitHub website using this data: - - .. code-block:: - - head-fork: YOUR_GITHUB_USERNAME/proplot - compare: your-branch-name - - base-fork: lukelbd/proplot - base: master - -Note that you can create the pull request while you're working on this. The PR will update -as you add more commits. ProPlot developers and contributors can then review your code -and offer suggestions. - - -Release procedure -================= - -ProPlot follows semantic versioning, e.g. ``vX.Y.Z``. A major version (``X``) causes incompatible -API changes, a minor version (``Y``) adds functionality, and a patch (``Z``) covers bug fixes. - -For now, `Luke Davis `__ is the only one who can publish releases on PyPi, but this will change in the future. Releases should be carried out as follows: - - -#. Create a new branch ``release-vX.Y.Z`` with the version for the release. In this branch, update ``CHANGELOG.rst``, and make sure all new changes are reflected in the documentation. - - .. code-block:: bash - - git add CHANGELOG.rst - git commit -m "Changelog updates" - - -#. Open a new pull request for this branch targeting ``master``. - -#. After all tests pass and the pull request has been approved, merge into ``master``. - -#. Get the latest version of the master branch: - - .. code-block:: bash - - git checkout master - git pull - -#. Tag the current commit and push to github: - - .. code-block:: bash - - git tag -a vX.Y.Z -m "Version X.Y.Z" - git push origin master --tags - -#. Build and publish release on PyPI: - - .. code-block:: bash - - # Remove previous build products and build the package - rm -r dist build *.egg-info - python setup.py sdist bdist_wheel --universal - # Check the source and upload to the test repository - twine check dist/* - twine upload --repository-url https://test.pypi.org/legacy/ dist/* - # Go to https://test.pypi.org/project/proplot/ and make sure everything looks ok - # Then make sure the package is installable - pip install --index-url https://test.pypi.org/simple/ proplot - # Register and push to pypi - twine upload dist/* diff --git a/INSTALL.rst b/INSTALL.rst index f92503266..1286ff762 100644 --- a/INSTALL.rst +++ b/INSTALL.rst @@ -1,14 +1,16 @@ Installation ============ -ProPlot is published on `PyPi `__ and `conda-forge `__. It can be installed with ``pip`` or ``conda`` as follows: +Proplot is published on `PyPi `__ +and `conda-forge `__. It can be installed +with ``pip`` or ``conda`` as follows: .. code-block:: bash pip install proplot conda install -c conda-forge proplot -Likewise, an existing installation of ProPlot can be upgraded to the latest version with: +Likewise, an existing installation of proplot can be upgraded to the latest version with: .. code-block:: bash @@ -16,7 +18,13 @@ Likewise, an existing installation of ProPlot can be upgraded to the latest vers conda upgrade proplot -If you used ``pip install git+https://github.com/lukelbd/proplot.git`` to install ProPlot before it was released on PyPi, you may need to run ``pip uninstall proplot`` before upgrading. -To install a development version of ProPlot, you can use this same method, or clone the repository and run ``pip install --upgrade .`` inside the ``proplot`` folder. +To install a development version of proplot, you can use +``pip install git+https://github.com/proplot-dev/proplot.git`` +or clone the repository and run ``pip install -e .`` inside +the ``proplot`` folder. -ProPlot's only hard dependency is `matplotlib `__. The *soft* dependencies are `cartopy `__, `basemap `__, `xarray `__, and `pandas `__. See the documentation for details. +Proplot's only hard dependency is `matplotlib `__. +The *soft* dependencies are `cartopy `__, +`basemap `__, +`xarray `__, and `pandas `__. +See the documentation for details. diff --git a/LICENSE.txt b/LICENSE.txt index 99e795d9b..96f1555df 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,4 +1,4 @@ -Copyright (c) 2018-2019 ProPlot contributors +Copyright (c) 2018 The Python Packaging Authority Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.rst b/README.rst index 1d5425543..fe288f961 100644 --- a/README.rst +++ b/README.rst @@ -1,84 +1,106 @@ -.. image:: https://github.com/lukelbd/proplot/blob/master/docs/_static/logo_long.png?raw=true +.. image:: https://github.com/proplot-dev/proplot/blob/master/docs/_static/logo_long.svg?raw=true :width: 1000px -|build-status| |docs| |pypi| |code-style| |pr-welcome| |license| |gitter| +|build-status| |docs| |pypi| |code-style| |pr-welcome| |license| |gitter| |doi| -A comprehensive `matplotlib `__ wrapper for making beautiful, publication-quality graphics. +A succinct `matplotlib `__ wrapper for making beautiful, +publication-quality graphics. + +Ultraplot: Proplot's Spiritual Successor +======================================== + +Development on ``proplot`` has been indefinitely halted since summer 2023. A spiritual successor in ``ultraplot`` has been launched. It uses the ``proplot`` codebase and has modernized it to support recent versions of ``matplotlib``, ``cartopy``, ``python``, etc. and is adding new features and enhancements. Check out the project over at the `ultraplot repo `__ and `ultraplot docs `__! + +Documentation +============= + +The documentation is `published on readthedocs `__. Installation ============ -ProPlot is published on `PyPi `__ and `conda-forge `__. It can be installed with ``pip`` or ``conda`` as follows: +Proplot is published on `PyPi `__ and +`conda-forge `__. It can be installed with ``pip`` or +``conda`` as follows: .. code-block:: bash pip install proplot conda install -c conda-forge proplot -Likewise, an existing installation of ProPlot can be upgraded to the latest version with: +Likewise, an existing installation of proplot can be upgraded +to the latest version with: .. code-block:: bash pip install --upgrade proplot conda upgrade proplot -If you used ``pip install git+https://github.com/lukelbd/proplot.git`` to install ProPlot before it was released on PyPi, you may need to run ``pip uninstall proplot`` before upgrading. -To install a development version of ProPlot, you can use this same method, or clone the repository and run ``pip install --upgrade .`` inside the ``proplot`` folder. +To install a development version of proplot, you can use +``pip install git+https://github.com/proplot-dev/proplot.git`` +or clone the repository and run ``pip install -e .`` +inside the ``proplot`` folder. -Documentation -============= -The documentation is `published on readthedocs `__. - - -.. |code-style| image:: https://img.shields.io/badge/code%20style-pep8-green.svg - :alt: pep8 - :target: https://www.python.org/dev/peps/pep-0008/ -.. |build-status| image:: https://travis-ci.com/lukelbd/proplot.svg?branch=master +.. |build-status| image:: https://travis-ci.com/proplot-dev/proplot.svg?branch=master :alt: build status - :target: https://travis-ci.org/lukelbd/proplot - -.. |license| image:: https://img.shields.io/github/license/lukelbd/proplot.svg - :alt: license - :target: LICENSE.txt + :target: https://app.travis-ci.com/proplot-dev/proplot .. |docs| image:: https://readthedocs.org/projects/proplot/badge/?version=latest :alt: docs :target: https://proplot.readthedocs.io/en/latest/?badge=latest +.. |pypi| image:: https://img.shields.io/pypi/v/proplot?color=83%20197%2052 + :alt: pypi + :target: https://pypi.org/project/proplot/ + +.. |code-style| image:: https://img.shields.io/badge/code%20style-black-000000.svg + :alt: black + :target: https://github.com/psf/black + .. |pr-welcome| image:: https://img.shields.io/badge/PR-Welcome-green.svg? :alt: PR welcome :target: https://git-scm.com/book/en/v2/GitHub-Contributing-to-a-Project -.. |pypi| image:: https://img.shields.io/pypi/v/proplot?color=83%20197%2052 - :alt: pypi - :target: https://pypi.org/project/proplot/ +.. |license| image:: https://img.shields.io/github/license/proplot-dev/proplot.svg + :alt: license + :target: LICENSE.txt .. |gitter| image:: https://badges.gitter.im/gitterHQ/gitter.svg :alt: gitter :target: https://gitter.im/pro-plot/community +.. |doi| image:: https://zenodo.org/badge/DOI/10.5281/zenodo.3873878.svg + :alt: doi + :target: https://doi.org/10.5281/zenodo.3873878 + + +.. + |code-style| image:: https://img.shields.io/badge/code%20style-pep8-green.svg + :alt: pep8 + :target: https://www.python.org/dev/peps/pep-0008/ + .. - |coverage| image:: https://codecov.io/gh/lukelbd/proplot.org/branch/master/graph/badge.svg + |coverage| image:: https://codecov.io/gh/proplot-dev/proplot/branch/master/graph/badge.svg :alt: coverage - :target: https://codecov.io/gh/lukelbd/proplot.org + :target: https://codecov.io/gh/proplot-dev/proplot .. |quality| image:: https://api.codacy.com/project/badge/Grade/931d7467c69c40fbb1e97a11d092f9cd :alt: quality - :target: https://www.codacy.com/app/lukelbd/proplot?utm_source=github.com&utm_medium=referral&utm_content=lukelbd/proplot&utm_campaign=Badge_Grade + :target: https://www.codacy.com/app/proplot-dev/proplot?utm_source=github.com&utm_medium=referral&utm_content=proplot-dev/proplot&utm_campaign=Badge_Grade .. - |hits| image:: http://hits.dwyl.io/lukelbd/lukelbd/proplot.svg + |hits| image:: http://hits.dwyl.com/proplot-dev/proplot.svg :alt: hits - :target: http://hits.dwyl.io/lukelbd/lukelbd/proplot + :target: http://hits.dwyl.com/proplot-dev/proplot .. |contributions| image:: https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat :alt: contributions - :target: https://github.com/lukelbd/issues + :target: https://github.com/proplot-dev/issues .. - |issues| image:: https://img.shields.io/github/issues/lukelbd/proplot.svg - :alt: issues - :target: https://github.com/lukelbd/issues + |issues| image:: https://img.shields.io/github/issues/proplot-dev/proplot.svg + :alt: issueks + :target: https://github.com/proplot-dev/issues diff --git a/WHATSNEW.rst b/WHATSNEW.rst new file mode 100644 index 000000000..9bfd63ddc --- /dev/null +++ b/WHATSNEW.rst @@ -0,0 +1,2318 @@ +.. + Valid rubrics: + - Deprecated + - Style changes + - Features + - Bug fixes + - Internals + - Documentation + +.. _whats_new: + +=========== +What's new? +=========== + +This page lists the API changes with each version. Authors are shown next to +each change. Where not indicated, `Luke Davis`_ was the author. See the +:ref:`author page ` for a list of contributors, and see the +:ref:`contribution guide ` if you are interested in +submitting your own changes. + +.. important:: + + Please note that when classes, functions, keywords, or settings are deprecated, + they are not removed -- using the old syntax will result in a warning rather than + an error and preserve the original functionality. Since proplot adheres to `semantic + versioning `__, we will not consider removing the deprecated + syntax until the first major release (i.e., version 1.0.0). + +Version 1.0.0 (####-##-##) +========================== + +This will be published when more comprehensive testing is completed +and stability is improved. + +Version 0.10.0 (2022-##-##) +=========================== + +Deprecated +---------- + +* Remove the obscure `proplot.figure.Figure.format` keyword `mathtext_fallback`, + so that :rcraw:`mathtext.fallback` can only be changed globally (:commit:`5ce23a59`). +* Rename `rasterize` and :rcraw:`colorbar.rasterize` to `rasterized`, consistent + with the existing matplotlib ``rasterized`` property (:commit:`31efafea`). +* Rename `basemap` and :rcraw:`basemap` to `backend` and :rcraw:`geo.backend`, which + can take either of the values ``'cartopy'`` or ``'basemap'``, and auto-translate and + emit warning when `basemap` is used (:commit:`613ab0ea`, :commit:`eb77cbca`). +* Rename :rcraw:`cartopy.autoextent`, :rcraw:`cartopy.circular` to :rcraw:`geo.extent`, + :rcraw:`geo.round`, with :rcraw:`geo.extent` taking either of the values ``'globe'`` + or ``'auto'`` (``cartopy.autoextent`` is translated when used) (:commit:`c4b93c9d`). +* Improve the `~proplot.gridspec.GridSpec` "panel" obfuscation by + renaming `~proplot.gridspec.GridSpec.get_subplot_geometry` to + `~proplot.gridspec.GridSpec.get_geometry`, `~proplot.gridspec.GridSpec.get_geometry` + to `~proplot.gridspec.GridSpec.get_total_geometry` (:commit:`52f57094`). +* Improve the `~proplot.gridspec.GridSpec` "panel" obfuscation by having the public + `~proplot.gridspec.GridSpec` properties ``gs.nrows``, ``gs.ncols``, ``gs.wratios``, + ``gs.hratios``, ``gs.wspace``, ``gs.hspace``, ``gs.wpad``, and ``gs.hpad`` refer to + the reduced non-panel geometry (:commit:`52f57094`). +* Deprecate `maxn` and `maxn_minor` passed to `~proplot.axes.Axes.colorbar` and + recommend the alternative ``locator_kw={'nbins': n}`` (:commit:`b94a9b1e`). + The new default locator `~proplot.ticker.DiscreteLocator` means that these + settings should not need to be used as much (see below). +* Constructor funcs `~proplot.constructor.Locator`, `~proplot.constructor.Formatter`, + `~proplot.constructor.Scale`, and `~proplot.constructor.Norm` now return a `copy.copy` + when an instance of the relevant class is passed (:commit:`521351a2`). This helps + prevent unexpected and hard-to-debug behavior caused by reusing mutable instances. + +Style changes +------------- + +* Disable automatic reversal of dependent variable coordinates when the axis limits + were previously fixed, and add documentation for this feature (:issue:`300`). +* Automatically disable minor colorbar and axis ticks when applying non-numeric major + tick labels with a `~matplotlib.ticker.FixedFormatter` (:commit:`c747ae44`). +* Use `~proplot.ticker.DiscreteLocator` for major/minor discrete colorbar ticks instead + of `~matplotlib.ticker.FixedLocator` and auto-update the tick selection whenever + the axes is drawn (:commit:`b94a9b1e`, :commit:`92bb937e`, :commit:`302c239e`). +* Disable matplotlib's auto-removal of gridlines in presence of `pcolor` plots in all + versions and silence the matplotlib 3.5 deprecation warning (:commit:`ba405ac0`). + Now gridlines appear on top of pcolor meshes by default, just like filled contours. +* Apply the :rcraw:`geo.round` setting (formerly :rcraw:`cartopy.circular`) when + instantiating polar basemap projections (:commit:`5f1c67cc`). Previously + this setting was only used for cartopy projections. +* Put outer legends or colorbars on the same panel axes if their `align` values + differ and (for colorbars only) their `length`\ s do not overlap (:commit:`91ac49b7`). + This permits e.g. aligned "bottom left" and "bottom right" outer legends. +* Change the sample `~proplot.demos.show_fonts` text with `math` keyword to show math + or non-math, sort fonts by input order or by their appearance in the `rc` list, and + permit `FontProperties` or fontspec input and property keywords (:commit:`34d6ec14`). +* Change :rcraw:`mathtext.default` from ``'regular'`` to ``'it'``, and change ``'sans'`` + appearing in the :rcraw:`mathtext.rm`, :rcraw:`mathtext.sf`, :rcraw:`mathtext.bf`, and + :rcraw:`mathtext.it` settings to ``'regular'`` (:commit:`323`). See below for details. +* Change :rcraw:`grid.labelpad` from ``4.0`` to ``3.0`` (:commit:`f95b828a`). This + makes cartopy grid labels and polar axes labels a bit more compact. +* Change :rcraw:`legend.handleheight` from ``1.5`` to ``2.0`` for less compressed + `~matplotlib.patches.Patch` handles (e.g. with error shading) (:commit:`2a5f6b48`). + +Features +-------- + +* Support passing lists for the `proplot.axes.Axes.format` keywords `abc` and `title`, + in which case the label is picked by selecting the `~proplot.axes.Axes.number` + (minus 1) entry from the list (:pr:`294`) by `Pratiman Patel`_. +* Permit disabling a-b-c labels for a particular subplot by passing e.g. ``number=None`` + instead of ``number=False`` (:commit:`f7308cbe`). ``None`` is a bit more intuitive. +* Add the modifiable `proplot.figure.Figure.tight` property to retrieve and optionally + toggle the tight layout setting (:commit:`46f46c26`). +* Add a top-level `~proplot.ui.subplot` command that returns a figure and a single + subplot, analogous to `~proplot.ui.subplots` (:commit:`8459c24c`). +* Improve performance of the "tight layout" algorithm in cartopy axes by skipping + artists clipped by the axes background patch boundary (:commit:`f891e4f0`). +* Improve default appearance of figures with top/right panels and colorbars and with + the tight layout algorithm disabled (:commit:`c4a3babb`). +* Allow passing `wequal`, `hequal`, and `equal` to `~proplot.figure.Figure` + along with other scalar gridspec parameters (:commit:`d9e62c54`). +* Add the :rcraw:`subplots.equalspace` and :rcraw:`subplots.groupspace` settings + to control tight layout default values for `equal` and `group` (:commit:`b4bf072d`). +* Add the `wgroup`, `hgroup`, and `group` keywords (analogous to `wequal`, etc.) + to optionally disable tight layout behavior of comparing adjacent subplot "groups" + rather than all subplots in the row or column (:commit:`b4bf072d`). +* Permit passing `~proplot.gridspec.GridSpec` instances to + `~proplot.figure.Figure.add_subplots` to quickly draw a subplot + inside each gridspec slot in row or column-major order (:commit:`a9ad7429`). +* Add `~proplot.gridspec.GridSpec.copy` method to re-use the same gridspec geometry + for multiple figures (re-using an existing gridspec is otherwise not possible) + (:commit:`8dc7fe3e`, :commit:`be410341`, :commit:`a82a512c`). +* Permit adding additional outer panels or colorbars (or panels) by calling methods + from the panel rather than the main subplot (:commit:`cfaeb177`). +* Permit adding "filled" colorbars to non-subplots and `length` greater than one + by implementing with a non-subplot child axes and inset locator (:commit:`9fc94d21`). +* Allow using the `~proplot.constructor.Proj` keyword `latlim` as Mercator projection + limits and `lon0`, `lat0` aliases for `lon_0`, `lat_0` (:commit:`5f1c67cc`). +* Add the `~proplot.axes.GeoAxes` `labels` side options ``'neither'``, ``'both'``, and + ``'all'``, analogous to Cartesian axes `tickloc` options (:commit:`0f4e03d2`). +* Add the `proplot.axes.GeoAxes.gridlines_major`, `proplot.axes.GeoAxes.gridlines_minor` + properties for additional customization or debugging issues (:commit:`869f300f`). +* Move the `extent` and `round` keywords (formerly `autoextent` and `circular` -- + see above) from `~proplot.axes.GeoAxes.__init__` to `proplot.axes.GeoAxes.format`, + supporting toggling and passage to e.g. `~proplot.ui.subplots` (:commit:`5f1c67cc`). +* Add :rcraw:`grid.geolabels` setting that auto-includes cartopy ``'geo'`` location + when toggling labels with e.g. ``lonlabels='left'`` or ``labels=True``, and support + passing it explicitly with e.g. ``labels='geo'`` (:commit:`9040cde0`). +* Add the :rcraw:`grid.checkoverlap` setting to optionally disable the auto-removal of + overlapping cartopy grid labels (only works in cartopy >= 0.20) (:commit:`3ff02a38`). +* Add the public proplot class `proplot.ticker.IndexFormatter`, since the matplotlib + version was entirely removed in version 3.5 (:commit:`c2dd8b2e`). +* Replace `matplotlib.ticker.IndexLocator` with `proplot.ticker.IndexLocator`, + consistent with `~proplot.ticker.IndexFormatter`, and remove the limitation + requiring data to be plotted on the axis (:commit:`c2dd8b2e`). +* Permit picking the `~matplotlib.ticker.NullFormatter`, `~proplot.ticker.AutoFormatter` + `~matplotlib.ticker.NullLocator`, and `~matplotlib.ticker.AutoLocator` by passing + ``True`` or ``False`` to the corresponding constructor functions (:commit:`92ae0575`). +* Add `proplot.ticker.DiscreteLocator` analogous to `~matplotlib.ticker.FixedLocator` + that ticks from a subset of fixed values, and add a `discrete` keyword and register + as ``'discrete'`` in `proplot.constructor.Locator` (:commit:`b94a9b1e`). +* Support specifying `transform` plotting command arguments as registered cartopy + projections rather than `~cartopy.crs.CRS` instances (:commit:`c7a9fc95`). +* Permit passing `vmin` and `vmax` to `proplot.axes.Axes.colorbar`, as quick + alternative to using `norm_kw` (:commit:`eb9565bd`). +* Permit discretizing continuous colormaps passed to `~proplot.axes.Axes.colorbar` using + `values`, instead of ignoring `values` when colormaps are passed (:commit:`503af4be`). +* Ensure the default ticks are aligned with levels when passing discrete colormap + instances to `~proplot.axes.Axes.colorbar` (:commit:`503af4be`). +* Emit warning when both a scalar mappable and `vmin`, `vmax`, `norm`, or `values` + are passed to `~proplot.axes.Axes.colorbar` (:commit:`503af4be`). +* Support TeX modifiers :rcraw:`mathtext.it`, :rcraw:`mathtext.bf`, etc. that act on + the "regular" font ``'regular'`` rather than a global font family like ``'sans'`` + when :rcraw:`mathtext.fontset` is ``'custom'`` (:pr:`323`). +* Automatically load from "local" folders named ``proplot_cmaps``, ``proplot_cycles``, + ``proplot_colors``, and ``proplot_fonts`` in current or parent directories, + consistent with "local" ``proplotrc`` files (:commit:`a3a7bb33`). +* Add the `proplot.config.Configurator.local_folders` function, analogous to + `~proplot.config.Configurator.local_files`, and add a `local` keyword to + each ``register`` function (:commit:`a3a7bb33`). + +Bug fixes +--------- + +* Fix matplotlib >= 3.5 issue preventing basic application of "shared" + axes with `share`, `sharex`, `sharey` (:issue:`305`). +* Fix matplotlib >= 3.5 issue preventing basic usage of `proplot.colors.DiscreteNorm` + and colorbars scaled by `proplot.colors.DiscreteNorm` (:issue:`302`). +* Fix matplotlib >= 3.5 issue where date axes are not correctly detected + due to a new default date converter (:commit:`63deee21`). +* Fix matplotlib >= 3.4 issue with fixed-aspect log-log axes due to deprecation + of `~matplotlib.axes.Axes.get_data_ratio_log` (:commit:`29ed6cce`). +* Fix matplotlib >= 3.4 issue where position of child axes in presence of + subfigures is incorrect (:commit:`9246835f`). +* Fix matplotlib >= 3.4 issue where alternate axes are drawn twice due to adding them + as child axes and failing to remove from the ``fig._localaxes`` stack (:issue:`303`). +* Fix matplotlib < 3.2.0 annoying :rcraw:`examples.directory` deprecation + warning message (:issue:`196`). +* Fix matplotlib < 3.2.0 issue where :rcraw:`axes.inbounds` feature fails due + to private API invocation (:commit:`e3e739e4`). +* Fix basic matplotlib < 3.1.2 usage issue due to missing + `~matplotlib.rcsetup.validate_fontweight` validator (:commit:`1d2d05b7`). +* Fix cartopy >= 0.20 issue where added projections like ``'wintri'`` fail + due to an ImportError (:issue:`324`). +* Fix cartopy >= 0.20 issue where inline longitude and latitude gridline labels + can no longer be turned on (:issue:`307`). +* Fix cartopy >= 0.20 issue where user-specified longitude/latitude gridline label + sides ignored due to using booleans instead of ``'x'``, ``'y'`` (:commit:`2ac40715`). +* Fix cartopy >= 0.18 issue where longitude gridlines and labels 360 degrees east of + gridlines on the left edge of the map are unnecessarily removed (:commit:`bcf4fde3`). +* Fix cartopy < 0.18 issue where longitude gridlines and labels east of dateline are + not drawn, and remove outdated gridliner monkey patches (:commit:`aa51512b`). +* Fix issue where tight layout algorithm can fail when labels from another subplot + span over an empty gridspec slot (:issue:`313`). +* Fix issue where tight layout algorithm fails in the presence of subplots + with overlapping or identical subplotspecs (:commit:`87f098b6`). +* Fix issue where super label settings (e.g. size) cannot be updated after they + are initially created (:commit:`2cd72fd3`). +* Fix issue where `proplot.axes.CartesianAxes.format` keyword arguments cannot be + passed to `~proplot.axes.Axes.panel_axes` (:commit:`1b3d0d48`). +* Fix issue where outer colorbars are drawn twice due to adding them as both + figure-wide axes and child axes (:issue:`304`). +* Fix issue where silently-deprecated `aspect` parameter passed to + `proplot.ui.subplots` is not translated to `refaspect` (:commit:`2406a2ae`). +* Fix issue where `proplot.gridspec.GridSpec.figure` is allowed to change -- instead + raise error that recommends `~proplot.gridspec.GridSpec.copy` (:commit:`d8898f5f`). +* Fix issue where `proplot.gridspec.GridSpec.update` cannot be called + on gridspecs without companion `~proplot.figure.Figure`\ s (:commit:`e69fd041`). +* Fix issues passing pandas datetime coordinates and object-type coordinate + arrays to plotting methods (:issue:`320`). +* Fix issue where hatching passed to `~proplot.axes.Axes.bar` does nothing unless + `edgecolor` is explicitly passed (:issue:`389`). +* Fix issue where `boxpctiles` is not recognized by e.g. `~proplot.axes.PlotAxes.bar` + but `boxpctile` is due to typo (:issue:`382`). +* Fix issue where list-of-string colors passed to `~proplot.axes.Axes.scatter` + are interpreted as data values (:issue:`316`). +* Fix issue where `~proplot.axes.PlotAxes.step` `where` parameter is ignored due + to `drawstyle` conversion (:issue:`359`). +* Fix issue where *x* and *y* axis limits are reversed when passing to + `~proplot.axes.PlotAxes.hexbin` and `~proplot.axes.PlotAxes.hist2d` (:issue:`334`). +* Fix regression where *x* or *y* axis limits are reversed when passing to + `~proplot.axes.PlotAxes.hist` and `~proplot.axes.PlotAxes.histh` (:issue:`334`). +* Fix issue where settings passed to `~proplot.axes.Axes.colorbar` after calling e.g. + `~proplot.axes.PlotAxes.pcolor` with `colorbar_kw` are ignored (:issue:`314`). +* Fix issues where passing the colorbar `orientation` without a `loc`, or using a non- + standard `orientation` for a given `loc`, triggers tickloc error (:issue:`314`). +* Fix issue where background properties like `color` and `linewidth` cannot be + passed to `~proplot.axes.Axes` instantiation commands (:commit:`b67b046c`). +* Fix issue where manual data aspect ratio passed with `~proplot.axes.Axes.format` + or `~matplotlib.axes.Axes.set_aspect` is inverted (:commit:`7cda3b23`). +* Fix issue where continuous normalizer `vmin` and `vmax` are not set to min and + max of `levels` when passed to `~proplot.colors.DiscreteNorm` (:commit:`e9ed16c1`). +* Fix issue where unevenly-spaced `levels` combined with + `~proplot.colors.DiscreteColormap` incorrectly samples the color list (:issue:`299`). +* Fix issue where `~proplot.axes.Axes.legend` ignores the user-input `fontsize` + (:issue:`331`). +* Fix issue where `~proplot.axes.Axes.legend` ignores the user-input `facecolor` + but not the shorthand `fc` (:issue:`402`). +* Fix issue where passing invalid rc setting to ``.format`` results in persistent + invalid `rc` state requiring restarting the session/configurator (:issue:`348`). +* Fix issue where ``proplotrc`` settings are ignored if a subsequent line contains + an overlapping meta-setting (:issue:`333`). +* Fix issue where setting :rcraw:`legend.facecolor` or :rcraw:`legend.edgecolor` to + ``'inherit'`` (or passing as keyword argument) raises error (:issue:`298`). +* Fix issue where settings :rcraw:`grid.pad` and :rcraw:`grid.labelpad` and settings + :rcraw:`tick.pad` and :rcraw:`tick.labelpad` are not synced (:commit:`2b96eb0d`). +* Fix issue where the unchanged :rcraw:`figure.figsize` setting is incorrectly included + in the `~proplot.rconfig.Configurator.changed` dictionary (:commit:`d862395b`). + +Documentation +------------- + +* Indicate default values in type-specification rather than + parameter descriptions (:commit:`50546dee`). +* Improve website style: lighter headers, wider text, and no more + clumsy boxes around code literals (:commit:`450ede53`). +* Improve colorbar and legend documentation, expound + added features more carefully (:commit:`43631840`). + +Version 0.9.5 (2021-10-19) +========================== + +Style changes +------------- + +* Switch default :rcraw:`cmap.diverging` from ``'NegPos'`` to the more + popular and contrasty colormap ``'RdBu_r'`` (:commit:`b0b8557f`). +* Switch default :rcraw:`cmap.qualitative` from ``'flatui'`` to ``'colorblind10'``, + consistent with the default color cycle ``'colorblind'`` (:commit:`b0b8557f`). + +Features +-------- + +* Apply ``positive=True``, ``negative=True``, and ``symmetric=True`` by modifying `vmin` + and `vmax` rather than levels (:commit:`fbca1063`). This permits using these keywords + even when ``discrete=False`` and fixes too-few-levels issues when ``discrete=True``. +* Improve default string representation of axes generated with + `~proplot.axes.CartesianAxes.altx`, `~proplot.axes.CartesianAxes.alty`, + or `~proplot.axes.Axes.inset_axes` (:commit:`a570fca7`). + +Bug fixes +--------- + +* Fix issue where "auto-diverging" application fails when colormap + is not explicitly specified (:commit:`9ce6c61c`). +* Fix issue where "auto-diverging" application is not disabled when + qualitative colormaps are specified with `colors` (:commit:`44322db2`). +* Fix issue where ``sequential=True``, ``cyclic=True``, or ``qualitative=True`` + are ignored when "auto-diverging" is applied (:commit:`cb4910fa`). +* Fix issues where version 7.0 cyclic/diverging "scientific colour maps" are + not internally recognized as cyclic/diverging (:commit:`df11445a`). +* Fix issue where :rcraw:`cmap.discrete` set to ``False`` is used even + for contour plots rather than ignored (:commit:`a527cc52`). +* Fix issue where "cyclic" colormaps are allowed to have `extend` other + than ``'neither'`` when specified with ``cyclic=True`` rather than + passing a cyclic `cmap` (:commit:`e91d9bf3`). +* Fix issue where "qualitative" colormaps are allowed to have `discrete` + set to ``False`` when specified with ``qualitative=True`` rather than + passing a discrete `cmap` (:commit:`789f224b`). +* Fix issue where `~proplot.colors.SegmentedNorm` cannot be specified with + ``norm='segmented'`` and ``norm_kw={'levels': level}`` when `discrete` + is also disabled (:commit:`a4f6e838`). +* Fix issue where more than one of mutually-exclusive `sequential`, `diverging`, + `cyclic`, and `qualitative` keywords can be set to ``True`` and others + are silently ignored without warning (:commit:`f14aa263`). + +Version 0.9.4 (2021-10-16) +========================== + +Features +-------- + +* Permit passing arbitrary ``format`` arguments to multi-axes creation commands + like `~proplot.ui.subplots` (:commit:`0b801442`). +* Permit passing ``format`` arguments for different projections during the same + `proplot.gridspec.SubplotGrid.format` or `proplot.figure.Figure.format` call + (:commit:`f5e25598`). Invalid projection-specific keywords are ignored. +* Update `Scientific Colour maps `__ + to version 7.0 (adds ``'bam'``, ``'bamO'``, ``'batlowK'``, ``'batlowW'``, + ``'bukavu'``, ``'fes'``, and ``'vanimo'``) (:commit:`c172a74b`). +* Add `[xy]labelsize`, `[xy]labelweight`, `[xy]ticklabelsize`, `[xy]ticklabelweight` + keywords to `proplot.axes.CartesianAxes.format` (:commit:`975025df`). +* Add `labelsize` and `labelweight` keywords to `proplot.axes.PolarAxes.format`, + `proplot.axes.GeoAxes.format` (:commit:`975025df`). +* Automatically set `xpineloc` and `yspineloc` to ``'bottom'`` and ``'left'`` + when `xbounds` or `ybounds` are passed to `proplot.axes.CartesianAxes.format` only + if both spines are currently visible (:commit:`a2396afe`). +* Automatically use the top/right spine rather than the bottom/left spine when setting + `xspineloc` or `yspineloc` to the position ``('axes', coord)`` or ``('data', coord)`` + when ``coord`` is more than halfway across the axis (:commit:`a2396afe`). +* Passing ``[loninline|latinline|inlinelabels]=True`` to `~proplot.axes.GeoAxes.format` + now implies ``[lonlabels|latlabels|labels]=True`` unless specified otherwise + (:commit:`ed372d64`). This fixes annoying redundancy when calling ``format``. +* Improve default `~proplot.colors.ContinuousColormap.reversed` and + `~proplot.colors.ContinuousColormap.shifted` colormap names (:commit:`a4218e09`). + +Bug fixes +--------- + +* Fix issue where arguments can only be passed to `~proplot.axes.CartesianAxes.altx` + and `~proplot.axes.CartesianAxes.alty`, but not `~proplot.axes.CartesianAxes.twinx` + and `~proplot.axes.CartesianAxes.twiny` (:commit:`223b55a6`). +* Fix issue where `xbounds`, `ybounds`, and `fixticks` fail due to + errors in tick restriction algorithm (:commit:`a2396afe`). +* Fix issue where passing `fontsize` to `~proplot.axes.Axes.format` fails to + update fontsize-relative title and a-b-c label sizes (:commit:`64406726`). +* Fix issue where `lonlim`, `latlim`, and `latbounds` cannot be passed to e.g. + ``add_subplot`` during `~proplot.axes.GeoAxes` initialization (:commit:`d9d3c91a`) +* Fix issue where `vmin` and `vmax` are ignored when making plots + with discrete levels (:issue:`276`). +* Fix issue where `autodiverging` is disabled even when known diverging colormaps + are passed to `~proplot.axes.PlotAxes` commands (:commit:`2eca2198`). +* Fix issue where colormaps made with `~proplot.constructor.Colormap` with unspecified + `name` cannot be assigned as `~proplot.config.rc` defaults (:commit:`0e93b7fa`). +* Fix issue where registered colormaps with trailing ``_r`` or ``_s`` cannot be + retrieved due to automatic reversing/shifting feature (:commit:`345680c9`). + +Documentation +------------- + +* Populate docs with examples of passing ``format`` arguments to figure and axes + instantiation commands (e.g. ``pplt.figure``, ``fig.subplot``) (:commit:`803a889f`). +* Improve website colormap and cycle table rendering time by rasterizing colorbar + data and add `rasterize` as optional keyword arg (:commit:`1a875fc2`). + +Version 0.9.3 (2021-10-09) +========================== + +Style changes +------------- + +* Stop changing default background of figure when `~proplot.axes.ThreeAxes` is present + -- instead just set the default axes background to transparent (:commit:`e933614d`). + +Features +-------- + +* Permit passing background patch-related ``format`` keywords like + `facecolor` on axes instantiation (:commit:`f863afd8`). +* Add :rcraw:`land.alpha`, :rcraw:`ocean.alpha`, :rcraw:`coast.alpha`, + :rcraw:`rivers.alpha`, :rcraw:`lakes.alpha`, :rcraw:`borders.alpha`, + and :rcraw:`innerborders.alpha` settings to change opacity of geographic + features (:commit:`8bb49a02`). Also add missing :rcraw:`coast.zorder`. +* Add `xtickcolor`, `ytickcolor`, `xticklabelcolor`, and `yticklabelcolor` + `~proplot.axes.CartesianAxes.format` keywords to control tick mark and label colors + (:commit:`68cba1af`). Also add documentation for `xlabelcolor` and `ylabelcolor`. +* Add `xticklenratio` and `yticklenratio` `~proplot.axes.CartesianAxes.format` + keywords to scale minor tick lengths (:commit:`26fdadf6`). +* Add `xtickwidth`, `ytickwidth`, `xtickwidthratio`, and `ytickwidthratio` keywords + to `~proplot.axes.CartesianAxes.format` to scale tick widths (:commit:`30a250f0`). +* Set default `gridlabelcolor` to `color` when latter is passed to polar or geo + axes ``format`` methods, consistent with `proplot.axes.CartesianAxes` `color`. +* Add `ticklen`, `ticklenratio`, `tickwidth`, `tickwidthratio` keywords to + `~proplot.axes.Axes.colorbar` to manage colorbar ticks (:commit:`08498abf`). +* Add `labelloc` keyword to `~proplot.axes.Axes.colorbar` to change + the colorbar label location separately from `tickloc` (:commit:`32069370`). +* Permit specifying `linewidth` and `markersize` keywords (and aliases) with arbitrary + physical units for format/colorbar/plotting commands (:commit:`c1ffbc8c`). +* Add `absolute_size` key to `~proplot.axes.PlotAxes.scatter` (analogous to + `absolute_width` used with `~proplot.axes.PlotAxes.bar`) to bypass + auto-scaling of array input (:commit:`b4701411`). +* Add more intuitive ``bars``, ``boxes``, ``shade``, ``fade`` keywords as alternatives + to ``barstds``, ``boxstds``, ``shadestds``, and ``fadestds`` (:commit:`15812cd4`). +* Ignore masked and invalid values in datasets passed to ``boxplot`` and + ``violinplot`` (:commit:`daa666e2`). +* Convert ``showextrema=True`` passed to `~proplot.axes.Axes.violinplot` to + ``barpctiles=True`` (i.e., show 0--100 percentile range) (:commit:`42f613d6`). +* Add `borderstyle` `~proplot.axes.Axes.text` keyword to change the `joinstyle` used + for the path effects border (:commit:`25e21c76`). + +Bug fixes +--------- + +* Fix fatal error instantiating `~proplot.axes.ThreeAxes` (:issue:`389`). +* Fix issue with plotting in `~proplot.axes.ThreeAxes` by inheriting from from + `~proplot.axes.Axes` instead of `~proplot.axes.PlotAxes` (:commit:`64623d92`). +* Fix issue where `~proplot.axes.CartesianAxes.format` ignores `margin` rather than + using it for both `xmargin` and `ymargin` (:commit:`ba32fd1a`). +* Fix issue where `color` passed to ``format`` triggers deprecation warning even + though it is a valid background patch property (:commit:`a50eab0e`). +* Fix issue where calling `~proplot.axes.PlotAxes.violinplot` always emits + warning due to masked array input (:commit:`daa666e2`). +* Fix issue where calling `~proplot.axes.PlotAxes.pcolorfast` with image + output emits warning (:commit:`5d081306`). +* Fix issue where passing ``tickwidth=0`` to ``format`` changes the tick + length persistently outside of context block (:commit:`4966c8ab`). +* Fix issue where ``tickratio`` and ``lenratio`` applied in successive calls to + `~proplot.axes.CartesianAxes.format` fails to update properly (:commit:`26fdadf6`). +* Fix issue with default `~proplot.axes.PlotAxes.scatter` `smin` and `smax` (used + to convert array-like input sizes `s` from data units to ``points ** 2``) by + switching defaults to ``1`` and :rcraw:`lines.markersize` rather than the + data minimum and maximum (:commit:`b4701411`). + +Documentation +------------- + +* Change stylized name "ProPlot" to simply lowercase "proplot", consistent + with matplotlib, cartopy, numpy, etc. (:commit:`b876b214`). + +Version 0.9.2 (2021-09-30) +========================== + +Features +-------- + +* Permit passing `includepanels` and `mathtext_fallback` as + `proplot.figure.Figure.format` keywords instead of just + ``__init__`` keywords (:commit:`33bff576`). +* Permit passing ``loc`` `proplot.axes.CartesianAxes.format` keyword argument(s) to + ``alt[xy]`` (:commit:`eaab8658`). For example ``ax.alty(loc='left')`` changes the + spine, tick mark, tick label, axis label, and offset label sides to the opposite of + the default: *left* for the new alternate axes, *right* for the original axes. +* Improve documentation for ``loc`` `proplot.axes.CartesianAxes.format` keywords + (:commit:`1fa90f87`, :commit:`48dc346d`). Inheritance order is ``loc`` or + ``spineloc`` --> ``tickloc`` --> ``ticklabelloc`` --> ``labelloc`` and ``offsetloc``, + e.g. ``xloc='bottom'`` implies ``xtickloc='bottom'`` unless specified otherwise. +* Do not inherit ``tickloc`` from ``spineloc`` if it is invalid (e.g., ``'zero'``), + do not propagate ``spineloc`` and ``tickloc`` to other settings if they are inferred + from updated rc settings, and issue error message if ``tickloc`` or ``ticklabelloc`` + are invalid (:commit:`616d81fa`, :commit:`219e4b21`, :commit:`bc5a692c`). +* Add documentation for previously-hidden `xticklabelloc`, `yticklabelloc`, `xlabelloc`, + and `ylabelloc` `proplot.axes.CartesianAxes.format` keywords (:commit:`1fa90f87`). +* Add `xoffsetloc`, `yoffsetloc` keywords to control position of order-of-magnitude + indicator location for x and y axes (with large numbers) (:commit:`96a37e53`). +* Add `xlabelcolor` and `ylabelcolor` keywords as alternatives to `xcolor` and `ycolor` + to change just the label color and nothing else (:commit:`d2f20970`). +* Add `base` keyword to `~proplot.ticker.SigFigFormatter` to optionally round to + multiples other than factors of 10 (:commit:`3b00e8a0`). +* Pass ``[major|minor]_[locator|formatter]`` `~proplot.scale.FuncScale` arguments + through the constructor functions (:commit:`e238d4db`). +* Support single-color parametric plots with e.g. ``ax.parametric(x, y, color='red')`` + as quick alternative to `plot` without "sticky edges" (:commit:`98504b86`). +* Support legend entries for parametric lines by interpreting `label` and `value` + separately from `labels` and `values` (:commit:`14a0cfdc`). +* Increase `zorder` of title/a-b-c text from ``3`` to ``3.5`` so it overlies + e.g. text contour labels (:commit:`77fa01da`). +* Ensure contour `labels` appear on top of inner titles/a-b-c labels by decreasing + default `zorder` from ``cntr_zorder + 2`` to ``cntr_zorder + 1`` (:commit:`59222164`). +* Implement "descending level" support directly inside `~proplot.colors.DiscreteNorm` + rather than cmap parser in `~proplot.axes.PlotAxes` commands, and auto-reverse + descending levels passed to `~proplot.colors.SegmentedNorm` (:commit:`46d8bedc`). +* Improve ``show_cmaps`` and ``show_cycles``: Stop passing arguments through + constructor functions, preserve case for user colormap labels, and avoid + showing leading ``_`` and trailing ``_copy`` in labels (:commit:`c41db8d8`). + +Bug fixes +--------- + +* Fix accidental commit of debugging print statement + (:commit:`259a263b`). +* Fix issue where `includepanels` is not applied for spanning axis labels + in presence of panels but only one spanning subplot (:commit:`b8bc55ec`). +* Fix issue where default outer legend axes-relative `loc` does not take into + account the underlying "panel" side (:commit:`2446acc1`). +* Fix issue where axis label color is overwritten during ``__init__`` + call to `proplot.axes.CartesianAxes.format` (:commit:`b454a513`). +* Fix issue where setting `xspineloc` or `yspineloc` to invalid `tickloc` + (e.g., ``'zero'`` or ``'center'``) also disables the ticks (:commit:`616d81fa`). +* Fix issue where setting axis label color without specifying label + text erases the old label text (:commit:`7a7852f9`). +* Fix issue where axis label settings are overridden by settings from + invisible x/y axis on alternate axes (:commit:`c6db292b`). +* Fix `~proplot.ticker.AutoFormatter` issue where `orderOfMagnitude` is + not taken into account when correcting small tick values truncated to + zero on (usually logarithmic) axis scales (:commit:`54fbef0b`). +* Fix issue where `proplot.utils.arange` is not endpoint-inclusive + for negative (descending) step size (:commit:`ec1f8410`). +* Fix confusing behavior where explicitly passed `vmin` and `vmax` are ignored + if `norm` was passed as an already-instantiated class (:commit:`1ee79d36`). +* Fix issue where segment data of ``matplotlib.cm`` colormap instances + is overwritten during conversion to proplot subclasses (:issue:`283`). +* Fix issue where color of contour `labels` cannot be changed + with `labels_kw` (:commit:`d101575d`). +* Fix keyword conflict where `sizes` are interpreted as ``Collection`` + marker sizes when passed to `~proplot.axes.PlotAxes.barb` (:issue:`287`). +* Fix issue where "sticky edges" fail for datetime data + (:commit:`33fa9654`). + +Version 0.9.1 (2021-09-14) +========================== + +Style changes +------------- + +* Revert back to original color names for ``'cyan'``, ``'magenta'``, and ``'yellow'`` + rather than overwriting with ``'c'``, ``'m'``, and ``'y'`` (:issue:`280`). +* Treat ``'ochre'`` and ``'ocher'`` as synonyms (consistent with existing + ``'grey'`` and ``'gray'`` synonyms) (:commit:`c949e505`). + +Features +-------- + +* Permit passing ``format`` keywords when instantiating figures and axes + (:commit:`ae98378d`). For example: ``pplt.figure(suptitle='Super title')`` + or ``fig.add_subplot(111, xcolor='gray', xticks=10)`` . +* Add back `color` as a valid `proplot.axes.CartesianAxes.format` keyword + arg for consistency with `xcolor` and `ycolor` (:commit:`ecb6fa3f`). + +Bug fixes +--------- + +* Fix issue where single-level single-color contour plots + do not draw the contour (:issue:`281`). +* Fix issue with dictionaries passed to `proj` when calling + `~proplot.figure.Figure.add_subplots` (:commit:`21b165df`). +* Fix issue with `includepanels` disabling spanning axis labels + in the presence of panels (:commit:`332ba702`). +* Remove useless "unexpected version" warning when cartopy + is not installed (:commit:`6dbab1bc`). +* Improve backwards compatibility with `matplotlib.figure.Figure.colorbar` + by permitting positional `cax` and `ax` args (:commit:`5003f9a8`). +* Try to auto-disable relative bar widths for seaborn plots that use + the `bar` and `barh` commands (:commit:`b79b9c60`). + +Documentation +------------- + +* Fix documentation compiling issue due to Natural + Earth server change (:commit:`d1d47911`). + +Version 0.9.0 (2021-09-08) +========================== + +Deprecated +---------- + +* Rename :rcraw:`cmap.edgefix` to :rcraw:`edgefix` (:commit:`515f5132`). It now + applies to bar and area plot elements, not just scalar mappables (see below). +* Deprecate passing lists of colors to ``boxplot`` and ``violinplot`` in favor + of using the property cycler instead (see below) (:commit:`67d95349`). +* The ``violinplot`` functions now return `~matplotlib.collection.PolyCollection` + of violin bodies or tuples of (bodies, error bars) instead of a singleton + dictionary containing just the ``'bodies'`` entry (:commit:`45774536`). +* Deprecate recently-introduced `proplot.gridspec.SubplotGrid.legend` and + `proplot.gridspec.SubplotGrid.colorbar` methods (:commit:`d21a61a3`). Idea + was this could be used to add an auto-legend to each subplot with ``axs.legend()`` + or identical colorbars with ``axs.colorbar(m)``, but in the future want to + instead use these methods to add colorbars and legends along the edge of + arbitrary subplots with e.g. ``axs[0, :2].colorbar(m, loc='bottom')``. +* Deprecate recently-introduced `proplot.gridspec.SubplotGrid.text` + (:commit:`80deb71a`). Idea was this could be used to add identical text to + each subplot but that is pretty niche, does not need a dedicated command. + +Style changes +------------- + +* Fix issue where CSS/XKCD colors overwrite "base" color definitions, resulting in + e.g. ``'yellow'`` different from ``'y'`` (:commit:`01db1223`, :commit:`b90bee8c`). +* Make default label rotation for colorbar-of-artist string labels ``0``, consistent + with string tick labels applied with ``autoformat=True`` (:commit:`3f191f3b`). +* Use default ``discrete=False`` for `~proplot.axes.PlotAxes.hist2d` plots, + consistent with `~proplot.axes.PlotAxes.hexbin` (:commit:`267dd161`). Now + "discrete" levels are only enabled for pcolor/contour plots by default. +* Trigger ``adjust_grays`` hue adjustments for gray-like color names passed to + `~proplot.colors.PerceptualColormap.from_list` that aren't technically pure + gray, including ``'charcoal'``, ``'light gray'``/``'light grey'``, and + ``'gray[0-9]'``/``'grey[0-9]'`` (:commit:`6cf42896`, :commit:`49bb9370`). +* Implement "edgefix" and add `edgefix` keyword for ``bar``, ``hist``, ``area``, and + ``pie`` to fix the "white-lines-between-patches" issue with saved vector graphics, + just like ``pcolor`` and ``contourf`` (:commit:`cc602349`, :commit:`b291b2be`). +* Revert back to matplotlib default behavior of ``edgecolor='none'`` for `bar` and + `pie` plots (:commit:`cc602349`, :commit:`b291b2be`). Previously this behavior often + resulted in "white lines" issue but now `edgefix` is applied to these plots. +* Skip "edgefix" option when patch/collection `alpha` is less than ``1`` to prevent + appearance of overlapping edges (:commit:`5bf9b1cc`). Previously this was only + skipped if `ScalarMappable` colormap included transparency. Also remove + manual blending of colorbar solids (no longer needed) (:commit:`4d059a31`). +* The ``boxplot`` and ``violinplot`` functions now iterate through the property + cycler for each box/violin by default (similar to seaborn) (:commit:`67d95349`). + The cycle can be changed with `cycle` and `cycle_kw` arguments. + +Features +-------- + +* Add `align` keyword with options ``'bottom'``, ``'top'``, ``'left'``, ``'right'``, + or ``'center'`` (with optional single-char shorthands) to change alignment for + outer legends/colorbars (:commit:`4a50b4b2`). Previously they had to be centered. +* Add `transpose` keyword as alternative to `order` for 2D `~proplot.axes.PlotAxes` + commands (:issue:`72`). ``transpose=True`` is equivalent to ``order='F'``. +* Return homogeneous groupings of matplotlib artists in `~matplotlib.cbook.silent_list` + objects to simplify repr (:commit:`d59f9c40`, :commit:`667cc068`, + :commit:`240f0b31`, :commit:`0a6d74b7`). +* Use built-in matplotlib logic for plotting multiple `hist` columns, with + support for `stack` as alias of `stacked` and `width` as alias of `rwidth` + (consistent with `bar` keywords) (:commit:`734329a5`). By default, histograms + for successive columns are now grouped side-by-side instead of overlaid. +* Add `fill` and `filled` keywords to `~proplot.axes.PlotAxes.hist`, analogous to + `stack` and `stacked`, and make passage of these keywords set the corresponding + default `histtype` (:commit:`4a85773b`). Also add `filled` alias of `fill` + to `boxplot` for consistency (:commit:`b5caf550`). +* Always copy colormaps returned by `~proplot.constructor.Colormap` + to avoid subsequently changing global colormap properties with e.g. + ``set_alpha`` (:commit:`7a3c3f64`). +* Add leading underscore to all default colormap names (``_name_r`` for reversed, + ``_name_s`` for shifted, ``_name1_name2`` for merged, and ``_name_copy`` for all + other modifications) and never register colormaps returned by `~contructor.Colormap` + that begin with underscore (:commit:`a6fab19f`, :commit:`1f6e6188`). This is + analogous to `legend` ignoring labels with leading underscore. +* Control colorbar frame properties using same syntax as legend frame properties + -- `edgewidth`, `edgecolor`, and optional rounded box with ``fancybox=True`` + (:commit:`58ce2c95`). Colorbar outline is now controlled with `linewidth` + and `color`. Previously these settings had to be in sync. +* Auto-expand components of `~matplotlib.cbook.silent_list` and + `~matplotlib.collection.Collection` passed to `~proplot.axes.Axes.legend` + that have valid labels, similar to tuple group expansion (:issue:`277`) +* Add `handle_kw` to `~proplot.axes.Axes.legend` to optionally control + handle settings that conflict with frame settings (:commit:`58ce2c95`). + Example: ``handle_kw={'edgecolor': 'k'}``. +* Interpret ``'grey'`` as a synonym of ``'gray'`` by translating substrings in color + database (:commit:`6cf42896`, :commit:`04538bad`). Permits e.g. ``color='grey1'``. +* Permit loading color names from files without ``.txt`` extension + (:commit:`55481a9c`). This restriction was unnecessary. +* Set ``default=True`` automatically if users pass `margin` or `space` to + `~proplot.config.register_colors` to permit quickly/succinctly experimenting + with XKCD color filtering algorithm (:commit:`cfc3cef6`). +* Add cartopy-based ``LongitudeLocator``, ``LatitudeLocator``, ``DegreeLocator``, + ``LongitudeFormatter``, ``LatitudeFormatter``, ``DegreeFormatter`` to + public API for consistency with other "registered" tickers (:commit:`76e45c0c`). + +Bug fixes +--------- + +* Fix issue where tuple `~proplot.config.rc` values are truncated + to first scalar value when saving a ``proplotrc`` (:commit:`e731c709`). +* Fix issue where channel-setting and scaling functions like ``scale_luminance`` + drop the opacity channel (:commit:`58ce2c95`). +* Fix issue where line plot coordinates get unnecessarily offset by ``360`` + by removing unnecessary ``_geo_monotonic`` standardization (:issue:`274`). +* Fix regression where `vmin` is ignored without explicitly specifying `vmax` and + vice versa (:issue:`276`). +* Fix issue where `~proplot.axes.PlotAxes.scatter` ignores ``facecolors`` + input by treating it the same as other color aliases (:issue:`275`). +* Fix issue where calling ``legend()`` without arguments generates + duplicate labels for histograms (:issue:`277`). +* Fix issue where list-of-list style input to `~proplot.axes.Axes.legend` + fails to trigger centered legend (:commit:`e598b470`). +* Fix issue where `alpha` passed to contour/pcolor/vlines/hlines commands was + ignored due to translating as `alphas` rather than `alpha` (:commit:`e5faf4d6`). +* Fix unexpected behavior where `~proplot.axes.PlotAxes` tries to make + list-of-artist style colorbars from successive calls to 2D plotting + commands rather than making individual colorbars (:commit:`20ce93a1`). +* Fix issue where ``diverging=True`` is applied for datasets with both + ``discrete=False`` and `vmin` or `vmax` equivalent to ``0`` (:commit:`84b9f86e`). +* Fix issue where `~proplot.axes.PlotAxes.scatter` does not accept N x 3 or + N x 4 RGB[A] style arrays (:commit:`13df1841`). +* Fix issue where importing seaborn issues 100 warnings due to overwriting + seaborn colormaps added by proplot (:commit:`006aef5f`). +* Fix issue where `inbounds` passed to `~proplot.axes.PlotAxes.scatter` applies + only to axis-limit scaling, not cmap normalization scaling (:commit:`3d7636f2`). +* Fix issue with color-parsing due to ``_plot_errorshading`` coming after + ``_parse_cycle`` rather than before (:commit:`acf545e2`). +* Fix issue where violin plots cannot be drawn without adding error bars + (e.g., with ``means=True``) or an error is raised (:commit:`c0d04835`). +* Fix issue where explicitly specifying ``bar[stds|pctiles]`` for + ``violinplot`` turns off the boxes if they were not specified + (and vice versa for ``box[stds|pctiles]``) (:commit:`0edfff4e`) + +Internals +--------- + +* Add helpful warning message when `legend` detects invalid inputs + rather than silently ignoring them (:commit:`b75ca185`). +* Improve warning message when users pass both `colors` and `cmap` + by recommending they use `edgecolor` to set edges (:commit:`1067eddf`). +* Improve universal "rebuilding font cache" warning message when new + users import proplot for the first time (:commit:`9abc894e`). +* Remove unused, mostly undocumented :rcraw:`axes.titleabove` setting + (:commit:`9d9d0db7`). Users should be using :rcraw:`title.above` instead. +* Move `~proplot.gridspec.SubplotGrid` from ``figure.py`` to ``gridspec.py`` + (:commit:`7b688fc8`). Makes more sense there. +* Improve organization of internal functions, add ``data.py``, ``context.py``, + and ``text.py`` to ``internals`` and rename and re-sort related ``PlotAxes`` + parsing utilities (:commit:`58ce2c95`). +* Hide the "registered" axes names (i.e., `name` attributes) from public + API (:commit:`ece1102b`). Users do not interact with the native matplotlib + projection registration system. + +Documentation +------------- + +* Update napoleon type aliases and specifiers (:commit:`c20ed1d1`). Use `sequence` + instead of `list` wherever params accept arbitrary sequences (:commit:`e627e95b`). +* Improve documentation of style-type arguments like `lw`, `linewidth`, + etc. on `~proplot.axes.PlotAxes` commands (:commit:`cc602349`). +* Improve documentation of `proplot.gridspec.SubplotGrid` methods + (:commit:`902502cc`). Docstrings are no longer stubs. + +Version 0.8.1 (2021-08-22) +========================== + +Features +-------- + +* Add `~proplot.colors.PerceptualColormap.from_list` ``adjust_grays`` option + (enabled by default) to help make diverging colormaps with an intermediate + hueless white, gray, or black color (:commit:`2e8cb495`). +* Add the axis sharing level ``4`` or ``'all'`` to share the limits, scales, + and tick labels between axes not in the same row/column (:commit:`73f355a2`). +* Allow adding contours to `legend` by interpreting `label` keyword and using + central handle from ``ContourSet.legend_elements`` (:commit:`26bc77a4`). +* Extend mixed auto-manual legend label input (e.g. ``labels=[None, 'override']``) + to case where legend handles are automatically retrieved from the axes + rather than manually passed to ``legend()`` (:commit:`26bc77a4`). +* Add `inlinelabels` option to `~proplot.axes.GeoAxes.format` to set both + ``loninline=True`` and ``latinline=True`` at once, and change the + :rcraw:`grid.loninline` and :rcraw:`grid.latinline` settings to the + single :rcraw:`grid.inlinelabels` (consistent with :rcraw:`grid.rotatelabels` + and :rcraw:`grid.dmslabels`) (:commit:`560ed978`). + +Bug fixes +--------- + +* Fix regression where dimension reduction with e.g. `barstds` or `barptiles` + no longer ignores NaN values (:issue:`257`, :commit:`d1906fce`). +* Fix regression where ``legend()`` cannot be called without + the input handles (:issue:`188`, :commit:`fdd53a6c`). +* Fix issue where edge colors of area plots with ``negpos=True`` + cannot be changed (:commit:`bb50dea4`). +* Fix issue where `legend` `order` keyword arg is ignored and default is + changed back to ``'F'`` (:commit:`06666296`). +* Fix issues where ``setup_matplotlib`` is not called for pint quantity + input and column iteration of 2D input to 1D funcs fails (:commit:`e57d238e`). +* Fix issue where pint quantity *x* and *y* coordinates fail when passing + as pcolor centers or when :rcraw:`cmap.inbounds` enabled (:commit:`fd76af3a`). +* Fix issue where pint quantity *z* data do not have units stripped + unless in xarray dataarray (:commit:`aadc65f9`). +* Fix issue where making single-color contour plots creates just one contour by + making default ``levels`` count independent from `colors` (:commit:`63eaf10e`). +* Fix issue where common legend handle properties cannot be overridden due to + searching for ``collection`` props rather than ``line`` props (:commit:`26bc77a4`). +* Fix issue where title/abc padding is overwritten in the presence of top panels + and make title deflection to top panels generally more robust (:commit:`d27d05cf`). +* Fix issues with the ``%qt`` backend using ``forward=False`` + during subplot additions (:issue:`244`, :commit:`ac12bbc2`) +* Fix issue where ``%matpolotlib notebook`` and ``%matplotlib widget`` display + unusable/cutoff figure previews by fixing the figure size at creation time and + issuing one-time warning if size was not fixed explicitly (:commit:`88fc2868`). + +Documentation +------------- + +* Make docstring utils explicitly private and convert `_snippets` dictionary to + callable dictionary-like `_SnippetsManager` instance (:commit:`b73fe9e3`). This + helps prevent bug where assigned snippets have unfilled ``%(snippet)s`` markers. + +Version 0.8.0 (2021-08-18) +========================== + +Deprecated +---------- + +* Numbers passed to `pad`, `wpad`, `hpad`, `space`, `wspace`, `hspace`, `left`, + `right`, `top`, and `bottom` are now interpreted as em-widths instead of inches + (:commit:`20502345`). Unfortunately this is a major breaking change that cannot be + "gently" phased in with warnings, but this will be much more convenient going forward. +* Interpret ``sharex/sharey=True`` as ``3`` (i.e., "turn all sharing on") instead + of ``1`` (integer conversion of ``True``) (:issue:`51967ce3`). This is more + intuitive and matches convention elsewhere. Also allow specifying level 1 with + ``'labels'`` and level 2 with ``'limits'``. +* Rename `~proplot.ui.SubplotsContainer` to simpler `~proplot.figure.SubplotGrid` + and move definition to ``figure.py`` (:commit:`51967ce3`). +* Deprecate arbitrary ``__getattr__`` override for `~proplot.figure.SubplotGrid` + (:commit:`51967ce3`). Instead have dedicated ``format``, ``colorbar``, ``legend``, + ``[alt|dual|twin][xy]``, ``panel[_axes]``, and ``inset[_axes]`` methods. +* Rename setting :rcraw:`abc.style` to :rcraw:`abc` (:commit:`a50d5264`). Setting this + to ``False`` still "turns off" labels, setting to ``True`` "turns on" labels with + the default style ``'a'``, and setting to a string "turns on" labels with this style. +* Rename ``image`` category settings to :rcraw:`cmap.inbounds`, + :rcraw:`cmap.discrete`, :rcraw:`cmap.edgefix`, :rcraw:`cmap.levels`, and + :rcraw:`cmap.lut` (:commit:`a50d5264`). +* Rename confusing :rcraw:`text.labelsize` and :rcraw:`text.titlesize` settings + to clearer :rcraw:`font.smallsize` and :rcraw:`font.largesize` with shorthands + :rcraw:`font.small` and :rcraw:`font.large` (analogous to :rcraw:`font.size`) + (:commit:`a50d5264`). Previous names were bad because "label size" applies to more + than just axis or tick labels and "title size" applies to more than just axes titles. +* Rename :rcraw:`tick.ratio` to :rcraw:`tick.widthratio` and add missing + :rcraw:`tick.width` setting (:commit:`a50d5264`). +* Rename vague shorthands :rcraw:`alpha` and :rcraw:`facecolor` back to native + :rcraw:`axes.alpha` and :rcraw:`axes.facecolor` and rename :rcraw:`linewidth` + and :rcraw:`color` to :rcraw:`meta.width` and :rcraw:`meta.color` + (:commit:`41b5e400`). Axes can still be updated by passing `alpha`, `linewidth`, + `facecolor`, and `edgecolor` to ``format``, and now ``format`` supports *arbitrary* + patch artist settings and aliases like `lw`, `ec`, `fc`, `hatch`, etc. +* Change `~proplot.config.Configurator` iteration behavior to loop over keys, not + item pairs, and make it a `~collections.abc.MutableMapping` (:commit:`5626bc88`). +* Rename `proplot.config.Configurator.load_file` to `proplot.config.Configurator.load` + in order to match ``save`` (:commit:`1769d349`). +* Change the default `~proplot.config.Configurator` save location from the home + directory to the *current directory* and change the default filename to + ``proplotrc`` (without the leading dot) (:commit:`41b5e400`). +* Rename `~proplot.config.Configurator.get` to `~proplot.config.Configurator.find` + (:commit:`e8559f3d`). Confusing since ``get`` didn't accept a "fallback" second + positional argument. Now ``get`` is the "dictionary-like" inherited method. +* Rename obscure `LinearSegmentedColormap`, `PerceptuallyUniformColormap`, and + `ListedColormap` to more intuitive/succinct `~proplot.colors.ContinuousColormap`, + `~proplot.colors.PerceptualColormap`, and `~proplot.colors.DiscreteColormap` + (:commit:`ade787f9`). Important due to the "qualitative colormap" behaviors triggered + when a `~proplot.colors.DiscreteColormap` is passed to plot commands (see features). +* Following above change, rename `LinearSegmentedNorm` to simpler `SegmentedNorm`, + rename `~proplot.constructor.Colormap` argument `to_listed` to `discrete`, + change `listmode` options from ``'listed'``, ``'linear'`` to ``'discrete'``, + ``'continuous'``, and add `filemode` option (:commit:`ade787f9`, :commit:`5ccd6c01`). +* Deprecate ``boxes`` and ``violins`` shorthands in favor of singular + `~proplot.axes.PlotAxes.box` and `~proplot.axes.PlotAxes.violin` + (:commit:`6382cf91`). This feel analogous to existing ``bar`` and ``barh``. +* Rename the confusingly-capitalized `~proplot.constructor.Colors` to + `~proplot.utils.get_colors` and move to ``utils.py`` (:commit:`51d480da`). This + is not a "class constructor" -- it just returns lists of colors. +* Rename the ``show`` function keyword `categories` to `include`, + consistent with the new `ignore` keyword (:commit:`c45d5fa1`). + +Style changes +------------- + +* Make default reference subplot size, panel widths, colorbar widths independent of + :rcraw:`font.size` (:commit:`a50d5264`). Default space size should definitely sync + with font size, since larger fonts produce larger labels between subplots, but the + same reasoning does not apply for subplot size. +* Add :rcraw:`leftlabel.rotation`, :rcraw:`toplabel.rotation`, + :rcraw:`rightlabel.rotation`, :rcraw:`bottomlabel.rotation` settings, and make + default row label rotation match y label rotation (:commit:`bae85113`). +* Treat 2D ``scatter`` arguments by iterating over columns and default-styling each + column with the property cycle rather than unraveling 2D arguments into 1D + arrays (:commit:`6382cf91`). Can also iterate over ``s`` and ``c`` columns. +* Exclude out-of-bounds data when determining automatic y (x) axis limits when x (y) + limits have been explicitly set for `plot` and `scatter` plots (:commit:`6382cf91`). + Controlled by the :rcraw:`axes.inbounds` property, analogous to :rcraw:`cmap.inbounds` + used for cmap scaling. This feature leverages proplot's input standardization. +* Capture `colors` passed to commands like ``contour`` and ``pcolor`` and use + it to build qualitative `~proplot.colors.DiscreteColormap` maps (:commit:`6382cf91`). + This matches the behavior of xarray plotting utilities. No longer use `color` + to change "edge color" of filled contours/grid boxes. +* Add special qualitative cmap handling when ``colors=colors``, ``qualitative=True``, + or ``cmap=pcolors.DiscreteColormap(...)`` -- always apply ``DiscreteNorm`` (ignore + and warn if user passed ``discrete=False``), truncate or wrap colors if there are too + many/not enough for the levels, and add default extremes with ``set_under`` or + ``set_over`` depending on user `extend` (:commit:`6382cf91`). +* Select :rcraw:`cmap.diverging` and apply `~proplot.colors.DivergingNorm` automatically + based on input data, similar to xarray and seaborn (:commit:`6382cf91`). This is + controlled with `autodiverging` and the :rcraw:`cmap.autodiverging` setting. It is + also disabled when a cmap is explicitly passed (unless it is a known diverging cmap). +* Set default linewidth to 0.3 when adding "edges" to filled contours + (:commit:`6382cf91`). This matches matplotlib behavior when passing + edgecolor to a ``pcolor`` command. +* Only modify `heatmap` major and minor tick locations if the default + tickers are active (:commit:`6382cf91`). Do not override user tickers. +* Use default luminance of ``90`` rather than ``100`` for auto-colormaps generated + for barb, scatter, and streamline plots (:commit:`6382cf91`). +* Sync 3D axes figure background color with axes background to avoid weird + misaligned white square behind axes (:commit:`30a112bd`). +* Treat :rcraw:`tick.label` and :rcraw:`grid.label` font size, color, and weight + settings as *synonyms* (:commit:`a50d5264`). In general the tick vs. grid distinction + is not meaningful for text labels. However we often want different padding so still + allow :rcraw:`tick.labelpad` and :rcraw:`grid.labelpad` to be distinct. +* Change default :rcraw:`legend.facecolor` to white instead of inheriting from + axes background (:commit:`6382cf91`). Also set default :rcraw:`legend.edgecolor` + to :rcraw:`meta.color` (black by default) and have `legend` read from rc + settings rather than setting default `legend` input arguments. + +Features +-------- + +* Dynamically add classes that are "registered" by contructor functions + to the top-level namespace (:commit:`4382a1b1`). This is consistent with + behavior of importing custom-proplot tickers, norms, etc. to top-level namespace. + Now e.g. ``pplt.MultipleLocator`` or ``pplt.LogNorm`` are allowed. +* Allow creating subplots with `~proplot.ui.figure` and either (1) subsequently + calling `~proplot.figure.Fiugure.subplots` or (2) passing integers or subplot specs + generated by `~proplot.gridspec.GridSpec` to `~proplot.figure.Figure.add_subplot` + (:commit:`51967ce3`). This is convenient for complex grids or mixed proj types. +* Add consistent/intuitive aliases `~proplot.figure.Figure.subplot` and + `~proplot.figure.Figure.add_subplots` for native matplotlib commands + `~proplot.figure.Figure.add_subplot` and `~proplot.figure.Figure.subplots` + (:commit:`51967ce3`). +* Add `~proplot.figure.Figure.subplotgrid` property to access a + `~proplot.figure.SubplotGrid` after drawing subplots one-by-one + (:commit:`fb83384f`). +* Implement physical-units `left`, `right`, `top`, `bottom`, `wspace`, and `hspace` + spaces directly on the `~proplot.gridspec.GridSpec` rather than externally + (:commit:`20502345`). Now absolute spaces are always preserved when figure size + changes even if tight layout is disabled. +* Have `~proplot.gridspec.GridSpec` directly handle "panel slots" (:commit:`20502345`). + Adding panels to a figure adds row or column "panel slots" to the gridspec and + subsequently indexing the gridspec ignores those slots. +* Add tight layout "padding" arguments to `~proplot.gridspec.GridSpec` and add gridspec + parameters as optional arguments to `~proplot.figure.Figure` (:commit:`20502345`). + When a gridspec is added to the figure the arguments are passed to the gridspec. This + replaces matplotlib's `subplotpars` and ``subplots_adjust``. +* Allow variable tight layout padding between subplot panels using `wpad` and + `hpad`, analogous to `wspace` and `hspace` (:commit:`20502345`). Previously + this was fixed at :rcraw:`subplots.innerpad`. +* Add `pad` keyword to `legend`, `colorbar`, and `panel` that controls local + tight layout padding, analogous to `space` (:commit:`20502345`). Previously this + was fixed at :rcraw:`subplots.panelpad`. +* Ensure `wequal` and `hequal` only apply to the main subplot rows and columns; + always ignore panel and colorbar spaces (:commit:`20502345`). +* Improve default behavior in presence of 'outer' colorbars + legends when + :rcraw:`subplots.tight` is disabled (:commit:`20502345`). +* Add a `~proplot.figure.Figure.format` method for formatting every subplot in + the figure when you don't have a ``SubplotGrid`` available (:commit:`20502345`). + Also move internal implementation of figure-wide settings there. Figure-wide + settings like `suptitle` can still be updated from ``Axes.format``. +* Permit mutability of `~proplot.figure.SubplotGrid` (:commit:`51967ce3`). + Power users may want to manipulate their own grids. +* Permit 2d indexing of `~proplot.figure.SubplotGrid` with arbitrary gridspec + geometry by looking up subplotspec indices (:commit:`51967ce3`). Previously 2d + indexing of ``SubplotGrid`` with complex geometry would just return a wrong result. +* Issue warning message when users try ``fig.subplots_adjust()`` or + ``pplt.figure(subplotpars=SubplotParams)`` and auto-disable and warn when + matplotlib "tight layout" rc settings are toggled (:commit:`51967ce3`). +* Add nicer string representations of figures, gridspecs, subplotspecs, and + axes clearly showing the geometry and layout (:commit:`51967ce3`, :commit:`6382cf91`). +* Set default location for new axes panels to ``'right'``, allowing for empty + ``ax.panel_axes()`` calls (:commit:`51967ce3`). +* Convert valid keyword arguments to positional arguments for virtually all + plotting functions rather than a subset (:commit:`6382cf91`). This expands the + use of the `data` keyword and permits a seaborn-like workflow (for example, + ``ax.plot(x='x_key', y='y_key', data=xarray_dataset)``). +* Support `pint.Quantity` arguments by auto-applying ``setup_matplotlib`` with + the quantity's unit registry when a quantity is passed (:commit:`6382cf91`). +* Support `pint.Quantity` input for *z* coordinates (e.g., to ``ax.contourf``) + by stripping the units to prevent warning (:commit:`6382cf91`). +* Support `xarray.DataArray` arguments containing `pint.Quantity` arrays by + accessing ``data`` rather than accessing ``.values`` (:commit:`6382cf91`). +* Apply `pint.Quantity` default unit labels to plots by formatting the units + with the new :rcraw:`unitformat` setting (:commit:`6382cf91`). +* Add :rc:`cmap.sequential`, :rc:`cmap.diverging`, :rc:`cmap.cyclic`, and + :rc:`cmap.qualitative` settings to control the default sequential, diverging, + cyclic, and qualitative cmaps, and add boolean `sequential`, `diverging`, `cyclic`, + and `qualitative` keywords to select corresponding default cmaps (:commit:`6382cf91`). +* Add `robust` keyword argument and :rc:`cmap.robust` setting to ignore + outliers when selecting auto colormap ranges (:issue:`6382cf91`). It can take the + value ``True``, a percentile range, or a 2-tuple percentile interval. +* Add :rc:`colorbar.rasterize` setting to control whether default + colorbar solids are rasterized (:commit:`a50d5264`). +* Allow omitting the colormap name when instantiating colormap classes or using + class methods like ``from_list`` (:commit:`ade787f9`). This is more intuitive. +* Improve matplotlib-proplot colormap translation by converting + `matplotlib.colors.ListedColormap` to `proplot.colors.DiscreteColormap` only if it + has fewer than :rcraw:`cmap.listedthresh` levels (:commit:`ade787f9`). This is + critical in case users import cmaps from other projects. +* Permit constructing property cycles with `~proplot.constructor.Cycle` by passing + ``color`` as keyword argument (:commit:`86a50eb2`). This is matplotlib-like workflow. +* Permit disabling property cycling with e.g. ``cycle=False``, ``cycle='none'``, + or ``cycle=()``, and re-enabling the default with ``cycle=True`` (:commit:`86a50eb2`). +* Override `~matplotlib.axes.Axes.set_prop_cycle` to pass the input arguments + through `~proplot.constructor.Cycle` (:commit:`86a50eb2`). Features are a superset + and this also lets me cache the cycler for comparison with on-the-fly inputs. +* Add shorthands :rcraw:`grid.width`, :rcraw:`grid.style`, :rcraw:`gridminor.width`, + and :rcraw:`gridminor.style` for the respective ``linewidth`` and ``linestyle`` + settings (:commit:`a50d5264`) +* Permit "registering stuff" by passing files or objects to + `~proplot.config.register_cmaps`, `~proplot.config.register_cycles`, + `~proplot.config.register_colors`, and `~proplot.config.register_fonts` + rather than forcing users to use the ``.proplot`` folder (:commit:`ad999e95`). +* Support case insensitivity when calling matplotlib's ``unregister_cmap`` + by improving `~proplot.colors.ColormapDatabase` so it derives from a + `~collections.abc.MutableMapping` rather than `dict` (:commit:`ade787f9`). +* Add public `~proplot.config.Configurator.changed` property to display a dictionary + of settings changed from proplot defaults (:commit:`41b5e400`). +* Add public `~proplot.config.Configurator.user_file` and + `~proplot.config.Configurator.user_folder` static methods for displaying + folder locations (:commit:`b11d744a`). +* Support XDG directories for proplot config files on Linux (:issue:`204`, + :commit:`5e6367dc`). Also accept the file ``~/.proplotrc`` and the folder + ``~/.proplot`` on all systems and raise a warning if duplicate valid files + or folders are found. +* Make `~proplot.config.rc_proplot` and `~proplot.config.rc_matplotlib` containers + of proplot/matplotlib settings part of the public API (:commit:`a50d5264`). +* Allow conversion of numeric inputs with `~proplot.utils.units` using e.g. + ``pplt.units(num, 'in', 'cm')`` (:commit:`88f3dc88`). +* Add more intuitive :rcraw:`grid.labelpad` and :rcraw:`tick.labelpad` + as aliases for :rcraw:`grid.pad` and :rcraw:`tick.pad` (:commit:`a50d5264`). +* Add `~proplot.axes.PlotAxes.line` and `~proplot.axes.PlotAxes.linex` command + aliases for `~proplot.axes.PlotAxes.plot` and `~proplot.axes.PlotAxes.plotx` + (:commit:`6382cf91`). This is more intuitive. +* Add `~proplot.axes.PlotAxes.stepx` and `~proplot.axes.PlotAxes.stemx` commands + analogous to `~proplot.axes.PlotAxes.plotx`, and add `~proplot.axes.PlotAxes.histh`, + `~proplot.axes.PlotAxes.boxploth` (shorthand `~proplot.axes.PlotAxes.boxh`), + and `~proplot.axes.PlotAxes.violinploth` (shorthand `~proplot.axes.PlotAxes.violinh`) + commands analogous to `~proplot.axes.PlotAxes.barh` (:commit:`6382cf91`). +* Let 1D `~proplot.axes.PlotAxes` commands iterate over columns of 2D *x* and *y* + coordinate arrays instead of only 2D *y* coordinate arrays (:commit:`6382cf91`.) +* Support expanded and consistent artist synonyms throughout plotting overrides, + e.g. ``ec`` for `edgecolor`, `lw` for `linewidth`, `fc` and `fillcolor` for + `facecolor` (:commit:`6382cf91`). This is a superset of matplotlib. +* Support passing positional fifth-argument colors to `~proplot.axes.PlotAxes.barbs` + and `~proplot.axes.PlotAxes.quiver`, just like `~proplot.axes.PlotAxes.scatter` + (:commit:`6382cf91`). This was previously not possible. +* Support automatic labels for ``tricontour`` and ``tripcolor`` plots alongside + the more common ``contour`` and ``pcolor``. (:commit:`6382cf91`). +* Add `rasterize` keyword to `colorbar` so that colorbar solids rasterization can + be turned on (proplot turns off by default) (:commit:`6382cf91`). +* Add `edgefix` keyword to `colorbar` to control colorbar-solid edges and + use shared ``_fix_edges`` function (:commit:`6382cf91`). +* Add `location` keyword as alternative to `loc` for legend and + colorbar funcs (:commit:`5cb839fd`). +* Add `alphabetize` keyword to `legend` to optionally alphabetize handles by + their labels (:commit:`6382cf91`). +* Apply auto-detected xarray and pandas legend/colorbar titles even if the + legend/colorbar are not drawn on-the-fly (:issue:`6382cf91`). +* Add :rcraw:`colorbar.facecolor` and :rcraw:`colorbar.edgecolor` properties + analogous to legend properties for controlling frame (:commit:`6382cf91`). +* Treat singleton lists and tuple `legend` input same as scalar + handle input, i.e. never triggers "centered row" specification (:commit:`6382cf91`). +* Support auto-detection of tuple-grouped `legend` handle labels when labels + not passed explicitly (:commit:`6382cf91`). +* Automatically pull out grouped tuples of artists passed to `legend` if they have + differing labels (:commit:`6382cf91`). This is useful for passing error shade groups. +* Silently ignore non-artist and non-container `legend` input -- e.g., ignore the bins + and values returned by `hist` (:commit:`6382cf91`). +* Allow list-of-list "centered row" `legend` specification with e.g. + ``[h, [h1, h2, h3]]`` (i.e., mixed list and non-list input) (:commit:`6382cf91`). +* Permit partial specification of `legend` labels, e.g. ``[h1, h2]`` paired + with ``['label', None]`` overrides the artist label for ``h1`` but uses + the artist label for ``h2`` (:commit:`6382cf91`). +* Interpret all native matplotlib `legend` spacing arguments (e.g., `borderpad` + and `columnspacing`) with `~proplot.utils.units` (:commit:`6382cf91`). +* Control edge width for legend frames with `ew` or `edgewidth` rather than + `lw` and `linewidth` to avoid conflict with feature that permits modifying + legend handle properties (:commit:`6382cf91`). +* Make `proplot.axes.Axes.colorbar` capture matplotlib-native `format` + keyword as alias for `formatter` and `ticklabels` (:issue:`262`). +* Support list-of-string parametric coordinates and format on-the-fly colorbar + ticks with those string labels (:commit:`02fbda45`). This may be a common + use case for parametric plots. +* Add `ignore` keyword to omit specific ``show_cmaps``, ``show_cycles``, and + ``show_colors`` categories from the tables (:commit:`c45d5fa1`). +* Allow case-insensitive specification of ``show_cmaps``, ``show_cycles``, and + ``show_colors`` categories and never ignore input colormaps even if they + match an ignored name like ``'jet'`` (:commit:`c45d5fa1`). +* Support restricting cartopy bounds in cartopy 0.19 by leveraging the + `ylim` `~cartopy.mpl.gridliner.Gridliner` property (:commit:`e190b66c`). +* Add `xlabelpad`, `ylabelpad`, `xticklabelpad`, `yticklabelpad` keywords + to `~proplot.axes.CartesianAxes.format` and read and apply changed + :rcraw:`axes.labelpad` (:commit:`e7d86b8f`). +* Add support for "minor" radial and azimuthal gridlines in + `proplot.axes.PolarAxes.format`, controlled with keywords like + `rminorlocator`, and `thetaminorlocator` (:commit:`59c85f0e`). +* Add `thetagrid`, `rgrid`, `thetagridminor`, and `rgridminor` keys to + `proplot.axes.PolarAxes.format` to toggle gridlines, and read and apply changed + toggles from rc settings -- consistent with Cartesian axes (:commit:`59c85f0e`). +* Add `title_kw`, `suptitle_kw`, `leftlabels_kw`, `rightlabels_kw`, `toplabels_kw`, + and `bottomlabels_kw` to `proplot.axes.Axes.format` for arbitrarily modifying + label text objects -- consistent with `xlabel_kw` and `ylabel_kw` used + for `proplot.axes.CartesianAxes.format` (:commit:`6382cf91`). + +Bug fixes +--------- + +* Fix issue with unpacking iterables inside return statements in python < 3.8 + (:pr:`268`) by `Eli Knaap`_. +* Fix issue where auto layout algorithm recurses in popup backends (:commit:`51967ce3`). +* Fix issue where auto layout algorithm blows up in mpl 3.4+ (:commit:`51967ce3`). +* Fix issue where tight layout is effectively deactivated in mpl >= 3.4 due to + ``set_position`` automatically calling ``set_in_layout(False)`` (:commit:`20502345`). +* Fix issue where thin pyplot-function wrappers e.g. ``isinteractive`` + do not return results (:commit:`e62e3655`). +* Fix issue where `proplot.config.Configurator.save` preserves the ``'#'`` + in HEX strings, resulting in values that cannot be read back in with + `proplot.config.Configurator.load` (:commit:`41b5e400`). +* Fix issue where deprecated `aspect` `~proplot.ui.subplots` argument + is ignored (:commit:`70a8b87d`). +* Fix issue where explicit user-input ``width`` is ignored when creating + colorbars or panels and gridspec slot already exists (:commit:`51967ce3`). +* Fix bug where the default space selection failed to use the + figure-wide share setting (:commit:`51967ce3`). +* Fix bug where the reference subplot aspect ratio not preserved in + presence of complex geometry with panels (:commit:`51967ce3`). +* Fix issue where a-b-c labels are removed in presence of ``'top'`` panels + with ``titleabove=True`` (:commit:`7873d5e0`). +* Fix issue where 'aligned' labels fail in recent matplotlib versions + due to private matplotlib API change (:commit:`51967ce3`). +* Fix issue where ``cmap.reverse()`` returns strange monochrome colormaps + when channel values are specified by functions (e.g., ``cubehelix``) due + to loop scope overwriting a non-local lambda function variable (:commit:`ade787f9`). +* Fix issue where ``_restrict_inbounds`` fails for reversed/descending axis + limits (:commit:`6382cf91`). +* Fix issues where cartopy minor gridlines are toggled on when map bounds are changed + and basemap map boundary props cannot be modified (:commit:`c1f1a7de`). +* Turn off ``_restrict_inbounds`` for geographic projections to prevent issue where + lon/lat coordinates are compared to map coordinates (:commit:`6382cf91`). In-bounds + colormap scaling for geographic projections may be added in a future version. +* Fix issue where error indications do not ignore masked values + in masked numpy arrays (:commit:`6382cf91`). +* Fix issue where error shading objects are grouped into lists rather than tuples + and are not combined into single handle when passed to ``legend`` (:issue:`260`). +* Fix issue where `~proplot.axes.Axes.parametric` ignores `interp` when + selecting `DiscreteNorm` colormap levels (:commit:`152a3a81`). +* Fix issue where tight layout padding is not respected for panels created from + twin axes by ensuring panel parent is always the main axes (:commit:`e7d86b8f`). +* Fix obscure bug where axis labels in presence of mixed panels and + non-panels are improperly shared (:commit:`06666296`). +* Stop overwriting user-input `spineloc` when combined with user-input + spine `bounds` (:commit:`e7d86b8f`). +* Include *children* of ``key`` when triggering complex synced settings + (e.g., now we trigger application of :rcraw:`tick.widthratio` when either + :rcraw:`tick.width` or :rcraw:`meta.width` are changed) (:commit:`5626bc88`). + +Internals +--------- + +* Convert all plotting wrappers to dedicated overrides of individual functions + in `~proplot.axes.PlotAxes` class (:commit:`6382cf91`). This massively simplifies + the internals and makes learning and adopting proplot much easier for users. +* Implement "panel" tracking and translation of physical spacing units directly + on the `~proplot.gridspec.GridSpec` instead of cumbersome hidden methods + in `~proplot.figure.Figure` (:commit:`20502345`). +* Validate all setting assignments to `~proplot.config.Configurator` using a new + `~proplot.config.rc_proplot` dictionary, analogous to ``rcParams`` + (:pr:`109`, :commit:`5626bc88`). This helps avoid mysterious delayed bugs. +* Move ``text``, ``legend``, and ``colorbar`` overrides to base `~proplot.axes.Axes` + class separate from `~proplot.axes.PlotAxes` (:commit:`6382cf91`). +* Automatically redirect all internal plotting calls to native matplotlib methods + (:commit:`6382cf91`). This significantly improves stability. +* Move ``register_colors`` internals from ``config.py`` to ``colors.py`` + by breaking up into smaller functions (:commit:`ad999e95`). +* Move ``_version`` to a separate ``dependencies.py`` file and + allow more versatile comparison operations (:commit:`8806631d`). +* Efficiently impose `~proplot.axes.GeoAxes` defaults ``latlon=True`` and + ``transform=PlateCarree()`` in 90% fewer lines by looping over funcs. + +Documentation +------------- + +* Move all plotting wrapper documentation to dedicated methods and remove + references to wrappers in User Guide and Getting Started. +* Embed `proplot.figure.Figure` documentation inside `proplot.ui.subplots` + instead of just referencing it. +* Embed `proplot.axes.Axes.format` documentation inside ``format`` + documentation for subclasses instead of just referencing it. +* Document the relative font size scalings with a table in + `~proplot.axes.Axes.text` (:commit:`6382cf91`). +* Deprecate scattershot `~proplot.figure.Figure` immutable/documented + properties (:commit:`51967ce3`). These properties were just for documentation. +* Remove ancient deprecated getters and setters for ``sharex``, ``spanx``, etc. + once used with figure objects (:commit:`51967ce3`). These properties were + just for introspection, did not add any functionality. +* Rename `~proplot.config.RcConfigurator` to `~proplot.config.Configurator` + (:commit:`5626bc88`). Previous name was redundant and needlessly verbose + (the ``c`` in ``rc`` already stands for "configuration"...). This class + is public just for documentation -- was not directly used by users. +* Rename `~proplot.axes.Axes3D` to `~proplot.axes.ThreeAxes` so that class name + fits more nicely amongst other class names (:commit:`30a112bd`). +* Make `~proplot.axes.CartopyAxes` and `~proplot.axes.BasemapAxes` private and + remove the documentation (:commit:`25e759b0`). These classes are just for internal + implementation of different cartographic "backends" -- behavior of public + methods is the same for both. Instead just document `proplot.axes.GeoAxes`. + +Version 0.7.0 (2021-07-11) +========================== + +Deprecated +---------- + +* Rename SciVisColor colormaps from ``Blue1``, ``Blue2``, etc. to plurals ``Blues1``, + ``Blues2``, etc. to avoid name conflict with open-color colors (:commit:`8be0473f`). +* Requesting the old names (case-sensitive) redirects to the new names + (:commit:`3f0794d0`). This permits making monochromatic open-color maps with e.g. + ``plot.Colormap('blue9')`` and feels more consistent with ColorBrewer convention of + using plurals like ``Blues``, ``Reds``, etc. +* Shuffle various SciVisColor colormap names to make them consistent/succinct. Make + ``Browns1`` the most colorful/vibrant one, just like ``Greens1`` and ``Blues1``; + split up the ``RedPurple`` maps into ``Reds`` and ``Purples``; and add + the ``Yellows`` category from the ``Oranges`` maps (:commit:`8be0473f`). Requesting + the old names (case-sensitive) redirects to the new names (:commit:`3f0794d0`). +* Add :rcraw:`image.discrete` options and `discrete` keyword for toggling + `~proplot.colors.DiscreteNorm` application, and disable by default for `imshow`, + `matshow`, `spy`, `hexbin`, and `hist2d` plots (:issue:`233`, :commit:`5a7e05e4`). + Also make `hexbin` and `hist2d` behavior with ``discrete=True`` more sane by using + maximum possible counts for autoscaling, and change `~proplot.colors.DiscreteNorm` + argument `extend` to more intuitive name `unique`. +* Rename :rcraw:`subplots.pad` and :rcraw:`subplots.axpad` to more intuitive + :rcraw:`subplots.outerpad` and :rcraw:`subplots.innerpad` (:commit:`3c7a33a8`). + Also rename `~proplot.figure.Figure` keywords. +* Rename `width` and `height` `~proplot.subplots.subplots` keyword args to `figwidth` + and `figheight` to avoid confusion with `refwidth`/`refheight` (:commit:`12d01996`). + Will accept old keyword args without warning since they are used heavily. +* Rename `aspect`, `axwidth`, and `axheight` keyword args to more intuitive + `refaspect`, `refwidth`, and `refheight` (:commit:`12d01996`). Will accept old + keyword args without warning since they are used heavily. +* Rename `abovetop` keyword for moving title/abc labels above top panels, colorbars, + and legends to :rcraw:`title.above` (:commit:`9ceacb7b`). Example usage: + ``ax.format(title='Title', titleabove=True)``. +* Rename the `proplot.colors.PerceptuallyUniformColormap.from_color` keywords `shade`, + `fade` to `luminance`, `saturation` keyword (:commit:`3d8e7dd0`). These can also + be passed to `~proplot.contructor.Colormap` when it is called with positional arguments. +* Rename seldom-used `Figure` argument `fallback_to_cm` to more understandable + `mathtext_fallback` (:pr:`251`). +* `legend_extras` no longer returns the background patch generated for centered-row + legends (:pr:`254`). This is consistent with `colorbar_extras` not returning + background patches generated for inset colorbars. Until proplot adds new subclasses, + it makes more sense if these functions only return `~matplotlib.legend.Legend` and + `~matplotlib.colorbar.Colorbar` instances. + +Style changes +------------- + +* Use proplot TeX Gyre fonts with `~proplot.config.use_style` styles unless + specified otherwise (:commit:`6d7444fe`). Styles build on matplotlib defaults + rather than proplot defaults for all other settings. +* Change default :rcraw:`savefig.transparent` back to ``False`` (:pr:`252`). Dubious + justification for ``True`` in the first place, and makes default PNG proplot figures + unreadable wherever "dark mode" is enabled. +* Reduce default :rcraw:`savefig.dpi` to 1000 (:commit:`bfda9c98`). Nature recommends + 1000, Science recommends "more than 300", PNAS recommends 1000--1200. So 1000 is fine. +* Increase default :rcraw:`colorbar.insetpad` to avoid recurring issue where ticklabels + run close to the background patch (:commit:`f5435976`) +* When using ``medians=True`` or ``means=True`` with `indicate_error` plot simple + error bars by default instead of bars and "boxes" (:commit:`4e30f415`). Only plot + "boxes" with central "markers" by default for violin plots (:commit:`13b45ccd`). +* Determine colormap levels using only in-bounds data if the *x* or *y* axis limits + were explicitly set (:issue:`209`). Add `inbounds` `~proplot.axes.apply_cmap` + keyword and :rcraw:`image.inbounds` setting to control this. +* Use `Artist` labels for the default list-of-artist colorbar tick labels if `values` + was not passed -- and if labels are non-numeric, rotate them 90 degrees for horizontal + colorbars by default (:commit:`ed8e1314`). Makes the choice between "traditional" + legends and "colorbar-style" legends more seamless. +* Use same default-level generation algorithm for contour plots without colormaps as for + all other colormap plots (:commit:`10e0f13b`). Makes automatically-generated + solid-color contours and colormap-style contours identical. +* Use "sticky" edges in x-direction for lines drawn with `plot()` and in y-direction + for lines drawn with `plotx()` (:pr:`258`). This eliminates padding along the + "dependent" axis when limits are not specified, similar to histograms and + barplots and matching a feature we previously added to `fill_between` (:pr:`166`). +* If available, use :rcraw:`pcolormesh.snap` to repair overlap in transparent colorbar + solids rather than manual-blending workaround (:commit:`c9f59e49`). + +Features +-------- + +* Add the remaining commonly-used backend-related `pyplot` functions `ion`, `ioff`, + `isinteractive`, and `switch_backend` to the top-level `proplot` namespace + (:commit:`cd440155`). This avoids forcing users to import pyplot inside a proplot + session (the remaining pyplot functions are related to the "non-object-oriented" + workflow, which proplot explicitly discourages). +* Add support for local ``proplotrc`` files in addition to "hidden" + ``.proplotrc`` files with leading dot (:commit:`8a989aca`). +* Add minimal support for "3D" `~matplotlib.mpl_toolkits.mplot3d.Axes3D` axes + (:issue:`249`). Example usage: ``fig.subplots(proj='3d')``. +* Add `wequal`, `hequal`, and `equal` options to still use automatic spacing but + force the tight layout algorithm to make spacings equal (:pr:`215`, :issue:`64`) + by `Zachary Moon`_. +* Allow calling `proplot.colors.PerceptuallyUniformColormap.from_hsl` by passing + `hue`, `saturation`, or `luminance` to `~proplot.constructor.Colormap` without + any positional arguments (:commit:`3d8e7dd0`). +* Allow passing `alpha`, `luminance`, `saturation` to `~proplot.constructor.Colormap` + as lists to be applied to each component cmap (:commit:`3d8e7dd0`). +* Add convenient shorthands for channel references throughout colormap functions -- + e.g. `h` for hue, `l` for `luminance`, etc. (:commit:`3d8e7dd0`). +* Add the ``'Flare'`` and ``'Crest'`` seaborn colormaps (:commit:`14bc16c9`). These + are seaborn's color cycle-friendly alternatives to existing maps. +* Add the `~proplot.utils.shift_hue` function analogous to `scale_saturation` + and `scale_luminance` (:commit:`67488bb1`). +* Add the `~proplot.utils.to_hex` function and make all color-manipulation funcs return + HEX strings by default (:commit:`67488bb1`). Otherwise `scatter` throws warnings. +* Use ``90`` as the default `luminance` when creating monochromatic colormaps with + `to_listed` set to ``True`` (as when `~proplot.constructor.Cycle` calls + `~proplot.constructor.Colormap`; :commit:`3d8e7dd0`). +* Add `~proplot.axes.Axes.plotx` and `~proplot.axes.Axes.scatterx` commands that + interpret plotting args as ``(y, x)`` rather than ``(x, y)``, analogous to + `~proplot.axes.Axes.areax` (:pr:`258`). +* Add support for `~proplot.axes.indicate_error` *horizontal* error bars and shading + for *horizontal* plotting commands `barh`, `plotx`, and `scatterx` (:pr:`258`). +* Add support for ``ax.plot_command('x_key', 'y_key', data=dataset)`` for virtually + all plotting commands using `standardize_1d` and `standardize_2d` (:pr:`258`). + This was an existing `~matplotlib.axes.Axes.plot` feature. +* Add support for the plotting style ``ax.plot(x1, y1, fmt1, x2, y2, fmt2, ...)`` + as allowed by matplotlib (:pr:`258`). +* Add `absolute_width` keyword to `~proplot.plot.bar_extras` to make `width` + argument absolute (:pr:`258`). Remains ``False`` by default. +* Add support for "stacked" plots to `~matplotlib.axes.Axes.vlines` and + `~matplotlib.axes.Axes.hlines` (:pr:`258`). +* Add `stack` as alternative to `stacked` for bar and area plots (:commit:`4e30f415`). + Imperative keywords are better. +* Allow passing e.g. ``barstds=3`` or ``barpctiles=90`` to request error bars + denoting +/-3 standard deviations and 5-95 percentile range (:commit:`4e30f415`). +* Add singular `indicate_error` keywords `barstd`, `barpctile`, etc. as + alternatives to `barstds`, `barpctiles`, etc. (:commit:`81151a58`). + Also prefer them in the documentation. +* Permit different colors for `~matplotlib.axes.Axes.boxplot` and + `~matplotlib.axes.Axes.violinplot` using color lists (:issue:`217`, :pr:`218`) + by `Mickaël Lalande`_. Also allow passing other args as lists (:commit:`4e30f415`). +* Allow passing ``means=True`` to `boxplot` to toggle mean line + (:commit:`4e30f415`). +* Allow setting the mean and median boxplot linestyle with + ``(mean|median)(ls|linestyle)`` keywords (:commit:`4e30f415`). +* Automatically set ``fill=True`` when passing a fill color or color(s) + to `boxplot_wrapper` (:commit:`4e30f415`). +* Allow updating `vlines` and `hlines` styling with singular `color` and `linestyle` + and all of their aliases (:pr:`258`). +* Allow updating axes fonts that use scalings like ``'small'`` and ``'large'`` + by passing ``fontsize=N`` to `format` (:issue:`212`). +* Add `titlebbox` and `abcbbox` as alternatives to `titleborder` and `abcborder` for + "inner" titles and a-b-c labels (:pr:`240`) by `Pratiman Patel`_. Borders are still + used by default. +* Allow putting `title` and `abc` in the same location -- the title and label + are simply offset away from ech other (:issue:`402214f9`). Padding between + them is controlled by the new param :rcraw:`abc.titlepad`. +* Add new :rcraw:`suptitle.pad`, :rcraw:`leftlabel.pad`, :rcraw:`toplabel.pad`, + :rcraw:`bottomlabel.pad`, :rcraw:`rightlabel.pad` settings to control padding + used when aligning super labels (:commit:`402214f9`). These can also be passed + to `~proplot.axes.Axes.format` and applied locally. The new defaults increase + super title padding by a bit. +* More robust interpretation of :rcraw:`abc.style` -- now match case with first + ``'a'`` or ``'A'`` in string, and only replace that one (:issue:`201`). +* Interpret fontsize-relative legend rc params like ``legend.borderpad`` + with ``'em'`` as default units rather than ``'pt'`` (:commit:`6d98fd44`). +* Add :rcraw:`basemap` setting for changing the default backend (:commit:`c9ca0bdd`). If + users have a cartopy vs. basemap preference, they probably want to use it globally. +* Add :rcraw:`cartopy.circular` setting for optionally disabling the "circular bounds + on polar projections" feature (:commit:`c9ca0bdd`). +* Support the standard aliases ``'ls'``, ``'linestyle'``, ``'linestyles'``, etc. + in `~proplot.constructor.Cycle` calls (:commit:`3d8e7dd0`). +* Add `queue` keyword to `colorbar` and `legend` to support workflow where users + successively add handles to location (:pr:`254`). +* Add `nozero` keyword arg to `apply_cmap` to remove the zero contour + from automatically generated levels (:commit:`10e0f13b`). + Example usage: ``ax.contour(x, y, z, nozero=True)``. +* Add `positive` and `negative` keyword args to `apply_cmap` for requesting + automatically-generated all-positive or all-negative levels (:commit:`335d58f4`). + Example usage: ``ax.contourf(x, y, z, positive=True)``. +* Add `rotation` keyword to `colorbar_wrapper` for rotating colorbar tick + labels, like `xrotation` and `yrotation` (:commit:`2d835f20`). +* Add `tickdir` and `tickdirection` keywords to `colorbar_wrapper` for + controlling tick style, like `xtickdir` and `ytickdir` (:commit:`f377f090`). +* Allow specifying labels for auto-generated legends using a ``'labels'`` key + in a `legend_kw` keyword argument (:commit:`a11d1813`). +* Replace legends drawn in the same location by default rather than drawing two + legends on top of each other (:pr:`254`). +* Add suffix ``'_copy'`` to colormaps converted with `to_listed` and + `to_linear_segmented` to avoid accidental overwriting (:commit:`91998e93`). +* Add `xmin`, `xmax`, `ymin`, and `ymax` keyword args to + `~proplot.axes.CartesianAxes.format` as alternatives to `xlim` and `ylim` + (:commit:`ae0719b7`). Example usage: ``ax.format(xmin=0)`` as opposed to + ``ax.format(xlim=(0, None))``. +* Allow passing full "side" names to `lonlabels` and `latlabels` rather than + abbreviations, e.g. ``'left'`` instead of ``'l'`` (:commit:`a5060f67`). This is + more consistent with rest of package. +* Set default transform to ``ccrs.PlateCarree`` when calling `matplotlib.axes.Axes.fill` + on `CartopyAxes` (:issue:`193`). This is more consistent with rest of package. + +Bug fixes +--------- + +* Fix 3 fatal issues preventing proplot import and basic usage in matplotlib >= 3.4 + (:pr:`251`). +* Fix deprecation warnings associated with matplotlib 3.4 refactoring of + subplot classes (:pr:`251`). +* Fix deprecated reference to :rc:`fallback_to_cm` in matplotlib >= 3.3 + (:pr:`251`). +* Fix `~matplotlib.ticker.IndexFormatter` deprecation warning in matplotlib >= 3.3 by + replacing with proplot-local copy (:pr:`251`). +* Fix deprecation warning in matplotlib >= 3.3 -- add `extend` as mappable attribute + rather than passing it to `colorbar()` (:commit:`a23e7043`). +* Fix issue where figures with fixed-aspect axes don't scale properly + in matplotlib >= 3.3 (:issue:`210`, :issue:`235`). +* Fix issue where "twin" ("alternate") axes content always hidden beneath "parent" + content due to adding as children (:issue:`223`). +* Fix issue where default layout in complex subplot grids with non-adjacent + edges is incorrect (:issue:`221`). +* Fix issue where `apply_cycle` fails to merge mean-uncertainty legend handles + due to presence of placeholder labels (:commit:`4e30f415`). +* Fix issue where `standardize_1d` inappropriately infers legend entries from + y-coordinate metadata rather than column metadata (:commit:`4e30f415`). +* Fix issue where `barb` and `quiver` cannot accept 1D data arrays (:issue:`255`). +* Fix issue where cannot set ``rc.style = 'default'`` (:pr:`240`) by `Pratiman Patel`_. +* Fix issue where `get_legend` returns None even with legends present (:issue:`224`). +* Fix issue where new child axes reset row/col label settings (:commit:`f32d9703`). +* Fix issue where `~xarray.DataArray` string coordinates are not extracted from + container before applying as tick labels (:issue:`214`). +* Fix issue where cannot set `extend` other than ``'neither'`` for + `~matplotlib.axes.Axes.scatter` colorbars (:issue:`206`). +* Fix issue where `~matplotlib.axes.Axes.hexbin` ignores `vmin` and `vmax` + keywords (:issue:`250`). +* Fix issue where parametric plot *x* axis is reversed (:commit:`3bde6c47`). +* Fix issue where e.g. `ax.area(x, 0, y2, negpos=True` has positive colors + below x-axis and negative above x-axis (:pr:`258`). +* Fix issue where "negpos" plots ignore `edgecolor` because they pass + `color` rather than `facecolor` to plotting commands. +* Fix issue where cannot have datetime labels on `area` plots (:issue:`255`). +* Fix issue where default orientation of `barh` vertical axis is reversed + (:commit:`258`). +* Fix issue where `hist` with `xarray.DataArray` or `pandas.Dataframe` input causes + erroneous axis labels; use labels for legend instead (:issue:`195`). +* Fix issue where axis is accidentally inverted for histogram plots (:issue:`191`). +* Fix issue where `[xy]minorlocator=1` is not allowed (:issue:`219`). +* Fix issue where inner titles ignore axes-local `titlepad` (:commit:`14f3d0e3`). +* Fix issue where we again fail to sufficiently pad title above tick marks + with tick marks on top x-axis (:commit:`402214f9`). +* Fix issue where non-Cartesian `heatmap` errors rather than warns (:issue:`238`). +* Fix issue where ``labels=True`` with no contours causes error (:issue:`238`). +* Fix issue where `~proplot.colors.Cycle` fails to register new names and fails to + display in `~proplot.demos.show_cycles` (:commit:`94ffc1dc`, :commit:`4a7a3c79`). +* Fix issue where proplot ignores `set_under` and `set_over` values when translating + matplotlib colormap classes to proplot subclasses (:issue:`190`). +* Fix issue where `~proplot.colors.DiscreteNorm` does not account for `set_under` and + `set_over` colors distinct from adjacent in-bounds colors (:issue:`190`). +* Fix issue where proplot fails to detect legend entries for "outer" + legends (:issue:`189`). +* Fix issue where list-of-list-style `legend()` handle and label input fails completely + (:commit:`a298f81f`). This input style is used to specify "centered" legend rows. +* Fix error message when no legend handles are found (:commit:`2c6bf3e2`). +* Fix issue where multiple-artist legend entries (e.g., for lines indicating means and + shading indicating uncertainty) are accidentally truncated (:commit:`a11d1813`). +* Fix issue where numeric zero cannot be applied as legend label (:commit:`02417c8c`). +* Fix issue where simple `pandas.DataFrame.plot` calls with ``legend=True`` fail + (:pr:`254`, :issue:`198`). +* Fix unnecessary restriction where users can only draw <2 "alt" axes and clean + up the `alt[xy]` and `dual[xy]` internals (:issue:`226`). +* Fix matplotlib bug where `altx` and `alty` reset the minor locator of the shared + axis to ``AutoMinorLocator`` even if the axis scale is ``'log'`` (:commit:`2f64361d`). +* Fix issue where axis coordinates are incorrect when `violinplot` or `boxplot` + receive non-DataFrame input (:commit:`b5c3ec4c`). +* Fix issue where `indicate_error` cannot accept 1D error bounds (:commit:`ef2d72cd`). +* Fix issue where `show_cmaps` cannot display reversed colormaps (:commit:`2dd51177`). +* Fix issue where ``'grays_r'`` translated to ``'greys'`` (:commit:`074c6aef`). +* First reverse, *then* shift ``cmap_r_s`` colormaps (:commit:`e5156294`). +* Fix obscure `~proplot.axes.Axes.parametric` bug where `numpy.stack` tries to make + nested ragged arrays from parametric coords (:commit:`b16d56a8`). +* Fix issue where where `SubplotSpec.get_active_rows_columns` returned incorrect + number of "active" rows and columns (:commit:`5cf20b84`). +* For rc lookup with ``context=True``, use most restrictive search mode rather than least. + Otherwise `ax.format()` calls inside context blocks can be overwritten with the + default rc values in subsequent `ax.format()` calls (:commit:`8005fcc1`). + +Internals +--------- + +* Refactor massive `standardize_(1d|2d)` and `(cmap|cycle)_changer` wrappers to break + things into manageable chunks (:pr:`258`, :commit:`6af22567`, :commit:`d3352720`). +* Refactor `colorbar` and `legend` methods and their massive wrappers to clean + things up and expand the "queueing" feature beyond wrappers (:pr:`254`). +* Add prefix ``'proplot_'`` to registered axes "projections" (:commit:`be7ef21e`). More + clear and guards against conflicts with external packages and other mpl versions. +* Add system for processing flexible keyword arguments across different commands + to ``internals/__init__.py``. Analogous to mpl ``_alias`` processing. + +Documentation +------------- + +* Finally use ``pplt`` as the recommended abbreviation: ``import proplot as pplt``. +* Major clean up of "Why proplot?" page and user guide pages. +* Fix incomplete ``cmap.from_file`` docstrings (:commit:`54f1bc7c`). +* Rename "Changelog" to "What's New?" and list all contributors in "About the Authors". +* Remove v0.6.0 renamed classes (e.g. `ProjAxes`) from top-level namespace + (:commit:`442e6aa6`). These classes were public just for documentation. +* Rename public/documented funcs ending in `_wrapper` to ending in `_extras` to avoid + implication they are the only funcs wrapping those commands (:commit:`d1e1e85b`). +* Rename public/documented func `make_mapping_array` to private function, + following lead of matplotlib's `makeMappingArray` (:commit:`66ae574b`). +* Rename public/documented funcs `cmap_changer` and `cycle_changer` + to `apply_cmap` and `apply_cycle` (:commit:`86f7699a`). + +Version 0.6.4 (2020-06-13) +========================== + +Features +-------- + +* Change ``autoformat`` from a `Figure` keyword argument into the + :rcraw:`autoformat` rc setting (:commit:`3a7e5a7c`). +* Combine shading and lines when drawing on-the-fly legends with `indicate_error` + shading using tuple of `fill_between`, `plot` handles, and have `shadelabel` and + `fadelabel` instead create separate entries *only when passed* (:issue:`187`). + +Bug fixes +--------- + +* Fix major issue where calling ``legend()`` without any handles + triggers error rather than using default handles (:issue:`188`). +* Fix issue where on-the-fly colorbar labels were + ignored (:commit:`a642eeed`). +* Stop overwriting existing axis labels when ``autoformat=True`` + and DataArrays or DataFrames passed to plotting command (:commit:`76c7c586`). +* Support single-level contours with colormap colors (:issue:`182`). +* Support changing line width, line style, and color properties + for barb, quiver, streamplot, matshow, spy, and hist2d plots + (:issue:`177`). +* Use :rcraw:`patch.linewidth` for default bar edge width, stop setting + default histogram plot linewidth to zero, and set :rcraw:`patch.linewidth` + to ``0.6`` to match proplot's default line width for lines, axes edges, and + hatches (:issue:`186`). + +Version 0.6.3 (2020-06-02) +========================== + +Bug fixes +--------- + +* Fix issue where proplot import fails if cartopy is not installed (:commit:`e29d49e8`). + +Version 0.6.2 (2020-06-02) +========================== + +Features +-------- + +* Add `autoformat` as `~proplot.axes.standardize_1d` and + `~proplot.axes.standardize_2d` keyword arg, so inheriting labels can + be turned on/off for individual plots (:commit:`61258280`). +* Share *initial* limits/scales/tickers from parent subplots when making + new panels (:commit:`cf0d5d4e`). +* Permit negative "cuts" with `~proplot.colors.LinearSegmentedColormap.cut` + to expand the neutral zone of a diverging cmap (:commit:`94548d09`). +* Add valid `format` arguments to `altx` and `alty`, including ``[x|y]lim`` + (:commit:`734f5940`). +* Pass string `dual[x|y]` arguments like ``'inverse'`` through the + `~proplot.constructor.Scale` constructor (:commit:`413e1781`). +* Add ``'dms'`` locator and formatter, for degree-minute-second labels + without cardinal direction indicators (:commit:`1b180cd2`). +* Add `"tau" formatter `__ + (:commit:`fc6a9752`). +* Restore default :rcraw:`title.pad` to matplotlib value, stop artificially bumping + up :rcraw:`title.pad` for "inner" titles (:commit:`7de1c1f4`). +* Make custom formatters like ``SciFormatter`` *classes* rather than functions + returning `~matplotlib.ticker.FuncFormatter` (:commit:`7591f474`). + +Bug fixes +--------- + +* Various improvements to auto-figure sizing with Qt backend and when calling + `print_figure` (:commit:`db4e48d5`, :commit:`82457347`, :commit:`744d7d37`). +* Suppress warning when ``matplotlibrc`` contains non-style param + (:commit:`4a0c7f10`). +* Fix fatal `standardize_2d` error when ``autoformat=False`` (:issue:`181`) +* Fix issue where ``Colormap(..., alpha=alpha)`` made persistent changes + to the original registered colormap (:commit:`cb24ea51`). +* Prevent matplotlib deprecation warning by removing `set_smart_bounds` + dependency and improving axis scale transforms (:commit:`432576d8`). +* Fix panel sharing issue in presence of stacked or multiple panels + (:commit:`28eaf0ca`). +* Fix geographic feature toggling, zorder bugs + (:commit:`acf0d5d4`, :commit:`ea151b25`). +* Fix `~matplotlib.axes.Axes.hist` bug due to ``bar(..., width=width)`` now + being *relative* to the *x* step size (:commit:`e32ed0bc`). +* Fix bug where `~matplotlib.figure.Figure.savefig` receives ``Path`` instead + of string (:issue:`176`). + +Documentation +------------- + +* Various improvements the API docstrings. +* Improve overall website style (:commit:`89d6f5bd`). +* Make website "dark mode" darker (:commit:`979c8188`). +* Prevent website from flashing light mode when changing pages (:commit:`75e4d6a1`). +* Add documentation for `proplot.figure.Figure.save` method (:commit:`da25266a`). +* Remove `~proplot.figure.Figure` setters like `set_sharex`, replace with + read-only properties (:commit:`7b455008`). The getters were only for object + introspection. The setters never worked properly/were unused in examples. + +Version 0.6.1 (2020-05-20) +========================== + +Bug fixes +--------- + +* Fix issue where cartopy version checking fails if cartopy is not installed + (:commit:`86cd50b8`). +* Fix issue where "tight" layout of geographic plots was broken in pre-v0.18 + cartopy (:commit:`72cb93c6`). +* Fix issue where gridline coverage was incomplete in some zoomed-in + projections (:commit:`458c6d7c`). +* Fix issue where basemap minor gridlines did not update when + major gridlines were updated (:commit:`427326a7`). + +Version 0.6.0 (2020-05-20) +========================== + +Deprecated +---------- + +* Remove the ``geoaxes`` and ``geogrid`` rc settings (:pr:`168`). Gridline + settings are now controlled with ``grid``. +* Remove the ``lonstep`` and ``latstep`` settings -- we now use + `~proplot.ticker.LongitudeLocator` and `~proplot.ticker.LatitudeLocator` + to select "nice" gridline locations even when zoomed in (:pr:`168`) +* Rename the ``cartopy.global`` rc setting to ``cartopy.autoextent`` + (:commit:`7c0f118a`) and add an `autoextent` keyword (:commit:`23db0498`). +* Rename several "error indication" keyword arguments and rename `add_errorbars` + wrapper to `~proplot.axes.indicate_error` (:pr:`166`, :commit:`d8c50a8d`). +* Remove ``'rgbcycle'`` setting (:pr:`166`, :commit:`6653b7f0`). + This was complicated to implement/did not add critical functionality. +* Deprecate support for "parametric" plots inside `~matplotlib.axes.Axes.plot`, + instead use `~proplot.axes.Axes.parametric` (:commit:`64210bce`). +* Change `~proplot.utils.units` ``units`` keyword argument to more natural + ``dest`` (:commit:`62903b48`). +* Drop support for ``.xrgb`` and ``.xrgba`` files (:commit:`4fa72b0c`). Not + sure if any online sources produce these kinds of files. +* Drop support for ``.rgba`` files, but optionally read 4th opacity column + from ``.rgb`` and ``.txt`` files (:commit:`4fa72b0c`). +* Remove ``'Blue0'`` SciVisColor colormap (:pr:`149`, :commit:`7cb4ce0f`). It was odd + man out in the table, and not even really perceptually uniform. +* Remove custom proplot cycles -- these should be thought out much more + carefully (:commit:`43f65d17`). +* Remove "crayola" colors and clean up the `~proplot.setup.register_colors` algorithm + (:pr:`149`, :commit:`8922d6de`). Crayola color names less intuitive than XKCD. +* Use ``'cmap_s'`` instead of ``'cmap_shifted'`` to quickly get a 180 + degree-shifted colormap, similar to ``'cmap_r'`` (:pr:`149`, :commit:`da4ccb08`). +* Rename ``GrayCycle`` colormap to ``MonoCycle`` to more accurately reflect + colormap design origins (:pr:`149`, :commit:`d67e45bf`). +* Rename `~proplot.colors.MidpointNorm` to more intuitive + `~proplot.colors.DivergingNorm`, and make "fair" color scaling the default + behavior (:commit:`2f549c9`). +* Rename `BinNorm` to `~proplot.styletools.DiscreteNorm` + and fix issues with diverging norm color scaling (:pr:`149`, :commit:`98a976f1`). +* Rename `~proplot.styletools.LinearSegmentedColormap.concatenate` to + `~proplot.styletools.LinearSegmentedColormap.append`, + `~proplot.styletools.LinearSegmentedColormap.updated` to + `~proplot.styletools.LinearSegmentedColormap.copy`, + `~proplot.styletools.LinearSegmentedColormap.truncated` to + `~proplot.styletools.LinearSegmentedColormap.truncate`, and + `~proplot.styletools.LinearSegmentedColormap.punched` to + `~proplot.styletools.LinearSegmentedColormap.cut` (:pr:`149`, :commit:`e1a08930`). + The old method names remain with a deprecation warning. + +Style changes +------------- + +* Increase default :rcraw:`savefig.dpi` to 1200, matching recommendations from academic + journals (:pr:`167`, :commit:`c00e7314`). Also add detailed discussion to user guide. +* Stop reversing the ``'Spectral'`` colormap when proplot is imported + (:pr:`149`, :commit:`ce4ef6a0`). +* Change default rc settings closer to matplotlib, including margins and line + width (:pr:`166`, :commit:`f801852b`). Many were changed for no good reason. +* Change default line style for geographic gridlines from ``':'`` to ``'-'`` + and match style from primary gridlines (:pr:`166`, :commit:`f801852b`). +* Make default `areax` and `areay` bounds "sticky", similar to + histograms and barplots (:pr:`166`). Also make `vlines` and `hlines` + perpendicular bounds sticky if either the min/max coordinates are scalar. +* *Hide* bad colormaps like ``'jet'`` from the + `~proplot.styletools.show_cmaps` table instead of deleting them outright, + just like CSS4 colors (:pr:`149`, :commit:`ce4ef6a0`). + +Features +-------- + +* Permit drawing "outer" axes and figure legends without explicitly passing handles + (:pr:`149`, :commit:`a69b48eb`). Figure legends use the handles from all axes. +* Use `_LonAxis` and `_LatAxis` dummy axes with custom `LongitudeLocator` + and `LatitudeLocator` to control geographic gridlines (:pr:`168`). This + improves accuracy of automatic gridline generation. +* Add ``'dmslat'`` and ``'dmslon'`` as formatters for cartopy projections, + along with ``dms`` `format` keyword argument. This labels points with + degrees/minutes/seconds when appropriate (:pr:`168`). +* Support "minor" geographic gridlines with the ``gridminor`` keyword + arg and existing ``gridminor`` settings (:pr:`168`). Default locator + used for minor gridlines is `~matplotlib.ticker.AutoMinorLocator`. +* Add `loninline`, `latinline`, and `rotatelabels` keywords for controlling + cartopy gridliner behavior (:pr:`168`). +* Support `cartopy 0.18 `__ + locators, formatters, deprecations, and new labelling features (:pr:`158`). +* Add :rcraw:`geogrid.labelpad` and :rcraw:`geogrid.rotatelabels` settings + for cartopy gridline labels (:pr:`158`). +* Add `~proplot.ticker.SigFigFormatter` (:pr:`149`, :commit:`da6105d2`) and + `~proplot.ticker.SciFormatter` (:pr:`175`, :commit:`c43f7f91`) axis formatters. +* Support more `~proplot.ticker.AutoFormatter` features on + `~proplot.ticker.SimpleFormatter` (:pr:`152`, :commit:`6decf962`). +* Enable passing callables to `~proplot.axistools.Formatter` to create a + `~proplot.axistools.FuncFormatter` instance. +* Add `proplot.config.RcConfigurator.save` and + `proplot.config.RcConfigurator.from_file` methods (:pr:`167`, :commit:`e6dd8314`). +* No longer distinguish between "quick" settings and proplot's "added" + settings (:pr:`167`, :commit:`e6dd8314`). Quick settings, added settings, and + matplotlib settings can all have "children" so the distinction no longer makes sense. +* Add opacity-preserving functions `~proplot.utils.to_rgba` + and `~proplot.utils.to_xyza`, plus `~proplot.utils.set_alpha` for + changing alpha channel of arbitrary color (:pr:`171`, :commit:`81c647da`). +* Add to `~proplot.colors.LinearSegmentedColormap.set_alpha` the ability to + create an *opacity gradation*, rather than just an opacity for the entire + colormap (:pr:`171`, :commit:`4583736`). +* Support passing colormap objects, not just names, to `~proplot.demos.show_cmaps` + and `~proplot.demos.show_cycles` (:pr:`171`, :commit:`7f8ca59f`). +* Add options to `~proplot.axes.indicate_error` for adding *shading* + to arbitrary plots (:pr:`166`, :commit:`d8c50a8d`). Also support automatic legend + entries for shading and ensure `indicate_error` preserves metadata. +* Wrap ``pcolorfast`` just like ``pcolor`` and ``pcolormesh`` are + wrapped (:pr:`166`, :commit:`50a262dd`). +* Add ``negpos`` feature to `~proplot.axes.bar_wrapper` and new :rcraw:`negcolor` + and :rcraw:`poscolor` rc keyword arguments (:pr:`166`, :commit:`ab4d6746`). +* Support `~matplotlib.axes.Axes.vlines` and `~matplotlib.axes.Axes.hlines` + flexible arguments and add ``negpos`` feature + (:pr:`166`, :commit:`1c53e947`, :commit:`e42ee913`). +* Support building a colormap and `DiscreteNorm` inside `~matplotlib.axes.Axes.scatter`, + just like `contourf` and `pcolormesh` (:pr:`162`). +* Permit special colormap normalization and level scaling for + colormap-colored contour plots, just like contourf (:pr:`149`, :commit:`054cceb5`). +* Support drawing colorbars with descending levels when input `levels`/`values` + are monotonically descending lists (:pr:`149`, :commit:`10763146`) +* Add support for matplotlib stylesheets with `~proplot.config.use_style` + function and ``style`` rc param (:pr:`149`, :commit:`edc6f3c9`). +* Make ``'Grays'`` and ``'Greys'`` synonyms for the same ColorBrewer colormap + (:pr:`149`, :commit:`da4ccb08`). +* Add `~proplot.styletools.LinearSegmentedColormap.to_listed` and + `~proplot.styletools.PerceptuallyUniformColormap.to_linear_segmented` + methods for handling conversions (:pr:`149`, :commit:`e1a08930`). +* Permit merging mixed colormap types `~proplot.styletools.LinearSegmentedColormap` + with `~proplot.styletools.PerceptuallyUniformColormap` (:commit:`972956b1`). +* Include the `alpha` channel when saving colormaps and cycles by default + (:pr:`149`, :commit:`117e05f2`). +* Permit 8-character hex strings with alpha channels when loading colormaps + and color cycles from hex files (:pr:`149`, :commit:`381a84d4`). +* Support sampling `~prolot.styletools.LinearSegmentedColormap` into + `~proplot.styletools.ListedColormap` inside of + `~proplot.styletools.Colormap` rather than `~proplot.styletools.Cycle` + (:issue:`84`, :commit:`972956b1`). +* Add `categories` keyword arg to `~proplot.styletools.show_cmaps` and + `~proplot.styletools.show_cycles` (:pr:`149`, :commit:`79be642d`). +* Draw `~proplot.styletools.show_colors` table as single figure with category + labels, similar to `~proplot.styletools.show_cmaps` (:pr:`149`, :commit:`c8ca2909`). +* Return both figure and axes in ``show_`` functions; this gives users access + to the axes and prevents drawing them twice in notebooks + (:pr:`149`, :commit:`2f600bc9`). +* Publicly support "filling" axes with colorbars using ``loc='fill'`` + (:pr:`149`, :commit:`057c9895`). + +Bug fixes +--------- + +* Fix various issues with axis label sharing and axis sharing for + twinned axes and panel axes (:pr:`164`). +* Permit modifying existing cartopy geographic features with successive + calls to `~proplot.axes.GeoAxes.format` (:pr:`168`). +* Fix issue drawing bar plots with datetime *x* axes (:pr:`156`). +* Fix issue where `~proplot.ticker.AutoFormatter` tools were not locale-aware, i.e. use + comma as decimal point sometimes (:pr:`152`, :commit:`c7636296`). +* Fix issue where `~proplot.ticker.AutoFormatter` nonzero-value correction algorithm was + right for wrong reasons and could be wrong in rare circumstances + (:pr:`152`, :commit:`c7636296`). +* Fix issue where ``matplotlib.style.use`` resets backend + (:pr:`149`, :commit:`c8319104`). +* Fix issue with colormaps with dots in name (:pr:`149`, :commit:`972956b1`). +* Fix logarithmic scale argument parsing deprecation (:pr:`149`, :commit:`6ed7dbc5`). +* Fix deprecation of direct access to ``matplotlib.cm.cmap_d`` + in matplotlib >=3.2.0 (:pr:`149`, :commit:`a69c16da`). +* Fix issues with string font sizes (:pr:`149`, :commit:`6121de03`). Add hidden + `~proplot.config.RcConfigurator._get_font_size` method to + translate font size to numeric. +* Fix issue where passing actual projection instances generated with + `~proplot.constructor.Proj` to `~proplot.ui.subplots` could incorrectly + pair cartopy projections with basemap axes and vice versa (:pr:`149`). +* Fix issue where could not draw colorbar from list of single-color + `~matplotlib.collections.PathCollection`\ s, i.e. + scatter plots (:pr:`149`, :commit:`e893900b`). +* Fix issue where importing proplot in jupyter notebooks resets the default + inline backend (:pr:`149`, :commit:`6121de03`). +* Improve axis label sharing algorithm (:commit:`6535b219`). +* Fix main axis label sharing bugs in presence of panels + (:commit:`7b709db9`). +* Fix v0.4.0 regression where panel sharing no longer works + (:commit:`289e5538`). +* Fix `~proplot.axistools.AutoFormatter` bug with values close + to zero (:issue:`124`, :commit:`9b7f89fd`) +* Fix `~proplot.axistools.AutoFormatter` bug with small negative + numbers (:issue:`117`). +* Fix issue where Scientific colour maps not interpreted as cyclic, so end + colors not standardized properly (:commit:`e10a3109`). + +Internals +--------- + +* **Major** internal change: Move functions into smaller separate + files to mimic how matplotlib library is divided up (:pr:`149`). +* Add `internals` folder containing default proplot rc params, deprecation + helper functions, and other internal tools (:pr:`149`). +* Make colorbar axes instances of `~proplot.axes.CartesianAxes`, just + like panel axes. +* Rename ubiquitous `_notNone` function to `_not_none` and change to more + sensible behavior. +* Turn some private `~proplot.config` functions into static + methods (:commit:`6121de03`). +* Remove "smart bounds" feature from `FuncScale` (:pr:`166`, :commit:`9ac149ea`). +* Clean up axes iterators (:pr:`149`, :commit:`c8a0768a`). + +Documentation +------------- + +* Call figure objects `fig` instead of `f`. +* Major clean up of notebook examples (:commit:`f86542b5`). +* Major clean up `~proplot.wrappers` documentation (:commit:`9648c18f`) +* Fix dead "See Also" links (:commit:`d32c6506`). +* Use "Other parameters" tables more often (:commit:`d32c6506`). +* Remove the public objects `normalizers`, `locators`, `formatters`, + `cartopy_projs`, `basemap_kwargs`, `cmaps`, `colors`, and `fonts` (:pr:`149`). + These objects were public just for introspection/documentation. +* Rename `~proplot.config.rc_configurator` and `~proplot.ui.subplot_grid` to + `~proplot.config.RcConfigurator` and `~proplot.ui.SubplotsContainer` + to match capitalized class naming convention (:pr:`149`). These + classes are public just for documentation. +* Rename `XYAxes` to `~proplot.axes.CartesianAxes`, `~proplot.axes.GeoAxes` + to `~proplot.axes.CartopyAxes`, and `~proplot.axes.ProjAxes` to + `~proplot.axes.GeoAxes` (:pr:`149`, :commit:`4a6a0e34`). These classes + are public just for documentation. +* Rename `ColorDict` to `~proplot.colors.ColorDatabase`, `CmapDict` to + `~proplot.colors.ColormapDatabase` (:pr:`149`, :commit:`9d7fd3e0`). + These classes are public just for documentation. + +Version 0.5.0 (2020-02-10) +========================== + +Deprecated +---------- + +* Remove `abcformat` from `~proplot.axes.Axes.format` (:commit:`2f295e18`). +* Rename `top` to `abovetop` in `~proplot.axes.Axes.format` (:commit:`500dd381`). +* Rename `abc.linewidth` and `title.linewidth` to ``borderwidth`` (:commit:`54eb4bee`). +* Rename `~proplot.wrappers.text_wrapper` `linewidth` and `invert` to + `borderwidth` and `borderinvert` (:commit:`54eb4bee`). + +Features +-------- + +* Add back `Fabio Crameri's scientific colour maps + `__ (:pr:`116`). +* Permit both e.g. `locator` and `xlocator` as keyword arguments to + `~proplot.axes.Axes.altx`, etc. (:commit:`57fab860`). +* Permit *descending* `~proplot.styletools.BinNorm` and + `~proplot.styletools.LinearSegmentedNorm` levels (:pr:`119`). +* Permit overriding the font weight, style, and stretch in the + `~proplot.styletools.show_fonts` table (:commit:`e8b9ee38`). +* Permit hiding "unknown" colormaps and color cycles in the + `~proplot.styletools.show_cmaps` and `~proplot.styletools.show_cycles` + tables (:commit:`cb206f19`). + +Bug fixes +--------- + +* Fix issue where `~proplot.styletools.show_cmaps` and + `~proplot.styletools.show_cycles` colormap names were messed up + (:commit:`13045599`) +* Fix issue where `~proplot.styletools.show_cmaps` and + `~proplot.styletools.show_cycles` did not return figure instance + (:commit:`98209e87`). +* Fix issue where user `values` passed to + `~proplot.wrappers.colorbar_wrapper` were sometimes ignored + (:commit:`fd4f8d5f`). +* Permit passing *lists of colors* to manually shade line contours and filled + contours in `~proplot.wrappers.cmap_changer`. +* Prevent formatting rightmost meridian label as ``1e-10`` on cartopy map + projections (:commit:`37fdd1eb`). +* Support CF-time axes by fixing bug in `~proplot.wrappers.standardize_1d` + and `~proplot.wrappers.standardize_2d` (:issue:`103`, :pr:`121`). +* Redirect to the "default" location when using ``legend=True`` and + ``colorbar=True`` to generate on-the-fly legends and colorbars + (:commit:`c2c5c58d`). This feature was accidentally removed. +* Let `~proplot.wrappers.colorbar_wrapper` accept lists of colors + (:commit:`e5f11591`). This feature was accidentally removed. + +Internals +--------- + +* Remove various unused keyword arguments (:commit:`33654a42`). +* Major improvements to the API controlling axes titles and a-b-c + labels (:commit:`1ef7e65e`). +* Always use full names ``left``, ``right``, ``top``, and ``bottom`` instead + of ``l``, ``r``, ``b``, and ``t``, for clarity (:commit:`1ef7e65e`). +* Improve ``GrayCycle`` colormap, is now much shorter and built from + reflected Fabio ``GrayC`` colormaps (:commit:`5b2c7eb7`). + +Version 0.4.3 (2020-01-21) +========================== + +Features +-------- + +* Permit comments at the head of colormap and color files + (:commit:`0ffc1d15`). +* Make `~proplot.axes.Axes.parametric` match ``plot`` autoscaling behavior + (:commit:`ecdcba82`). + +Internals +--------- + +* Use `~proplot.axes.Axes.colorbar` instead of `~matplotlib.axes.Axes.imshow` + for `~proplot.styletools.show_cmaps` and `~proplot.styletools.show_cycles` + displays (:pr:`107`). +* Remove `~proplot.rctools.ipython_autoreload`, + `~proplot.rctools.ipython_autosave`, and `~proplot.rctools.ipython_matplotlib` + (:issue:`112`, :pr:`113`). Move inline backend configuration to a hidden + method that gets called whenever the ``rc_configurator`` is initalized. + +Version 0.4.2 (2020-01-09) +========================== + +Features +-------- + +* Add ``family`` keyword arg to `~proplot.styletools.show_fonts` (:pr:`106`). +* Package the `TeX Gyre `__ + font series with proplot (:pr:`106`). Remove a couple other fonts. +* Put the TeX Gyre fonts at the head of the serif, sans-serif, monospace, + cursive, and fantasy ``rcParams`` font family lists (:issue:`104`, :pr:`106`). + +Bug fixes +--------- + +* Fix issues with Fira Math weights unrecognized by matplotlib (:pr:`106`). + +Version 0.4.1 (2020-01-08) +========================== + +Features +-------- + +* Comments (lines starting with ``#``) are now permitted in all RGB and HEX style + colormap and cycle files (:pr:`100`). +* Break down `~proplot.styletools.show_cycles` bars into categories, just + like `~proplot.styletools.show_cmaps` (:pr:`100`). + +Bug fixes +--------- + +* Fix issue where `~proplot.styletools.show_cmaps` and `~proplot.styletools.show_cycles` + draw empty axes (:pr:`100`). +* Add back the default .proplorc file section to docs (:pr:`101`). + To do this, ``conf.py`` auto-generates a file in ``_static``. + +Internals +--------- + +* Add ``geogrid.color/linewidth/etc`` and ``gridminor.color/linewidth/etc`` + props as *children* of ``grid.color/linewidth/etc`` (:pr:`101`). +* Change the default ``.proplotrc`` format from YAML to the ``.matplotlibrc`` + syntax (:pr:`101`). +* Various `~proplot.rctools.rc_configurator` improvements, remove outdated + global variables (:pr:`101`). +* Better error handling when loading colormap/cycle files, and calls to + `~proplot.styletools.Colormap` and `~proplot.styletools.Cycle` now raise + errors while calls to `~proplot.styletools.register_cmaps` and + `~proplot.styletools.register_cycles` still issue warnings (:pr:`100`). + +Version 0.4.0 (2020-01-07) +========================== + +Deprecated +---------- + +* Rename `basemap_defaults` to `~proplot.projs.basemap_kwargs` and + `cartopy_projs` to `~proplot.projs.cartopy_names` (:commit:`431a06ce`). +* Remove ``subplots.innerspace``, ``subplots.titlespace``, + ``subplots.xlabspace``, and ``subplots.ylabspace`` spacing arguments, + automatically calculate default non-tight spacing using `~proplot.subplots._get_space` + based on current tick lengths, label sizes, etc. +* Remove redundant `~proplot.rctools.use_fonts`, use + ``rcParams['sans-serif']`` precedence instead (:pr:`95`). +* `~proplot.axes.Axes.dualx` and `~proplot.axes.Axes.dualy` no longer accept + "scale-spec" arguments. Must be a function, two functions, or an axis + scale instance (:pr:`96`). +* Remove `~proplot.axes.Axes` ``share[x|y]``, ``span[x|y]``, and + ``align[x|y]`` kwargs (:pr:`99`). These settings are now always + figure-wide. +* Rename `~proplot.styletools.Cycle` ``samples`` to ``N``, rename + `~proplot.styletools.show_colors` ``nbreak`` to ``nhues`` (:pr:`98`). + +Features +-------- + +* Add `~proplot.styletools.LinearSegmentedColormap.from_file` static methods + (:pr:`98`). You can now load files by passing a name to + `~proplot.styletools.Colormap`. +* Add TeX Gyre Heros as open source Helvetica-alternative; this is the new + default font. Add Fira Math as DejaVu Sans-alternative; has complete set + of math characters (:pr:`95`). +* Add `xlinewidth`, `ylinewidth`, `xgridcolor`, `ygridcolor` keyword args to + `~proplot.axes.XYAxes.format` (:pr:`95`). +* Add getters and setters for various `~proplot.subplots.Figure` settings + like ``share[x|y]``, ``span[x|y]``, and ``align[x|y]`` (:pr:`99`). +* Let `~proplot.axes.Axes.twinx`, `~proplot.axes.Axes.twiny`, + `~proplot.axes.Axes.altx`, and `~proplot.axes.Axes.alty` accept + `~proplot.axes.XYAxes.format` keyword args just like + `~proplot.axes.Axes.dualx` and `~proplot.axes.Axes.dualy` (:pr:`99`). +* Add `~proplot.subplots.Figure` ``fallback_to_cm`` kwarg. This is used by + `~proplot.styletools.show_fonts` to show dummy glyphs to clearly illustrate + when fonts are missing characters, but preserve graceful fallback for user. +* Improve `~proplot.projs.Proj` constructor function. It now accepts + `~cartopy.crs.Projection` and `~mpl_toolkits.basemap.Basemap` instances, + just like other constructor functions, and returns only the projection + instance (:pr:`92`). +* `~proplot.rctools.rc` `~proplot.rctools.rc_configurator.__getitem__` always + returns the setting. To get context block-restricted settings, you must + explicitly pass ``context=True`` to `~proplot.rctools.rc_configurator.get`, + `~proplot.rctools.rc_configurator.fill`, or + `~proplot.rctools.rc_configurator.category` (:pr:`91`). + +Bug fixes +--------- + +* Fix `~proplot.rctools.rc_configurator.context` bug (:issue:`80` and :pr:`91`). +* Fix issues with `~proplot.axes.Axes.dualx` and `~proplot.axes.Axes.dualy` + with non-linear parent scales (:pr:`96`). +* Ignore TTC fonts because they cannot be saved in EPS/PDF figures + (:issue:`94` and :pr:`95`). +* Do not try to use Helvetica Neue because "thin" font style is read as + regular (:issue:`94` and :pr:`95`). + +Documentation +------------- + +* Use the imperative mood for docstring summaries (:pr:`92`). +* Fix `~proplot.styletools.show_cycles` bug (:pr:`90`) and show cycles using + colorbars rather than lines (:pr:`98`). + +Internals +--------- + +* Define `~proplot.rctools.rc` default values with inline dictionaries rather + than with a default ``.proplotrc`` file, change the auto-generated user + ``.proplotrc`` (:pr:`91`). +* Remove useless `panel_kw` keyword arg from + `~proplot.wrappers.legend_wrapper` and `~proplot.wrappers.colorbar_wrapper` + (:pr:`91`). Remove `wflush`, `hflush`, and `flush` keyword args from + `~proplot.subplots.subplots` that should have been removed long ago. + +Version 0.3.1 (2019-12-16) +========================== + +Bug fixes +--------- + +* Fix issue where custom fonts were not synced (:commit:`a1b47b4c`). +* Fix issue with latest versions of matplotlib where ``%matplotlib inline`` + fails *silently* so the backend is not instantiated (:commit:`cc39dc56`). + +Version 0.3.0 (2019-12-15) +========================== + +Deprecated +---------- + +* Remove ``'Moisture'`` colormap (:commit:`cf8952b1`). + +Features +-------- + +* Add `~proplot.styletools.use_font`, only sync Google Fonts fonts + (:pr:`87`). +* New ``'DryWet'`` colormap is colorblind friendly (:commit:`0280e266`). +* Permit shifting arbitrary colormaps by ``180`` degrees by appending the + name with ``'_shifted'``, just like ``'_r'`` (:commit:`e2e2b2c7`). + +Bug fixes +--------- + +* Add brute force workaround for saving colormaps with *callable* segmentdata + (:commit:`8201a806`). +* Fix issue with latest versions of matplotlib where ``%matplotlib inline`` + fails *silently* so the backend is not instantiated (:commit:`cc39dc56`). +* Fix `~proplot.styletools.LinearSegmentedColormap.shifted` when `shift` is + not ``180`` (:commit:`e2e2b2c7`). +* Save the ``cyclic`` and ``gamma`` attributes in JSON files too + (:commit:`8201a806`). + +Documentation +------------- + +* Cleanup notebooks, especially the colormaps demo (e.g. :commit:`952d4cb3`). + +Internals +--------- + +* Change `~time.clock` to `~time.perf_counter` (:pr:`86`). + +Version 0.2.7 (2019-12-09) +========================== + +Bug fixes +--------- + +* Fix issue where `~proplot.styletools.AutoFormatter` logarithmic scale + points are incorrect (:commit:`9b164733`). + +Documentation +------------- + +* Improve :ref:`Configuring proplot` documentation (:commit:`9d50719b`). + +Internals +--------- + +* Remove `prefix`, `suffix`, and `negpos` keyword args from + `~proplot.styletools.SimpleFormatter`, remove `precision` keyword arg from + `~proplot.styletools.AutoFormatter` (:commit:`8520e363`). +* Make ``'deglat'``, ``'deglon'``, ``'lat'``, ``'lon'``, and ``'deg'`` + instances of `~proplot.styletools.AutoFormatter` instead of + `~proplot.styletools.SimpleFormatter` (:commit:`8520e363`). The latter + should just be used for contours. + +Version 0.2.6 (2019-12-08) +========================== + +Bug fixes +--------- + +* Fix issue where twin axes are drawn *twice* (:commit:`56145122`). + +Version 0.2.5 (2019-12-07) +========================== + +Features +-------- + +* Improve `~proplot.axistools.CutoffScale` algorithm, + permit arbitrary cutoffs (:pr:`83`). + +Version 0.2.4 (2019-12-07) +========================== + +Deprecated +---------- + +* Rename `ColorCacheDict` to `~proplot.styletools.ColorDict` + (:commit:`aee7d1be`). +* Rename lower-case `colors` to `~proplot.styletools.Colors` + (:commit:`aee7d1be`) +* Remove `fonts_system` and `fonts_proplot`, rename `colordict` to + `~proplot.styletools.colors`, make top-level variables more robust + (:commit:`861583f8`). + +Documentation +------------- + +* Add params table for `~proplot.styletools.show_fonts` (:commit:`861583f8`). + +Internals +--------- + +* Improve `~proplot.styletools.register_colors` internals. + +Version 0.2.3 (2019-12-05) +========================== + +Bug fixes +--------- + +* Fix issue with overlapping gridlines using monkey patches on gridliner + instances (:commit:`8960ebdc`). +* Fix issue where auto colorbar labels are not applied when ``globe=True`` + (:commit:`ecb3c899`). +* More sensible zorder for gridlines (:commit:`90d94e55`). +* Fix issue where customized super title settings are overridden when new + axes are created (:commit:`35cb21f2`). + +Documentation +------------- + +* Organize ipython notebook documentation (:commit:`35cb21f2`). + +Internals +--------- + +* Major cleanup of the `~proplot.wrappers.colorbar_wrapper` source code, + handle minor ticks using the builtin matplotlib API just like major ticks + (:commit:`b9976220`). + +Version 0.2.2 (2019-12-04) +========================== + +Deprecated +---------- + +* Rename `~proplot.subplots.axes_grid` to `~proplot.subplots.subplot_grid` + (:commit:`ac14e9dd`). + +Bug fixes +--------- + +* Fix shared *x* and *y* axis bugs (:commit:`ac14e9dd`). + +Documentation +------------- + +* Make notebook examples PEP8 compliant (:commit:`97f5ffd4`). Much more + readable now. + +Version 0.2.1 (2019-12-02) +========================== + +Deprecated +---------- + +* Rename `autoreload_setup`, `autosave_setup`, and `matplotlib_setup` to + `~proplot.rctools.ipython_autoreload`, `~proplot.rctools.ipython_autosave`, + and `~proplot.rctools.ipython_matplotlib`, respectively + (:commit:`84e80c1e`). + +Version 0.2.0 (2019-12-02) +========================== + +Deprecated +---------- + +* Remove the ``nbsetup`` rc setting in favor of separate ``autosave``, + ``autoreload``, and ``matplotlib`` settings for triggering the respective + ``%`` magic commands. (:commit:`3a622887`; ``nbsetup`` is still accepted + but no longer documented). +* Rename the ``format`` rc setting in favor of the ``inlinefmt`` setting + (:commit:`3a622887`; ``format`` is still accepted but no longer + documented). +* Rename ``FlexibleGridSpec`` and ``FlexibleSubplotSpec`` to ``GridSpec`` and + ``SubplotSpec`` (:commit:`3a622887`; until :pr:`110` is merged it is + impossible to use these manually, so this won't bother anyone). + +Features +-------- + +* Support manual resizing for all backends, including ``osx`` and ``qt`` + (:commit:`3a622887`). + +Bug fixes +--------- + +* Disable automatic resizing for the ``nbAgg`` interactive inline backend. + Found no suitable workaround (:commit:`3a622887`). + +Internals +--------- + +* Organize the ``rc`` documentation and the default ``.proplotrc`` file + (:commit:`3a622887`). +* Rename ``rcParamsCustom`` to ``rcParamsLong`` (:commit:`3a622887`; this is + inaccessible to the user). +* When calling ``fig.canvas.print_figure()`` on a stale figure, call + ``fig.canvas.draw()`` first. May be overkill for + `~matplotlib.figure.Figure.savefig` but critical for correctly displaying + already-drawn notebook figures. + +Version 0.1.0 (2019-12-01) +========================== + +Internals +--------- + +* Include `flake8` in Travis CI testing (:commit:`8743b857`). +* Enforce source code PEP8 compliance (:commit:`78da51a7`). +* Use pre-commit for all future commits (:commit:`e14f6809`). +* Implement tight layout stuff with canvas monkey patches (:commit:`67221d10`). + Proplot now works for arbitrary backends, not just inline and qt. + +Documentation +------------- + +* Various `RTD bugfixes + `__ (e.g. + :commit:`37633a4c`). + +Version 0.0.0 (2019-11-27) +========================== + +The first version released on `PyPi `__. + +.. _Luke Davis: https://github.com/lukelbd + +.. _Riley Brady: https://github.com/bradyrx + +.. _Stephane Raynaud: https://github.com/stefraynaud + +.. _Mickaël Lalande: https://github.com/mickaellalande + +.. _Pratiman Patel: https://github.com/pratiman-91 + +.. _Zachary Moon: https://github.com/zmoon + +.. _Eli Knaap: https://github.com/knaaptime diff --git a/ci/environment.yml b/ci/environment.yml new file mode 100644 index 000000000..f316c156d --- /dev/null +++ b/ci/environment.yml @@ -0,0 +1,39 @@ +# Hard requirements for running tests +# See docs/environment.yml for dependency notes +# WARNING: Keep this up-to-date with ci/environment.yml +name: proplot-dev +channels: + - conda-forge +dependencies: + - python==3.8 + - numpy==1.19.5 + - pandas + - xarray + - matplotlib==3.2.2 + - cartopy==0.20.2 + - ipykernel + - pandoc + - python-build + - setuptools + - setuptools_scm + - setuptools_scm_git_archive + - wheel + - pip + - pip: + - .. + - flake8 + - isort + - black + - doc8 + - pytest + - pytest-sugar + - pyqt5 + - docutils==0.16 + - sphinx>=3.0 + - sphinx-copybutton + - sphinx-rtd-light-dark + - jinja2==2.11.3 + - markupsafe==2.0.1 + - nbsphinx==0.8.1 + - jupytext + - git+https://github.com/proplot-dev/sphinx-automodapi@proplot-mods diff --git a/ci/run-linter.sh b/ci/run-linter.sh new file mode 100755 index 000000000..c59d7cabc --- /dev/null +++ b/ci/run-linter.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# Run the travis CI tests +# WARNING: Make sure to keep flags in sync with .pre-commit.config.yaml +set -e +set -eo pipefail + +echo 'Code Styling with (flake8, isort)' + +echo '[flake8]' +flake8 proplot docs --exclude .ipynb_checkpoints --max-line-length=88 --ignore=W503,E402,E741 + +echo '[isort]' +isort --recursive --check-only --line-width=88 --skip __init__.py --multi-line=3 --force-grid-wrap=0 --trailing-comma proplot + +# echo '[black]' +# black --check -S proplot + +# echo '[doc8]' +# doc8 ./*.rst docs/*.rst --ignore D001 # ignore line-too-long due to RST tables diff --git a/docs/.proplotrc b/docs/.proplotrc deleted file mode 100644 index d431d570d..000000000 --- a/docs/.proplotrc +++ /dev/null @@ -1,7 +0,0 @@ -# Overrides for sphinx -# SVG because quality of examples is highest priority -# Larger font sizes than local configuration for visibility -# Tested SVG vs. PNG and speeds are comparable! -small: 9 -large: 10 -inlinefmt: svg diff --git a/docs/1dplots.ipynb b/docs/1dplots.ipynb deleted file mode 100644 index 24b024f82..000000000 --- a/docs/1dplots.ipynb +++ /dev/null @@ -1,546 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# 1d plotting" - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "ProPlot adds new features to various `~matplotlib.axes.Axes` plotting methods using a set of \"wrapper\" functions. When a plotting method like `~matplotlib.axes.Axes.plot` is \"wrapped\" by one of these functions, it accepts the same parameters as the \"wrapper\". These features are a strict *superset* of the matplotlib API; if you want, you can use the plotting methods exactly as you always have.\n", - "\n", - "This section documents the features added by wrapper functions to \"1d\" plotting commands like `~matplotlib.axes.Axes.plot`, `~matplotlib.axes.Axes.scatter`, `~matplotlib.axes.Axes.bar`, and `~matplotlib.axes.Axes.barh`." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Standardized input " - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "The `~proplot.wrappers.standardize_1d` wrapper is used to standardize the positional arguments for \"1d\" plotting methods. \n", - "`~proplot.wrappers.standardize_1d` allows you to optionally omit *x* coordinates (in which case they are inferred from the *y* coordinates) or pass 2D *y* coordinate arrays (in which case the plotting method is called for each column of the array)." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Pandas and xarray" - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "The `~proplot.wrappers.standardize_1d` wrapper also integrates \"1d\" plotting methods with pandas `~pandas.DataFrame`\\ s and xarray `~xarray.DataArray`\\ s. When you pass a DataFrame or DataArray to any plotting command, the x-axis label, y-axis label, legend label, colorbar label, and/or title are configured from the metadata. This restores some of the convenience you get with the builtin `pandas `__ and `xarray `__ plotting functions. This feature is *optional*; installation of pandas and xarray are not required." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import xarray as xr\n", - "import numpy as np\n", - "import pandas as pd\n", - "\n", - "# DataArray\n", - "state = np.random.RandomState(51423)\n", - "data = np.sin(np.linspace(0, 2*np.pi, 20))[:, None] \\\n", - " + state.rand(20, 8).cumsum(axis=1)\n", - "da = xr.DataArray(data, dims=('x', 'cat'), coords={\n", - " 'x': xr.DataArray(np.linspace(0, 1, 20), dims=('x',), attrs={'long_name': 'distance', 'units': 'km'}),\n", - " 'cat': xr.DataArray(np.arange(0, 80, 10), dims=('cat',), attrs={'long_name': 'parameter', 'units': 'K'})\n", - "}, name='position series')\n", - "\n", - "# DataFrame\n", - "ts = pd.date_range('1/1/2000', periods=20)\n", - "data = (np.cos(np.linspace(0, 2*np.pi, 20))**4)[:, None] + state.rand(20, 5)**2\n", - "df = pd.DataFrame(data, index=ts, columns=['foo', 'bar', 'baz', 'zap', 'baf'])\n", - "df.name = 'time series'\n", - "df.index.name = 'time (s)'\n", - "df.columns.name = 'columns'" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", - "f, axs = plot.subplots(ncols=2, axwidth=2.2, share=0)\n", - "axs.format(suptitle='Automatic subplot formatting')\n", - "\n", - "# Plot DataArray\n", - "color = plot.shade('light blue', 0.4)\n", - "cycle = plot.Cycle(color, fade=90, space='hpl')\n", - "axs[0].plot(da, cycle=cycle, lw=3, colorbar='ul', colorbar_kw={'locator': 20})\n", - "\n", - "# Plot Dataframe\n", - "color = plot.shade('jade', 0.4)\n", - "cycle = plot.Cycle(color, fade=90, space='hpl')\n", - "axs[1].plot(df, cycle=cycle, lw=3, legend='uc')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Local color cycles" - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "The `~proplot.wrappers.cycle_changer` wrapper is applied to every \"1d\" plotting method. It integrates plotting methods with the `~proplot.styletools.Cycle` constructor function, just like `~proplot.axes.Axes.format` is integrated with the `~proplot.axes.Axes.Locator`, `~proplot.axes.Axes.Formatter`, and `~proplot.axes.Axes.Scale` (see :ref:`X and Y axis settings` for details).\n", - "\n", - "Plotting methods wrapped by `~proplot.wrappers.cycle_changer` accept the `cycle` and `cycle_kw` arguments, which are passed to `~proplot.styletools.Cycle`. The result is used as the property cycler for things like lines and markers. This lets you make fancy new property cycles on-the-fly, e.g. cycles comprised of *colormap* colors with ``cycle='colormap name'`` (see :ref:`Color cycles` for details). It also lets you apply different color cycles to different subplots or plot elements. For more info on property cycles, see `this matplotlib tutorial `__." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Error bars" - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "The `~proplot.wrappers.add_errorbars` wrapper lets you draw error bars on-the-fly by passing certain keyword arguments to `~matplotlib.axes.Axes.plot`, `~matplotlib.axes.Axes.scatter`, `~matplotlib.axes.Axes.bar`, `~matplotlib.axes.Axes.barh`, or `~matplotlib.axes.Axes.violinplot`.\n", - "\n", - "If you pass 2D arrays to these methods with ``means=True`` or ``medians=True``, the means or medians of each column are drawn as points, lines, or bars, and error bars are drawn to represent the spread in each column. `~proplot.wrappers.add_errorbars` lets you draw both thin error \"bars\" with optional whiskers, and thick error \"boxes\" overlayed on top of these bars (this can be used to represent different percentil ranges). Instead of using 2D arrays, you can also pass error bar coordinates *manually* with the `bardata` and `boxdata` keyword arguments. See `~proplot.wrappers.add_errorbars` for details." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", - "import numpy as np\n", - "import pandas as pd\n", - "plot.rc['title.loc'] = 'uc'\n", - "plot.rc['axes.ymargin'] = plot.rc['axes.xmargin'] = 0.05\n", - "state = np.random.RandomState(51423)\n", - "data = state.rand(20, 8).cumsum(axis=0).cumsum(axis=1)[:, ::-1] \\\n", - " + 20*state.normal(size=(20, 8)) + 30\n", - "f, axs = plot.subplots(\n", - " nrows=3, aspect=1.5, axwidth=4,\n", - " share=0, hratios=(2, 1, 1)\n", - ")\n", - "axs.format(suptitle='Error bars with various plotting commands')\n", - "axs[1:].format(xlabel='column number', xticks=1, xgrid=False)\n", - "\n", - "# Asking add_errorbars to calculate bars\n", - "ax = axs[0]\n", - "obj = ax.barh(data, color='red orange', means=True)\n", - "ax.format(title='Column statistics')\n", - "ax.format(ylabel='column number', title='Bar plot', ygrid=False)\n", - "\n", - "# Showing a standard deviation range instead of percentile range\n", - "ax = axs[1]\n", - "ax.scatter(\n", - " data, color='k', marker='x', markersize=50, barcolor='gray5',\n", - " medians=True, barstd=True, barrange=(-1, 1), barzorder=0, boxes=False, capsize=2\n", - ")\n", - "ax.format(title='Scatter plot')\n", - "\n", - "# Supplying error bar data manually\n", - "ax = axs[2]\n", - "boxdata = np.percentile(data, (25, 75), axis=0)\n", - "bardata = np.percentile(data, (5, 95), axis=0)\n", - "ax.plot(\n", - " data.mean(axis=0), boxes=False, marker='o', markersize=5,\n", - " edgecolor='k', color='cerulean', boxdata=boxdata, bardata=bardata\n", - ")\n", - "ax.format(title='Line plot')\n", - "plot.rc.reset()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Bar plots" - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "The `~matplotlib.axes.Axes.bar` and `~matplotlib.axes.Axes.barh` methods are wrapped by `~proplot.wrappers.bar_wrapper`, `~proplot.wrappers.cycle_changer`, and `~proplot.wrappers.standardize_1d`. These wrappers make it easier to generate useful bar plots.\n", - "\n", - "You can now *group* or *stack* columns of data together by passing 2D arrays to `~matplotlib.axes.Axes.bar` or `~matplotlib.axes.Axes.barh`, just like in `pandas`. Also, `~matplotlib.axes.Axes.bar` and `~matplotlib.axes.Axes.barh` now employ \"default\" *x* coordinates if you failed to provide them explicitly, just like `~matplotlib.axes.Axes.plot`. See `~proplot.wrappers.bar_wrapper` for details." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", - "import numpy as np\n", - "import pandas as pd\n", - "plot.rc.titleloc = 'uc'\n", - "plot.rc.margin = 0.05\n", - "f, axs = plot.subplots(nrows=2, aspect=2, axwidth=4, share=0, hratios=(3, 2))\n", - "state = np.random.RandomState(51423)\n", - "data = state.rand(5, 5).cumsum(axis=0).cumsum(axis=1)[:, ::-1]\n", - "data = pd.DataFrame(\n", - " data, columns=pd.Index(np.arange(1, 6), name='column'),\n", - " index=pd.Index(['a', 'b', 'c', 'd', 'e'], name='row idx')\n", - ")\n", - "\n", - "# Side-by-side bars\n", - "ax = axs[0]\n", - "obj = ax.bar(\n", - " data, cycle='Reds', colorbar='ul',\n", - " edgecolor='red9', colorbar_kw={'frameon': False}\n", - ")\n", - "ax.format(\n", - " xlocator=1, xminorlocator=0.5, ytickminor=False,\n", - " title='Side-by-side', suptitle='Bar plot wrapper demo'\n", - ")\n", - "\n", - "# Stacked bars\n", - "ax = axs[1]\n", - "obj = ax.barh(\n", - " data.iloc[::-1, :], cycle='Blues',\n", - " legend='ur', edgecolor='blue9', stacked=True\n", - ")\n", - "ax.format(title='Stacked')\n", - "axs.format(grid=False)\n", - "plot.rc.reset()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Area plots" - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "To make filled \"area\" plots, use the new `~proplot.axes.Axes.area` and `~proplot.axes.Axes.areax` methods. These are simply alises for `~matplotlib.axes.Axes.fill_between` and `~matplotlib.axes.Axes.fill_betweenx`, which are now wrapped by `~proplot.wrappers.fill_between_wrapper` and `~proplot.wrappers.fill_betweenx_wrapper`.\n", - "\n", - "The `~proplot.wrappers.fill_between_wrapper` and `~proplot.wrappers.fill_betweenx_wrapper` wrappers enable stacking and overlaying successive columns of a 2D input array, like in `pandas`. You can also now draw area plots that *change color* when the fill boundaries cross each other by passing ``negpos=True`` to `~matplotlib.axes.Axes.fill_between`. The most common use case for this is highlighting negative and positive areas with different colors, as shown below." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", - "import numpy as np\n", - "plot.rc.margin = 0\n", - "f, axs = plot.subplots(array=[[1, 2], [3, 3]], hratios=(1, 0.8), share=0)\n", - "axs.format(xlabel='xlabel', ylabel='ylabel', suptitle='Area plot demo')\n", - "state = np.random.RandomState(51423)\n", - "data = state.rand(5, 3).cumsum(axis=0)\n", - "cycle = ('gray3', 'gray5', 'gray7')\n", - "\n", - "# Overlaid and stacked area patches\n", - "ax = axs[0]\n", - "ax.area(\n", - " np.arange(5), data, data + state.rand(5)[:, None], cycle=cycle, alpha=0.5,\n", - " legend='uc', legend_kw={'center': True, 'ncols': 2, 'labels': ['z', 'y', 'qqqq']},\n", - ")\n", - "ax.format(title='Fill between columns')\n", - "ax = axs[1]\n", - "ax.area(\n", - " np.arange(5), data, stacked=True, cycle=cycle, alpha=0.8,\n", - " legend='ul', legend_kw={'center': True, 'ncols': 2, 'labels': ['z', 'y', 'qqqq']},\n", - ")\n", - "ax.format(title='Stack between columns')\n", - "\n", - "# Positive and negative color area patches\n", - "ax = axs[2]\n", - "data = 5*(state.rand(20)-0.5)\n", - "ax.area(data, negpos=True, negcolor='blue7', poscolor='red7')\n", - "ax.format(title='Negative and positive data', xlabel='xlabel', ylabel='ylabel')\n", - "axs.format(grid=False)\n", - "plot.rc.reset()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Box and violin plots" - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "The `~matplotlib.axes.Axes.boxplot` and `~matplotlib.axes.Axes.violinplot` methods are now wrapped with `~proplot.wrappers.boxplot_wrapper`, `~proplot.wrappers.violinplot_wrapper`, `~proplot.wrappers.cycle_changer`, and `~proplot.wrappers.standardize_1d`. These wrappers add some useful options and apply aesthetically pleasing default settings. They also automatically apply axis labels based on the `~pandas.DataFrame` column labels or the input *x* coordinate labels." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", - "import numpy as np\n", - "import pandas as pd\n", - "N = 500\n", - "state = np.random.RandomState(51423)\n", - "f, axs = plot.subplots(ncols=2, axwidth=2.5)\n", - "data = state.normal(size=(N, 5)) + 2*(state.rand(N, 5)-0.5)*np.arange(5)\n", - "data = pd.DataFrame(\n", - " data,\n", - " columns=pd.Index(['a', 'b', 'c', 'd', 'e'], name='xlabel')\n", - ")\n", - "axs.format(\n", - " ymargin=0.1, xmargin=0.1, grid=False,\n", - " suptitle='Boxes and violins demo'\n", - ")\n", - "\n", - "# Box plots\n", - "ax = axs[0]\n", - "obj1 = ax.boxplot(\n", - " data, lw=0.7, marker='x', fillcolor='gray5',\n", - " medianlw=1, mediancolor='k'\n", - ")\n", - "ax.format(title='Box plots', titleloc='uc')\n", - "\n", - "# Violin plots\n", - "ax = axs[1]\n", - "obj2 = ax.violinplot(\n", - " data, lw=0.7, fillcolor='gray7',\n", - " points=500, bw_method=0.3, means=True\n", - ")\n", - "ax.format(title='Violin plots', titleloc='uc')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Parametric plots" - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "To make \"parametric\" plots, use the new `~proplot.axes.Axes.parametric` method or pass the `cmap` and `values` keyword arguments to `~matplotlib.axes.Axes.plot`. Parametric plots are `~matplotlib.collections.LineCollections`\\ s that map individual line segments to individual colors, where each segment represents a \"parametric\" coordinate (e.g. time). The parametric coordinates are specified with the `values` keyword argument. See `~proplot.axes.Axes.parametric` for details. As shown below, it is also easy to build colorbars from the `~matplotlib.collections.LineCollection` returned by `~proplot.axes.Axes.parametric`. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", - "import numpy as np\n", - "N = 50\n", - "cmap = 'IceFire'\n", - "values = np.linspace(-N/2, N/2, N)\n", - "f, axs = plot.subplots(\n", - " share=0, ncols=2, wratios=(2, 1),\n", - " axwidth='7cm', aspect=(2, 1)\n", - ")\n", - "axs.format(suptitle='Parametric plots demo')\n", - "\n", - "# Parametric line with smooth gradations\n", - "ax = axs[0]\n", - "state = np.random.RandomState(51423)\n", - "m = ax.plot((state.rand(N) - 0.5).cumsum(), state.rand(N),\n", - " cmap=cmap, values=values, lw=7, extend='both')\n", - "ax.format(\n", - " xlabel='xlabel', ylabel='ylabel',\n", - " title='Line with smooth gradations'\n", - ")\n", - "ax.format(xlim=(-1, 5), ylim=(-0.2, 1.2))\n", - "ax.colorbar(m, loc='b', label='parametric coordinate', locator=5)\n", - "\n", - "# Parametric line with stepped gradations\n", - "N = 12\n", - "ax = axs[1]\n", - "values = np.linspace(-N/2, N/2, N + 1)\n", - "radii = np.linspace(1, 0.2, N + 1)\n", - "angles = np.linspace(0, 4*np.pi, N + 1)\n", - "x = radii*np.cos(1.4*angles)\n", - "y = radii*np.sin(1.4*angles)\n", - "m = ax.plot(x, y, values=values, linewidth=15, interp=False, cmap=cmap)\n", - "ax.format(\n", - " xlim=(-1, 1), ylim=(-1, 1), title='Step gradations',\n", - " xlabel='cosine angle', ylabel='sine angle'\n", - ")\n", - "ax.colorbar(m, loc='b', maxn=10, label=f'parametric coordinate')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Scatter plots" - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "The `~matplotlib.axes.Axes.scatter` method is now wrapped by `~proplot.wrappers.scatter_wrapper`, `~proplot.wrappers.cycle_changer`, and `~proplot.wrappers.standardize_1d`. This means that `~matplotlib.axes.Axes.scatter` now accepts 2D arrays, just like `~matplotlib.axes.Axes.plot`. Also, successive calls to `~matplotlib.axes.Axes.scatter` now use the property cycler properties (e.g. `color`, `marker`, and `markersize`), and `~matplotlib.axes.Axes.scatter` now optionally accepts keywords that look like `~matplotlib.axes.Axes.plot` keywords (e.g. `color` instead of `c` and `markersize` instead of `s`).\n", - "\n", - "We are also considering supporting 2D array input and property cycle iteration for more obscure matplotlib plotting commands like `~matplotlib.axes.Axes.stem`, `~matplotlib.axes.Axes.step`, `~matplotlib.axes.Axes.vlines`, and `~matplotlib.axes.Axes.hlines`. Stay tuned." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", - "import numpy as np\n", - "import pandas as pd\n", - "f, axs = plot.subplots(ncols=2, share=1)\n", - "state = np.random.RandomState(51423)\n", - "x = (state.rand(20)-0).cumsum()\n", - "data = (state.rand(20, 4)-0.5).cumsum(axis=0)\n", - "data = pd.DataFrame(data, columns=pd.Index(['a', 'b', 'c', 'd'], name='label'))\n", - "\n", - "# Scatter plot with property cycler\n", - "ax = axs[0]\n", - "ax.format(title='Extra prop cycle properties', suptitle='Scatter plot demo')\n", - "obj = ax.scatter(\n", - " x, data, legend='ul', cycle='Set2', legend_kw={'ncols': 2},\n", - " cycle_kw={'marker': ['x', 'o', 'x', 'o'], 'markersize': [5, 10, 20, 30]}\n", - ")\n", - "\n", - "# Scatter plot with colormap\n", - "ax = axs[1]\n", - "ax.format(title='Scatter plot with cmap')\n", - "data = state.rand(2, 100)\n", - "obj = ax.scatter(\n", - " *data, color=data.sum(axis=0), size=state.rand(100), smin=3, smax=30,\n", - " marker='o', cmap='plum', colorbar='lr', vmin=0, vmax=2,\n", - " colorbar_kw={'label': 'label', 'locator':0.5}\n", - ")\n", - "axs.format(xlabel='xlabel', ylabel='ylabel')" - ] - } - ], - "metadata": { - "celltoolbar": "Raw Cell Format", - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.3" - }, - "toc": { - "colors": { - "hover_highlight": "#ece260", - "navigate_num": "#000000", - "navigate_text": "#000000", - "running_highlight": "#FF0000", - "selected_highlight": "#fff968", - "sidebar_border": "#ffffff", - "wrapper_background": "#ffffff" - }, - "moveMenuLeft": false, - "nav_menu": { - "height": "12px", - "width": "250px" - }, - "navigate_menu": true, - "number_sections": true, - "sideBar": true, - "threshold": 4, - "toc_cell": false, - "toc_section_display": "block", - "toc_window_display": true, - "widenNotebook": false - }, - "varInspector": { - "cols": { - "lenName": 16, - "lenType": 16, - "lenVar": 40 - }, - "kernels_config": { - "python": { - "delete_cmd_postfix": "", - "delete_cmd_prefix": "del ", - "library": "var_list.py", - "varRefreshCmd": "print(var_dic_list())" - }, - "r": { - "delete_cmd_postfix": ") ", - "delete_cmd_prefix": "rm(", - "library": "var_list.r", - "varRefreshCmd": "cat(var_dic_list()) " - } - }, - "types_to_exclude": [ - "module", - "function", - "builtin_function_or_method", - "instance", - "_Feature" - ], - "window_display": false - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/docs/1dplots.py b/docs/1dplots.py new file mode 100644 index 000000000..1bcb00443 --- /dev/null +++ b/docs/1dplots.py @@ -0,0 +1,618 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.11.4 +# kernelspec: +# display_name: Python 3 +# language: python +# name: python3 +# --- + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _pandas: https://pandas.pydata.org +# +# .. _xarray: http://xarray.pydata.org/en/stable/ +# +# .. _seaborn: https://seaborn.pydata.org +# +# .. _ug_1dplots: +# +# 1D plotting commands +# ==================== +# +# Proplot adds :ref:`several new features ` to matplotlib's +# plotting commands using the intermediate `~proplot.axes.PlotAxes` class. +# For the most part, these additions represent a *superset* of matplotlib -- if +# you are not interested, you can use the plotting commands just like you would +# in matplotlib. This section documents the features added for 1D plotting commands +# like `~proplot.axes.PlotAxes.plot`, `~proplot.axes.PlotAxes.scatter`, +# and `~proplot.axes.PlotAxes.bar`. + + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_1dstd: +# +# Data arguments +# -------------- +# +# The treatment of data arguments passed to the 1D `~proplot.axes.PlotAxes` +# commands is standardized. For each command, you can optionally omit +# the dependent variable coordinates, in which case they are inferred from the data +# (see :ref:`xarray and pandas integration `), or pass +# 2D dependent or independent variable coordinates, in which case the +# plotting command is called for each column of the 2D array(s). If coordinates +# are string labels, they are converted to indices and tick labels using +# `~proplot.ticker.IndexLocator` and `~proplot.ticker.IndexFormatter`. +# If coordinates are descending and the axis limits are unset, the axis +# direction is automatically reversed. All positional arguments can also be +# specified as keyword arguments (see the documentation for each plotting command). +# +# .. note:: +# +# By default, when choosing the *x* or *y* axis limits, +# proplot ignores out-of-bounds data along the other axis if it was explicitly +# fixed by `~matplotlib.axes.Axes.set_xlim` or `~matplotlib.axes.Axes.set_ylim` (or, +# equivalently, by passing `xlim` or `ylim` to `proplot.axes.CartesianAxes.format`). +# This can be useful if you wish to restrict the view along a "dependent" variable +# axis within a large dataset. To disable this feature, pass ``inbounds=False`` to +# the plotting command or set :rcraw:`axes.inbounds` to ``False`` (see also +# the :rcraw:`cmap.inbounds` setting and the :ref:`user guide `). + +# %% +import proplot as pplt +import numpy as np + +N = 5 +state = np.random.RandomState(51423) +with pplt.rc.context({'axes.prop_cycle': pplt.Cycle('Grays', N=N, left=0.3)}): + # Sample data + x = np.linspace(-5, 5, N) + y = state.rand(N, 5) + fig = pplt.figure(share=False, suptitle='Standardized input demonstration') + + # Plot by passing both x and y coordinates + ax = fig.subplot(121, title='Manual x coordinates') + ax.area(x, -1 * y / N, stack=True) + ax.bar(x, y, linewidth=0, alpha=1, width=0.8) + ax.plot(x, y + 1, linewidth=2) + ax.scatter(x, y + 2, marker='s', markersize=5**2) + + # Plot by passing just y coordinates + # Default x coordinates are inferred from DataFrame, + # inferred from DataArray, or set to np.arange(0, y.shape[0]) + ax = fig.subplot(122, title='Auto x coordinates') + ax.area(-1 * y / N, stack=True) + ax.bar(y, linewidth=0, alpha=1) + ax.plot(y + 1, linewidth=2) + ax.scatter(y + 2, marker='s', markersize=5**2) + fig.format(xlabel='xlabel', ylabel='ylabel') + +# %% +import proplot as pplt +import numpy as np + +# Sample data +cycle = pplt.Cycle('davos', right=0.8) +state = np.random.RandomState(51423) +N, M = 400, 20 +xmax = 20 +x = np.linspace(0, 100, N) +y = 100 * (state.rand(N, M) - 0.42).cumsum(axis=0) + +# Plot the data +fig = pplt.figure(refwidth=2.2, share=False) +axs = fig.subplots([[0, 1, 1, 0], [2, 2, 3, 3]], wratios=(2, 1, 1, 2)) +axs[0].axvspan( + 0, xmax, zorder=3, edgecolor='red', facecolor=pplt.set_alpha('red', 0.2), +) +for i, ax in enumerate(axs): + inbounds = i == 1 + title = f'Restricted xlim inbounds={inbounds}' + title += ' (default)' if inbounds else '' + ax.format( + xmax=(None if i == 0 else xmax), + title=('Default xlim' if i == 0 else title), + ) + ax.plot(x, y, cycle=cycle, inbounds=inbounds) +fig.format( + xlabel='xlabel', + ylabel='ylabel', + suptitle='Default ylim restricted to in-bounds data' +) + + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_1dintegration: +# +# Pandas and xarray integration +# ----------------------------- +# +# The 1D `~proplot.axes.PlotAxes` commands recognize `pandas`_ +# and `xarray`_ data structures. If you omit dependent variable coordinates, +# the commands try to infer them from the `pandas.Series`, `pandas.DataFrame`, +# or `xarray.DataArray`. If you did not explicitly set the *x* or *y* axis label +# or :ref:`legend or colorbar ` label(s), the commands +# try to retrieve them from the `pandas.DataFrame` or `xarray.DataArray`. +# The commands also recognize `pint.Quantity` structures and apply +# unit string labels with formatting specified by :rc:`unitformat`. +# +# These features restore some of the convenience you get with the builtin +# `pandas`_ and `xarray`_ plotting functions. They are also *optional* -- +# installation of pandas and xarray are not required to use proplot. The +# automatic labels can be disabled by setting :rcraw:`autoformat` to ``False`` +# or by passing ``autoformat=False`` to any plotting command. +# +# .. note:: +# +# For every plotting command, you can pass a `~xarray.Dataset`, `~pandas.DataFrame`, +# or `dict` to the `data` keyword with strings as data arguments instead of arrays +# -- just like matplotlib. For example, ``ax.plot('y', data=dataset)`` and +# ``ax.plot(y='y', data=dataset)`` are translated to ``ax.plot(dataset['y'])``. +# This is the preferred input style for most `seaborn`_ plotting commands. +# Also, if you pass a `pint.Quantity` or `~xarray.DataArray` +# containing a `pint.Quantity`, proplot will automatically call +# `~pint.UnitRegistry.setup_matplotlib` so that the axes become unit-aware. + +# %% +import xarray as xr +import numpy as np +import pandas as pd + +# DataArray +state = np.random.RandomState(51423) +data = ( + np.sin(np.linspace(0, 2 * np.pi, 20))[:, None] + + state.rand(20, 8).cumsum(axis=1) +) +coords = { + 'x': xr.DataArray( + np.linspace(0, 1, 20), + dims=('x',), + attrs={'long_name': 'distance', 'units': 'km'} + ), + 'num': xr.DataArray( + np.arange(0, 80, 10), + dims=('num',), + attrs={'long_name': 'parameter'} + ) +} +da = xr.DataArray( + data, dims=('x', 'num'), coords=coords, name='energy', attrs={'units': 'kJ'} +) + +# DataFrame +data = ( + (np.cos(np.linspace(0, 2 * np.pi, 20))**4)[:, None] + state.rand(20, 5) ** 2 +) +ts = pd.date_range('1/1/2000', periods=20) +df = pd.DataFrame(data, index=ts, columns=['foo', 'bar', 'baz', 'zap', 'baf']) +df.name = 'data' +df.index.name = 'date' +df.columns.name = 'category' + +# %% +import proplot as pplt +fig = pplt.figure(share=False, suptitle='Automatic subplot formatting') + +# Plot DataArray +cycle = pplt.Cycle('dark blue', space='hpl', N=da.shape[1]) +ax = fig.subplot(121) +ax.scatter(da, cycle=cycle, lw=3, colorbar='t', colorbar_kw={'locator': 20}) + +# Plot Dataframe +cycle = pplt.Cycle('dark green', space='hpl', N=df.shape[1]) +ax = fig.subplot(122) +ax.plot(df, cycle=cycle, lw=3, legend='t', legend_kw={'frame': False}) + + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_apply_cycle: +# +# Changing the property cycle +# --------------------------- +# +# It is often useful to create custom `property cycles +# `__ +# on-the-fly and use different property cycles for different plot elements. +# You can do so using the `cycle` and `cycle_kw` keywords, available +# with most 1D `~proplot.axes.PlotAxes` commands. `cycle` and `cycle_kw` are +# passed to the `~proplot.constructor.Cycle` :ref:`constructor function +# `, and the resulting property cycle is used for the plot. You +# can specify `cycle` once with 2D input data (in which case each column is +# plotted in succession according to the property cycle) or call a plotting +# command multiple times with the same `cycle` argument (the property +# cycle is not reset). You can also disable property cycling with ``cycle=False``, +# ``cycle='none'``, or ``cycle=()`` and re-enable the default property cycle with +# ``cycle=True`` (note that as usual, you can also simply override the property cycle +# with relevant artist keywords like `color`). For more information on property cycles, +# see the :ref:`color cycles section ` and `this matplotlib tutorial +# `__. + +# %% +import proplot as pplt +import numpy as np + +# Sample data +M, N = 50, 5 +state = np.random.RandomState(51423) +data1 = (state.rand(M, N) - 0.48).cumsum(axis=1).cumsum(axis=0) +data2 = (state.rand(M, N) - 0.48).cumsum(axis=1).cumsum(axis=0) * 1.5 +data1 += state.rand(M, N) +data2 += state.rand(M, N) + +with pplt.rc.context({'lines.linewidth': 3}): + # Use property cycle for columns of 2D input data + fig = pplt.figure(share=False) + ax = fig.subplot(121, title='Single plot call') + ax.plot( + 2 * data1 + data2, + cycle='black', # cycle from monochromatic colormap + cycle_kw={'ls': ('-', '--', '-.', ':')} + ) + + # Use property cycle with successive plot() calls + ax = fig.subplot(122, title='Multiple plot calls') + for i in range(data1.shape[1]): + ax.plot(data1[:, i], cycle='Reds', cycle_kw={'N': N, 'left': 0.3}) + for i in range(data1.shape[1]): + ax.plot(data2[:, i], cycle='Blues', cycle_kw={'N': N, 'left': 0.3}) + fig.format( + xlabel='xlabel', ylabel='ylabel', suptitle='On-the-fly property cycles' + ) + + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_lines: +# +# Line plots +# ---------- +# +# Line plots can be drawn with `~proplot.axes.PlotAxes.plot` or +# `~proplot.axes.PlotAxes.plotx` (or their aliases, `~proplot.axes.PlotAxes.line` +# or `~proplot.axes.PlotAxes.linex`). For the ``x`` commands, positional +# arguments are interpreted as *x* coordinates or (*y*, *x*) pairs. This is analogous +# to `~proplot.axes.PlotAxes.barh` and `~proplot.axes.PlotAxes.fill_betweenx`. +# Also, the default *x* bounds for lines drawn with `~proplot.axes.PlotAxes.plot` +# and *y* bounds for lines drawn with `~proplot.axes.PlotAxes.plotx` are now +# "sticky", i.e. there is no padding between the lines and axes edges by default. +# +# Step and stem plots can be drawn with `~proplot.axes.PlotAxes.step`, +# `~proplot.axes.PlotAxes.stepx`, `~proplot.axes.PlotAxes.stem`, and +# `~proplot.axes.PlotAxes.stemx`. Plots of parallel vertical and horizontal +# lines can be drawn with `~proplot.axes.PlotAxes.vlines` and +# `~proplot.axes.PlotAxes.hlines`. You can have different colors for "negative" and +# "positive" lines using ``negpos=True`` (see :ref:`below ` for details). + +# %% +import proplot as pplt +import numpy as np +state = np.random.RandomState(51423) +gs = pplt.GridSpec(nrows=3, ncols=2) +fig = pplt.figure(refwidth=2.2, span=False, share='labels') + +# Vertical vs. horizontal +data = (state.rand(10, 5) - 0.5).cumsum(axis=0) +ax = fig.subplot(gs[0], title='Dependent x-axis') +ax.line(data, lw=2.5, cycle='seaborn') +ax = fig.subplot(gs[1], title='Dependent y-axis') +ax.linex(data, lw=2.5, cycle='seaborn') + +# Vertical lines +gray = 'gray7' +data = state.rand(20) - 0.5 +ax = fig.subplot(gs[2], title='Vertical lines') +ax.area(data, color=gray, alpha=0.2) +ax.vlines(data, negpos=True, lw=2) + +# Horizontal lines +ax = fig.subplot(gs[3], title='Horizontal lines') +ax.areax(data, color=gray, alpha=0.2) +ax.hlines(data, negpos=True, lw=2) + +# Step +ax = fig.subplot(gs[4], title='Step plot') +data = state.rand(20, 4).cumsum(axis=1).cumsum(axis=0) +cycle = ('gray6', 'blue7', 'red7', 'gray4') +ax.step(data, cycle=cycle, labels=list('ABCD'), legend='ul', legend_kw={'ncol': 2}) + +# Stems +ax = fig.subplot(gs[5], title='Stem plot') +data = state.rand(20) +ax.stem(data) +fig.format(suptitle='Line plots demo', xlabel='xlabel', ylabel='ylabel') + + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_scatter: +# +# Scatter plots +# ------------- +# +# The `~proplot.axes.PlotAxes.scatter` command now permits omitting *x* +# coordinates and accepts 2D *y* coordinates, just like `~proplot.axes.PlotAxes.plot`. +# As with `~proplot.axes.PlotAxes.plotx`, the `~proplot.axes.PlotAxes.scatterx` +# command is just like `~proplot.axes.PlotAxes.scatter`, except positional +# arguments are interpreted as *x* coordinates and (*y*, *x*) pairs. +# `~proplot.axes.PlotAxes.scatter` also now accepts keywords +# that look like `~proplot.axes.PlotAxes.plot` keywords (e.g., `color` instead of +# `c` and `markersize` instead of `s`). This way, `~proplot.axes.PlotAxes.scatter` +# can be used to simply "plot markers, not lines" without changing the input +# arguments relative to `~proplot.axes.PlotAxes.plot`. +# +# The property cycler used by `~proplot.axes.PlotAxes.scatter` can be changed +# using the `cycle` keyword argument, and unlike matplotlib it can include +# properties like `marker` and `markersize`. The colormap `cmap` and normalizer +# `norm` used with the optional `c` color array are now passed through the +# `~proplot.constructor.Colormap` and `~proplot.constructor.Norm` constructor +# functions. + +# .. important:: +# +# In matplotlib, arrays passed to the marker size keyword `s` always represent the +# area in units ``points ** 2``. In proplot, arrays passed to `s` are scaled so +# that the minimum data value has the area ``1`` while the maximum data value +# has the area :rcraw:`lines.markersize` squared. These minimum and maximum marker +# sizes can also be specified manually with the `smin` and `smax` keywords, +# analogous to `vmin` and `vmax` used to scale the color array `c`. This feature +# can be disabled by passing ``absolute_size=True`` to `~proplot.axes.Axes.scatter` +# or `~proplot.axes.Axes.scatterx`. This is done automatically when `seaborn`_ +# calls `~proplot.axes.Axes.scatter` internally. + +# %% +import proplot as pplt +import numpy as np +import pandas as pd + +# Sample data +state = np.random.RandomState(51423) +x = (state.rand(20) - 0).cumsum() +data = (state.rand(20, 4) - 0.5).cumsum(axis=0) +data = pd.DataFrame(data, columns=pd.Index(['a', 'b', 'c', 'd'], name='label')) + +# Figure +gs = pplt.GridSpec(ncols=2, nrows=2) +fig = pplt.figure(refwidth=2.2, share='labels', span=False) + +# Vertical vs. horizontal +ax = fig.subplot(gs[0], title='Dependent x-axis') +ax.scatter(data, cycle='538') +ax = fig.subplot(gs[1], title='Dependent y-axis') +ax.scatterx(data, cycle='538') + +# Scatter plot with property cycler +ax = fig.subplot(gs[2], title='With property cycle') +obj = ax.scatter( + x, data, legend='ul', legend_kw={'ncols': 2}, + cycle='Set2', cycle_kw={'m': ['x', 'o', 'x', 'o'], 'ms': [5, 10, 20, 30]} +) + +# Scatter plot with colormap +ax = fig.subplot(gs[3], title='With colormap') +data = state.rand(2, 100) +obj = ax.scatter( + *data, + s=state.rand(100), smin=6, smax=60, marker='o', + c=data.sum(axis=0), cmap='maroon', + colorbar='lr', colorbar_kw={'label': 'label'}, +) +fig.format(suptitle='Scatter plot demo', xlabel='xlabel', ylabel='ylabel') + + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_parametric: +# +# Parametric plots +# ---------------- +# +# Parametric plots can be drawn using the new `~proplot.axes.PlotAxes.parametric` +# command. This creates `~matplotlib.collections.LineCollection`\ s that map +# individual line segments to individual colors, where each segment represents a +# "parametric" coordinate (e.g., time). The parametric coordinates are specified with +# a third positional argument or with the keywords `c`, `color`, `colors` or `values`. +# Representing parametric coordinates with colors instead of text labels can be +# cleaner. The below example makes a simple `~proplot.axes.PlotAxes.parametric` +# plot with a colorbar indicating the parametric coordinate. + +# %% +import proplot as pplt +import numpy as np +import pandas as pd +gs = pplt.GridSpec(ncols=2, wratios=(2, 1)) +fig = pplt.figure(figwidth='16cm', refaspect=(2, 1), share=False) +fig.format(suptitle='Parametric plots demo') +cmap = 'IceFire' + +# Sample data +state = np.random.RandomState(51423) +N = 50 +x = (state.rand(N) - 0.52).cumsum() +y = state.rand(N) +c = np.linspace(-N / 2, N / 2, N) # color values +c = pd.Series(c, name='parametric coordinate') + +# Parametric line with smooth gradations +ax = fig.subplot(gs[0]) +m = ax.parametric( + x, y, c, interp=10, capstyle='round', joinstyle='round', + lw=7, cmap=cmap, colorbar='b', colorbar_kw={'locator': 5} +) +ax.format(xlabel='xlabel', ylabel='ylabel', title='Line with smooth gradations') + +# Sample data +N = 12 +radii = np.linspace(1, 0.2, N + 1) +angles = np.linspace(0, 4 * np.pi, N + 1) +x = radii * np.cos(1.4 * angles) +y = radii * np.sin(1.4 * angles) +c = np.linspace(-N / 2, N / 2, N + 1) + +# Parametric line with stepped gradations +ax = fig.subplot(gs[1]) +m = ax.parametric(x, y, c, cmap=cmap, lw=15) +ax.format( + xlim=(-1, 1), ylim=(-1, 1), title='Step gradations', + xlabel='cosine angle', ylabel='sine angle' +) +ax.colorbar(m, loc='b', locator=2, label='parametric coordinate') + + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_bar: +# +# Bar plots and area plots +# ------------------------ +# +# The `~proplot.axes.PlotAxes.bar` and `~proplot.axes.PlotAxes.barh` commands +# apply default *x* or *y* coordinates if you failed to provide them explicitly +# and can *group* or *stack* successive columns of data if you pass 2D arrays instead +# of 1D arrays -- just like `pandas`_. When bars are grouped, their widths and +# positions are adjusted according to the number of bars in the group. Grouping +# is the default behavior and stacking can be enabled with ``stack=True`` +# or ``stacked=True``. +# +# The `~proplot.axes.PlotAxes.fill_between` and `~proplot.axes.PlotAxes.fill_betweenx` +# commands have the new shorthands `~proplot.axes.PlotAxes.area` +# and `~proplot.axes.PlotAxes.areax`. Similar to `~proplot.axes.PlotAxes.bar` and +# `~proplot.axes.PlotAxes.barh`, they apply default *x* coordinates if you failed +# to provide them explicitly, and can *overlay* or *stack* successive columns of +# data if you pass 2D arrays instead of 1D arrays -- just like `pandas`_. Overlaying +# is the default behavior but stacking can be enabled with ``stack=True`` or +# ``stacked=True``. Also note the default *x* bounds for shading drawn with +# `~proplot.axes.PlotAxes.area` and *y* bounds for shading drawn with +# `~proplot.axes.PlotAxes.areax` is now "sticky", i.e. there is no padding +# between the shading and axes edges by default. + +# .. important:: +# +# In matplotlib, bar widths for horizontal `~matplotlib.axes.Axes.barh` plots +# are expressed with the `height` keyword. In proplot, bar widths are always +# expressed with the `width` keyword. Note that bar widths can also be passed +# as a third positional argument. +# Additionally, matplotlib bar widths are always expressed in data units, +# while proplot bar widths are expressed in step size-relative units by +# default. For example, ``width=1`` with a dependent coordinate step +# size of ``2`` fills 100% of the space between each bar rather than 50%. This +# can be disabled by passing ``absolute_width=True`` to `~proplot.axes.Axes.bar` +# or `~proplot.axes.Axes.barh`. This is done automatically when `seaborn`_ calls +# `~proplot.axes.Axes.bar` or `~proplot.axes.Axes.barh` internally. + +# %% +import proplot as pplt +import numpy as np +import pandas as pd + +# Sample data +state = np.random.RandomState(51423) +data = state.rand(5, 5).cumsum(axis=0).cumsum(axis=1)[:, ::-1] +data = pd.DataFrame( + data, columns=pd.Index(np.arange(1, 6), name='column'), + index=pd.Index(['a', 'b', 'c', 'd', 'e'], name='row idx') +) + +# Figure +pplt.rc.abc = 'a.' +pplt.rc.titleloc = 'l' +gs = pplt.GridSpec(nrows=2, hratios=(3, 2)) +fig = pplt.figure(refaspect=2, refwidth=4.8, share=False) + +# Side-by-side bars +ax = fig.subplot(gs[0], title='Side-by-side') +obj = ax.bar( + data, cycle='Reds', edgecolor='red9', colorbar='ul', colorbar_kw={'frameon': False} +) +ax.format(xlocator=1, xminorlocator=0.5, ytickminor=False) + +# Stacked bars +ax = fig.subplot(gs[1], title='Stacked') +obj = ax.barh( + data.iloc[::-1, :], cycle='Blues', edgecolor='blue9', legend='ur', stack=True, +) +fig.format(grid=False, suptitle='Bar plot demo') +pplt.rc.reset() + +# %% +import proplot as pplt +import numpy as np + +# Sample data +state = np.random.RandomState(51423) +data = state.rand(5, 3).cumsum(axis=0) +cycle = ('gray3', 'gray5', 'gray7') + +# Figure +pplt.rc.abc = 'a.' +pplt.rc.titleloc = 'l' +fig = pplt.figure(refwidth=2.3, share=False) + +# Overlaid area patches +ax = fig.subplot(121, title='Fill between columns') +ax.area( + np.arange(5), data, data + state.rand(5)[:, None], cycle=cycle, alpha=0.7, + legend='uc', legend_kw={'center': True, 'ncols': 2, 'labels': ['z', 'y', 'qqqq']}, +) + +# Stacked area patches +ax = fig.subplot(122, title='Stack between columns') +ax.area( + np.arange(5), data, stack=True, cycle=cycle, alpha=0.8, + legend='ul', legend_kw={'center': True, 'ncols': 2, 'labels': ['z', 'y', 'qqqq']}, +) +fig.format(grid=False, xlabel='xlabel', ylabel='ylabel', suptitle='Area plot demo') +pplt.rc.reset() + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_negpos: +# +# Negative and positive colors +# ---------------------------- +# +# You can use different colors for "negative" and +# "positive" data by passing ``negpos=True`` to any of the +# `~proplot.axes.PlotAxes.fill_between`, `~proplot.axes.PlotAxes.fill_betweenx` +# (shorthands `~proplot.axes.PlotAxes.area`, `~proplot.axes.PlotAxes.areax`), +# `~proplot.axes.PlotAxes.vlines`, `~proplot.axes.PlotAxes.hlines`, +# `~proplot.axes.PlotAxes.bar`, or `~proplot.axes.PlotAxes.barh` commands. +# The default negative and positive colors are controlled with :rcraw:`negcolor` and +# :rcraw:`poscolor` but the colors can be modified for particular plots by passing +# ``negcolor=color`` and ``poscolor=color`` to the `~proplot.axes.PlotAxes` commands. + +# %% +import proplot as pplt +import numpy as np + +# Sample data +state = np.random.RandomState(51423) +data = 4 * (state.rand(40) - 0.5) + +# Figure +pplt.rc.abc = 'a.' +pplt.rc.titleloc = 'l' +fig, axs = pplt.subplots(nrows=3, refaspect=2, figwidth=5) +axs.format( + xmargin=0, xlabel='xlabel', ylabel='ylabel', grid=True, + suptitle='Positive and negative colors demo', +) +for ax in axs: + ax.axhline(0, color='k', linewidth=1) # zero line + +# Line plot +ax = axs[0] +ax.vlines(data, linewidth=3, negpos=True) +ax.format(title='Line plot') + +# Bar plot +ax = axs[1] +ax.bar(data, width=1, negpos=True, edgecolor='k') +ax.format(title='Bar plot') + +# Area plot +ax = axs[2] +ax.area(data, negpos=True, lw=0.5, edgecolor='k') +ax.format(title='Area plot') + +# Reset title styles changed above +pplt.rc.reset() diff --git a/docs/2dplots.ipynb b/docs/2dplots.ipynb deleted file mode 100644 index 191851691..000000000 --- a/docs/2dplots.ipynb +++ /dev/null @@ -1,443 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# 2d plotting" - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "ProPlot adds new features to various `~matplotlib.axes.Axes` plotting methods using a set of \"wrapper\" functions. When a plotting method like `~matplotlib.axes.Axes.contourf` is \"wrapped\" by one of these functions, it accepts the same parameters as the \"wrapper\". These features are a strict *superset* of the matplotlib API; if you want, you can use the plotting methods exactly as you always have.\n", - "\n", - "This section documents the features added by \"wrapper\" functions to 2d plotting commands like `~matplotlib.axes.Axes.contour`, `~matplotlib.axes.Axes.contourf`, `~matplotlib.axes.Axes.pcolor`, and `~matplotlib.axes.Axes.pcolormesh`." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Standardized input" - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "The `~proplot.wrappers.standardize_2d` wrapper is used to standardize the positional arguments for \"2d\" plotting methods.\n", - "It allows you to optionally omit *x* and *y* coordinates, in which case they are infered from the data array; guesses coordinate *edges* for `~matplotlib.axes.Axes.pcolor` and `~matplotlib.axes.Axes.pcolormesh` plots when you supply coordinate *centers*; and optionally enforces global data coverage when plotting in map projections (see :ref:`Plotting geophysical data` for details)." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Pandas and xarray" - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "The `~proplot.wrappers.standardize_2d` wrapper also integrates \"2d\" plotting methods with pandas `~pandas.DataFrame`\\ s and xarray `~xarray.DataArray`\\ s. When you pass a DataFrame or DataArray to any plotting command, the x-axis label, y-axis label, legend label, colorbar label, and/or title are configured from the metadata. This restores some of the convenience you get with the builtin `pandas `__ and `xarray `__ plotting functions. This feature is *optional*; installation of pandas and xarray are not required." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import xarray as xr\n", - "import numpy as np\n", - "import pandas as pd\n", - "from string import ascii_lowercase\n", - "\n", - "# DataArray\n", - "state = np.random.RandomState(51423)\n", - "data = 50*(np.sin(np.linspace(0, 2*np.pi, 20) + 0)**2) * \\\n", - " np.cos(np.linspace(0, np.pi, 20) + np.pi/2)[:, None]**2\n", - "da = xr.DataArray(data, dims=('plev', 'lat'), coords={\n", - " 'plev': xr.DataArray(np.linspace(1000, 0, 20), dims=('plev',), attrs={'long_name': 'pressure', 'units': 'hPa'}),\n", - " 'lat': xr.DataArray(np.linspace(-90, 90, 20), dims=('lat',), attrs={'units': 'degN'}),\n", - "}, name='u', attrs={'long_name': 'zonal wind', 'units': 'm/s'})\n", - "\n", - "# DataFrame\n", - "data = state.rand(20, 20)\n", - "df = pd.DataFrame(\n", - " data.cumsum(axis=0).cumsum(axis=1),\n", - " index=[*'JFMAMJJASONDJFMAMJJA']\n", - ")\n", - "df.name = 'temporal data'\n", - "df.index.name = 'index'\n", - "df.columns.name = 'time (days)'" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", - "f, axs = plot.subplots(nrows=2, axwidth=2.2, share=0)\n", - "axs.format(collabels=['Automatic subplot formatting'])\n", - "\n", - "# Plot DataArray\n", - "axs[0].contourf(\n", - " da, cmap='Greens', cmap_kw={'left': 0.05}, colorbar='l', linewidth=0.7, color='gray7'\n", - ")\n", - "axs[0].format(yreverse=True)\n", - "\n", - "# Plot DataFrame\n", - "axs[1].contourf(\n", - " df, cmap='Blues', colorbar='r', linewidth=0.7, color='gray7'\n", - ")\n", - "axs[1].format(xtickminor=False)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Colormaps and normalizers" - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "The `~proplot.wrappers.cmap_changer` wrapper is applied to every \"2d\" plotting method that accepts a `cmap` argument. It integrates plotting methods with the `~proplot.styletools.Colormap` and `~proplot.styletools.Norm` constructor functions, just like `~proplot.axes.Axes.format` uses the `~proplot.axes.Axes.Locator`, `~proplot.axes.Axes.Formatter`, and `~proplot.axes.Axes.Scale` (see :ref:`X and Y axis settings` for details).\n", - "\n", - "Plotting methods wrapped by `~proplot.wrappers.cmap_changer` accept the `cmap` and `cmap_kw` keyword arguments, which are passed to `~proplot.styletools.Colormap`. The result is used as the colormap for your plot. This lets you draw fancy new colormaps on-the-fly, e.g. *monochromatic* colormaps with ``cmap='color name'`` (see :ref:`Colormaps` for details).\n", - "\n", - "These plotting methods also accept the `norm` and `norm_kw` keyword arguments, which are passed to `~proplot.styletools.Norm`. The result is used as the colormap normalizer for your plot. For more info on colormap normalization, see `this matplotlib tutorial `__." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Discrete colormap levels" - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "The `~proplot.wrappers.cmap_changer` wrapper also applies the `~proplot.styletools.BinNorm` normalizer to every colormap plot. `~proplot.styletools.BinNorm` converts data values to colormap colors by (1) transforming data using an arbitrary *continuous* normalizer (e.g. `~matplotlib.colors.LogNorm`), then (2) mapping the normalized data to *discrete* colormap levels (just like `~matplotlib.axes.Axes.BoundaryNorm`).\n", - "\n", - "By applying `~proplot.styletools.BinNorm` to every plot, ProPlot permits distinct \"levels\" even for commands like `~matplotlib.axes.Axes.pcolor` and `~matplotlib.axes.Axes.pcolormesh`. Distinct levels can help the reader discern exact numeric values and tends to reveal qualitative structure in the figure. They are also critical for users that would *prefer* contours, but have complex 2D coordinate matrices that trip up the contouring algorithm.\n", - "\n", - "`~proplot.styletools.BinNorm` also fixes the colormap end-colors by ensuring the following conditions are met (this may seem nitpicky, but it is crucial for plots with very few levels):\n", - "\n", - "#. All colormaps always span the *entire color range*, independent of the `extend` setting.\n", - "#. Cyclic colormaps always have *distinct color levels* on either end of the colorbar." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", - "import numpy as np\n", - "\n", - "# Pcolor plot with and without distinct levels\n", - "f, axs = plot.subplots(ncols=2, axwidth=2)\n", - "state = np.random.RandomState(51423)\n", - "data = (state.normal(0, 1, size=(33, 33))).cumsum(axis=0).cumsum(axis=1)\n", - "axs.format(suptitle='Pcolor plot with levels')\n", - "for ax, n, mode, side in zip(axs, (200, 10), ('Ambiguous', 'Discernible'), 'lr'):\n", - " ax.pcolor(data, cmap='spectral', N=n, symmetric=True, colorbar=side)\n", - " ax.format(title=f'{mode} level boundaries', yformatter='null')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", - "import numpy as np\n", - "f, axs = plot.subplots(\n", - " [[0, 0, 1, 1, 0, 0], [2, 3, 3, 4, 4, 5]],\n", - " wratios=(1.5, 0.5, 1, 1, 0.5, 1.5), axwidth=1.7, ref=1, right='2em'\n", - ")\n", - "axs.format(suptitle='BinNorm color-range standardization')\n", - "levels = plot.arange(0, 360, 45)\n", - "state = np.random.RandomState(51423)\n", - "data = (20*(state.rand(20, 20) - 0.4).cumsum(axis=0).cumsum(axis=1)) % 360\n", - "\n", - "# Cyclic colorbar with distinct end colors\n", - "ax = axs[0]\n", - "ax.pcolormesh(\n", - " data, levels=levels, cmap='phase', extend='neither',\n", - " colorbar='b', colorbar_kw={'locator': 90}\n", - ")\n", - "ax.format(title='cyclic colormap\\nwith distinct end colors')\n", - "\n", - "# Colorbars with different extend values\n", - "for ax, extend in zip(axs[1:], ('min', 'max', 'neither', 'both')):\n", - " ax.pcolormesh(\n", - " data[:, :10], levels=levels, cmap='oxy',\n", - " extend=extend, colorbar='b', colorbar_kw={'locator': 90}\n", - " )\n", - " ax.format(title=f'extend={extend!r}')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## New colormap normalizers" - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "ProPlot introduces the following useful new `~matplotlib.colors.Normalize` classes:\n", - "\n", - "* `~proplot.styletools.LinearSegmentedNorm` provides even color gradations *with respect to index* for arbitrary monotonically increasing `level` lists. This is *automatically applied* if you pass unevenly spaced `levels` to a plotting command, or can be manually applied using e.g. ``norm='segments'``.\n", - "* `~proplot.styletools.MidpointNorm` is similar to matplotlib's `~matplotlib.colors.DivergingNorm`. It warps your values so that the colormap midpoint lies on some *central* data value (usually ``0``), even if `vmin`, `vmax`, or `levels` are asymmetric with respect to the central value. This can be manually applied using e.g. ``norm='midpoint'``." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", - "import numpy as np\n", - "\n", - "# Linear segmented norm\n", - "state = np.random.RandomState(51423)\n", - "data = 10**(2*state.rand(20, 20).cumsum(axis=0)/7)\n", - "f, axs = plot.subplots(ncols=2, axwidth=2.5, aspect=1.5)\n", - "ticks = [5, 10, 20, 50, 100, 200, 500, 1000]\n", - "for i, (norm, title) in enumerate(zip(('linear', 'segments'), ('Linear normalizer', 'LinearSegmentedNorm'))):\n", - " m = axs[i].contourf(\n", - " data, levels=ticks, extend='both',\n", - " cmap='Mako', norm=norm,\n", - " colorbar='b', colorbar_kw={'ticks': ticks},\n", - " )\n", - " axs[i].format(title=title)\n", - "axs.format(suptitle='Segmented normalizer demo')\n", - "\n", - "# Midpoint norm\n", - "data1 = (state.rand(20, 20) - 0.43).cumsum(axis=0)\n", - "data2 = (state.rand(20, 20) - 0.57).cumsum(axis=0)\n", - "f, axs = plot.subplots(ncols=2, axwidth=2.5, aspect=1.5)\n", - "cmap = plot.Colormap('DryWet', cut=0.1)\n", - "axs.format(suptitle='Midpoint normalizer demo')\n", - "for ax, data, mode in zip(axs, (data1, data2), ('positive', 'negative')):\n", - " m = ax.contourf(data, norm='midpoint', cmap=cmap)\n", - " ax.colorbar(m, loc='b', locator=1, minorlocator=0.25)\n", - " ax.format(title=f'Skewed {mode} data')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Pcolor and contour labels" - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "The `~proplot.wrappers.cmap_changer` wrapper also allows you to quickly add *labels* to `~proplot.axes.Axes.heatmap`, `~matplotlib.axes.Axes.pcolor`, `~matplotlib.axes.Axes.pcolormesh`, `~matplotlib.axes.Axes.contour`, and `~matplotlib.axes.Axes.contourf` plots by simply using ``labels=True``. Critically, the label text is colored black or white depending on the luminance of the underlying grid box or filled contour.\n", - "\n", - "`~proplot.wrappers.cmap_changer` draws contour labels with `~matplotlib.axes.Axes.clabel` and grid box labels with `~matplotlib.axes.Axes.text`. You can pass keyword arguments to these functions using the `labels_kw` dictionary keyword argument, and change the label precision with the `precision` keyword argument. See `~proplot.wrappers.cmap_changer` for details." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", - "import pandas as pd\n", - "import numpy as np\n", - "f, axs = plot.subplots(\n", - " [[1, 1, 2, 2], [0, 3, 3, 0]],\n", - " axwidth=2.2, share=1, span=False, hratios=(1, 0.9)\n", - ")\n", - "state = np.random.RandomState(51423)\n", - "data = state.rand(6, 6)\n", - "data = pd.DataFrame(data, index=pd.Index(['a', 'b', 'c', 'd', 'e', 'f']))\n", - "axs.format(xlabel='xlabel', ylabel='ylabel', suptitle='Labels demo')\n", - "\n", - "# Heatmap with labeled boxes\n", - "ax = axs[0]\n", - "m = ax.heatmap(\n", - " data, cmap='rocket', labels=True,\n", - " precision=2, labels_kw={'weight': 'bold'}\n", - ")\n", - "ax.format(title='Heatmap plot with labels')\n", - "\n", - "# Filled contours with labels\n", - "ax = axs[1]\n", - "m = ax.contourf(\n", - " data.cumsum(axis=0), labels=True,\n", - " cmap='rocket', labels_kw={'weight': 'bold'}\n", - ")\n", - "ax.format(title='Filled contour plot with labels')\n", - "\n", - "# Line contours with labels\n", - "ax = axs[2]\n", - "ax.contour(\n", - " data.cumsum(axis=1) - 2, color='gray8',\n", - " labels=True, lw=2, labels_kw={'weight': 'bold'}\n", - ")\n", - "ax.format(title='Line contour plot with labels')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Heatmap plots" - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "The new `~proplot.axes.Axes.heatmap` command simply calls `~matplotlib.axes.Axes.pcolormesh` and applies default formatting that is suitable for heatmaps: no gridlines, no minor ticks, and major ticks at the center of each box. Among other things, this is useful for displaying covariance and correlation matrices. See the below example." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", - "import numpy as np\n", - "import pandas as pd\n", - "\n", - "# Covariance data\n", - "state = np.random.RandomState(51423)\n", - "data = state.normal(size=(10, 10)).cumsum(axis=0)\n", - "data = (data - data.mean(axis=0)) / data.std(axis=0)\n", - "data = (data.T @ data) / data.shape[0]\n", - "data[np.tril_indices(data.shape[0], -1)] = np.nan # fill half with empty boxes\n", - "data = pd.DataFrame(data, columns=list('abcdefghij'), index=list('abcdefghij'))\n", - "\n", - "# Covariance matrix plot\n", - "f, ax = plot.subplots(axwidth=4.5)\n", - "m = ax.heatmap(\n", - " data, cmap='ColdHot', vmin=-1, vmax=1, N=100,\n", - " lw=0.5, edgecolor='k', labels=True, labels_kw={'weight': 'bold'},\n", - " clip_on=False, # turn off clipping so box edges are not cut in half\n", - ")\n", - "ax.format(\n", - " suptitle='Heatmap demo', title='Table of correlation coefficients', alpha=0, linewidth=0,\n", - " xloc='top', yloc='right', yreverse=True, ticklabelweight='bold',\n", - " ytickmajorpad=4, # the ytick.major.pad rc setting; adds extra space\n", - ")" - ] - } - ], - "metadata": { - "celltoolbar": "Raw Cell Format", - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.3" - }, - "toc": { - "colors": { - "hover_highlight": "#ece260", - "navigate_num": "#000000", - "navigate_text": "#000000", - "running_highlight": "#FF0000", - "selected_highlight": "#fff968", - "sidebar_border": "#ffffff", - "wrapper_background": "#ffffff" - }, - "moveMenuLeft": false, - "nav_menu": { - "height": "156px", - "width": "252px" - }, - "navigate_menu": true, - "number_sections": true, - "sideBar": true, - "threshold": 4, - "toc_cell": false, - "toc_section_display": "block", - "toc_window_display": true, - "widenNotebook": false - }, - "varInspector": { - "cols": { - "lenName": 16, - "lenType": 16, - "lenVar": 40 - }, - "kernels_config": { - "python": { - "delete_cmd_postfix": "", - "delete_cmd_prefix": "del ", - "library": "var_list.py", - "varRefreshCmd": "print(var_dic_list())" - }, - "r": { - "delete_cmd_postfix": ") ", - "delete_cmd_prefix": "rm(", - "library": "var_list.r", - "varRefreshCmd": "cat(var_dic_list()) " - } - }, - "types_to_exclude": [ - "module", - "function", - "builtin_function_or_method", - "instance", - "_Feature" - ], - "window_display": false - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/docs/2dplots.py b/docs/2dplots.py new file mode 100644 index 000000000..07ff162ce --- /dev/null +++ b/docs/2dplots.py @@ -0,0 +1,686 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.11.4 +# kernelspec: +# display_name: Python 3 +# language: python +# name: python3 +# --- + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _pandas: https://pandas.pydata.org +# +# .. _xarray: http://xarray.pydata.org/en/stable/ +# +# .. _seaborn: https://seaborn.pydata.org +# +# .. _ug_2dplots: +# +# 2D plotting commands +# ==================== +# +# Proplot adds :ref:`several new features ` to matplotlib's +# plotting commands using the intermediate `~proplot.axes.PlotAxes` class. +# For the most part, these additions represent a *superset* of matplotlib -- if +# you are not interested, you can use the plotting commands just like you would +# in matplotlib. This section documents the features added for 2D plotting commands +# like `~proplot.axes.PlotAxes.contour`, `~proplot.axes.PlotAxes.pcolor`, +# and `~proplot.axes.PlotAxes.imshow`. +# +# .. important:: +# +# By default, proplot automatically adjusts the width of +# `~proplot.axes.PlotAxes.contourf` and `~proplot.axes.PlotAxes.pcolor` edges +# to eliminate the appearance of `"white lines" in saved vector graphic files +# `__. However, this can significantly +# slow down the drawing time for large datasets. To disable this feature, +# pass ``edgefix=False`` to the relevant `~proplot.axes.PlotAxes` command, +# or set :rcraw:`edgefix` to ``False`` to disable globally. + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_2dstd: +# +# Data arguments +# -------------- +# +# The treatment of data arguments passed to the 2D `~proplot.axes.PlotAxes` +# commands is standardized. For each command, you can optionally omit the *x* +# and *y* coordinates, in which case they are inferred from the data +# (see :ref:`xarray and pandas integration `). If coordinates +# are string labels, they are converted to indices and tick labels using +# `~proplot.ticker.IndexLocator` and `~proplot.ticker.IndexFormatter`. +# If coordinates are descending and the axis limits are unset, the axis +# direction is automatically reversed. If coordinate *centers* are passed to commands +# like `~proplot.axes.PlotAxes.pcolor` and `~proplot.axes.PlotAxes.pcolormesh`, they +# are automatically converted to edges using `~proplot.utils.edges` or +# `~proplot.utils.edges2d`, and if coordinate *edges* are passed to commands like +# `~proplot.axes.PlotAxes.contour` and `~proplot.axes.PlotAxes.contourf`, they are +# automatically converted to centers (notice the locations of the rectangle edges +# in the ``pcolor`` plots below). All positional arguments can also be specified +# as keyword arguments (see the documentation for each plotting command). +# +# .. note:: +# +# By default, when choosing the colormap :ref:`normalization +# range `, proplot ignores data outside the *x* or *y* axis +# limits if they were previously fixed by `~matplotlib.axes.Axes.set_xlim` or +# `~matplotlib.axes.Axes.set_ylim` (or, equivalently, by passing `xlim` or +# `ylim` to `proplot.axes.CartesianAxes.format`). This can be useful if you +# wish to restrict the view along the *x* or *y* axis within a large dataset. +# To disable this feature, pass ``inbounds=False`` to the plotting command or +# set :rcraw:`cmap.inbounds` to ``False`` (see also the :rcraw:`axes.inbounds` +# setting and the :ref:`user guide `). + +# %% +import proplot as pplt +import numpy as np + +# Sample data +state = np.random.RandomState(51423) +x = y = np.array([-10, -5, 0, 5, 10]) +xedges = pplt.edges(x) +yedges = pplt.edges(y) +data = state.rand(y.size, x.size) # "center" coordinates +lim = (np.min(xedges), np.max(xedges)) + +with pplt.rc.context({'cmap': 'Grays', 'cmap.levels': 21}): + # Figure + fig = pplt.figure(refwidth=2.3, share=False) + axs = fig.subplots(ncols=2, nrows=2) + axs.format( + xlabel='xlabel', ylabel='ylabel', + xlim=lim, ylim=lim, xlocator=5, ylocator=5, + suptitle='Standardized input demonstration', + toplabels=('Coordinate centers', 'Coordinate edges'), + ) + + # Plot using both centers and edges as coordinates + axs[0].pcolormesh(x, y, data) + axs[1].pcolormesh(xedges, yedges, data) + axs[2].contourf(x, y, data) + axs[3].contourf(xedges, yedges, data) + +# %% +import proplot as pplt +import numpy as np + +# Sample data +cmap = 'turku_r' +state = np.random.RandomState(51423) +N = 80 +x = y = np.arange(N + 1) +data = 10 + (state.normal(0, 3, size=(N, N))).cumsum(axis=0).cumsum(axis=1) +xlim = ylim = (0, 25) + +# Plot the data +fig, axs = pplt.subplots( + [[0, 1, 1, 0], [2, 2, 3, 3]], wratios=(1.3, 1, 1, 1.3), span=False, refwidth=2.2, +) +axs[0].fill_between( + xlim, *ylim, zorder=3, edgecolor='red', facecolor=pplt.set_alpha('red', 0.2), +) +for i, ax in enumerate(axs): + inbounds = i == 1 + title = f'Restricted lims inbounds={inbounds}' + title += ' (default)' if inbounds else '' + ax.format( + xlim=(None if i == 0 else xlim), + ylim=(None if i == 0 else ylim), + title=('Default axis limits' if i == 0 else title), + ) + ax.pcolor(x, y, data, cmap=cmap, inbounds=inbounds) +fig.format( + xlabel='xlabel', + ylabel='ylabel', + suptitle='Default vmin/vmax restricted to in-bounds data' +) + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_2dintegration: +# +# Pandas and xarray integration +# ----------------------------- +# +# The 2D `~proplot.axes.PlotAxes` commands recognize `pandas`_ +# and `xarray`_ data structures. If you omit *x* and *y* coordinates, +# the commands try to infer them from the `pandas.DataFrame` or +# `xarray.DataArray`. If you did not explicitly set the *x* or *y* axis label +# or :ref:`legend or colorbar ` label(s), the commands +# try to retrieve them from the `pandas.DataFrame` or `xarray.DataArray`. +# The commands also recognize `pint.Quantity` structures and apply +# unit string labels with formatting specified by :rc:`unitformat`. +# +# These features restore some of the convenience you get with the builtin +# `pandas`_ and `xarray`_ plotting functions. They are also *optional* -- +# installation of pandas and xarray are not required to use proplot. The +# automatic labels can be disabled by setting :rcraw:`autoformat` to ``False`` +# or by passing ``autoformat=False`` to any plotting command. +# +# .. note:: +# +# For every plotting command, you can pass a `~xarray.Dataset`, `~pandas.DataFrame`, +# or `dict` to the `data` keyword with strings as data arguments instead of arrays +# -- just like matplotlib. For example, ``ax.plot('y', data=dataset)`` and +# ``ax.plot(y='y', data=dataset)`` are translated to ``ax.plot(dataset['y'])``. +# This is the preferred input style for most `seaborn`_ plotting commands. +# Also, if you pass a `pint.Quantity` or `~xarray.DataArray` +# containing a `pint.Quantity`, proplot will automatically call +# `~pint.UnitRegistry.setup_matplotlib` so that the axes become unit-aware. + +# %% +import xarray as xr +import numpy as np +import pandas as pd + +# DataArray +state = np.random.RandomState(51423) +linspace = np.linspace(0, np.pi, 20) +data = 50 * state.normal(1, 0.2, size=(20, 20)) * ( + np.sin(linspace * 2) ** 2 + * np.cos(linspace + np.pi / 2)[:, None] ** 2 +) +lat = xr.DataArray( + np.linspace(-90, 90, 20), + dims=('lat',), + attrs={'units': '\N{DEGREE SIGN}N'} +) +plev = xr.DataArray( + np.linspace(1000, 0, 20), + dims=('plev',), + attrs={'long_name': 'pressure', 'units': 'hPa'} +) +da = xr.DataArray( + data, + name='u', + dims=('plev', 'lat'), + coords={'plev': plev, 'lat': lat}, + attrs={'long_name': 'zonal wind', 'units': 'm/s'} +) + +# DataFrame +data = state.rand(12, 20) +df = pd.DataFrame( + (data - 0.4).cumsum(axis=0).cumsum(axis=1)[::1, ::-1], + index=pd.date_range('2000-01', '2000-12', freq='MS') +) +df.name = 'temperature (\N{DEGREE SIGN}C)' +df.index.name = 'date' +df.columns.name = 'variable (units)' + +# %% +import proplot as pplt +fig = pplt.figure(refwidth=2.5, share=False, suptitle='Automatic subplot formatting') + +# Plot DataArray +cmap = pplt.Colormap('PuBu', left=0.05) +ax = fig.subplot(121, yreverse=True) +ax.contourf(da, cmap=cmap, colorbar='t', lw=0.7, ec='k') + +# Plot DataFrame +ax = fig.subplot(122, yreverse=True) +ax.contourf(df, cmap='YlOrRd', colorbar='t', lw=0.7, ec='k') +ax.format(xtickminor=False, yformatter='%b', ytickminor=False) + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_apply_cmap: +# +# Changing the colormap +# --------------------- +# +# It is often useful to create custom colormaps on-the-fly, +# without explicitly calling the `~proplot.constructor.Colormap` +# :ref:`constructor function `. You can do so using the `cmap` +# and `cmap_kw` keywords, available with most `~proplot.axes.PlotAxes` 2D plot +# commands. For example, to create and apply a monochromatic colormap, you can use +# ``cmap='color_name'`` (see the :ref:`colormaps section ` for more info). +# You can also create on-the-fly "qualitative" `~proplot.colors.DiscreteColormap`\ s +# by passing lists of colors to the keyword `c`, `color`, or `colors`. +# +# Proplot defines :ref:`global defaults ` for four different colormap +# types: :ref:`sequential ` (setting :rcraw:`cmap.sequential`), +# :ref:`diverging ` (setting :rcraw:`cmap.diverging`), +# :ref:`cyclic ` (setting :rcraw:`cmap.cyclic`), +# and :ref:`qualitative ` (setting :rcraw:`cmap.qualitative`). +# To use the default colormap for a given type, pass ``sequential=True``, +# ``diverging=True``, ``cyclic=True``, or ``qualitative=True`` to any plotting +# command. If the colormap type is not explicitly specified, `sequential` is +# used with the default linear normalizer when data is strictly positive +# or negative, and `diverging` is used with the :ref:`diverging normalizer ` +# when the data limits or colormap levels cross zero (see :ref:`below `). + +# %% +import proplot as pplt +import numpy as np + +# Sample data +N = 18 +state = np.random.RandomState(51423) +data = np.cumsum(state.rand(N, N), axis=0) + +# Custom defaults of each type +pplt.rc['cmap.sequential'] = 'PuBuGn' +pplt.rc['cmap.diverging'] = 'PiYG' +pplt.rc['cmap.cyclic'] = 'bamO' +pplt.rc['cmap.qualitative'] = 'flatui' + +# Make plots. Note the default behavior is sequential=True or diverging=True +# depending on whether data contains negative values (see below). +fig = pplt.figure(refwidth=2.2, span=False, suptitle='Colormap types') +axs = fig.subplots(ncols=2, nrows=2) +axs.format(xformatter='none', yformatter='none') +axs[0].pcolor(data, sequential=True, colorbar='l', extend='max') +axs[1].pcolor(data - 5, diverging=True, colorbar='r', extend='both') +axs[2].pcolor(data % 8, cyclic=True, colorbar='l') +axs[3].pcolor(data, levels=pplt.arange(0, 12, 2), qualitative=True, colorbar='r') +types = ('sequential', 'diverging', 'cyclic', 'qualitative') +for ax, typ in zip(axs, types): + ax.format(title=typ.title() + ' colormap') +pplt.rc.reset() + +# %% +import proplot as pplt +import numpy as np + +# Sample data +N = 20 +state = np.random.RandomState(51423) +data = np.cumsum(state.rand(N, N), axis=1) - 6 + +# Continuous "diverging" colormap +fig = pplt.figure(refwidth=2.3, spanx=False) +ax = fig.subplot(121, title="Diverging colormap with 'cmap'", xlabel='xlabel') +ax.contourf( + data, + norm='div', + cmap=('cobalt', 'white', 'violet red'), + cmap_kw={'space': 'hsl', 'cut': 0.15}, + colorbar='b', +) + +# Discrete "qualitative" colormap +ax = fig.subplot(122, title="Qualitative colormap with 'colors'") +ax.contourf( + data, + levels=pplt.arange(-6, 9, 3), + colors=['red5', 'blue5', 'yellow5', 'gray5', 'violet5'], + colorbar='b', +) +fig.format(xlabel='xlabel', ylabel='ylabel', suptitle='On-the-fly colormaps') + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_apply_norm: +# +# Changing the normalizer +# ----------------------- +# +# Matplotlib `colormap "normalizers" +# `__ +# translate raw data values into normalized colormap indices. In proplot, +# you can select the normalizer from its "registered" name using the +# `~proplot.constructor.Norm` :ref:`constructor function `. You +# can also build a normalizer on-the-fly using the `norm` and `norm_kw` keywords, +# available with most 2D `~proplot.axes.PlotAxes` commands. +# If you want to work with the normalizer classes directly, they are available in +# the top-level namespace (e.g., ``norm=pplt.LogNorm(...)`` is allowed). To +# explicitly set the normalization range, you can pass the usual `vmin` and `vmax` +# keywords to the plotting command. See :ref:`below ` for more +# details on colormap normalization in proplot. + +# %% +import proplot as pplt +import numpy as np + +# Sample data +N = 20 +state = np.random.RandomState(51423) +data = 11 ** (0.25 * np.cumsum(state.rand(N, N), axis=0)) + +# Create figure +gs = pplt.GridSpec(ncols=2) +fig = pplt.figure(refwidth=2.3, span=False, suptitle='Normalizer types') + +# Different normalizers +ax = fig.subplot(gs[0], title='Default linear normalizer') +ax.pcolormesh(data, cmap='magma', colorbar='b') +ax = fig.subplot(gs[1], title="Logarithmic normalizer with norm='log'") +ax.pcolormesh(data, cmap='magma', norm='log', colorbar='b') + + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_norm: +# +# Special normalizers +# ------------------- +# +# Proplot includes two new :ref:`"continuous" normalizers `. The +# `~proplot.colors.SegmentedNorm` normalizer provides even color gradations with respect +# to index for an arbitrary monotonically increasing or decreasing list of levels. This +# is automatically applied if you pass unevenly spaced `levels` to a plotting command, +# or it can be manually applied using e.g. ``norm='segmented'``. This can be useful for +# datasets with unusual statistical distributions or spanning many orders of magnitudes. +# +# The `~proplot.colors.DivergingNorm` normalizer ensures that colormap midpoints lie +# on some central data value (usually ``0``), even if `vmin`, `vmax`, or `levels` +# are asymmetric with respect to the central value. This is automatically applied +# if your data contains negative and positive values (see :ref:`below `), +# or it can be manually applied using e.g. ``diverging=True`` or ``norm='diverging'``. +# It can also be configured to scale colors "fairly" or "unfairly": +# +# * With fair scaling (the default), gradations on either side of the midpoint +# have equal intensity. If `vmin` and `vmax` are not symmetric about zero, the most +# intense colormap colors on one side of the midpoint will be truncated. +# * With unfair scaling, gradations on either side of the midpoint are warped +# so that the full range of colormap colors is always traversed. This configuration +# should be used with care, as it may lead you to misinterpret your data. +# +# The below examples demonstrate how these normalizers +# affect the interpretation of different datasets. + +# %% +import proplot as pplt +import numpy as np + +# Sample data +state = np.random.RandomState(51423) +data = 11 ** (2 * state.rand(20, 20).cumsum(axis=0) / 7) + +# Linear segmented norm +fig, axs = pplt.subplots(ncols=2, refwidth=2.4) +fig.format(suptitle='Segmented normalizer demo') +ticks = [5, 10, 20, 50, 100, 200, 500, 1000] +for ax, norm in zip(axs, ('linear', 'segmented')): + m = ax.contourf( + data, levels=ticks, extend='both', + cmap='Mako', norm=norm, + colorbar='b', colorbar_kw={'ticks': ticks}, + ) + ax.format(title=norm.title() + ' normalizer') + +# %% +import proplot as pplt +import numpy as np + +# Sample data +state = np.random.RandomState(51423) +data1 = (state.rand(20, 20) - 0.485).cumsum(axis=1).cumsum(axis=0) +data2 = (state.rand(20, 20) - 0.515).cumsum(axis=0).cumsum(axis=1) + +# Figure +fig, axs = pplt.subplots(nrows=2, ncols=2, refwidth=2.2, order='F') +axs.format(suptitle='Diverging normalizer demo') +cmap = pplt.Colormap('DryWet', cut=0.1) + +# Diverging norms +i = 0 +for data, mode, fair in zip( + (data1, data2), ('positive', 'negative'), ('fair', 'unfair'), +): + for fair in ('fair', 'unfair'): + norm = pplt.Norm('diverging', fair=(fair == 'fair')) + ax = axs[i] + m = ax.contourf(data, cmap=cmap, norm=norm) + ax.colorbar(m, loc='b') + ax.format(title=f'{mode.title()}-skewed + {fair} scaling') + i += 1 + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_discrete: +# +# Discrete levels +# --------------- +# +# By default, proplot uses `~proplot.colors.DiscreteNorm` to "discretize" +# the possible colormap colors for contour and pseudocolor `~proplot.axes.PlotAxes` +# commands (e.g., `~proplot.axes.PlotAxes.contourf`, `~proplot.axes.PlotAxes.pcolor`). +# This is analogous to `matplotlib.colors.BoundaryNorm`, except +# `~proplot.colors.DiscreteNorm` can be paired with arbitrary +# continuous normalizers specified by `norm` (see :ref:`above `). +# Discrete color levels can help readers discern exact numeric values and +# tend to reveal qualitative structure in the data. `~proplot.colors.DiscreteNorm` +# also repairs the colormap end-colors by ensuring the following conditions are met: +# +# #. All colormaps always span the *entire color range* +# regardless of the `extend` parameter. +# #. Cyclic colormaps always have *distinct color levels* +# on either end of the colorbar. +# +# To explicitly toggle discrete levels on or off, change :rcraw:`cmap.discrete` +# or pass ``discrete=False`` or ``discrete=True`` to any plotting command +# that accepts a `cmap` argument. The level edges or centers used with +# `~proplot.colors.DiscreteNorm` can be explicitly specified using the `levels` or +# `values` keywords, respectively (`~proplot.utils.arange` and `~proplot.utils.edges` +# are useful for generating `levels` and `values` lists). You can also pass an integer +# to these keywords (or to the `N` keyword) to automatically generate approximately this +# many level edges or centers at "nice" intervals. The algorithm used to generate levels +# is similar to matplotlib's algorithm for generarting contour levels. The default +# number of levels is controlled by :rcraw:`cmap.levels`, and the level selection +# is constrainted by the keywords `vmin`, `vmax`, `locator`, and `locator_kw` -- for +# example, ``vmin=100`` ensures the minimum level is greater than or equal to ``100``, +# and ``locator=5`` ensures a level step size of 5 (see :ref:`this section +# ` for more on locators). You can also use the keywords `negative`, +# `positive`, or `symmetric` to ensure that your levels are strictly negative, +# positive, or symmetric about zero, or use the `nozero` keyword to remove +# the zero level (useful for single-color `~proplot.axes.PlotAxes.contour` plots). + +# %% +import proplot as pplt +import numpy as np + +# Sample data +state = np.random.RandomState(51423) +data = 10 + state.normal(0, 1, size=(33, 33)).cumsum(axis=0).cumsum(axis=1) + +# Figure +fig, axs = pplt.subplots([[1, 1, 2, 2], [0, 3, 3, 0]], ref=3, refwidth=2.3) +axs.format(yformatter='none', suptitle='Discrete vs. smooth colormap levels') + +# Pcolor +axs[0].pcolor(data, cmap='viridis', colorbar='l') +axs[0].set_title('Pcolor plot\ndiscrete=True (default)') +axs[1].pcolor(data, discrete=False, cmap='viridis', colorbar='r') +axs[1].set_title('Pcolor plot\ndiscrete=False') + +# Imshow +m = axs[2].imshow(data, cmap='oslo', colorbar='b') +axs[2].format(title='Imshow plot\ndiscrete=False (default)', yformatter='auto') + +# %% +import proplot as pplt +import numpy as np + +# Sample data +state = np.random.RandomState(51423) +data = (20 * (state.rand(20, 20) - 0.4).cumsum(axis=0).cumsum(axis=1)) % 360 +levels = pplt.arange(0, 360, 45) + +# Figure +gs = pplt.GridSpec(nrows=2, ncols=4, hratios=(1.5, 1)) +fig = pplt.figure(refwidth=2.4, right=2) +fig.format(suptitle='DiscreteNorm end-color standardization') + +# Cyclic colorbar with distinct end colors +cmap = pplt.Colormap('twilight', shift=-90) +ax = fig.subplot(gs[0, 1:3], title='distinct "cyclic" end colors') +ax.pcolormesh( + data, cmap=cmap, levels=levels, + colorbar='b', colorbar_kw={'locator': 90}, +) + +# Colorbars with different extend values +for i, extend in enumerate(('min', 'max', 'neither', 'both')): + ax = fig.subplot(gs[1, i], title=f'extend={extend!r}') + ax.pcolormesh( + data[:, :10], levels=levels, cmap='oxy', + extend=extend, colorbar='b', colorbar_kw={'locator': 180} + ) + +# %% [raw] raw_mimetype="text/restructuredtext" tags=[] +# .. _ug_autonorm: +# +# Auto normalization +# ------------------ +# +# By default, colormaps are normalized to span from roughly the minimum +# data value to the maximum data value. However in the presence of outliers, +# this is not desirable. Proplot adds the `robust` keyword to change this +# behavior, inspired by the `xarray keyword +# `__ +# of the same name. Passing ``robust=True`` to a `~proplot.axes.PlotAxes` +# 2D plot command will limit the default colormap normalization between +# the 2nd and 98th data percentiles. This range can be customized by passing +# an integer to `robust` (e.g. ``robust=90`` limits the normalization range +# between the 5th and 95th percentiles) or by passing a 2-tuple to `robust` +# (e.g. ``robust=(0, 90)`` limits the normalization range between the +# data minimum and the 90th percentile). This can be turned on persistently +# by setting :rcraw:`cmap.robust` to ``True``. +# +# Additionally, `similar to xarray +# `__, +# proplot can automatically detect "diverging" datasets. By default, +# the 2D `~proplot.axes.PlotAxes` commands will apply the diverging colormap +# :rc:`cmap.diverging` (rather than :rc:`cmap.sequential`) and the diverging +# normalizer `~proplot.colors.DivergingNorm` (rather than `~matplotlib.colors.Normalize` +# -- see :ref:`above `) if the following conditions are met: +# +# #. If discrete levels are enabled (see :ref:`above `) and the +# level list includes at least 2 negative and 2 positive values. +# #. If discrete levels are disabled (see :ref:`above `) and the +# normalization limits `vmin` and `vmax` are negative and positive. +# #. A colormap was not explicitly passed, or a colormap was passed but it +# matches the name of a :ref:`known diverging colormap `. +# +# The automatic detection of "diverging" datasets can be disabled by +# setting :rcraw:`cmap.autodiverging` to ``False``. + +# %% +import proplot as pplt +import numpy as np +N = 20 +state = np.random.RandomState(51423) +data = N * 2 + (state.rand(N, N) - 0.45).cumsum(axis=0).cumsum(axis=1) * 10 +fig, axs = pplt.subplots( + nrows=2, ncols=2, refwidth=2, + suptitle='Auto normalization demo' +) + +# Auto diverging +pplt.rc['cmap.sequential'] = 'lapaz_r' +pplt.rc['cmap.diverging'] = 'vik' +for i, ax in enumerate(axs[:2]): + ax.pcolor(data - i * N * 6, colorbar='b') + ax.format(title='Diverging ' + ('on' if i else 'off')) + +# Auto range +pplt.rc['cmap.sequential'] = 'lajolla' +data = data[::-1, :] +data[-1, 0] = 2e3 +for i, ax in enumerate(axs[2:]): + ax.pcolor(data, robust=bool(i), colorbar='b') + ax.format(title='Robust ' + ('on' if i else 'off')) +pplt.rc.reset() + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_labels: +# +# Quick labels +# ------------ +# +# You can now quickly add labels to `~proplot.axes.PlotAxes.contour`, +# `~proplot.axes.PlotAxes.contourf`, `~proplot.axes.PlotAxes.pcolor`, +# `~proplot.axes.PlotAxes.pcolormesh`, and `~proplot.axes.PlotAxes.heatmap`, +# plots by passing ``labels=True`` to the plotting command. The +# label text is colored black or white depending on the luminance of the underlying +# grid box or filled contour (see the section on :ref:`colorspaces `). +# Contour labels are drawn with `~matplotlib.axes.Axes.clabel` and grid box +# labels are drawn with `~proplot.axes.Axes.text`. You can pass keyword arguments +# to these functions by passing a dictionary to `labels_kw`, and you can +# change the label precision using the `precision` keyword. See the plotting +# command documentation for details. + +# %% +import proplot as pplt +import pandas as pd +import numpy as np + +# Sample data +state = np.random.RandomState(51423) +data = state.rand(6, 6) +data = pd.DataFrame(data, index=pd.Index(['a', 'b', 'c', 'd', 'e', 'f'])) + +# Figure +fig, axs = pplt.subplots( + [[1, 1, 2, 2], [0, 3, 3, 0]], + refwidth=2.3, share='labels', span=False, +) +axs.format(xlabel='xlabel', ylabel='ylabel', suptitle='Labels demo') + +# Heatmap with labeled boxes +ax = axs[0] +m = ax.heatmap( + data, cmap='rocket', + labels=True, precision=2, labels_kw={'weight': 'bold'} +) +ax.format(title='Heatmap with labels') + +# Filled contours with labels +ax = axs[1] +m = ax.contourf( + data.cumsum(axis=0), cmap='rocket', + labels=True, labels_kw={'weight': 'bold'} +) +ax.format(title='Filled contours with labels') + +# Line contours with labels and no zero level +data = 5 * (data - 0.45).cumsum(axis=0) - 2 +ax = axs[2] +ax.contour( + data, nozero=True, color='gray8', + labels=True, labels_kw={'weight': 'bold'} +) +ax.format(title='Line contours with labels') + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_heatmap: +# +# Heatmap plots +# ------------- +# +# The `~proplot.axes.PlotAxes.heatmap` command can be used to draw "heatmaps" of +# 2-dimensional data. This is a convenience function equivalent to +# `~proplot.axes.PlotAxes.pcolormesh`, except the axes are configured with settings +# suitable for heatmaps: fixed aspect ratios (ensuring "square" grid boxes), no +# gridlines, no minor ticks, and major ticks at the center of each box. Among other +# things, this is useful for displaying covariance and correlation matrices, as shown +# below. `~proplot.axes.PlotAxes.heatmap` should generally only be used with +# `~proplot.axes.CartesianAxes`. + +# %% +import proplot as pplt +import numpy as np +import pandas as pd + +# Covariance data +state = np.random.RandomState(51423) +data = state.normal(size=(10, 10)).cumsum(axis=0) +data = (data - data.mean(axis=0)) / data.std(axis=0) +data = (data.T @ data) / data.shape[0] +data[np.tril_indices(data.shape[0], -1)] = np.nan # fill half with empty boxes +data = pd.DataFrame(data, columns=list('abcdefghij'), index=list('abcdefghij')) + +# Covariance matrix plot +fig, ax = pplt.subplots(refwidth=4.5) +m = ax.heatmap( + data, cmap='ColdHot', vmin=-1, vmax=1, N=100, lw=0.5, ec='k', + labels=True, precision=2, labels_kw={'weight': 'bold'}, + clip_on=False, # turn off clipping so box edges are not cut in half +) +ax.format( + suptitle='Heatmap demo', title='Table of correlation coefficients', + xloc='top', yloc='right', yreverse=True, ticklabelweight='bold', + alpha=0, linewidth=0, tickpad=4, +) diff --git a/docs/Makefile b/docs/Makefile index 8e1ad0215..2691ced5b 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -26,4 +26,3 @@ clean: # "make mode" option. See: https://github.com/sphinx-doc/sphinx/issues/6603 %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) - diff --git a/docs/_static/custom.css b/docs/_static/custom.css deleted file mode 100644 index 913447c0d..000000000 --- a/docs/_static/custom.css +++ /dev/null @@ -1,469 +0,0 @@ -/* This file contains edits to the RTD theme CSS style - * sheets. Requires a _templates/layout.html file. - * https://learn.shayhowe.com/advanced-html-css/complex-selectors/ - * - item is tag e.g. - * - #item is id e.g.

- * - .item is class e.g.

- * - item[key] or item[key=*] filters tags e.g. - * - .item1.item2 {} matches tag belonging to *both* classes/ids - * - .item1 .item2 {} matches where item2 is *descendent* of item tag/class/id - * - .item1, .item2 {} matches tag belonging to *either* tag/class/id - * - item1:key {} matches "pseudo-class" e.g. item1:hover or "pseudo-element" e..g item1:first-line - * - item1 + item2 matches item2 *immediately following* item1, but not inside - * - default blue for header and links: 2f81b7 - * - better blue used before: 2d6ab0 - * Alabaster scroll sidebar: https://stackoverflow.com/a/57040610/4970632 - * Rtd edits: https://stackoverflow.com/a/48281750/4970632 - * Note we only use !important where RTD uses !important - * Otherwise just try to match specificity of RTD selectors - */ - -/* Default light mode variables */ -:root { - --dark-color: #404040; /* for toggle button */ - --light-color: #f0f0f0; /* for toggle button */ - --main-color: #404040; - --call-color: #606060; - --versions-color: #808080; - --highlight-color: #f1c40f; - --code-color: #e65820; - --link-color: #2d6ab0; - --link-hover-color: #0079ff; - --link-visited-color: #452bb0; - --code-border-color: #e0e0e0; - --menu-border-color: #909090; - --search-border-color: #aaa; - --search-shadow-color: #ddd; - --main-bg-color: #fcfcfc; - --code-bg-color: #ffffff; - --empty-bg-color: #f4f4f4; - --l1-bg-color: #e9e9e9; - --l2-bg-color: #dcdcdc; - --l3-bg-color: #d0d0d0; - --l4-bg-color: #c3c3c3; - --l5-bg-color: #b6b6b6; - --l6-bg-color: #a9a9a9; - --l7-bg-color: #9c9c9c; - --block-bg-color: #f0f0f0; - --accent-bg-color: #d0d0d0; -} - -/* Dark mode variables */ -/* See: https://dev.to/ananyaneogi/create-a-dark-light-mode-switch-with-css-variables-34l8 */ -[data-theme="dark"] { - --main-color: #fcfcfc; - --call-color: #d0d0d0; - --versions-color: #b0b0b0; - --code-color: #ff8f4f; - --link-color: #69acff; - --highlight-color: #b27600; - --link-hover-color: #549aeb; - --link-visited-color: #c194ff; - --code-border-color: #707070; - --menu-border-color: #909090; - --search-border-color: #555; - --search-shadow-color: #444; - --main-bg-color: #303030; - --code-bg-color: #3a3a3a; - --empty-bg-color: #363636; - --l1-bg-color: #404040; - --l2-bg-color: #4c4c4c; - --l3-bg-color: #595959; - --l4-bg-color: #666666; - --l5-bg-color: #737373; - --l6-bg-color: #808080; - --l7-bg-color: #8c8c8c; - --block-bg-color: #3a3a3a; - --accent-bg-color: #606060; -} - -/* RST content background color */ -body.scroll-up, -body.scroll-down, -.wy-nav-content { - background: var(--main-bg-color); -} - -/* Horizontal rules */ -hr { - color: var(--code-border-color); -} - -/* Code background color */ -code, -.rst-content tt, -.rst-content code { - border: solid 1px var(--code-border-color); - background: var(--code-bg-color); -} - -/* For literal code blocks, do not inherit main text color */ -/* Need !important because nbshpinx hardcodes + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/_static/logo_long.png b/docs/_static/logo_long.png index fb979c1f7..480c19a76 100644 Binary files a/docs/_static/logo_long.png and b/docs/_static/logo_long.png differ diff --git a/docs/_static/logo_long.svg b/docs/_static/logo_long.svg new file mode 100644 index 000000000..0f2a5bfcb --- /dev/null +++ b/docs/_static/logo_long.svg @@ -0,0 +1,518 @@ + + + + + + + + 2022-06-22T14:25:52.780693 + image/svg+xml + + + Matplotlib v3.5.2, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/_static/logo_social.png b/docs/_static/logo_social.png new file mode 100644 index 000000000..8cf153558 Binary files /dev/null and b/docs/_static/logo_social.png differ diff --git a/docs/_static/logo_social.svg b/docs/_static/logo_social.svg new file mode 100644 index 000000000..ad6108a92 --- /dev/null +++ b/docs/_static/logo_social.svg @@ -0,0 +1,322 @@ + + + + + + + + 2022-06-22T14:25:51.550193 + image/svg+xml + + + Matplotlib v3.5.2, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/_static/logo_square.png b/docs/_static/logo_square.png index 834390c71..40da64f78 100644 Binary files a/docs/_static/logo_square.png and b/docs/_static/logo_square.png differ diff --git a/docs/_static/logo_square.svg b/docs/_static/logo_square.svg new file mode 100644 index 000000000..a8855ee72 --- /dev/null +++ b/docs/_static/logo_square.svg @@ -0,0 +1,592 @@ + + + + + + + + 2022-06-22T14:25:52.026790 + image/svg+xml + + + Matplotlib v3.5.2, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/_static/pygments/abap.css b/docs/_static/pygments/abap.css deleted file mode 100644 index 308fc54e3..000000000 --- a/docs/_static/pygments/abap.css +++ /dev/null @@ -1,60 +0,0 @@ -.highlight .hll { background-color: #ffffcc } -.highlight { background: #ffffff; } -.highlight .c { color: #888888; font-style: italic } /* Comment */ -.highlight .err { color: #FF0000 } /* Error */ -.highlight .k { color: #0000ff } /* Keyword */ -.highlight .n { color: #000000 } /* Name */ -.highlight .ch { color: #888888; font-style: italic } /* Comment.Hashbang */ -.highlight .cm { color: #888888; font-style: italic } /* Comment.Multiline */ -.highlight .cp { color: #888888; font-style: italic } /* Comment.Preproc */ -.highlight .cpf { color: #888888; font-style: italic } /* Comment.PreprocFile */ -.highlight .c1 { color: #888888; font-style: italic } /* Comment.Single */ -.highlight .cs { color: #888888; font-style: italic } /* Comment.Special */ -.highlight .kc { color: #0000ff } /* Keyword.Constant */ -.highlight .kd { color: #0000ff } /* Keyword.Declaration */ -.highlight .kn { color: #0000ff } /* Keyword.Namespace */ -.highlight .kp { color: #0000ff } /* Keyword.Pseudo */ -.highlight .kr { color: #0000ff } /* Keyword.Reserved */ -.highlight .kt { color: #0000ff } /* Keyword.Type */ -.highlight .m { color: #33aaff } /* Literal.Number */ -.highlight .s { color: #55aa22 } /* Literal.String */ -.highlight .na { color: #000000 } /* Name.Attribute */ -.highlight .nb { color: #000000 } /* Name.Builtin */ -.highlight .nc { color: #000000 } /* Name.Class */ -.highlight .no { color: #000000 } /* Name.Constant */ -.highlight .nd { color: #000000 } /* Name.Decorator */ -.highlight .ni { color: #000000 } /* Name.Entity */ -.highlight .ne { color: #000000 } /* Name.Exception */ -.highlight .nf { color: #000000 } /* Name.Function */ -.highlight .nl { color: #000000 } /* Name.Label */ -.highlight .nn { color: #000000 } /* Name.Namespace */ -.highlight .nx { color: #000000 } /* Name.Other */ -.highlight .py { color: #000000 } /* Name.Property */ -.highlight .nt { color: #000000 } /* Name.Tag */ -.highlight .nv { color: #000000 } /* Name.Variable */ -.highlight .ow { color: #0000ff } /* Operator.Word */ -.highlight .mb { color: #33aaff } /* Literal.Number.Bin */ -.highlight .mf { color: #33aaff } /* Literal.Number.Float */ -.highlight .mh { color: #33aaff } /* Literal.Number.Hex */ -.highlight .mi { color: #33aaff } /* Literal.Number.Integer */ -.highlight .mo { color: #33aaff } /* Literal.Number.Oct */ -.highlight .sa { color: #55aa22 } /* Literal.String.Affix */ -.highlight .sb { color: #55aa22 } /* Literal.String.Backtick */ -.highlight .sc { color: #55aa22 } /* Literal.String.Char */ -.highlight .dl { color: #55aa22 } /* Literal.String.Delimiter */ -.highlight .sd { color: #55aa22 } /* Literal.String.Doc */ -.highlight .s2 { color: #55aa22 } /* Literal.String.Double */ -.highlight .se { color: #55aa22 } /* Literal.String.Escape */ -.highlight .sh { color: #55aa22 } /* Literal.String.Heredoc */ -.highlight .si { color: #55aa22 } /* Literal.String.Interpol */ -.highlight .sx { color: #55aa22 } /* Literal.String.Other */ -.highlight .sr { color: #55aa22 } /* Literal.String.Regex */ -.highlight .s1 { color: #55aa22 } /* Literal.String.Single */ -.highlight .ss { color: #55aa22 } /* Literal.String.Symbol */ -.highlight .bp { color: #000000 } /* Name.Builtin.Pseudo */ -.highlight .fm { color: #000000 } /* Name.Function.Magic */ -.highlight .vc { color: #000000 } /* Name.Variable.Class */ -.highlight .vg { color: #000000 } /* Name.Variable.Global */ -.highlight .vi { color: #000000 } /* Name.Variable.Instance */ -.highlight .vm { color: #000000 } /* Name.Variable.Magic */ -.highlight .il { color: #33aaff } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/docs/_static/pygments/algol.css b/docs/_static/pygments/algol.css deleted file mode 100644 index c21f022d8..000000000 --- a/docs/_static/pygments/algol.css +++ /dev/null @@ -1,44 +0,0 @@ -.highlight .hll { background-color: #ffffcc } -.highlight { background: #ffffff; } -.highlight .c { color: #888888; font-style: italic } /* Comment */ -.highlight .err { border: 1px solid #FF0000 } /* Error */ -.highlight .k { font-weight: bold; text-decoration: underline } /* Keyword */ -.highlight .ch { color: #888888; font-style: italic } /* Comment.Hashbang */ -.highlight .cm { color: #888888; font-style: italic } /* Comment.Multiline */ -.highlight .cp { color: #888888; font-weight: bold } /* Comment.Preproc */ -.highlight .cpf { color: #888888; font-style: italic } /* Comment.PreprocFile */ -.highlight .c1 { color: #888888; font-style: italic } /* Comment.Single */ -.highlight .cs { color: #888888; font-weight: bold } /* Comment.Special */ -.highlight .kc { font-weight: bold; text-decoration: underline } /* Keyword.Constant */ -.highlight .kd { font-weight: bold; font-style: italic; text-decoration: underline } /* Keyword.Declaration */ -.highlight .kn { font-weight: bold; text-decoration: underline } /* Keyword.Namespace */ -.highlight .kp { font-weight: bold; text-decoration: underline } /* Keyword.Pseudo */ -.highlight .kr { font-weight: bold; text-decoration: underline } /* Keyword.Reserved */ -.highlight .kt { font-weight: bold; text-decoration: underline } /* Keyword.Type */ -.highlight .s { color: #666666; font-style: italic } /* Literal.String */ -.highlight .nb { font-weight: bold; font-style: italic } /* Name.Builtin */ -.highlight .nc { color: #666666; font-weight: bold; font-style: italic } /* Name.Class */ -.highlight .no { color: #666666; font-weight: bold; font-style: italic } /* Name.Constant */ -.highlight .nf { color: #666666; font-weight: bold; font-style: italic } /* Name.Function */ -.highlight .nn { color: #666666; font-weight: bold; font-style: italic } /* Name.Namespace */ -.highlight .nv { color: #666666; font-weight: bold; font-style: italic } /* Name.Variable */ -.highlight .ow { font-weight: bold } /* Operator.Word */ -.highlight .sa { color: #666666; font-style: italic } /* Literal.String.Affix */ -.highlight .sb { color: #666666; font-style: italic } /* Literal.String.Backtick */ -.highlight .sc { color: #666666; font-style: italic } /* Literal.String.Char */ -.highlight .dl { color: #666666; font-style: italic } /* Literal.String.Delimiter */ -.highlight .sd { color: #666666; font-style: italic } /* Literal.String.Doc */ -.highlight .s2 { color: #666666; font-style: italic } /* Literal.String.Double */ -.highlight .se { color: #666666; font-style: italic } /* Literal.String.Escape */ -.highlight .sh { color: #666666; font-style: italic } /* Literal.String.Heredoc */ -.highlight .si { color: #666666; font-style: italic } /* Literal.String.Interpol */ -.highlight .sx { color: #666666; font-style: italic } /* Literal.String.Other */ -.highlight .sr { color: #666666; font-style: italic } /* Literal.String.Regex */ -.highlight .s1 { color: #666666; font-style: italic } /* Literal.String.Single */ -.highlight .ss { color: #666666; font-style: italic } /* Literal.String.Symbol */ -.highlight .bp { font-weight: bold; font-style: italic } /* Name.Builtin.Pseudo */ -.highlight .fm { color: #666666; font-weight: bold; font-style: italic } /* Name.Function.Magic */ -.highlight .vc { color: #666666; font-weight: bold; font-style: italic } /* Name.Variable.Class */ -.highlight .vg { color: #666666; font-weight: bold; font-style: italic } /* Name.Variable.Global */ -.highlight .vi { color: #666666; font-weight: bold; font-style: italic } /* Name.Variable.Instance */ -.highlight .vm { color: #666666; font-weight: bold; font-style: italic } /* Name.Variable.Magic */ \ No newline at end of file diff --git a/docs/_static/pygments/algol_nu.css b/docs/_static/pygments/algol_nu.css deleted file mode 100644 index 8ec2d1507..000000000 --- a/docs/_static/pygments/algol_nu.css +++ /dev/null @@ -1,44 +0,0 @@ -.highlight .hll { background-color: #ffffcc } -.highlight { background: #ffffff; } -.highlight .c { color: #888888; font-style: italic } /* Comment */ -.highlight .err { border: 1px solid #FF0000 } /* Error */ -.highlight .k { font-weight: bold } /* Keyword */ -.highlight .ch { color: #888888; font-style: italic } /* Comment.Hashbang */ -.highlight .cm { color: #888888; font-style: italic } /* Comment.Multiline */ -.highlight .cp { color: #888888; font-weight: bold } /* Comment.Preproc */ -.highlight .cpf { color: #888888; font-style: italic } /* Comment.PreprocFile */ -.highlight .c1 { color: #888888; font-style: italic } /* Comment.Single */ -.highlight .cs { color: #888888; font-weight: bold } /* Comment.Special */ -.highlight .kc { font-weight: bold } /* Keyword.Constant */ -.highlight .kd { font-weight: bold; font-style: italic } /* Keyword.Declaration */ -.highlight .kn { font-weight: bold } /* Keyword.Namespace */ -.highlight .kp { font-weight: bold } /* Keyword.Pseudo */ -.highlight .kr { font-weight: bold } /* Keyword.Reserved */ -.highlight .kt { font-weight: bold } /* Keyword.Type */ -.highlight .s { color: #666666; font-style: italic } /* Literal.String */ -.highlight .nb { font-weight: bold; font-style: italic } /* Name.Builtin */ -.highlight .nc { color: #666666; font-weight: bold; font-style: italic } /* Name.Class */ -.highlight .no { color: #666666; font-weight: bold; font-style: italic } /* Name.Constant */ -.highlight .nf { color: #666666; font-weight: bold; font-style: italic } /* Name.Function */ -.highlight .nn { color: #666666; font-weight: bold; font-style: italic } /* Name.Namespace */ -.highlight .nv { color: #666666; font-weight: bold; font-style: italic } /* Name.Variable */ -.highlight .ow { font-weight: bold } /* Operator.Word */ -.highlight .sa { color: #666666; font-style: italic } /* Literal.String.Affix */ -.highlight .sb { color: #666666; font-style: italic } /* Literal.String.Backtick */ -.highlight .sc { color: #666666; font-style: italic } /* Literal.String.Char */ -.highlight .dl { color: #666666; font-style: italic } /* Literal.String.Delimiter */ -.highlight .sd { color: #666666; font-style: italic } /* Literal.String.Doc */ -.highlight .s2 { color: #666666; font-style: italic } /* Literal.String.Double */ -.highlight .se { color: #666666; font-style: italic } /* Literal.String.Escape */ -.highlight .sh { color: #666666; font-style: italic } /* Literal.String.Heredoc */ -.highlight .si { color: #666666; font-style: italic } /* Literal.String.Interpol */ -.highlight .sx { color: #666666; font-style: italic } /* Literal.String.Other */ -.highlight .sr { color: #666666; font-style: italic } /* Literal.String.Regex */ -.highlight .s1 { color: #666666; font-style: italic } /* Literal.String.Single */ -.highlight .ss { color: #666666; font-style: italic } /* Literal.String.Symbol */ -.highlight .bp { font-weight: bold; font-style: italic } /* Name.Builtin.Pseudo */ -.highlight .fm { color: #666666; font-weight: bold; font-style: italic } /* Name.Function.Magic */ -.highlight .vc { color: #666666; font-weight: bold; font-style: italic } /* Name.Variable.Class */ -.highlight .vg { color: #666666; font-weight: bold; font-style: italic } /* Name.Variable.Global */ -.highlight .vi { color: #666666; font-weight: bold; font-style: italic } /* Name.Variable.Instance */ -.highlight .vm { color: #666666; font-weight: bold; font-style: italic } /* Name.Variable.Magic */ \ No newline at end of file diff --git a/docs/_static/pygments/arduino.css b/docs/_static/pygments/arduino.css deleted file mode 100644 index 69e723ac0..000000000 --- a/docs/_static/pygments/arduino.css +++ /dev/null @@ -1,61 +0,0 @@ -.highlight .hll { background-color: #ffffcc } -.highlight { background: #ffffff; } -.highlight .c { color: #95a5a6 } /* Comment */ -.highlight .err { color: #a61717 } /* Error */ -.highlight .k { color: #728E00 } /* Keyword */ -.highlight .n { color: #434f54 } /* Name */ -.highlight .o { color: #728E00 } /* Operator */ -.highlight .ch { color: #95a5a6 } /* Comment.Hashbang */ -.highlight .cm { color: #95a5a6 } /* Comment.Multiline */ -.highlight .cp { color: #728E00 } /* Comment.Preproc */ -.highlight .cpf { color: #95a5a6 } /* Comment.PreprocFile */ -.highlight .c1 { color: #95a5a6 } /* Comment.Single */ -.highlight .cs { color: #95a5a6 } /* Comment.Special */ -.highlight .kc { color: #00979D } /* Keyword.Constant */ -.highlight .kd { color: #728E00 } /* Keyword.Declaration */ -.highlight .kn { color: #728E00 } /* Keyword.Namespace */ -.highlight .kp { color: #00979D } /* Keyword.Pseudo */ -.highlight .kr { color: #00979D } /* Keyword.Reserved */ -.highlight .kt { color: #00979D } /* Keyword.Type */ -.highlight .m { color: #8A7B52 } /* Literal.Number */ -.highlight .s { color: #7F8C8D } /* Literal.String */ -.highlight .na { color: #434f54 } /* Name.Attribute */ -.highlight .nb { color: #728E00 } /* Name.Builtin */ -.highlight .nc { color: #434f54 } /* Name.Class */ -.highlight .no { color: #434f54 } /* Name.Constant */ -.highlight .nd { color: #434f54 } /* Name.Decorator */ -.highlight .ni { color: #434f54 } /* Name.Entity */ -.highlight .ne { color: #434f54 } /* Name.Exception */ -.highlight .nf { color: #D35400 } /* Name.Function */ -.highlight .nl { color: #434f54 } /* Name.Label */ -.highlight .nn { color: #434f54 } /* Name.Namespace */ -.highlight .nx { color: #728E00 } /* Name.Other */ -.highlight .py { color: #434f54 } /* Name.Property */ -.highlight .nt { color: #434f54 } /* Name.Tag */ -.highlight .nv { color: #434f54 } /* Name.Variable */ -.highlight .ow { color: #728E00 } /* Operator.Word */ -.highlight .mb { color: #8A7B52 } /* Literal.Number.Bin */ -.highlight .mf { color: #8A7B52 } /* Literal.Number.Float */ -.highlight .mh { color: #8A7B52 } /* Literal.Number.Hex */ -.highlight .mi { color: #8A7B52 } /* Literal.Number.Integer */ -.highlight .mo { color: #8A7B52 } /* Literal.Number.Oct */ -.highlight .sa { color: #7F8C8D } /* Literal.String.Affix */ -.highlight .sb { color: #7F8C8D } /* Literal.String.Backtick */ -.highlight .sc { color: #7F8C8D } /* Literal.String.Char */ -.highlight .dl { color: #7F8C8D } /* Literal.String.Delimiter */ -.highlight .sd { color: #7F8C8D } /* Literal.String.Doc */ -.highlight .s2 { color: #7F8C8D } /* Literal.String.Double */ -.highlight .se { color: #7F8C8D } /* Literal.String.Escape */ -.highlight .sh { color: #7F8C8D } /* Literal.String.Heredoc */ -.highlight .si { color: #7F8C8D } /* Literal.String.Interpol */ -.highlight .sx { color: #7F8C8D } /* Literal.String.Other */ -.highlight .sr { color: #7F8C8D } /* Literal.String.Regex */ -.highlight .s1 { color: #7F8C8D } /* Literal.String.Single */ -.highlight .ss { color: #7F8C8D } /* Literal.String.Symbol */ -.highlight .bp { color: #728E00 } /* Name.Builtin.Pseudo */ -.highlight .fm { color: #D35400 } /* Name.Function.Magic */ -.highlight .vc { color: #434f54 } /* Name.Variable.Class */ -.highlight .vg { color: #434f54 } /* Name.Variable.Global */ -.highlight .vi { color: #434f54 } /* Name.Variable.Instance */ -.highlight .vm { color: #434f54 } /* Name.Variable.Magic */ -.highlight .il { color: #8A7B52 } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/docs/_static/pygments/autumn.css b/docs/_static/pygments/autumn.css deleted file mode 100644 index ce8fc6016..000000000 --- a/docs/_static/pygments/autumn.css +++ /dev/null @@ -1,66 +0,0 @@ -.highlight .hll { background-color: #ffffcc } -.highlight { background: #ffffff; } -.highlight .c { color: #aaaaaa; font-style: italic } /* Comment */ -.highlight .err { color: #FF0000; background-color: #FFAAAA } /* Error */ -.highlight .k { color: #0000aa } /* Keyword */ -.highlight .ch { color: #aaaaaa; font-style: italic } /* Comment.Hashbang */ -.highlight .cm { color: #aaaaaa; font-style: italic } /* Comment.Multiline */ -.highlight .cp { color: #4c8317 } /* Comment.Preproc */ -.highlight .cpf { color: #aaaaaa; font-style: italic } /* Comment.PreprocFile */ -.highlight .c1 { color: #aaaaaa; font-style: italic } /* Comment.Single */ -.highlight .cs { color: #0000aa; font-style: italic } /* Comment.Special */ -.highlight .gd { color: #aa0000 } /* Generic.Deleted */ -.highlight .ge { font-style: italic } /* Generic.Emph */ -.highlight .gr { color: #aa0000 } /* Generic.Error */ -.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ -.highlight .gi { color: #00aa00 } /* Generic.Inserted */ -.highlight .go { color: #888888 } /* Generic.Output */ -.highlight .gp { color: #555555 } /* Generic.Prompt */ -.highlight .gs { font-weight: bold } /* Generic.Strong */ -.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ -.highlight .gt { color: #aa0000 } /* Generic.Traceback */ -.highlight .kc { color: #0000aa } /* Keyword.Constant */ -.highlight .kd { color: #0000aa } /* Keyword.Declaration */ -.highlight .kn { color: #0000aa } /* Keyword.Namespace */ -.highlight .kp { color: #0000aa } /* Keyword.Pseudo */ -.highlight .kr { color: #0000aa } /* Keyword.Reserved */ -.highlight .kt { color: #00aaaa } /* Keyword.Type */ -.highlight .m { color: #009999 } /* Literal.Number */ -.highlight .s { color: #aa5500 } /* Literal.String */ -.highlight .na { color: #1e90ff } /* Name.Attribute */ -.highlight .nb { color: #00aaaa } /* Name.Builtin */ -.highlight .nc { color: #00aa00; text-decoration: underline } /* Name.Class */ -.highlight .no { color: #aa0000 } /* Name.Constant */ -.highlight .nd { color: #888888 } /* Name.Decorator */ -.highlight .ni { color: #880000; font-weight: bold } /* Name.Entity */ -.highlight .nf { color: #00aa00 } /* Name.Function */ -.highlight .nn { color: #00aaaa; text-decoration: underline } /* Name.Namespace */ -.highlight .nt { color: #1e90ff; font-weight: bold } /* Name.Tag */ -.highlight .nv { color: #aa0000 } /* Name.Variable */ -.highlight .ow { color: #0000aa } /* Operator.Word */ -.highlight .w { color: #bbbbbb } /* Text.Whitespace */ -.highlight .mb { color: #009999 } /* Literal.Number.Bin */ -.highlight .mf { color: #009999 } /* Literal.Number.Float */ -.highlight .mh { color: #009999 } /* Literal.Number.Hex */ -.highlight .mi { color: #009999 } /* Literal.Number.Integer */ -.highlight .mo { color: #009999 } /* Literal.Number.Oct */ -.highlight .sa { color: #aa5500 } /* Literal.String.Affix */ -.highlight .sb { color: #aa5500 } /* Literal.String.Backtick */ -.highlight .sc { color: #aa5500 } /* Literal.String.Char */ -.highlight .dl { color: #aa5500 } /* Literal.String.Delimiter */ -.highlight .sd { color: #aa5500 } /* Literal.String.Doc */ -.highlight .s2 { color: #aa5500 } /* Literal.String.Double */ -.highlight .se { color: #aa5500 } /* Literal.String.Escape */ -.highlight .sh { color: #aa5500 } /* Literal.String.Heredoc */ -.highlight .si { color: #aa5500 } /* Literal.String.Interpol */ -.highlight .sx { color: #aa5500 } /* Literal.String.Other */ -.highlight .sr { color: #009999 } /* Literal.String.Regex */ -.highlight .s1 { color: #aa5500 } /* Literal.String.Single */ -.highlight .ss { color: #0000aa } /* Literal.String.Symbol */ -.highlight .bp { color: #00aaaa } /* Name.Builtin.Pseudo */ -.highlight .fm { color: #00aa00 } /* Name.Function.Magic */ -.highlight .vc { color: #aa0000 } /* Name.Variable.Class */ -.highlight .vg { color: #aa0000 } /* Name.Variable.Global */ -.highlight .vi { color: #aa0000 } /* Name.Variable.Instance */ -.highlight .vm { color: #aa0000 } /* Name.Variable.Magic */ -.highlight .il { color: #009999 } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/docs/_static/pygments/borland.css b/docs/_static/pygments/borland.css deleted file mode 100644 index 8a0621609..000000000 --- a/docs/_static/pygments/borland.css +++ /dev/null @@ -1,52 +0,0 @@ -.highlight .hll { background-color: #ffffcc } -.highlight { background: #ffffff; } -.highlight .c { color: #008800; font-style: italic } /* Comment */ -.highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */ -.highlight .k { color: #000080; font-weight: bold } /* Keyword */ -.highlight .ch { color: #008800; font-style: italic } /* Comment.Hashbang */ -.highlight .cm { color: #008800; font-style: italic } /* Comment.Multiline */ -.highlight .cp { color: #008080 } /* Comment.Preproc */ -.highlight .cpf { color: #008800; font-style: italic } /* Comment.PreprocFile */ -.highlight .c1 { color: #008800; font-style: italic } /* Comment.Single */ -.highlight .cs { color: #008800; font-weight: bold } /* Comment.Special */ -.highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */ -.highlight .ge { font-style: italic } /* Generic.Emph */ -.highlight .gr { color: #aa0000 } /* Generic.Error */ -.highlight .gh { color: #999999 } /* Generic.Heading */ -.highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */ -.highlight .go { color: #888888 } /* Generic.Output */ -.highlight .gp { color: #555555 } /* Generic.Prompt */ -.highlight .gs { font-weight: bold } /* Generic.Strong */ -.highlight .gu { color: #aaaaaa } /* Generic.Subheading */ -.highlight .gt { color: #aa0000 } /* Generic.Traceback */ -.highlight .kc { color: #000080; font-weight: bold } /* Keyword.Constant */ -.highlight .kd { color: #000080; font-weight: bold } /* Keyword.Declaration */ -.highlight .kn { color: #000080; font-weight: bold } /* Keyword.Namespace */ -.highlight .kp { color: #000080; font-weight: bold } /* Keyword.Pseudo */ -.highlight .kr { color: #000080; font-weight: bold } /* Keyword.Reserved */ -.highlight .kt { color: #000080; font-weight: bold } /* Keyword.Type */ -.highlight .m { color: #0000FF } /* Literal.Number */ -.highlight .s { color: #0000FF } /* Literal.String */ -.highlight .na { color: #FF0000 } /* Name.Attribute */ -.highlight .nt { color: #000080; font-weight: bold } /* Name.Tag */ -.highlight .ow { font-weight: bold } /* Operator.Word */ -.highlight .w { color: #bbbbbb } /* Text.Whitespace */ -.highlight .mb { color: #0000FF } /* Literal.Number.Bin */ -.highlight .mf { color: #0000FF } /* Literal.Number.Float */ -.highlight .mh { color: #0000FF } /* Literal.Number.Hex */ -.highlight .mi { color: #0000FF } /* Literal.Number.Integer */ -.highlight .mo { color: #0000FF } /* Literal.Number.Oct */ -.highlight .sa { color: #0000FF } /* Literal.String.Affix */ -.highlight .sb { color: #0000FF } /* Literal.String.Backtick */ -.highlight .sc { color: #800080 } /* Literal.String.Char */ -.highlight .dl { color: #0000FF } /* Literal.String.Delimiter */ -.highlight .sd { color: #0000FF } /* Literal.String.Doc */ -.highlight .s2 { color: #0000FF } /* Literal.String.Double */ -.highlight .se { color: #0000FF } /* Literal.String.Escape */ -.highlight .sh { color: #0000FF } /* Literal.String.Heredoc */ -.highlight .si { color: #0000FF } /* Literal.String.Interpol */ -.highlight .sx { color: #0000FF } /* Literal.String.Other */ -.highlight .sr { color: #0000FF } /* Literal.String.Regex */ -.highlight .s1 { color: #0000FF } /* Literal.String.Single */ -.highlight .ss { color: #0000FF } /* Literal.String.Symbol */ -.highlight .il { color: #0000FF } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/docs/_static/pygments/bw.css b/docs/_static/pygments/bw.css deleted file mode 100644 index 397d53657..000000000 --- a/docs/_static/pygments/bw.css +++ /dev/null @@ -1,39 +0,0 @@ -.highlight .hll { background-color: #ffffcc } -.highlight { background: #ffffff; } -.highlight .c { font-style: italic } /* Comment */ -.highlight .err { border: 1px solid #FF0000 } /* Error */ -.highlight .k { font-weight: bold } /* Keyword */ -.highlight .ch { font-style: italic } /* Comment.Hashbang */ -.highlight .cm { font-style: italic } /* Comment.Multiline */ -.highlight .cpf { font-style: italic } /* Comment.PreprocFile */ -.highlight .c1 { font-style: italic } /* Comment.Single */ -.highlight .cs { font-style: italic } /* Comment.Special */ -.highlight .ge { font-style: italic } /* Generic.Emph */ -.highlight .gh { font-weight: bold } /* Generic.Heading */ -.highlight .gp { font-weight: bold } /* Generic.Prompt */ -.highlight .gs { font-weight: bold } /* Generic.Strong */ -.highlight .gu { font-weight: bold } /* Generic.Subheading */ -.highlight .kc { font-weight: bold } /* Keyword.Constant */ -.highlight .kd { font-weight: bold } /* Keyword.Declaration */ -.highlight .kn { font-weight: bold } /* Keyword.Namespace */ -.highlight .kr { font-weight: bold } /* Keyword.Reserved */ -.highlight .s { font-style: italic } /* Literal.String */ -.highlight .nc { font-weight: bold } /* Name.Class */ -.highlight .ni { font-weight: bold } /* Name.Entity */ -.highlight .ne { font-weight: bold } /* Name.Exception */ -.highlight .nn { font-weight: bold } /* Name.Namespace */ -.highlight .nt { font-weight: bold } /* Name.Tag */ -.highlight .ow { font-weight: bold } /* Operator.Word */ -.highlight .sa { font-style: italic } /* Literal.String.Affix */ -.highlight .sb { font-style: italic } /* Literal.String.Backtick */ -.highlight .sc { font-style: italic } /* Literal.String.Char */ -.highlight .dl { font-style: italic } /* Literal.String.Delimiter */ -.highlight .sd { font-style: italic } /* Literal.String.Doc */ -.highlight .s2 { font-style: italic } /* Literal.String.Double */ -.highlight .se { font-weight: bold; font-style: italic } /* Literal.String.Escape */ -.highlight .sh { font-style: italic } /* Literal.String.Heredoc */ -.highlight .si { font-weight: bold; font-style: italic } /* Literal.String.Interpol */ -.highlight .sx { font-style: italic } /* Literal.String.Other */ -.highlight .sr { font-style: italic } /* Literal.String.Regex */ -.highlight .s1 { font-style: italic } /* Literal.String.Single */ -.highlight .ss { font-style: italic } /* Literal.String.Symbol */ \ No newline at end of file diff --git a/docs/_static/pygments/colorful.css b/docs/_static/pygments/colorful.css deleted file mode 100644 index ea02512ed..000000000 --- a/docs/_static/pygments/colorful.css +++ /dev/null @@ -1,69 +0,0 @@ -.highlight .hll { background-color: #ffffcc } -.highlight { background: #ffffff; } -.highlight .c { color: #888888 } /* Comment */ -.highlight .err { color: #FF0000; background-color: #FFAAAA } /* Error */ -.highlight .k { color: #008800; font-weight: bold } /* Keyword */ -.highlight .o { color: #333333 } /* Operator */ -.highlight .ch { color: #888888 } /* Comment.Hashbang */ -.highlight .cm { color: #888888 } /* Comment.Multiline */ -.highlight .cp { color: #557799 } /* Comment.Preproc */ -.highlight .cpf { color: #888888 } /* Comment.PreprocFile */ -.highlight .c1 { color: #888888 } /* Comment.Single */ -.highlight .cs { color: #cc0000; font-weight: bold } /* Comment.Special */ -.highlight .gd { color: #A00000 } /* Generic.Deleted */ -.highlight .ge { font-style: italic } /* Generic.Emph */ -.highlight .gr { color: #FF0000 } /* Generic.Error */ -.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ -.highlight .gi { color: #00A000 } /* Generic.Inserted */ -.highlight .go { color: #888888 } /* Generic.Output */ -.highlight .gp { color: #c65d09; font-weight: bold } /* Generic.Prompt */ -.highlight .gs { font-weight: bold } /* Generic.Strong */ -.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ -.highlight .gt { color: #0044DD } /* Generic.Traceback */ -.highlight .kc { color: #008800; font-weight: bold } /* Keyword.Constant */ -.highlight .kd { color: #008800; font-weight: bold } /* Keyword.Declaration */ -.highlight .kn { color: #008800; font-weight: bold } /* Keyword.Namespace */ -.highlight .kp { color: #003388; font-weight: bold } /* Keyword.Pseudo */ -.highlight .kr { color: #008800; font-weight: bold } /* Keyword.Reserved */ -.highlight .kt { color: #333399; font-weight: bold } /* Keyword.Type */ -.highlight .m { color: #6600EE; font-weight: bold } /* Literal.Number */ -.highlight .s { background-color: #fff0f0 } /* Literal.String */ -.highlight .na { color: #0000CC } /* Name.Attribute */ -.highlight .nb { color: #007020 } /* Name.Builtin */ -.highlight .nc { color: #BB0066; font-weight: bold } /* Name.Class */ -.highlight .no { color: #003366; font-weight: bold } /* Name.Constant */ -.highlight .nd { color: #555555; font-weight: bold } /* Name.Decorator */ -.highlight .ni { color: #880000; font-weight: bold } /* Name.Entity */ -.highlight .ne { color: #FF0000; font-weight: bold } /* Name.Exception */ -.highlight .nf { color: #0066BB; font-weight: bold } /* Name.Function */ -.highlight .nl { color: #997700; font-weight: bold } /* Name.Label */ -.highlight .nn { color: #0e84b5; font-weight: bold } /* Name.Namespace */ -.highlight .nt { color: #007700 } /* Name.Tag */ -.highlight .nv { color: #996633 } /* Name.Variable */ -.highlight .ow { color: #000000; font-weight: bold } /* Operator.Word */ -.highlight .w { color: #bbbbbb } /* Text.Whitespace */ -.highlight .mb { color: #6600EE; font-weight: bold } /* Literal.Number.Bin */ -.highlight .mf { color: #6600EE; font-weight: bold } /* Literal.Number.Float */ -.highlight .mh { color: #005588; font-weight: bold } /* Literal.Number.Hex */ -.highlight .mi { color: #0000DD; font-weight: bold } /* Literal.Number.Integer */ -.highlight .mo { color: #4400EE; font-weight: bold } /* Literal.Number.Oct */ -.highlight .sa { background-color: #fff0f0 } /* Literal.String.Affix */ -.highlight .sb { background-color: #fff0f0 } /* Literal.String.Backtick */ -.highlight .sc { color: #0044DD } /* Literal.String.Char */ -.highlight .dl { background-color: #fff0f0 } /* Literal.String.Delimiter */ -.highlight .sd { color: #DD4422 } /* Literal.String.Doc */ -.highlight .s2 { background-color: #fff0f0 } /* Literal.String.Double */ -.highlight .se { color: #666666; font-weight: bold; background-color: #fff0f0 } /* Literal.String.Escape */ -.highlight .sh { background-color: #fff0f0 } /* Literal.String.Heredoc */ -.highlight .si { background-color: #eeeeee } /* Literal.String.Interpol */ -.highlight .sx { color: #DD2200; background-color: #fff0f0 } /* Literal.String.Other */ -.highlight .sr { color: #000000; background-color: #fff0ff } /* Literal.String.Regex */ -.highlight .s1 { background-color: #fff0f0 } /* Literal.String.Single */ -.highlight .ss { color: #AA6600 } /* Literal.String.Symbol */ -.highlight .bp { color: #007020 } /* Name.Builtin.Pseudo */ -.highlight .fm { color: #0066BB; font-weight: bold } /* Name.Function.Magic */ -.highlight .vc { color: #336699 } /* Name.Variable.Class */ -.highlight .vg { color: #dd7700; font-weight: bold } /* Name.Variable.Global */ -.highlight .vi { color: #3333BB } /* Name.Variable.Instance */ -.highlight .vm { color: #996633 } /* Name.Variable.Magic */ -.highlight .il { color: #0000DD; font-weight: bold } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/docs/_static/pygments/default.css b/docs/_static/pygments/default.css deleted file mode 100644 index 631bc92ff..000000000 --- a/docs/_static/pygments/default.css +++ /dev/null @@ -1,69 +0,0 @@ -.highlight .hll { background-color: #ffffcc } -.highlight { background: #f8f8f8; } -.highlight .c { color: #408080; font-style: italic } /* Comment */ -.highlight .err { border: 1px solid #FF0000 } /* Error */ -.highlight .k { color: #008000; font-weight: bold } /* Keyword */ -.highlight .o { color: #666666 } /* Operator */ -.highlight .ch { color: #408080; font-style: italic } /* Comment.Hashbang */ -.highlight .cm { color: #408080; font-style: italic } /* Comment.Multiline */ -.highlight .cp { color: #BC7A00 } /* Comment.Preproc */ -.highlight .cpf { color: #408080; font-style: italic } /* Comment.PreprocFile */ -.highlight .c1 { color: #408080; font-style: italic } /* Comment.Single */ -.highlight .cs { color: #408080; font-style: italic } /* Comment.Special */ -.highlight .gd { color: #A00000 } /* Generic.Deleted */ -.highlight .ge { font-style: italic } /* Generic.Emph */ -.highlight .gr { color: #FF0000 } /* Generic.Error */ -.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ -.highlight .gi { color: #00A000 } /* Generic.Inserted */ -.highlight .go { color: #888888 } /* Generic.Output */ -.highlight .gp { color: #000080; font-weight: bold } /* Generic.Prompt */ -.highlight .gs { font-weight: bold } /* Generic.Strong */ -.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ -.highlight .gt { color: #0044DD } /* Generic.Traceback */ -.highlight .kc { color: #008000; font-weight: bold } /* Keyword.Constant */ -.highlight .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */ -.highlight .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */ -.highlight .kp { color: #008000 } /* Keyword.Pseudo */ -.highlight .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */ -.highlight .kt { color: #B00040 } /* Keyword.Type */ -.highlight .m { color: #666666 } /* Literal.Number */ -.highlight .s { color: #BA2121 } /* Literal.String */ -.highlight .na { color: #7D9029 } /* Name.Attribute */ -.highlight .nb { color: #008000 } /* Name.Builtin */ -.highlight .nc { color: #0000FF; font-weight: bold } /* Name.Class */ -.highlight .no { color: #880000 } /* Name.Constant */ -.highlight .nd { color: #AA22FF } /* Name.Decorator */ -.highlight .ni { color: #999999; font-weight: bold } /* Name.Entity */ -.highlight .ne { color: #D2413A; font-weight: bold } /* Name.Exception */ -.highlight .nf { color: #0000FF } /* Name.Function */ -.highlight .nl { color: #A0A000 } /* Name.Label */ -.highlight .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */ -.highlight .nt { color: #008000; font-weight: bold } /* Name.Tag */ -.highlight .nv { color: #19177C } /* Name.Variable */ -.highlight .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */ -.highlight .w { color: #bbbbbb } /* Text.Whitespace */ -.highlight .mb { color: #666666 } /* Literal.Number.Bin */ -.highlight .mf { color: #666666 } /* Literal.Number.Float */ -.highlight .mh { color: #666666 } /* Literal.Number.Hex */ -.highlight .mi { color: #666666 } /* Literal.Number.Integer */ -.highlight .mo { color: #666666 } /* Literal.Number.Oct */ -.highlight .sa { color: #BA2121 } /* Literal.String.Affix */ -.highlight .sb { color: #BA2121 } /* Literal.String.Backtick */ -.highlight .sc { color: #BA2121 } /* Literal.String.Char */ -.highlight .dl { color: #BA2121 } /* Literal.String.Delimiter */ -.highlight .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */ -.highlight .s2 { color: #BA2121 } /* Literal.String.Double */ -.highlight .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */ -.highlight .sh { color: #BA2121 } /* Literal.String.Heredoc */ -.highlight .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */ -.highlight .sx { color: #008000 } /* Literal.String.Other */ -.highlight .sr { color: #BB6688 } /* Literal.String.Regex */ -.highlight .s1 { color: #BA2121 } /* Literal.String.Single */ -.highlight .ss { color: #19177C } /* Literal.String.Symbol */ -.highlight .bp { color: #008000 } /* Name.Builtin.Pseudo */ -.highlight .fm { color: #0000FF } /* Name.Function.Magic */ -.highlight .vc { color: #19177C } /* Name.Variable.Class */ -.highlight .vg { color: #19177C } /* Name.Variable.Global */ -.highlight .vi { color: #19177C } /* Name.Variable.Instance */ -.highlight .vm { color: #19177C } /* Name.Variable.Magic */ -.highlight .il { color: #666666 } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/docs/_static/pygments/emacs.css b/docs/_static/pygments/emacs.css deleted file mode 100644 index 918ae67eb..000000000 --- a/docs/_static/pygments/emacs.css +++ /dev/null @@ -1,69 +0,0 @@ -.highlight .hll { background-color: #ffffcc } -.highlight { background: #f8f8f8; } -.highlight .c { color: #008800; font-style: italic } /* Comment */ -.highlight .err { border: 1px solid #FF0000 } /* Error */ -.highlight .k { color: #AA22FF; font-weight: bold } /* Keyword */ -.highlight .o { color: #666666 } /* Operator */ -.highlight .ch { color: #008800; font-style: italic } /* Comment.Hashbang */ -.highlight .cm { color: #008800; font-style: italic } /* Comment.Multiline */ -.highlight .cp { color: #008800 } /* Comment.Preproc */ -.highlight .cpf { color: #008800; font-style: italic } /* Comment.PreprocFile */ -.highlight .c1 { color: #008800; font-style: italic } /* Comment.Single */ -.highlight .cs { color: #008800; font-weight: bold } /* Comment.Special */ -.highlight .gd { color: #A00000 } /* Generic.Deleted */ -.highlight .ge { font-style: italic } /* Generic.Emph */ -.highlight .gr { color: #FF0000 } /* Generic.Error */ -.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ -.highlight .gi { color: #00A000 } /* Generic.Inserted */ -.highlight .go { color: #888888 } /* Generic.Output */ -.highlight .gp { color: #000080; font-weight: bold } /* Generic.Prompt */ -.highlight .gs { font-weight: bold } /* Generic.Strong */ -.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ -.highlight .gt { color: #0044DD } /* Generic.Traceback */ -.highlight .kc { color: #AA22FF; font-weight: bold } /* Keyword.Constant */ -.highlight .kd { color: #AA22FF; font-weight: bold } /* Keyword.Declaration */ -.highlight .kn { color: #AA22FF; font-weight: bold } /* Keyword.Namespace */ -.highlight .kp { color: #AA22FF } /* Keyword.Pseudo */ -.highlight .kr { color: #AA22FF; font-weight: bold } /* Keyword.Reserved */ -.highlight .kt { color: #00BB00; font-weight: bold } /* Keyword.Type */ -.highlight .m { color: #666666 } /* Literal.Number */ -.highlight .s { color: #BB4444 } /* Literal.String */ -.highlight .na { color: #BB4444 } /* Name.Attribute */ -.highlight .nb { color: #AA22FF } /* Name.Builtin */ -.highlight .nc { color: #0000FF } /* Name.Class */ -.highlight .no { color: #880000 } /* Name.Constant */ -.highlight .nd { color: #AA22FF } /* Name.Decorator */ -.highlight .ni { color: #999999; font-weight: bold } /* Name.Entity */ -.highlight .ne { color: #D2413A; font-weight: bold } /* Name.Exception */ -.highlight .nf { color: #00A000 } /* Name.Function */ -.highlight .nl { color: #A0A000 } /* Name.Label */ -.highlight .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */ -.highlight .nt { color: #008000; font-weight: bold } /* Name.Tag */ -.highlight .nv { color: #B8860B } /* Name.Variable */ -.highlight .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */ -.highlight .w { color: #bbbbbb } /* Text.Whitespace */ -.highlight .mb { color: #666666 } /* Literal.Number.Bin */ -.highlight .mf { color: #666666 } /* Literal.Number.Float */ -.highlight .mh { color: #666666 } /* Literal.Number.Hex */ -.highlight .mi { color: #666666 } /* Literal.Number.Integer */ -.highlight .mo { color: #666666 } /* Literal.Number.Oct */ -.highlight .sa { color: #BB4444 } /* Literal.String.Affix */ -.highlight .sb { color: #BB4444 } /* Literal.String.Backtick */ -.highlight .sc { color: #BB4444 } /* Literal.String.Char */ -.highlight .dl { color: #BB4444 } /* Literal.String.Delimiter */ -.highlight .sd { color: #BB4444; font-style: italic } /* Literal.String.Doc */ -.highlight .s2 { color: #BB4444 } /* Literal.String.Double */ -.highlight .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */ -.highlight .sh { color: #BB4444 } /* Literal.String.Heredoc */ -.highlight .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */ -.highlight .sx { color: #008000 } /* Literal.String.Other */ -.highlight .sr { color: #BB6688 } /* Literal.String.Regex */ -.highlight .s1 { color: #BB4444 } /* Literal.String.Single */ -.highlight .ss { color: #B8860B } /* Literal.String.Symbol */ -.highlight .bp { color: #AA22FF } /* Name.Builtin.Pseudo */ -.highlight .fm { color: #00A000 } /* Name.Function.Magic */ -.highlight .vc { color: #B8860B } /* Name.Variable.Class */ -.highlight .vg { color: #B8860B } /* Name.Variable.Global */ -.highlight .vi { color: #B8860B } /* Name.Variable.Instance */ -.highlight .vm { color: #B8860B } /* Name.Variable.Magic */ -.highlight .il { color: #666666 } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/docs/_static/pygments/friendly.css b/docs/_static/pygments/friendly.css deleted file mode 100644 index 21d91784f..000000000 --- a/docs/_static/pygments/friendly.css +++ /dev/null @@ -1,69 +0,0 @@ -.highlight .hll { background-color: #ffffcc } -.highlight { background: #f0f0f0; } -.highlight .c { color: #60a0b0; font-style: italic } /* Comment */ -.highlight .err { border: 1px solid #FF0000 } /* Error */ -.highlight .k { color: #007020; font-weight: bold } /* Keyword */ -.highlight .o { color: #666666 } /* Operator */ -.highlight .ch { color: #60a0b0; font-style: italic } /* Comment.Hashbang */ -.highlight .cm { color: #60a0b0; font-style: italic } /* Comment.Multiline */ -.highlight .cp { color: #007020 } /* Comment.Preproc */ -.highlight .cpf { color: #60a0b0; font-style: italic } /* Comment.PreprocFile */ -.highlight .c1 { color: #60a0b0; font-style: italic } /* Comment.Single */ -.highlight .cs { color: #60a0b0; background-color: #fff0f0 } /* Comment.Special */ -.highlight .gd { color: #A00000 } /* Generic.Deleted */ -.highlight .ge { font-style: italic } /* Generic.Emph */ -.highlight .gr { color: #FF0000 } /* Generic.Error */ -.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ -.highlight .gi { color: #00A000 } /* Generic.Inserted */ -.highlight .go { color: #888888 } /* Generic.Output */ -.highlight .gp { color: #c65d09; font-weight: bold } /* Generic.Prompt */ -.highlight .gs { font-weight: bold } /* Generic.Strong */ -.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ -.highlight .gt { color: #0044DD } /* Generic.Traceback */ -.highlight .kc { color: #007020; font-weight: bold } /* Keyword.Constant */ -.highlight .kd { color: #007020; font-weight: bold } /* Keyword.Declaration */ -.highlight .kn { color: #007020; font-weight: bold } /* Keyword.Namespace */ -.highlight .kp { color: #007020 } /* Keyword.Pseudo */ -.highlight .kr { color: #007020; font-weight: bold } /* Keyword.Reserved */ -.highlight .kt { color: #902000 } /* Keyword.Type */ -.highlight .m { color: #40a070 } /* Literal.Number */ -.highlight .s { color: #4070a0 } /* Literal.String */ -.highlight .na { color: #4070a0 } /* Name.Attribute */ -.highlight .nb { color: #007020 } /* Name.Builtin */ -.highlight .nc { color: #0e84b5; font-weight: bold } /* Name.Class */ -.highlight .no { color: #60add5 } /* Name.Constant */ -.highlight .nd { color: #555555; font-weight: bold } /* Name.Decorator */ -.highlight .ni { color: #d55537; font-weight: bold } /* Name.Entity */ -.highlight .ne { color: #007020 } /* Name.Exception */ -.highlight .nf { color: #06287e } /* Name.Function */ -.highlight .nl { color: #002070; font-weight: bold } /* Name.Label */ -.highlight .nn { color: #0e84b5; font-weight: bold } /* Name.Namespace */ -.highlight .nt { color: #062873; font-weight: bold } /* Name.Tag */ -.highlight .nv { color: #bb60d5 } /* Name.Variable */ -.highlight .ow { color: #007020; font-weight: bold } /* Operator.Word */ -.highlight .w { color: #bbbbbb } /* Text.Whitespace */ -.highlight .mb { color: #40a070 } /* Literal.Number.Bin */ -.highlight .mf { color: #40a070 } /* Literal.Number.Float */ -.highlight .mh { color: #40a070 } /* Literal.Number.Hex */ -.highlight .mi { color: #40a070 } /* Literal.Number.Integer */ -.highlight .mo { color: #40a070 } /* Literal.Number.Oct */ -.highlight .sa { color: #4070a0 } /* Literal.String.Affix */ -.highlight .sb { color: #4070a0 } /* Literal.String.Backtick */ -.highlight .sc { color: #4070a0 } /* Literal.String.Char */ -.highlight .dl { color: #4070a0 } /* Literal.String.Delimiter */ -.highlight .sd { color: #4070a0; font-style: italic } /* Literal.String.Doc */ -.highlight .s2 { color: #4070a0 } /* Literal.String.Double */ -.highlight .se { color: #4070a0; font-weight: bold } /* Literal.String.Escape */ -.highlight .sh { color: #4070a0 } /* Literal.String.Heredoc */ -.highlight .si { color: #70a0d0; font-style: italic } /* Literal.String.Interpol */ -.highlight .sx { color: #c65d09 } /* Literal.String.Other */ -.highlight .sr { color: #235388 } /* Literal.String.Regex */ -.highlight .s1 { color: #4070a0 } /* Literal.String.Single */ -.highlight .ss { color: #517918 } /* Literal.String.Symbol */ -.highlight .bp { color: #007020 } /* Name.Builtin.Pseudo */ -.highlight .fm { color: #06287e } /* Name.Function.Magic */ -.highlight .vc { color: #bb60d5 } /* Name.Variable.Class */ -.highlight .vg { color: #bb60d5 } /* Name.Variable.Global */ -.highlight .vi { color: #bb60d5 } /* Name.Variable.Instance */ -.highlight .vm { color: #bb60d5 } /* Name.Variable.Magic */ -.highlight .il { color: #40a070 } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/docs/_static/pygments/fruity.css b/docs/_static/pygments/fruity.css deleted file mode 100644 index bac047b6a..000000000 --- a/docs/_static/pygments/fruity.css +++ /dev/null @@ -1,78 +0,0 @@ -.highlight .hll { background-color: #333333 } -.highlight { background: #111111; color: #ffffff } -.highlight .c { color: #008800; font-style: italic; background-color: #0f140f } /* Comment */ -.highlight .err { color: #ffffff } /* Error */ -.highlight .esc { color: #ffffff } /* Escape */ -.highlight .g { color: #ffffff } /* Generic */ -.highlight .k { color: #fb660a; font-weight: bold } /* Keyword */ -.highlight .l { color: #ffffff } /* Literal */ -.highlight .n { color: #ffffff } /* Name */ -.highlight .o { color: #ffffff } /* Operator */ -.highlight .x { color: #ffffff } /* Other */ -.highlight .p { color: #ffffff } /* Punctuation */ -.highlight .ch { color: #008800; font-style: italic; background-color: #0f140f } /* Comment.Hashbang */ -.highlight .cm { color: #008800; font-style: italic; background-color: #0f140f } /* Comment.Multiline */ -.highlight .cp { color: #ff0007; font-weight: bold; font-style: italic; background-color: #0f140f } /* Comment.Preproc */ -.highlight .cpf { color: #008800; font-style: italic; background-color: #0f140f } /* Comment.PreprocFile */ -.highlight .c1 { color: #008800; font-style: italic; background-color: #0f140f } /* Comment.Single */ -.highlight .cs { color: #008800; font-style: italic; background-color: #0f140f } /* Comment.Special */ -.highlight .gd { color: #ffffff } /* Generic.Deleted */ -.highlight .ge { color: #ffffff } /* Generic.Emph */ -.highlight .gr { color: #ffffff } /* Generic.Error */ -.highlight .gh { color: #ffffff; font-weight: bold } /* Generic.Heading */ -.highlight .gi { color: #ffffff } /* Generic.Inserted */ -.highlight .go { color: #444444; background-color: #222222 } /* Generic.Output */ -.highlight .gp { color: #ffffff } /* Generic.Prompt */ -.highlight .gs { color: #ffffff } /* Generic.Strong */ -.highlight .gu { color: #ffffff; font-weight: bold } /* Generic.Subheading */ -.highlight .gt { color: #ffffff } /* Generic.Traceback */ -.highlight .kc { color: #fb660a; font-weight: bold } /* Keyword.Constant */ -.highlight .kd { color: #fb660a; font-weight: bold } /* Keyword.Declaration */ -.highlight .kn { color: #fb660a; font-weight: bold } /* Keyword.Namespace */ -.highlight .kp { color: #fb660a } /* Keyword.Pseudo */ -.highlight .kr { color: #fb660a; font-weight: bold } /* Keyword.Reserved */ -.highlight .kt { color: #cdcaa9; font-weight: bold } /* Keyword.Type */ -.highlight .ld { color: #ffffff } /* Literal.Date */ -.highlight .m { color: #0086f7; font-weight: bold } /* Literal.Number */ -.highlight .s { color: #0086d2 } /* Literal.String */ -.highlight .na { color: #ff0086; font-weight: bold } /* Name.Attribute */ -.highlight .nb { color: #ffffff } /* Name.Builtin */ -.highlight .nc { color: #ffffff } /* Name.Class */ -.highlight .no { color: #0086d2 } /* Name.Constant */ -.highlight .nd { color: #ffffff } /* Name.Decorator */ -.highlight .ni { color: #ffffff } /* Name.Entity */ -.highlight .ne { color: #ffffff } /* Name.Exception */ -.highlight .nf { color: #ff0086; font-weight: bold } /* Name.Function */ -.highlight .nl { color: #ffffff } /* Name.Label */ -.highlight .nn { color: #ffffff } /* Name.Namespace */ -.highlight .nx { color: #ffffff } /* Name.Other */ -.highlight .py { color: #ffffff } /* Name.Property */ -.highlight .nt { color: #fb660a; font-weight: bold } /* Name.Tag */ -.highlight .nv { color: #fb660a } /* Name.Variable */ -.highlight .ow { color: #ffffff } /* Operator.Word */ -.highlight .w { color: #888888 } /* Text.Whitespace */ -.highlight .mb { color: #0086f7; font-weight: bold } /* Literal.Number.Bin */ -.highlight .mf { color: #0086f7; font-weight: bold } /* Literal.Number.Float */ -.highlight .mh { color: #0086f7; font-weight: bold } /* Literal.Number.Hex */ -.highlight .mi { color: #0086f7; font-weight: bold } /* Literal.Number.Integer */ -.highlight .mo { color: #0086f7; font-weight: bold } /* Literal.Number.Oct */ -.highlight .sa { color: #0086d2 } /* Literal.String.Affix */ -.highlight .sb { color: #0086d2 } /* Literal.String.Backtick */ -.highlight .sc { color: #0086d2 } /* Literal.String.Char */ -.highlight .dl { color: #0086d2 } /* Literal.String.Delimiter */ -.highlight .sd { color: #0086d2 } /* Literal.String.Doc */ -.highlight .s2 { color: #0086d2 } /* Literal.String.Double */ -.highlight .se { color: #0086d2 } /* Literal.String.Escape */ -.highlight .sh { color: #0086d2 } /* Literal.String.Heredoc */ -.highlight .si { color: #0086d2 } /* Literal.String.Interpol */ -.highlight .sx { color: #0086d2 } /* Literal.String.Other */ -.highlight .sr { color: #0086d2 } /* Literal.String.Regex */ -.highlight .s1 { color: #0086d2 } /* Literal.String.Single */ -.highlight .ss { color: #0086d2 } /* Literal.String.Symbol */ -.highlight .bp { color: #ffffff } /* Name.Builtin.Pseudo */ -.highlight .fm { color: #ff0086; font-weight: bold } /* Name.Function.Magic */ -.highlight .vc { color: #fb660a } /* Name.Variable.Class */ -.highlight .vg { color: #fb660a } /* Name.Variable.Global */ -.highlight .vi { color: #fb660a } /* Name.Variable.Instance */ -.highlight .vm { color: #fb660a } /* Name.Variable.Magic */ -.highlight .il { color: #0086f7; font-weight: bold } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/docs/_static/pygments/igor.css b/docs/_static/pygments/igor.css deleted file mode 100644 index ec12005ab..000000000 --- a/docs/_static/pygments/igor.css +++ /dev/null @@ -1,34 +0,0 @@ -.highlight .hll { background-color: #ffffcc } -.highlight { background: #ffffff; } -.highlight .c { color: #FF0000; font-style: italic } /* Comment */ -.highlight .k { color: #0000FF } /* Keyword */ -.highlight .ch { color: #FF0000; font-style: italic } /* Comment.Hashbang */ -.highlight .cm { color: #FF0000; font-style: italic } /* Comment.Multiline */ -.highlight .cp { color: #FF0000; font-style: italic } /* Comment.Preproc */ -.highlight .cpf { color: #FF0000; font-style: italic } /* Comment.PreprocFile */ -.highlight .c1 { color: #FF0000; font-style: italic } /* Comment.Single */ -.highlight .cs { color: #FF0000; font-style: italic } /* Comment.Special */ -.highlight .kc { color: #0000FF } /* Keyword.Constant */ -.highlight .kd { color: #0000FF } /* Keyword.Declaration */ -.highlight .kn { color: #0000FF } /* Keyword.Namespace */ -.highlight .kp { color: #0000FF } /* Keyword.Pseudo */ -.highlight .kr { color: #0000FF } /* Keyword.Reserved */ -.highlight .kt { color: #0000FF } /* Keyword.Type */ -.highlight .s { color: #009C00 } /* Literal.String */ -.highlight .nc { color: #007575 } /* Name.Class */ -.highlight .nd { color: #CC00A3 } /* Name.Decorator */ -.highlight .nf { color: #C34E00 } /* Name.Function */ -.highlight .sa { color: #009C00 } /* Literal.String.Affix */ -.highlight .sb { color: #009C00 } /* Literal.String.Backtick */ -.highlight .sc { color: #009C00 } /* Literal.String.Char */ -.highlight .dl { color: #009C00 } /* Literal.String.Delimiter */ -.highlight .sd { color: #009C00 } /* Literal.String.Doc */ -.highlight .s2 { color: #009C00 } /* Literal.String.Double */ -.highlight .se { color: #009C00 } /* Literal.String.Escape */ -.highlight .sh { color: #009C00 } /* Literal.String.Heredoc */ -.highlight .si { color: #009C00 } /* Literal.String.Interpol */ -.highlight .sx { color: #009C00 } /* Literal.String.Other */ -.highlight .sr { color: #009C00 } /* Literal.String.Regex */ -.highlight .s1 { color: #009C00 } /* Literal.String.Single */ -.highlight .ss { color: #009C00 } /* Literal.String.Symbol */ -.highlight .fm { color: #C34E00 } /* Name.Function.Magic */ \ No newline at end of file diff --git a/docs/_static/pygments/inkpot.css b/docs/_static/pygments/inkpot.css deleted file mode 100644 index 0a8bdd6de..000000000 --- a/docs/_static/pygments/inkpot.css +++ /dev/null @@ -1,74 +0,0 @@ -.highlight .hll { background-color: #ffffcc } -.highlight { background: #1e1e27; color: #cfbfad } -.highlight .c { color: #cd8b00 } /* Comment */ -.highlight .err { color: #ffffff; background-color: #6e2e2e } /* Error */ -.highlight .k { color: #808bed } /* Keyword */ -.highlight .n { color: #cfbfad } /* Name */ -.highlight .o { color: #666666 } /* Operator */ -.highlight .x { color: #cfbfad } /* Other */ -.highlight .p { color: #cfbfad } /* Punctuation */ -.highlight .ch { color: #cd8b00 } /* Comment.Hashbang */ -.highlight .cm { color: #cd8b00 } /* Comment.Multiline */ -.highlight .cp { color: #409090 } /* Comment.Preproc */ -.highlight .cpf { color: #ffcd8b; background-color: #404040 } /* Comment.PreprocFile */ -.highlight .c1 { color: #cd8b00 } /* Comment.Single */ -.highlight .cs { color: #808bed } /* Comment.Special */ -.highlight .gd { color: #A00000 } /* Generic.Deleted */ -.highlight .ge { font-style: italic } /* Generic.Emph */ -.highlight .gr { color: #FF0000 } /* Generic.Error */ -.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ -.highlight .gi { color: #00A000 } /* Generic.Inserted */ -.highlight .go { color: #888888 } /* Generic.Output */ -.highlight .gp { color: #000080; font-weight: bold } /* Generic.Prompt */ -.highlight .gs { font-weight: bold } /* Generic.Strong */ -.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ -.highlight .gt { color: #0044DD } /* Generic.Traceback */ -.highlight .kc { color: #808bed } /* Keyword.Constant */ -.highlight .kd { color: #808bed } /* Keyword.Declaration */ -.highlight .kn { color: #808bed } /* Keyword.Namespace */ -.highlight .kp { color: #808bed } /* Keyword.Pseudo */ -.highlight .kr { color: #808bed } /* Keyword.Reserved */ -.highlight .kt { color: #ff8bff } /* Keyword.Type */ -.highlight .m { color: #f0ad6d } /* Literal.Number */ -.highlight .s { color: #ffcd8b; background-color: #404040 } /* Literal.String */ -.highlight .na { color: #cfbfad } /* Name.Attribute */ -.highlight .nb { color: #808bed } /* Name.Builtin */ -.highlight .nc { color: #ff8bff } /* Name.Class */ -.highlight .no { color: #409090 } /* Name.Constant */ -.highlight .nd { color: #409090 } /* Name.Decorator */ -.highlight .ni { color: #cfbfad } /* Name.Entity */ -.highlight .ne { color: #ff0000 } /* Name.Exception */ -.highlight .nf { color: #c080d0 } /* Name.Function */ -.highlight .nl { color: #808bed } /* Name.Label */ -.highlight .nn { color: #ff0000 } /* Name.Namespace */ -.highlight .nx { color: #cfbfad } /* Name.Other */ -.highlight .py { color: #cfbfad } /* Name.Property */ -.highlight .nt { color: #cfbfad } /* Name.Tag */ -.highlight .nv { color: #cfbfad } /* Name.Variable */ -.highlight .ow { color: #666666 } /* Operator.Word */ -.highlight .w { color: #434357 } /* Text.Whitespace */ -.highlight .mb { color: #f0ad6d } /* Literal.Number.Bin */ -.highlight .mf { color: #f0ad6d } /* Literal.Number.Float */ -.highlight .mh { color: #f0ad6d } /* Literal.Number.Hex */ -.highlight .mi { color: #f0ad6d } /* Literal.Number.Integer */ -.highlight .mo { color: #f0ad6d } /* Literal.Number.Oct */ -.highlight .sa { color: #ffcd8b; background-color: #404040 } /* Literal.String.Affix */ -.highlight .sb { color: #ffcd8b; background-color: #404040 } /* Literal.String.Backtick */ -.highlight .sc { color: #ffcd8b; background-color: #404040 } /* Literal.String.Char */ -.highlight .dl { color: #ffcd8b; background-color: #404040 } /* Literal.String.Delimiter */ -.highlight .sd { color: #808bed; background-color: #404040 } /* Literal.String.Doc */ -.highlight .s2 { color: #ffcd8b; background-color: #404040 } /* Literal.String.Double */ -.highlight .se { color: #ffcd8b; background-color: #404040 } /* Literal.String.Escape */ -.highlight .sh { color: #ffcd8b; background-color: #404040 } /* Literal.String.Heredoc */ -.highlight .si { color: #ffcd8b; background-color: #404040 } /* Literal.String.Interpol */ -.highlight .sx { color: #ffcd8b; background-color: #404040 } /* Literal.String.Other */ -.highlight .sr { color: #ffcd8b; background-color: #404040 } /* Literal.String.Regex */ -.highlight .s1 { color: #ffcd8b; background-color: #404040 } /* Literal.String.Single */ -.highlight .ss { color: #ffcd8b; background-color: #404040 } /* Literal.String.Symbol */ -.highlight .bp { color: #ffff00 } /* Name.Builtin.Pseudo */ -.highlight .fm { color: #c080d0 } /* Name.Function.Magic */ -.highlight .vc { color: #cfbfad } /* Name.Variable.Class */ -.highlight .vg { color: #cfbfad } /* Name.Variable.Global */ -.highlight .vi { color: #cfbfad } /* Name.Variable.Instance */ -.highlight .vm { color: #cfbfad } /* Name.Variable.Magic */ -.highlight .il { color: #f0ad6d } /* Literal.Number.Integer.Long */ diff --git a/docs/_static/pygments/lovelace.css b/docs/_static/pygments/lovelace.css deleted file mode 100644 index 1ddb28603..000000000 --- a/docs/_static/pygments/lovelace.css +++ /dev/null @@ -1,70 +0,0 @@ -.highlight .hll { background-color: #ffffcc } -.highlight { background: #ffffff; } -.highlight .c { color: #888888; font-style: italic } /* Comment */ -.highlight .err { background-color: #a848a8 } /* Error */ -.highlight .k { color: #2838b0 } /* Keyword */ -.highlight .o { color: #666666 } /* Operator */ -.highlight .p { color: #888888 } /* Punctuation */ -.highlight .ch { color: #287088; font-style: italic } /* Comment.Hashbang */ -.highlight .cm { color: #888888; font-style: italic } /* Comment.Multiline */ -.highlight .cp { color: #289870 } /* Comment.Preproc */ -.highlight .cpf { color: #888888; font-style: italic } /* Comment.PreprocFile */ -.highlight .c1 { color: #888888; font-style: italic } /* Comment.Single */ -.highlight .cs { color: #888888; font-style: italic } /* Comment.Special */ -.highlight .gd { color: #c02828 } /* Generic.Deleted */ -.highlight .ge { font-style: italic } /* Generic.Emph */ -.highlight .gr { color: #c02828 } /* Generic.Error */ -.highlight .gh { color: #666666 } /* Generic.Heading */ -.highlight .gi { color: #388038 } /* Generic.Inserted */ -.highlight .go { color: #666666 } /* Generic.Output */ -.highlight .gp { color: #444444 } /* Generic.Prompt */ -.highlight .gs { font-weight: bold } /* Generic.Strong */ -.highlight .gu { color: #444444 } /* Generic.Subheading */ -.highlight .gt { color: #2838b0 } /* Generic.Traceback */ -.highlight .kc { color: #444444; font-style: italic } /* Keyword.Constant */ -.highlight .kd { color: #2838b0; font-style: italic } /* Keyword.Declaration */ -.highlight .kn { color: #2838b0 } /* Keyword.Namespace */ -.highlight .kp { color: #2838b0 } /* Keyword.Pseudo */ -.highlight .kr { color: #2838b0 } /* Keyword.Reserved */ -.highlight .kt { color: #2838b0; font-style: italic } /* Keyword.Type */ -.highlight .m { color: #444444 } /* Literal.Number */ -.highlight .s { color: #b83838 } /* Literal.String */ -.highlight .na { color: #388038 } /* Name.Attribute */ -.highlight .nb { color: #388038 } /* Name.Builtin */ -.highlight .nc { color: #287088 } /* Name.Class */ -.highlight .no { color: #b85820 } /* Name.Constant */ -.highlight .nd { color: #287088 } /* Name.Decorator */ -.highlight .ni { color: #709030 } /* Name.Entity */ -.highlight .ne { color: #908828 } /* Name.Exception */ -.highlight .nf { color: #785840 } /* Name.Function */ -.highlight .nl { color: #289870 } /* Name.Label */ -.highlight .nn { color: #289870 } /* Name.Namespace */ -.highlight .nt { color: #2838b0 } /* Name.Tag */ -.highlight .nv { color: #b04040 } /* Name.Variable */ -.highlight .ow { color: #a848a8 } /* Operator.Word */ -.highlight .w { color: #a89028 } /* Text.Whitespace */ -.highlight .mb { color: #444444 } /* Literal.Number.Bin */ -.highlight .mf { color: #444444 } /* Literal.Number.Float */ -.highlight .mh { color: #444444 } /* Literal.Number.Hex */ -.highlight .mi { color: #444444 } /* Literal.Number.Integer */ -.highlight .mo { color: #444444 } /* Literal.Number.Oct */ -.highlight .sa { color: #444444 } /* Literal.String.Affix */ -.highlight .sb { color: #b83838 } /* Literal.String.Backtick */ -.highlight .sc { color: #a848a8 } /* Literal.String.Char */ -.highlight .dl { color: #b85820 } /* Literal.String.Delimiter */ -.highlight .sd { color: #b85820; font-style: italic } /* Literal.String.Doc */ -.highlight .s2 { color: #b83838 } /* Literal.String.Double */ -.highlight .se { color: #709030 } /* Literal.String.Escape */ -.highlight .sh { color: #b83838 } /* Literal.String.Heredoc */ -.highlight .si { color: #b83838; text-decoration: underline } /* Literal.String.Interpol */ -.highlight .sx { color: #a848a8 } /* Literal.String.Other */ -.highlight .sr { color: #a848a8 } /* Literal.String.Regex */ -.highlight .s1 { color: #b83838 } /* Literal.String.Single */ -.highlight .ss { color: #b83838 } /* Literal.String.Symbol */ -.highlight .bp { color: #388038; font-style: italic } /* Name.Builtin.Pseudo */ -.highlight .fm { color: #b85820 } /* Name.Function.Magic */ -.highlight .vc { color: #b04040 } /* Name.Variable.Class */ -.highlight .vg { color: #908828 } /* Name.Variable.Global */ -.highlight .vi { color: #b04040 } /* Name.Variable.Instance */ -.highlight .vm { color: #b85820 } /* Name.Variable.Magic */ -.highlight .il { color: #444444 } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/docs/_static/pygments/manni.css b/docs/_static/pygments/manni.css deleted file mode 100644 index 143bc8ae3..000000000 --- a/docs/_static/pygments/manni.css +++ /dev/null @@ -1,69 +0,0 @@ -.highlight .hll { background-color: #ffffcc } -.highlight { background: #f0f3f3; } -.highlight .c { color: #0099FF; font-style: italic } /* Comment */ -.highlight .err { color: #AA0000; background-color: #FFAAAA } /* Error */ -.highlight .k { color: #006699; font-weight: bold } /* Keyword */ -.highlight .o { color: #555555 } /* Operator */ -.highlight .ch { color: #0099FF; font-style: italic } /* Comment.Hashbang */ -.highlight .cm { color: #0099FF; font-style: italic } /* Comment.Multiline */ -.highlight .cp { color: #009999 } /* Comment.Preproc */ -.highlight .cpf { color: #0099FF; font-style: italic } /* Comment.PreprocFile */ -.highlight .c1 { color: #0099FF; font-style: italic } /* Comment.Single */ -.highlight .cs { color: #0099FF; font-weight: bold; font-style: italic } /* Comment.Special */ -.highlight .gd { background-color: #FFCCCC; border: 1px solid #CC0000 } /* Generic.Deleted */ -.highlight .ge { font-style: italic } /* Generic.Emph */ -.highlight .gr { color: #FF0000 } /* Generic.Error */ -.highlight .gh { color: #003300; font-weight: bold } /* Generic.Heading */ -.highlight .gi { background-color: #CCFFCC; border: 1px solid #00CC00 } /* Generic.Inserted */ -.highlight .go { color: #AAAAAA } /* Generic.Output */ -.highlight .gp { color: #000099; font-weight: bold } /* Generic.Prompt */ -.highlight .gs { font-weight: bold } /* Generic.Strong */ -.highlight .gu { color: #003300; font-weight: bold } /* Generic.Subheading */ -.highlight .gt { color: #99CC66 } /* Generic.Traceback */ -.highlight .kc { color: #006699; font-weight: bold } /* Keyword.Constant */ -.highlight .kd { color: #006699; font-weight: bold } /* Keyword.Declaration */ -.highlight .kn { color: #006699; font-weight: bold } /* Keyword.Namespace */ -.highlight .kp { color: #006699 } /* Keyword.Pseudo */ -.highlight .kr { color: #006699; font-weight: bold } /* Keyword.Reserved */ -.highlight .kt { color: #007788; font-weight: bold } /* Keyword.Type */ -.highlight .m { color: #FF6600 } /* Literal.Number */ -.highlight .s { color: #CC3300 } /* Literal.String */ -.highlight .na { color: #330099 } /* Name.Attribute */ -.highlight .nb { color: #336666 } /* Name.Builtin */ -.highlight .nc { color: #00AA88; font-weight: bold } /* Name.Class */ -.highlight .no { color: #336600 } /* Name.Constant */ -.highlight .nd { color: #9999FF } /* Name.Decorator */ -.highlight .ni { color: #999999; font-weight: bold } /* Name.Entity */ -.highlight .ne { color: #CC0000; font-weight: bold } /* Name.Exception */ -.highlight .nf { color: #CC00FF } /* Name.Function */ -.highlight .nl { color: #9999FF } /* Name.Label */ -.highlight .nn { color: #00CCFF; font-weight: bold } /* Name.Namespace */ -.highlight .nt { color: #330099; font-weight: bold } /* Name.Tag */ -.highlight .nv { color: #003333 } /* Name.Variable */ -.highlight .ow { color: #000000; font-weight: bold } /* Operator.Word */ -.highlight .w { color: #bbbbbb } /* Text.Whitespace */ -.highlight .mb { color: #FF6600 } /* Literal.Number.Bin */ -.highlight .mf { color: #FF6600 } /* Literal.Number.Float */ -.highlight .mh { color: #FF6600 } /* Literal.Number.Hex */ -.highlight .mi { color: #FF6600 } /* Literal.Number.Integer */ -.highlight .mo { color: #FF6600 } /* Literal.Number.Oct */ -.highlight .sa { color: #CC3300 } /* Literal.String.Affix */ -.highlight .sb { color: #CC3300 } /* Literal.String.Backtick */ -.highlight .sc { color: #CC3300 } /* Literal.String.Char */ -.highlight .dl { color: #CC3300 } /* Literal.String.Delimiter */ -.highlight .sd { color: #CC3300; font-style: italic } /* Literal.String.Doc */ -.highlight .s2 { color: #CC3300 } /* Literal.String.Double */ -.highlight .se { color: #CC3300; font-weight: bold } /* Literal.String.Escape */ -.highlight .sh { color: #CC3300 } /* Literal.String.Heredoc */ -.highlight .si { color: #AA0000 } /* Literal.String.Interpol */ -.highlight .sx { color: #CC3300 } /* Literal.String.Other */ -.highlight .sr { color: #33AAAA } /* Literal.String.Regex */ -.highlight .s1 { color: #CC3300 } /* Literal.String.Single */ -.highlight .ss { color: #FFCC33 } /* Literal.String.Symbol */ -.highlight .bp { color: #336666 } /* Name.Builtin.Pseudo */ -.highlight .fm { color: #CC00FF } /* Name.Function.Magic */ -.highlight .vc { color: #003333 } /* Name.Variable.Class */ -.highlight .vg { color: #003333 } /* Name.Variable.Global */ -.highlight .vi { color: #003333 } /* Name.Variable.Instance */ -.highlight .vm { color: #003333 } /* Name.Variable.Magic */ -.highlight .il { color: #FF6600 } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/docs/_static/pygments/monokai.css b/docs/_static/pygments/monokai.css deleted file mode 100644 index b6925c2b7..000000000 --- a/docs/_static/pygments/monokai.css +++ /dev/null @@ -1,70 +0,0 @@ -.highlight .hll { background-color: #49483e } -.highlight { background: #272822; color: #f8f8f2 } -.highlight .c { color: #75715e } /* Comment */ -.highlight .err { color: #960050; background-color: #1e0010 } /* Error */ -.highlight .k { color: #66d9ef } /* Keyword */ -.highlight .l { color: #ae81ff } /* Literal */ -.highlight .n { color: #f8f8f2 } /* Name */ -.highlight .o { color: #f92672 } /* Operator */ -.highlight .p { color: #f8f8f2 } /* Punctuation */ -.highlight .ch { color: #75715e } /* Comment.Hashbang */ -.highlight .cm { color: #75715e } /* Comment.Multiline */ -.highlight .cp { color: #75715e } /* Comment.Preproc */ -.highlight .cpf { color: #75715e } /* Comment.PreprocFile */ -.highlight .c1 { color: #75715e } /* Comment.Single */ -.highlight .cs { color: #75715e } /* Comment.Special */ -.highlight .gd { color: #f92672 } /* Generic.Deleted */ -.highlight .ge { font-style: italic } /* Generic.Emph */ -.highlight .gi { color: #a6e22e } /* Generic.Inserted */ -.highlight .gs { font-weight: bold } /* Generic.Strong */ -.highlight .gu { color: #75715e } /* Generic.Subheading */ -.highlight .kc { color: #66d9ef } /* Keyword.Constant */ -.highlight .kd { color: #66d9ef } /* Keyword.Declaration */ -.highlight .kn { color: #f92672 } /* Keyword.Namespace */ -.highlight .kp { color: #66d9ef } /* Keyword.Pseudo */ -.highlight .kr { color: #66d9ef } /* Keyword.Reserved */ -.highlight .kt { color: #66d9ef } /* Keyword.Type */ -.highlight .ld { color: #e6db74 } /* Literal.Date */ -.highlight .m { color: #ae81ff } /* Literal.Number */ -.highlight .s { color: #e6db74 } /* Literal.String */ -.highlight .na { color: #a6e22e } /* Name.Attribute */ -.highlight .nb { color: #f8f8f2 } /* Name.Builtin */ -.highlight .nc { color: #a6e22e } /* Name.Class */ -.highlight .no { color: #66d9ef } /* Name.Constant */ -.highlight .nd { color: #a6e22e } /* Name.Decorator */ -.highlight .ni { color: #f8f8f2 } /* Name.Entity */ -.highlight .ne { color: #a6e22e } /* Name.Exception */ -.highlight .nf { color: #a6e22e } /* Name.Function */ -.highlight .nl { color: #f8f8f2 } /* Name.Label */ -.highlight .nn { color: #f8f8f2 } /* Name.Namespace */ -.highlight .nx { color: #a6e22e } /* Name.Other */ -.highlight .py { color: #f8f8f2 } /* Name.Property */ -.highlight .nt { color: #f92672 } /* Name.Tag */ -.highlight .nv { color: #f8f8f2 } /* Name.Variable */ -.highlight .ow { color: #f92672 } /* Operator.Word */ -.highlight .w { color: #f8f8f2 } /* Text.Whitespace */ -.highlight .mb { color: #ae81ff } /* Literal.Number.Bin */ -.highlight .mf { color: #ae81ff } /* Literal.Number.Float */ -.highlight .mh { color: #ae81ff } /* Literal.Number.Hex */ -.highlight .mi { color: #ae81ff } /* Literal.Number.Integer */ -.highlight .mo { color: #ae81ff } /* Literal.Number.Oct */ -.highlight .sa { color: #e6db74 } /* Literal.String.Affix */ -.highlight .sb { color: #e6db74 } /* Literal.String.Backtick */ -.highlight .sc { color: #e6db74 } /* Literal.String.Char */ -.highlight .dl { color: #e6db74 } /* Literal.String.Delimiter */ -.highlight .sd { color: #e6db74 } /* Literal.String.Doc */ -.highlight .s2 { color: #e6db74 } /* Literal.String.Double */ -.highlight .se { color: #ae81ff } /* Literal.String.Escape */ -.highlight .sh { color: #e6db74 } /* Literal.String.Heredoc */ -.highlight .si { color: #e6db74 } /* Literal.String.Interpol */ -.highlight .sx { color: #e6db74 } /* Literal.String.Other */ -.highlight .sr { color: #e6db74 } /* Literal.String.Regex */ -.highlight .s1 { color: #e6db74 } /* Literal.String.Single */ -.highlight .ss { color: #e6db74 } /* Literal.String.Symbol */ -.highlight .bp { color: #f8f8f2 } /* Name.Builtin.Pseudo */ -.highlight .fm { color: #a6e22e } /* Name.Function.Magic */ -.highlight .vc { color: #f8f8f2 } /* Name.Variable.Class */ -.highlight .vg { color: #f8f8f2 } /* Name.Variable.Global */ -.highlight .vi { color: #f8f8f2 } /* Name.Variable.Instance */ -.highlight .vm { color: #f8f8f2 } /* Name.Variable.Magic */ -.highlight .il { color: #ae81ff } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/docs/_static/pygments/murphy.css b/docs/_static/pygments/murphy.css deleted file mode 100644 index 378f40643..000000000 --- a/docs/_static/pygments/murphy.css +++ /dev/null @@ -1,69 +0,0 @@ -.highlight .hll { background-color: #ffffcc } -.highlight { background: #ffffff; } -.highlight .c { color: #666666; font-style: italic } /* Comment */ -.highlight .err { color: #FF0000; background-color: #FFAAAA } /* Error */ -.highlight .k { color: #228899; font-weight: bold } /* Keyword */ -.highlight .o { color: #333333 } /* Operator */ -.highlight .ch { color: #666666; font-style: italic } /* Comment.Hashbang */ -.highlight .cm { color: #666666; font-style: italic } /* Comment.Multiline */ -.highlight .cp { color: #557799 } /* Comment.Preproc */ -.highlight .cpf { color: #666666; font-style: italic } /* Comment.PreprocFile */ -.highlight .c1 { color: #666666; font-style: italic } /* Comment.Single */ -.highlight .cs { color: #cc0000; font-weight: bold; font-style: italic } /* Comment.Special */ -.highlight .gd { color: #A00000 } /* Generic.Deleted */ -.highlight .ge { font-style: italic } /* Generic.Emph */ -.highlight .gr { color: #FF0000 } /* Generic.Error */ -.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ -.highlight .gi { color: #00A000 } /* Generic.Inserted */ -.highlight .go { color: #888888 } /* Generic.Output */ -.highlight .gp { color: #c65d09; font-weight: bold } /* Generic.Prompt */ -.highlight .gs { font-weight: bold } /* Generic.Strong */ -.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ -.highlight .gt { color: #0044DD } /* Generic.Traceback */ -.highlight .kc { color: #228899; font-weight: bold } /* Keyword.Constant */ -.highlight .kd { color: #228899; font-weight: bold } /* Keyword.Declaration */ -.highlight .kn { color: #228899; font-weight: bold } /* Keyword.Namespace */ -.highlight .kp { color: #0088ff; font-weight: bold } /* Keyword.Pseudo */ -.highlight .kr { color: #228899; font-weight: bold } /* Keyword.Reserved */ -.highlight .kt { color: #6666ff; font-weight: bold } /* Keyword.Type */ -.highlight .m { color: #6600EE; font-weight: bold } /* Literal.Number */ -.highlight .s { background-color: #e0e0ff } /* Literal.String */ -.highlight .na { color: #000077 } /* Name.Attribute */ -.highlight .nb { color: #007722 } /* Name.Builtin */ -.highlight .nc { color: #ee99ee; font-weight: bold } /* Name.Class */ -.highlight .no { color: #55eedd; font-weight: bold } /* Name.Constant */ -.highlight .nd { color: #555555; font-weight: bold } /* Name.Decorator */ -.highlight .ni { color: #880000 } /* Name.Entity */ -.highlight .ne { color: #FF0000; font-weight: bold } /* Name.Exception */ -.highlight .nf { color: #55eedd; font-weight: bold } /* Name.Function */ -.highlight .nl { color: #997700; font-weight: bold } /* Name.Label */ -.highlight .nn { color: #0e84b5; font-weight: bold } /* Name.Namespace */ -.highlight .nt { color: #007700 } /* Name.Tag */ -.highlight .nv { color: #003366 } /* Name.Variable */ -.highlight .ow { color: #000000; font-weight: bold } /* Operator.Word */ -.highlight .w { color: #bbbbbb } /* Text.Whitespace */ -.highlight .mb { color: #6600EE; font-weight: bold } /* Literal.Number.Bin */ -.highlight .mf { color: #6600EE; font-weight: bold } /* Literal.Number.Float */ -.highlight .mh { color: #005588; font-weight: bold } /* Literal.Number.Hex */ -.highlight .mi { color: #6666ff; font-weight: bold } /* Literal.Number.Integer */ -.highlight .mo { color: #4400EE; font-weight: bold } /* Literal.Number.Oct */ -.highlight .sa { background-color: #e0e0ff } /* Literal.String.Affix */ -.highlight .sb { background-color: #e0e0ff } /* Literal.String.Backtick */ -.highlight .sc { color: #8888FF } /* Literal.String.Char */ -.highlight .dl { background-color: #e0e0ff } /* Literal.String.Delimiter */ -.highlight .sd { color: #DD4422 } /* Literal.String.Doc */ -.highlight .s2 { background-color: #e0e0ff } /* Literal.String.Double */ -.highlight .se { color: #666666; font-weight: bold; background-color: #e0e0ff } /* Literal.String.Escape */ -.highlight .sh { background-color: #e0e0ff } /* Literal.String.Heredoc */ -.highlight .si { background-color: #eeeeee } /* Literal.String.Interpol */ -.highlight .sx { color: #ff8888; background-color: #e0e0ff } /* Literal.String.Other */ -.highlight .sr { color: #000000; background-color: #e0e0ff } /* Literal.String.Regex */ -.highlight .s1 { background-color: #e0e0ff } /* Literal.String.Single */ -.highlight .ss { color: #ffcc88 } /* Literal.String.Symbol */ -.highlight .bp { color: #007722 } /* Name.Builtin.Pseudo */ -.highlight .fm { color: #55eedd; font-weight: bold } /* Name.Function.Magic */ -.highlight .vc { color: #ccccff } /* Name.Variable.Class */ -.highlight .vg { color: #ff8844 } /* Name.Variable.Global */ -.highlight .vi { color: #aaaaff } /* Name.Variable.Instance */ -.highlight .vm { color: #003366 } /* Name.Variable.Magic */ -.highlight .il { color: #6666ff; font-weight: bold } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/docs/_static/pygments/native.css b/docs/_static/pygments/native.css deleted file mode 100644 index 73ff7b0e7..000000000 --- a/docs/_static/pygments/native.css +++ /dev/null @@ -1,78 +0,0 @@ -.highlight .hll { background-color: #404040 } -.highlight { background: #202020; color: #d0d0d0 } -.highlight .c { color: #999999; font-style: italic } /* Comment */ -.highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */ -.highlight .esc { color: #d0d0d0 } /* Escape */ -.highlight .g { color: #d0d0d0 } /* Generic */ -.highlight .k { color: #6ab825; font-weight: bold } /* Keyword */ -.highlight .l { color: #d0d0d0 } /* Literal */ -.highlight .n { color: #d0d0d0 } /* Name */ -.highlight .o { color: #d0d0d0 } /* Operator */ -.highlight .x { color: #d0d0d0 } /* Other */ -.highlight .p { color: #d0d0d0 } /* Punctuation */ -.highlight .ch { color: #999999; font-style: italic } /* Comment.Hashbang */ -.highlight .cm { color: #999999; font-style: italic } /* Comment.Multiline */ -.highlight .cp { color: #cd2828; font-weight: bold } /* Comment.Preproc */ -.highlight .cpf { color: #999999; font-style: italic } /* Comment.PreprocFile */ -.highlight .c1 { color: #999999; font-style: italic } /* Comment.Single */ -.highlight .cs { color: #e50808; font-weight: bold; background-color: #520000 } /* Comment.Special */ -.highlight .gd { color: #d22323 } /* Generic.Deleted */ -.highlight .ge { color: #d0d0d0; font-style: italic } /* Generic.Emph */ -.highlight .gr { color: #d22323 } /* Generic.Error */ -.highlight .gh { color: #ffffff; font-weight: bold } /* Generic.Heading */ -.highlight .gi { color: #589819 } /* Generic.Inserted */ -.highlight .go { color: #cccccc } /* Generic.Output */ -.highlight .gp { color: #aaaaaa } /* Generic.Prompt */ -.highlight .gs { color: #d0d0d0; font-weight: bold } /* Generic.Strong */ -.highlight .gu { color: #ffffff; text-decoration: underline } /* Generic.Subheading */ -.highlight .gt { color: #d22323 } /* Generic.Traceback */ -.highlight .kc { color: #6ab825; font-weight: bold } /* Keyword.Constant */ -.highlight .kd { color: #6ab825; font-weight: bold } /* Keyword.Declaration */ -.highlight .kn { color: #6ab825; font-weight: bold } /* Keyword.Namespace */ -.highlight .kp { color: #6ab825 } /* Keyword.Pseudo */ -.highlight .kr { color: #6ab825; font-weight: bold } /* Keyword.Reserved */ -.highlight .kt { color: #6ab825; font-weight: bold } /* Keyword.Type */ -.highlight .ld { color: #d0d0d0 } /* Literal.Date */ -.highlight .m { color: #3677a9 } /* Literal.Number */ -.highlight .s { color: #ed9d13 } /* Literal.String */ -.highlight .na { color: #bbbbbb } /* Name.Attribute */ -.highlight .nb { color: #24909d } /* Name.Builtin */ -.highlight .nc { color: #447fcf; text-decoration: underline } /* Name.Class */ -.highlight .no { color: #40ffff } /* Name.Constant */ -.highlight .nd { color: #ffa500 } /* Name.Decorator */ -.highlight .ni { color: #d0d0d0 } /* Name.Entity */ -.highlight .ne { color: #bbbbbb } /* Name.Exception */ -.highlight .nf { color: #447fcf } /* Name.Function */ -.highlight .nl { color: #d0d0d0 } /* Name.Label */ -.highlight .nn { color: #447fcf; text-decoration: underline } /* Name.Namespace */ -.highlight .nx { color: #d0d0d0 } /* Name.Other */ -.highlight .py { color: #d0d0d0 } /* Name.Property */ -.highlight .nt { color: #6ab825; font-weight: bold } /* Name.Tag */ -.highlight .nv { color: #40ffff } /* Name.Variable */ -.highlight .ow { color: #6ab825; font-weight: bold } /* Operator.Word */ -.highlight .w { color: #666666 } /* Text.Whitespace */ -.highlight .mb { color: #3677a9 } /* Literal.Number.Bin */ -.highlight .mf { color: #3677a9 } /* Literal.Number.Float */ -.highlight .mh { color: #3677a9 } /* Literal.Number.Hex */ -.highlight .mi { color: #3677a9 } /* Literal.Number.Integer */ -.highlight .mo { color: #3677a9 } /* Literal.Number.Oct */ -.highlight .sa { color: #ed9d13 } /* Literal.String.Affix */ -.highlight .sb { color: #ed9d13 } /* Literal.String.Backtick */ -.highlight .sc { color: #ed9d13 } /* Literal.String.Char */ -.highlight .dl { color: #ed9d13 } /* Literal.String.Delimiter */ -.highlight .sd { color: #ed9d13 } /* Literal.String.Doc */ -.highlight .s2 { color: #ed9d13 } /* Literal.String.Double */ -.highlight .se { color: #ed9d13 } /* Literal.String.Escape */ -.highlight .sh { color: #ed9d13 } /* Literal.String.Heredoc */ -.highlight .si { color: #ed9d13 } /* Literal.String.Interpol */ -.highlight .sx { color: #ffa500 } /* Literal.String.Other */ -.highlight .sr { color: #ed9d13 } /* Literal.String.Regex */ -.highlight .s1 { color: #ed9d13 } /* Literal.String.Single */ -.highlight .ss { color: #ed9d13 } /* Literal.String.Symbol */ -.highlight .bp { color: #24909d } /* Name.Builtin.Pseudo */ -.highlight .fm { color: #447fcf } /* Name.Function.Magic */ -.highlight .vc { color: #40ffff } /* Name.Variable.Class */ -.highlight .vg { color: #40ffff } /* Name.Variable.Global */ -.highlight .vi { color: #40ffff } /* Name.Variable.Instance */ -.highlight .vm { color: #40ffff } /* Name.Variable.Magic */ -.highlight .il { color: #3677a9 } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/docs/_static/pygments/paraiso-dark.css b/docs/_static/pygments/paraiso-dark.css deleted file mode 100644 index 7a6cdbfd8..000000000 --- a/docs/_static/pygments/paraiso-dark.css +++ /dev/null @@ -1,72 +0,0 @@ -.highlight .hll { background-color: #4f424c } -.highlight { background: #2f1e2e; color: #e7e9db } -.highlight .c { color: #776e71 } /* Comment */ -.highlight .err { color: #ef6155 } /* Error */ -.highlight .k { color: #815ba4 } /* Keyword */ -.highlight .l { color: #f99b15 } /* Literal */ -.highlight .n { color: #e7e9db } /* Name */ -.highlight .o { color: #5bc4bf } /* Operator */ -.highlight .p { color: #e7e9db } /* Punctuation */ -.highlight .ch { color: #776e71 } /* Comment.Hashbang */ -.highlight .cm { color: #776e71 } /* Comment.Multiline */ -.highlight .cp { color: #776e71 } /* Comment.Preproc */ -.highlight .cpf { color: #776e71 } /* Comment.PreprocFile */ -.highlight .c1 { color: #776e71 } /* Comment.Single */ -.highlight .cs { color: #776e71 } /* Comment.Special */ -.highlight .gd { color: #ef6155 } /* Generic.Deleted */ -.highlight .ge { font-style: italic } /* Generic.Emph */ -.highlight .gh { color: #e7e9db; font-weight: bold } /* Generic.Heading */ -.highlight .gi { color: #48b685 } /* Generic.Inserted */ -.highlight .gp { color: #776e71; font-weight: bold } /* Generic.Prompt */ -.highlight .gs { font-weight: bold } /* Generic.Strong */ -.highlight .gu { color: #5bc4bf; font-weight: bold } /* Generic.Subheading */ -.highlight .kc { color: #815ba4 } /* Keyword.Constant */ -.highlight .kd { color: #815ba4 } /* Keyword.Declaration */ -.highlight .kn { color: #5bc4bf } /* Keyword.Namespace */ -.highlight .kp { color: #815ba4 } /* Keyword.Pseudo */ -.highlight .kr { color: #815ba4 } /* Keyword.Reserved */ -.highlight .kt { color: #fec418 } /* Keyword.Type */ -.highlight .ld { color: #48b685 } /* Literal.Date */ -.highlight .m { color: #f99b15 } /* Literal.Number */ -.highlight .s { color: #48b685 } /* Literal.String */ -.highlight .na { color: #06b6ef } /* Name.Attribute */ -.highlight .nb { color: #e7e9db } /* Name.Builtin */ -.highlight .nc { color: #fec418 } /* Name.Class */ -.highlight .no { color: #ef6155 } /* Name.Constant */ -.highlight .nd { color: #5bc4bf } /* Name.Decorator */ -.highlight .ni { color: #e7e9db } /* Name.Entity */ -.highlight .ne { color: #ef6155 } /* Name.Exception */ -.highlight .nf { color: #06b6ef } /* Name.Function */ -.highlight .nl { color: #e7e9db } /* Name.Label */ -.highlight .nn { color: #fec418 } /* Name.Namespace */ -.highlight .nx { color: #06b6ef } /* Name.Other */ -.highlight .py { color: #e7e9db } /* Name.Property */ -.highlight .nt { color: #5bc4bf } /* Name.Tag */ -.highlight .nv { color: #ef6155 } /* Name.Variable */ -.highlight .ow { color: #5bc4bf } /* Operator.Word */ -.highlight .w { color: #e7e9db } /* Text.Whitespace */ -.highlight .mb { color: #f99b15 } /* Literal.Number.Bin */ -.highlight .mf { color: #f99b15 } /* Literal.Number.Float */ -.highlight .mh { color: #f99b15 } /* Literal.Number.Hex */ -.highlight .mi { color: #f99b15 } /* Literal.Number.Integer */ -.highlight .mo { color: #f99b15 } /* Literal.Number.Oct */ -.highlight .sa { color: #48b685 } /* Literal.String.Affix */ -.highlight .sb { color: #48b685 } /* Literal.String.Backtick */ -.highlight .sc { color: #e7e9db } /* Literal.String.Char */ -.highlight .dl { color: #48b685 } /* Literal.String.Delimiter */ -.highlight .sd { color: #776e71 } /* Literal.String.Doc */ -.highlight .s2 { color: #48b685 } /* Literal.String.Double */ -.highlight .se { color: #f99b15 } /* Literal.String.Escape */ -.highlight .sh { color: #48b685 } /* Literal.String.Heredoc */ -.highlight .si { color: #f99b15 } /* Literal.String.Interpol */ -.highlight .sx { color: #48b685 } /* Literal.String.Other */ -.highlight .sr { color: #48b685 } /* Literal.String.Regex */ -.highlight .s1 { color: #48b685 } /* Literal.String.Single */ -.highlight .ss { color: #48b685 } /* Literal.String.Symbol */ -.highlight .bp { color: #e7e9db } /* Name.Builtin.Pseudo */ -.highlight .fm { color: #06b6ef } /* Name.Function.Magic */ -.highlight .vc { color: #ef6155 } /* Name.Variable.Class */ -.highlight .vg { color: #ef6155 } /* Name.Variable.Global */ -.highlight .vi { color: #ef6155 } /* Name.Variable.Instance */ -.highlight .vm { color: #ef6155 } /* Name.Variable.Magic */ -.highlight .il { color: #f99b15 } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/docs/_static/pygments/paraiso-light.css b/docs/_static/pygments/paraiso-light.css deleted file mode 100644 index eec7c1784..000000000 --- a/docs/_static/pygments/paraiso-light.css +++ /dev/null @@ -1,72 +0,0 @@ -.highlight .hll { background-color: #a39e9b } -.highlight { background: #e7e9db; color: #2f1e2e } -.highlight .c { color: #8d8687 } /* Comment */ -.highlight .err { color: #ef6155 } /* Error */ -.highlight .k { color: #815ba4 } /* Keyword */ -.highlight .l { color: #f99b15 } /* Literal */ -.highlight .n { color: #2f1e2e } /* Name */ -.highlight .o { color: #5bc4bf } /* Operator */ -.highlight .p { color: #2f1e2e } /* Punctuation */ -.highlight .ch { color: #8d8687 } /* Comment.Hashbang */ -.highlight .cm { color: #8d8687 } /* Comment.Multiline */ -.highlight .cp { color: #8d8687 } /* Comment.Preproc */ -.highlight .cpf { color: #8d8687 } /* Comment.PreprocFile */ -.highlight .c1 { color: #8d8687 } /* Comment.Single */ -.highlight .cs { color: #8d8687 } /* Comment.Special */ -.highlight .gd { color: #ef6155 } /* Generic.Deleted */ -.highlight .ge { font-style: italic } /* Generic.Emph */ -.highlight .gh { color: #2f1e2e; font-weight: bold } /* Generic.Heading */ -.highlight .gi { color: #48b685 } /* Generic.Inserted */ -.highlight .gp { color: #8d8687; font-weight: bold } /* Generic.Prompt */ -.highlight .gs { font-weight: bold } /* Generic.Strong */ -.highlight .gu { color: #5bc4bf; font-weight: bold } /* Generic.Subheading */ -.highlight .kc { color: #815ba4 } /* Keyword.Constant */ -.highlight .kd { color: #815ba4 } /* Keyword.Declaration */ -.highlight .kn { color: #5bc4bf } /* Keyword.Namespace */ -.highlight .kp { color: #815ba4 } /* Keyword.Pseudo */ -.highlight .kr { color: #815ba4 } /* Keyword.Reserved */ -.highlight .kt { color: #fec418 } /* Keyword.Type */ -.highlight .ld { color: #48b685 } /* Literal.Date */ -.highlight .m { color: #f99b15 } /* Literal.Number */ -.highlight .s { color: #48b685 } /* Literal.String */ -.highlight .na { color: #06b6ef } /* Name.Attribute */ -.highlight .nb { color: #2f1e2e } /* Name.Builtin */ -.highlight .nc { color: #fec418 } /* Name.Class */ -.highlight .no { color: #ef6155 } /* Name.Constant */ -.highlight .nd { color: #5bc4bf } /* Name.Decorator */ -.highlight .ni { color: #2f1e2e } /* Name.Entity */ -.highlight .ne { color: #ef6155 } /* Name.Exception */ -.highlight .nf { color: #06b6ef } /* Name.Function */ -.highlight .nl { color: #2f1e2e } /* Name.Label */ -.highlight .nn { color: #fec418 } /* Name.Namespace */ -.highlight .nx { color: #06b6ef } /* Name.Other */ -.highlight .py { color: #2f1e2e } /* Name.Property */ -.highlight .nt { color: #5bc4bf } /* Name.Tag */ -.highlight .nv { color: #ef6155 } /* Name.Variable */ -.highlight .ow { color: #5bc4bf } /* Operator.Word */ -.highlight .w { color: #2f1e2e } /* Text.Whitespace */ -.highlight .mb { color: #f99b15 } /* Literal.Number.Bin */ -.highlight .mf { color: #f99b15 } /* Literal.Number.Float */ -.highlight .mh { color: #f99b15 } /* Literal.Number.Hex */ -.highlight .mi { color: #f99b15 } /* Literal.Number.Integer */ -.highlight .mo { color: #f99b15 } /* Literal.Number.Oct */ -.highlight .sa { color: #48b685 } /* Literal.String.Affix */ -.highlight .sb { color: #48b685 } /* Literal.String.Backtick */ -.highlight .sc { color: #2f1e2e } /* Literal.String.Char */ -.highlight .dl { color: #48b685 } /* Literal.String.Delimiter */ -.highlight .sd { color: #8d8687 } /* Literal.String.Doc */ -.highlight .s2 { color: #48b685 } /* Literal.String.Double */ -.highlight .se { color: #f99b15 } /* Literal.String.Escape */ -.highlight .sh { color: #48b685 } /* Literal.String.Heredoc */ -.highlight .si { color: #f99b15 } /* Literal.String.Interpol */ -.highlight .sx { color: #48b685 } /* Literal.String.Other */ -.highlight .sr { color: #48b685 } /* Literal.String.Regex */ -.highlight .s1 { color: #48b685 } /* Literal.String.Single */ -.highlight .ss { color: #48b685 } /* Literal.String.Symbol */ -.highlight .bp { color: #2f1e2e } /* Name.Builtin.Pseudo */ -.highlight .fm { color: #06b6ef } /* Name.Function.Magic */ -.highlight .vc { color: #ef6155 } /* Name.Variable.Class */ -.highlight .vg { color: #ef6155 } /* Name.Variable.Global */ -.highlight .vi { color: #ef6155 } /* Name.Variable.Instance */ -.highlight .vm { color: #ef6155 } /* Name.Variable.Magic */ -.highlight .il { color: #f99b15 } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/docs/_static/pygments/pastie.css b/docs/_static/pygments/pastie.css deleted file mode 100644 index 6b5cebb83..000000000 --- a/docs/_static/pygments/pastie.css +++ /dev/null @@ -1,68 +0,0 @@ -.highlight .hll { background-color: #ffffcc } -.highlight { background: #ffffff; } -.highlight .c { color: #888888 } /* Comment */ -.highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */ -.highlight .k { color: #008800; font-weight: bold } /* Keyword */ -.highlight .ch { color: #888888 } /* Comment.Hashbang */ -.highlight .cm { color: #888888 } /* Comment.Multiline */ -.highlight .cp { color: #cc0000; font-weight: bold } /* Comment.Preproc */ -.highlight .cpf { color: #888888 } /* Comment.PreprocFile */ -.highlight .c1 { color: #888888 } /* Comment.Single */ -.highlight .cs { color: #cc0000; font-weight: bold; background-color: #fff0f0 } /* Comment.Special */ -.highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */ -.highlight .ge { font-style: italic } /* Generic.Emph */ -.highlight .gr { color: #aa0000 } /* Generic.Error */ -.highlight .gh { color: #333333 } /* Generic.Heading */ -.highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */ -.highlight .go { color: #888888 } /* Generic.Output */ -.highlight .gp { color: #555555 } /* Generic.Prompt */ -.highlight .gs { font-weight: bold } /* Generic.Strong */ -.highlight .gu { color: #666666 } /* Generic.Subheading */ -.highlight .gt { color: #aa0000 } /* Generic.Traceback */ -.highlight .kc { color: #008800; font-weight: bold } /* Keyword.Constant */ -.highlight .kd { color: #008800; font-weight: bold } /* Keyword.Declaration */ -.highlight .kn { color: #008800; font-weight: bold } /* Keyword.Namespace */ -.highlight .kp { color: #008800 } /* Keyword.Pseudo */ -.highlight .kr { color: #008800; font-weight: bold } /* Keyword.Reserved */ -.highlight .kt { color: #888888; font-weight: bold } /* Keyword.Type */ -.highlight .m { color: #0000DD; font-weight: bold } /* Literal.Number */ -.highlight .s { color: #dd2200; background-color: #fff0f0 } /* Literal.String */ -.highlight .na { color: #336699 } /* Name.Attribute */ -.highlight .nb { color: #003388 } /* Name.Builtin */ -.highlight .nc { color: #bb0066; font-weight: bold } /* Name.Class */ -.highlight .no { color: #003366; font-weight: bold } /* Name.Constant */ -.highlight .nd { color: #555555 } /* Name.Decorator */ -.highlight .ne { color: #bb0066; font-weight: bold } /* Name.Exception */ -.highlight .nf { color: #0066bb; font-weight: bold } /* Name.Function */ -.highlight .nl { color: #336699; font-style: italic } /* Name.Label */ -.highlight .nn { color: #bb0066; font-weight: bold } /* Name.Namespace */ -.highlight .py { color: #336699; font-weight: bold } /* Name.Property */ -.highlight .nt { color: #bb0066; font-weight: bold } /* Name.Tag */ -.highlight .nv { color: #336699 } /* Name.Variable */ -.highlight .ow { color: #008800 } /* Operator.Word */ -.highlight .w { color: #bbbbbb } /* Text.Whitespace */ -.highlight .mb { color: #0000DD; font-weight: bold } /* Literal.Number.Bin */ -.highlight .mf { color: #0000DD; font-weight: bold } /* Literal.Number.Float */ -.highlight .mh { color: #0000DD; font-weight: bold } /* Literal.Number.Hex */ -.highlight .mi { color: #0000DD; font-weight: bold } /* Literal.Number.Integer */ -.highlight .mo { color: #0000DD; font-weight: bold } /* Literal.Number.Oct */ -.highlight .sa { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Affix */ -.highlight .sb { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Backtick */ -.highlight .sc { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Char */ -.highlight .dl { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Delimiter */ -.highlight .sd { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Doc */ -.highlight .s2 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Double */ -.highlight .se { color: #0044dd; background-color: #fff0f0 } /* Literal.String.Escape */ -.highlight .sh { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Heredoc */ -.highlight .si { color: #3333bb; background-color: #fff0f0 } /* Literal.String.Interpol */ -.highlight .sx { color: #22bb22; background-color: #f0fff0 } /* Literal.String.Other */ -.highlight .sr { color: #008800; background-color: #fff0ff } /* Literal.String.Regex */ -.highlight .s1 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Single */ -.highlight .ss { color: #aa6600; background-color: #fff0f0 } /* Literal.String.Symbol */ -.highlight .bp { color: #003388 } /* Name.Builtin.Pseudo */ -.highlight .fm { color: #0066bb; font-weight: bold } /* Name.Function.Magic */ -.highlight .vc { color: #336699 } /* Name.Variable.Class */ -.highlight .vg { color: #dd7700 } /* Name.Variable.Global */ -.highlight .vi { color: #3333bb } /* Name.Variable.Instance */ -.highlight .vm { color: #336699 } /* Name.Variable.Magic */ -.highlight .il { color: #0000DD; font-weight: bold } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/docs/_static/pygments/perldoc.css b/docs/_static/pygments/perldoc.css deleted file mode 100644 index c08cecb77..000000000 --- a/docs/_static/pygments/perldoc.css +++ /dev/null @@ -1,66 +0,0 @@ -.highlight .hll { background-color: #ffffcc } -.highlight { background: #eeeedd; } -.highlight .c { color: #228B22 } /* Comment */ -.highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */ -.highlight .k { color: #8B008B; font-weight: bold } /* Keyword */ -.highlight .ch { color: #228B22 } /* Comment.Hashbang */ -.highlight .cm { color: #228B22 } /* Comment.Multiline */ -.highlight .cp { color: #1e889b } /* Comment.Preproc */ -.highlight .cpf { color: #228B22 } /* Comment.PreprocFile */ -.highlight .c1 { color: #228B22 } /* Comment.Single */ -.highlight .cs { color: #8B008B; font-weight: bold } /* Comment.Special */ -.highlight .gd { color: #aa0000 } /* Generic.Deleted */ -.highlight .ge { font-style: italic } /* Generic.Emph */ -.highlight .gr { color: #aa0000 } /* Generic.Error */ -.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ -.highlight .gi { color: #00aa00 } /* Generic.Inserted */ -.highlight .go { color: #888888 } /* Generic.Output */ -.highlight .gp { color: #555555 } /* Generic.Prompt */ -.highlight .gs { font-weight: bold } /* Generic.Strong */ -.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ -.highlight .gt { color: #aa0000 } /* Generic.Traceback */ -.highlight .kc { color: #8B008B; font-weight: bold } /* Keyword.Constant */ -.highlight .kd { color: #8B008B; font-weight: bold } /* Keyword.Declaration */ -.highlight .kn { color: #8B008B; font-weight: bold } /* Keyword.Namespace */ -.highlight .kp { color: #8B008B; font-weight: bold } /* Keyword.Pseudo */ -.highlight .kr { color: #8B008B; font-weight: bold } /* Keyword.Reserved */ -.highlight .kt { color: #00688B; font-weight: bold } /* Keyword.Type */ -.highlight .m { color: #B452CD } /* Literal.Number */ -.highlight .s { color: #CD5555 } /* Literal.String */ -.highlight .na { color: #658b00 } /* Name.Attribute */ -.highlight .nb { color: #658b00 } /* Name.Builtin */ -.highlight .nc { color: #008b45; font-weight: bold } /* Name.Class */ -.highlight .no { color: #00688B } /* Name.Constant */ -.highlight .nd { color: #707a7c } /* Name.Decorator */ -.highlight .ne { color: #008b45; font-weight: bold } /* Name.Exception */ -.highlight .nf { color: #008b45 } /* Name.Function */ -.highlight .nn { color: #008b45; text-decoration: underline } /* Name.Namespace */ -.highlight .nt { color: #8B008B; font-weight: bold } /* Name.Tag */ -.highlight .nv { color: #00688B } /* Name.Variable */ -.highlight .ow { color: #8B008B } /* Operator.Word */ -.highlight .w { color: #bbbbbb } /* Text.Whitespace */ -.highlight .mb { color: #B452CD } /* Literal.Number.Bin */ -.highlight .mf { color: #B452CD } /* Literal.Number.Float */ -.highlight .mh { color: #B452CD } /* Literal.Number.Hex */ -.highlight .mi { color: #B452CD } /* Literal.Number.Integer */ -.highlight .mo { color: #B452CD } /* Literal.Number.Oct */ -.highlight .sa { color: #CD5555 } /* Literal.String.Affix */ -.highlight .sb { color: #CD5555 } /* Literal.String.Backtick */ -.highlight .sc { color: #CD5555 } /* Literal.String.Char */ -.highlight .dl { color: #CD5555 } /* Literal.String.Delimiter */ -.highlight .sd { color: #CD5555 } /* Literal.String.Doc */ -.highlight .s2 { color: #CD5555 } /* Literal.String.Double */ -.highlight .se { color: #CD5555 } /* Literal.String.Escape */ -.highlight .sh { color: #1c7e71; font-style: italic } /* Literal.String.Heredoc */ -.highlight .si { color: #CD5555 } /* Literal.String.Interpol */ -.highlight .sx { color: #cb6c20 } /* Literal.String.Other */ -.highlight .sr { color: #1c7e71 } /* Literal.String.Regex */ -.highlight .s1 { color: #CD5555 } /* Literal.String.Single */ -.highlight .ss { color: #CD5555 } /* Literal.String.Symbol */ -.highlight .bp { color: #658b00 } /* Name.Builtin.Pseudo */ -.highlight .fm { color: #008b45 } /* Name.Function.Magic */ -.highlight .vc { color: #00688B } /* Name.Variable.Class */ -.highlight .vg { color: #00688B } /* Name.Variable.Global */ -.highlight .vi { color: #00688B } /* Name.Variable.Instance */ -.highlight .vm { color: #00688B } /* Name.Variable.Magic */ -.highlight .il { color: #B452CD } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/docs/_static/pygments/rainbow_dash.css b/docs/_static/pygments/rainbow_dash.css deleted file mode 100644 index 2716be4ab..000000000 --- a/docs/_static/pygments/rainbow_dash.css +++ /dev/null @@ -1,62 +0,0 @@ -.highlight .hll { background-color: #ffffcc } -.highlight { background: #ffffff; color: #4d4d4d } -.highlight .c { color: #0080ff; font-style: italic } /* Comment */ -.highlight .err { color: #ffffff; background-color: #cc0000 } /* Error */ -.highlight .k { color: #2c5dcd; font-weight: bold } /* Keyword */ -.highlight .o { color: #2c5dcd } /* Operator */ -.highlight .ch { color: #0080ff; font-style: italic } /* Comment.Hashbang */ -.highlight .cm { color: #0080ff; font-style: italic } /* Comment.Multiline */ -.highlight .cp { color: #0080ff } /* Comment.Preproc */ -.highlight .cpf { color: #0080ff; font-style: italic } /* Comment.PreprocFile */ -.highlight .c1 { color: #0080ff; font-style: italic } /* Comment.Single */ -.highlight .cs { color: #0080ff; font-weight: bold; font-style: italic } /* Comment.Special */ -.highlight .gd { background-color: #ffcccc; border: 1px solid #c5060b } /* Generic.Deleted */ -.highlight .ge { font-style: italic } /* Generic.Emph */ -.highlight .gr { color: #ff0000 } /* Generic.Error */ -.highlight .gh { color: #2c5dcd; font-weight: bold } /* Generic.Heading */ -.highlight .gi { background-color: #ccffcc; border: 1px solid #00cc00 } /* Generic.Inserted */ -.highlight .go { color: #aaaaaa } /* Generic.Output */ -.highlight .gp { color: #2c5dcd; font-weight: bold } /* Generic.Prompt */ -.highlight .gs { font-weight: bold } /* Generic.Strong */ -.highlight .gu { color: #2c5dcd; font-weight: bold } /* Generic.Subheading */ -.highlight .gt { color: #c5060b } /* Generic.Traceback */ -.highlight .kc { color: #2c5dcd; font-weight: bold } /* Keyword.Constant */ -.highlight .kd { color: #2c5dcd; font-weight: bold } /* Keyword.Declaration */ -.highlight .kn { color: #2c5dcd; font-weight: bold } /* Keyword.Namespace */ -.highlight .kp { color: #2c5dcd } /* Keyword.Pseudo */ -.highlight .kr { color: #2c5dcd; font-weight: bold } /* Keyword.Reserved */ -.highlight .kt { color: #5918bb; font-weight: bold } /* Keyword.Type */ -.highlight .m { color: #5918bb; font-weight: bold } /* Literal.Number */ -.highlight .s { color: #00cc66 } /* Literal.String */ -.highlight .na { color: #2c5dcd; font-style: italic } /* Name.Attribute */ -.highlight .nb { color: #5918bb; font-weight: bold } /* Name.Builtin */ -.highlight .nc { text-decoration: underline } /* Name.Class */ -.highlight .no { color: #318495 } /* Name.Constant */ -.highlight .nd { color: #ff8000; font-weight: bold } /* Name.Decorator */ -.highlight .ni { color: #5918bb; font-weight: bold } /* Name.Entity */ -.highlight .ne { color: #5918bb; font-weight: bold } /* Name.Exception */ -.highlight .nf { color: #ff8000; font-weight: bold } /* Name.Function */ -.highlight .nt { color: #2c5dcd; font-weight: bold } /* Name.Tag */ -.highlight .ow { color: #2c5dcd; font-weight: bold } /* Operator.Word */ -.highlight .w { color: #cbcbcb } /* Text.Whitespace */ -.highlight .mb { color: #5918bb; font-weight: bold } /* Literal.Number.Bin */ -.highlight .mf { color: #5918bb; font-weight: bold } /* Literal.Number.Float */ -.highlight .mh { color: #5918bb; font-weight: bold } /* Literal.Number.Hex */ -.highlight .mi { color: #5918bb; font-weight: bold } /* Literal.Number.Integer */ -.highlight .mo { color: #5918bb; font-weight: bold } /* Literal.Number.Oct */ -.highlight .sa { color: #00cc66 } /* Literal.String.Affix */ -.highlight .sb { color: #00cc66 } /* Literal.String.Backtick */ -.highlight .sc { color: #00cc66 } /* Literal.String.Char */ -.highlight .dl { color: #00cc66 } /* Literal.String.Delimiter */ -.highlight .sd { color: #00cc66; font-style: italic } /* Literal.String.Doc */ -.highlight .s2 { color: #00cc66 } /* Literal.String.Double */ -.highlight .se { color: #c5060b; font-weight: bold } /* Literal.String.Escape */ -.highlight .sh { color: #00cc66 } /* Literal.String.Heredoc */ -.highlight .si { color: #00cc66 } /* Literal.String.Interpol */ -.highlight .sx { color: #318495 } /* Literal.String.Other */ -.highlight .sr { color: #00cc66 } /* Literal.String.Regex */ -.highlight .s1 { color: #00cc66 } /* Literal.String.Single */ -.highlight .ss { color: #c5060b; font-weight: bold } /* Literal.String.Symbol */ -.highlight .bp { color: #5918bb; font-weight: bold } /* Name.Builtin.Pseudo */ -.highlight .fm { color: #ff8000; font-weight: bold } /* Name.Function.Magic */ -.highlight .il { color: #5918bb; font-weight: bold } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/docs/_static/pygments/rrt.css b/docs/_static/pygments/rrt.css deleted file mode 100644 index 76b3475f8..000000000 --- a/docs/_static/pygments/rrt.css +++ /dev/null @@ -1,38 +0,0 @@ -.highlight .hll { background-color: #0000ff } -.highlight { background: #000000; } -.highlight .c { color: #00ff00 } /* Comment */ -.highlight .k { color: #ff0000 } /* Keyword */ -.highlight .ch { color: #00ff00 } /* Comment.Hashbang */ -.highlight .cm { color: #00ff00 } /* Comment.Multiline */ -.highlight .cp { color: #e5e5e5 } /* Comment.Preproc */ -.highlight .cpf { color: #00ff00 } /* Comment.PreprocFile */ -.highlight .c1 { color: #00ff00 } /* Comment.Single */ -.highlight .cs { color: #00ff00 } /* Comment.Special */ -.highlight .kc { color: #ff0000 } /* Keyword.Constant */ -.highlight .kd { color: #ff0000 } /* Keyword.Declaration */ -.highlight .kn { color: #ff0000 } /* Keyword.Namespace */ -.highlight .kp { color: #ff0000 } /* Keyword.Pseudo */ -.highlight .kr { color: #ff0000 } /* Keyword.Reserved */ -.highlight .kt { color: #ee82ee } /* Keyword.Type */ -.highlight .s { color: #87ceeb } /* Literal.String */ -.highlight .no { color: #7fffd4 } /* Name.Constant */ -.highlight .nf { color: #ffff00 } /* Name.Function */ -.highlight .nv { color: #eedd82 } /* Name.Variable */ -.highlight .sa { color: #87ceeb } /* Literal.String.Affix */ -.highlight .sb { color: #87ceeb } /* Literal.String.Backtick */ -.highlight .sc { color: #87ceeb } /* Literal.String.Char */ -.highlight .dl { color: #87ceeb } /* Literal.String.Delimiter */ -.highlight .sd { color: #87ceeb } /* Literal.String.Doc */ -.highlight .s2 { color: #87ceeb } /* Literal.String.Double */ -.highlight .se { color: #87ceeb } /* Literal.String.Escape */ -.highlight .sh { color: #87ceeb } /* Literal.String.Heredoc */ -.highlight .si { color: #87ceeb } /* Literal.String.Interpol */ -.highlight .sx { color: #87ceeb } /* Literal.String.Other */ -.highlight .sr { color: #87ceeb } /* Literal.String.Regex */ -.highlight .s1 { color: #87ceeb } /* Literal.String.Single */ -.highlight .ss { color: #87ceeb } /* Literal.String.Symbol */ -.highlight .fm { color: #ffff00 } /* Name.Function.Magic */ -.highlight .vc { color: #eedd82 } /* Name.Variable.Class */ -.highlight .vg { color: #eedd82 } /* Name.Variable.Global */ -.highlight .vi { color: #eedd82 } /* Name.Variable.Instance */ -.highlight .vm { color: #eedd82 } /* Name.Variable.Magic */ \ No newline at end of file diff --git a/docs/_static/pygments/sas.css b/docs/_static/pygments/sas.css deleted file mode 100644 index e0e811d82..000000000 --- a/docs/_static/pygments/sas.css +++ /dev/null @@ -1,60 +0,0 @@ -.highlight .hll { background-color: #ffffcc } -.highlight { background: #ffffff; } -.highlight .c { color: #008800; font-style: italic } /* Comment */ -.highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */ -.highlight .g { color: #2c2cff } /* Generic */ -.highlight .k { color: #2c2cff } /* Keyword */ -.highlight .x { background-color: #ffffe0 } /* Other */ -.highlight .ch { color: #008800; font-style: italic } /* Comment.Hashbang */ -.highlight .cm { color: #008800; font-style: italic } /* Comment.Multiline */ -.highlight .cp { color: #008800; font-style: italic } /* Comment.Preproc */ -.highlight .cpf { color: #008800; font-style: italic } /* Comment.PreprocFile */ -.highlight .c1 { color: #008800; font-style: italic } /* Comment.Single */ -.highlight .cs { color: #008800; font-style: italic } /* Comment.Special */ -.highlight .gd { color: #2c2cff } /* Generic.Deleted */ -.highlight .ge { color: #008800 } /* Generic.Emph */ -.highlight .gr { color: #d30202 } /* Generic.Error */ -.highlight .gh { color: #2c2cff } /* Generic.Heading */ -.highlight .gi { color: #2c2cff } /* Generic.Inserted */ -.highlight .go { color: #2c2cff } /* Generic.Output */ -.highlight .gp { color: #2c2cff } /* Generic.Prompt */ -.highlight .gs { color: #2c2cff } /* Generic.Strong */ -.highlight .gu { color: #2c2cff } /* Generic.Subheading */ -.highlight .gt { color: #2c2cff } /* Generic.Traceback */ -.highlight .kc { color: #2c2cff; font-weight: bold } /* Keyword.Constant */ -.highlight .kd { color: #2c2cff } /* Keyword.Declaration */ -.highlight .kn { color: #2c2cff } /* Keyword.Namespace */ -.highlight .kp { color: #2c2cff } /* Keyword.Pseudo */ -.highlight .kr { color: #353580; font-weight: bold } /* Keyword.Reserved */ -.highlight .kt { color: #2c2cff } /* Keyword.Type */ -.highlight .m { color: #2e8b57; font-weight: bold } /* Literal.Number */ -.highlight .s { color: #800080 } /* Literal.String */ -.highlight .nb { color: #2c2cff } /* Name.Builtin */ -.highlight .nf { font-weight: bold; font-style: italic } /* Name.Function */ -.highlight .nv { color: #2c2cff; font-weight: bold } /* Name.Variable */ -.highlight .w { color: #bbbbbb } /* Text.Whitespace */ -.highlight .mb { color: #2e8b57; font-weight: bold } /* Literal.Number.Bin */ -.highlight .mf { color: #2e8b57; font-weight: bold } /* Literal.Number.Float */ -.highlight .mh { color: #2e8b57; font-weight: bold } /* Literal.Number.Hex */ -.highlight .mi { color: #2e8b57; font-weight: bold } /* Literal.Number.Integer */ -.highlight .mo { color: #2e8b57; font-weight: bold } /* Literal.Number.Oct */ -.highlight .sa { color: #800080 } /* Literal.String.Affix */ -.highlight .sb { color: #800080 } /* Literal.String.Backtick */ -.highlight .sc { color: #800080 } /* Literal.String.Char */ -.highlight .dl { color: #800080 } /* Literal.String.Delimiter */ -.highlight .sd { color: #800080 } /* Literal.String.Doc */ -.highlight .s2 { color: #800080 } /* Literal.String.Double */ -.highlight .se { color: #800080 } /* Literal.String.Escape */ -.highlight .sh { color: #800080 } /* Literal.String.Heredoc */ -.highlight .si { color: #800080 } /* Literal.String.Interpol */ -.highlight .sx { color: #800080 } /* Literal.String.Other */ -.highlight .sr { color: #800080 } /* Literal.String.Regex */ -.highlight .s1 { color: #800080 } /* Literal.String.Single */ -.highlight .ss { color: #800080 } /* Literal.String.Symbol */ -.highlight .bp { color: #2c2cff } /* Name.Builtin.Pseudo */ -.highlight .fm { font-weight: bold; font-style: italic } /* Name.Function.Magic */ -.highlight .vc { color: #2c2cff; font-weight: bold } /* Name.Variable.Class */ -.highlight .vg { color: #2c2cff; font-weight: bold } /* Name.Variable.Global */ -.highlight .vi { color: #2c2cff; font-weight: bold } /* Name.Variable.Instance */ -.highlight .vm { color: #2c2cff; font-weight: bold } /* Name.Variable.Magic */ -.highlight .il { color: #2e8b57; font-weight: bold } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/docs/_static/pygments/solarized-dark.css b/docs/_static/pygments/solarized-dark.css deleted file mode 100644 index 80ac0f780..000000000 --- a/docs/_static/pygments/solarized-dark.css +++ /dev/null @@ -1,78 +0,0 @@ -.highlight .hll { background-color: #073642 } -.highlight { background: #002b36; color: #839496 } -.highlight .c { color: #586e75; font-style: italic } /* Comment */ -.highlight .err { color: #839496; background-color: #dc322f } /* Error */ -.highlight .esc { color: #839496 } /* Escape */ -.highlight .g { color: #839496 } /* Generic */ -.highlight .k { color: #859900 } /* Keyword */ -.highlight .l { color: #839496 } /* Literal */ -.highlight .n { color: #839496 } /* Name */ -.highlight .o { color: #586e75 } /* Operator */ -.highlight .x { color: #839496 } /* Other */ -.highlight .p { color: #839496 } /* Punctuation */ -.highlight .ch { color: #586e75; font-style: italic } /* Comment.Hashbang */ -.highlight .cm { color: #586e75; font-style: italic } /* Comment.Multiline */ -.highlight .cp { color: #d33682 } /* Comment.Preproc */ -.highlight .cpf { color: #586e75 } /* Comment.PreprocFile */ -.highlight .c1 { color: #586e75; font-style: italic } /* Comment.Single */ -.highlight .cs { color: #586e75; font-style: italic } /* Comment.Special */ -.highlight .gd { color: #dc322f } /* Generic.Deleted */ -.highlight .ge { color: #839496; font-style: italic } /* Generic.Emph */ -.highlight .gr { color: #dc322f } /* Generic.Error */ -.highlight .gh { color: #839496; font-weight: bold } /* Generic.Heading */ -.highlight .gi { color: #859900 } /* Generic.Inserted */ -.highlight .go { color: #839496 } /* Generic.Output */ -.highlight .gp { color: #839496 } /* Generic.Prompt */ -.highlight .gs { color: #839496; font-weight: bold } /* Generic.Strong */ -.highlight .gu { color: #839496; text-decoration: underline } /* Generic.Subheading */ -.highlight .gt { color: #268bd2 } /* Generic.Traceback */ -.highlight .kc { color: #2aa198 } /* Keyword.Constant */ -.highlight .kd { color: #2aa198 } /* Keyword.Declaration */ -.highlight .kn { color: #cb4b16 } /* Keyword.Namespace */ -.highlight .kp { color: #859900 } /* Keyword.Pseudo */ -.highlight .kr { color: #859900 } /* Keyword.Reserved */ -.highlight .kt { color: #b58900 } /* Keyword.Type */ -.highlight .ld { color: #839496 } /* Literal.Date */ -.highlight .m { color: #2aa198 } /* Literal.Number */ -.highlight .s { color: #2aa198 } /* Literal.String */ -.highlight .na { color: #839496 } /* Name.Attribute */ -.highlight .nb { color: #268bd2 } /* Name.Builtin */ -.highlight .nc { color: #268bd2 } /* Name.Class */ -.highlight .no { color: #268bd2 } /* Name.Constant */ -.highlight .nd { color: #268bd2 } /* Name.Decorator */ -.highlight .ni { color: #268bd2 } /* Name.Entity */ -.highlight .ne { color: #268bd2 } /* Name.Exception */ -.highlight .nf { color: #268bd2 } /* Name.Function */ -.highlight .nl { color: #268bd2 } /* Name.Label */ -.highlight .nn { color: #268bd2 } /* Name.Namespace */ -.highlight .nx { color: #839496 } /* Name.Other */ -.highlight .py { color: #839496 } /* Name.Property */ -.highlight .nt { color: #268bd2 } /* Name.Tag */ -.highlight .nv { color: #268bd2 } /* Name.Variable */ -.highlight .ow { color: #859900 } /* Operator.Word */ -.highlight .w { color: #839496 } /* Text.Whitespace */ -.highlight .mb { color: #2aa198 } /* Literal.Number.Bin */ -.highlight .mf { color: #2aa198 } /* Literal.Number.Float */ -.highlight .mh { color: #2aa198 } /* Literal.Number.Hex */ -.highlight .mi { color: #2aa198 } /* Literal.Number.Integer */ -.highlight .mo { color: #2aa198 } /* Literal.Number.Oct */ -.highlight .sa { color: #2aa198 } /* Literal.String.Affix */ -.highlight .sb { color: #2aa198 } /* Literal.String.Backtick */ -.highlight .sc { color: #2aa198 } /* Literal.String.Char */ -.highlight .dl { color: #2aa198 } /* Literal.String.Delimiter */ -.highlight .sd { color: #586e75 } /* Literal.String.Doc */ -.highlight .s2 { color: #2aa198 } /* Literal.String.Double */ -.highlight .se { color: #2aa198 } /* Literal.String.Escape */ -.highlight .sh { color: #2aa198 } /* Literal.String.Heredoc */ -.highlight .si { color: #2aa198 } /* Literal.String.Interpol */ -.highlight .sx { color: #2aa198 } /* Literal.String.Other */ -.highlight .sr { color: #cb4b16 } /* Literal.String.Regex */ -.highlight .s1 { color: #2aa198 } /* Literal.String.Single */ -.highlight .ss { color: #2aa198 } /* Literal.String.Symbol */ -.highlight .bp { color: #268bd2 } /* Name.Builtin.Pseudo */ -.highlight .fm { color: #268bd2 } /* Name.Function.Magic */ -.highlight .vc { color: #268bd2 } /* Name.Variable.Class */ -.highlight .vg { color: #268bd2 } /* Name.Variable.Global */ -.highlight .vi { color: #268bd2 } /* Name.Variable.Instance */ -.highlight .vm { color: #268bd2 } /* Name.Variable.Magic */ -.highlight .il { color: #2aa198 } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/docs/_static/pygments/solarized-light.css b/docs/_static/pygments/solarized-light.css deleted file mode 100644 index 3254cc92c..000000000 --- a/docs/_static/pygments/solarized-light.css +++ /dev/null @@ -1,78 +0,0 @@ -.highlight .hll { background-color: #eee8d5 } -.highlight { background: #fdf6e3; color: #657b83 } -.highlight .c { color: #93a1a1; font-style: italic } /* Comment */ -.highlight .err { color: #657b83; background-color: #dc322f } /* Error */ -.highlight .esc { color: #657b83 } /* Escape */ -.highlight .g { color: #657b83 } /* Generic */ -.highlight .k { color: #859900 } /* Keyword */ -.highlight .l { color: #657b83 } /* Literal */ -.highlight .n { color: #657b83 } /* Name */ -.highlight .o { color: #93a1a1 } /* Operator */ -.highlight .x { color: #657b83 } /* Other */ -.highlight .p { color: #657b83 } /* Punctuation */ -.highlight .ch { color: #93a1a1; font-style: italic } /* Comment.Hashbang */ -.highlight .cm { color: #93a1a1; font-style: italic } /* Comment.Multiline */ -.highlight .cp { color: #d33682 } /* Comment.Preproc */ -.highlight .cpf { color: #93a1a1 } /* Comment.PreprocFile */ -.highlight .c1 { color: #93a1a1; font-style: italic } /* Comment.Single */ -.highlight .cs { color: #93a1a1; font-style: italic } /* Comment.Special */ -.highlight .gd { color: #dc322f } /* Generic.Deleted */ -.highlight .ge { color: #657b83; font-style: italic } /* Generic.Emph */ -.highlight .gr { color: #dc322f } /* Generic.Error */ -.highlight .gh { color: #657b83; font-weight: bold } /* Generic.Heading */ -.highlight .gi { color: #859900 } /* Generic.Inserted */ -.highlight .go { color: #657b83 } /* Generic.Output */ -.highlight .gp { color: #657b83 } /* Generic.Prompt */ -.highlight .gs { color: #657b83; font-weight: bold } /* Generic.Strong */ -.highlight .gu { color: #657b83; text-decoration: underline } /* Generic.Subheading */ -.highlight .gt { color: #268bd2 } /* Generic.Traceback */ -.highlight .kc { color: #2aa198 } /* Keyword.Constant */ -.highlight .kd { color: #2aa198 } /* Keyword.Declaration */ -.highlight .kn { color: #cb4b16 } /* Keyword.Namespace */ -.highlight .kp { color: #859900 } /* Keyword.Pseudo */ -.highlight .kr { color: #859900 } /* Keyword.Reserved */ -.highlight .kt { color: #b58900 } /* Keyword.Type */ -.highlight .ld { color: #657b83 } /* Literal.Date */ -.highlight .m { color: #2aa198 } /* Literal.Number */ -.highlight .s { color: #2aa198 } /* Literal.String */ -.highlight .na { color: #657b83 } /* Name.Attribute */ -.highlight .nb { color: #268bd2 } /* Name.Builtin */ -.highlight .nc { color: #268bd2 } /* Name.Class */ -.highlight .no { color: #268bd2 } /* Name.Constant */ -.highlight .nd { color: #268bd2 } /* Name.Decorator */ -.highlight .ni { color: #268bd2 } /* Name.Entity */ -.highlight .ne { color: #268bd2 } /* Name.Exception */ -.highlight .nf { color: #268bd2 } /* Name.Function */ -.highlight .nl { color: #268bd2 } /* Name.Label */ -.highlight .nn { color: #268bd2 } /* Name.Namespace */ -.highlight .nx { color: #657b83 } /* Name.Other */ -.highlight .py { color: #657b83 } /* Name.Property */ -.highlight .nt { color: #268bd2 } /* Name.Tag */ -.highlight .nv { color: #268bd2 } /* Name.Variable */ -.highlight .ow { color: #859900 } /* Operator.Word */ -.highlight .w { color: #657b83 } /* Text.Whitespace */ -.highlight .mb { color: #2aa198 } /* Literal.Number.Bin */ -.highlight .mf { color: #2aa198 } /* Literal.Number.Float */ -.highlight .mh { color: #2aa198 } /* Literal.Number.Hex */ -.highlight .mi { color: #2aa198 } /* Literal.Number.Integer */ -.highlight .mo { color: #2aa198 } /* Literal.Number.Oct */ -.highlight .sa { color: #2aa198 } /* Literal.String.Affix */ -.highlight .sb { color: #2aa198 } /* Literal.String.Backtick */ -.highlight .sc { color: #2aa198 } /* Literal.String.Char */ -.highlight .dl { color: #2aa198 } /* Literal.String.Delimiter */ -.highlight .sd { color: #93a1a1 } /* Literal.String.Doc */ -.highlight .s2 { color: #2aa198 } /* Literal.String.Double */ -.highlight .se { color: #2aa198 } /* Literal.String.Escape */ -.highlight .sh { color: #2aa198 } /* Literal.String.Heredoc */ -.highlight .si { color: #2aa198 } /* Literal.String.Interpol */ -.highlight .sx { color: #2aa198 } /* Literal.String.Other */ -.highlight .sr { color: #cb4b16 } /* Literal.String.Regex */ -.highlight .s1 { color: #2aa198 } /* Literal.String.Single */ -.highlight .ss { color: #2aa198 } /* Literal.String.Symbol */ -.highlight .bp { color: #268bd2 } /* Name.Builtin.Pseudo */ -.highlight .fm { color: #268bd2 } /* Name.Function.Magic */ -.highlight .vc { color: #268bd2 } /* Name.Variable.Class */ -.highlight .vg { color: #268bd2 } /* Name.Variable.Global */ -.highlight .vi { color: #268bd2 } /* Name.Variable.Instance */ -.highlight .vm { color: #268bd2 } /* Name.Variable.Magic */ -.highlight .il { color: #2aa198 } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/docs/_static/pygments/stata-dark.css b/docs/_static/pygments/stata-dark.css deleted file mode 100644 index 2ed7c17b7..000000000 --- a/docs/_static/pygments/stata-dark.css +++ /dev/null @@ -1,48 +0,0 @@ -.highlight .hll { background-color: #49483e } -.highlight { background: #232629; color: #cccccc } -.highlight .c { color: #777777; font-style: italic } /* Comment */ -.highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */ -.highlight .k { color: #7686bb; font-weight: bold } /* Keyword */ -.highlight .ch { color: #777777; font-style: italic } /* Comment.Hashbang */ -.highlight .cm { color: #777777; font-style: italic } /* Comment.Multiline */ -.highlight .cp { color: #777777; font-style: italic } /* Comment.Preproc */ -.highlight .cpf { color: #777777; font-style: italic } /* Comment.PreprocFile */ -.highlight .c1 { color: #777777; font-style: italic } /* Comment.Single */ -.highlight .cs { color: #777777; font-style: italic } /* Comment.Special */ -.highlight .gp { color: #ffffff } /* Generic.Prompt */ -.highlight .kc { color: #7686bb; font-weight: bold } /* Keyword.Constant */ -.highlight .kd { color: #7686bb; font-weight: bold } /* Keyword.Declaration */ -.highlight .kn { color: #7686bb; font-weight: bold } /* Keyword.Namespace */ -.highlight .kp { color: #7686bb; font-weight: bold } /* Keyword.Pseudo */ -.highlight .kr { color: #7686bb; font-weight: bold } /* Keyword.Reserved */ -.highlight .kt { color: #7686bb; font-weight: bold } /* Keyword.Type */ -.highlight .m { color: #4FB8CC } /* Literal.Number */ -.highlight .s { color: #51cc99 } /* Literal.String */ -.highlight .nf { color: #6a6aff } /* Name.Function */ -.highlight .nx { color: #e2828e } /* Name.Other */ -.highlight .nv { color: #7AB4DB; font-weight: bold } /* Name.Variable */ -.highlight .w { color: #bbbbbb } /* Text.Whitespace */ -.highlight .mb { color: #4FB8CC } /* Literal.Number.Bin */ -.highlight .mf { color: #4FB8CC } /* Literal.Number.Float */ -.highlight .mh { color: #4FB8CC } /* Literal.Number.Hex */ -.highlight .mi { color: #4FB8CC } /* Literal.Number.Integer */ -.highlight .mo { color: #4FB8CC } /* Literal.Number.Oct */ -.highlight .sa { color: #51cc99 } /* Literal.String.Affix */ -.highlight .sb { color: #51cc99 } /* Literal.String.Backtick */ -.highlight .sc { color: #51cc99 } /* Literal.String.Char */ -.highlight .dl { color: #51cc99 } /* Literal.String.Delimiter */ -.highlight .sd { color: #51cc99 } /* Literal.String.Doc */ -.highlight .s2 { color: #51cc99 } /* Literal.String.Double */ -.highlight .se { color: #51cc99 } /* Literal.String.Escape */ -.highlight .sh { color: #51cc99 } /* Literal.String.Heredoc */ -.highlight .si { color: #51cc99 } /* Literal.String.Interpol */ -.highlight .sx { color: #51cc99 } /* Literal.String.Other */ -.highlight .sr { color: #51cc99 } /* Literal.String.Regex */ -.highlight .s1 { color: #51cc99 } /* Literal.String.Single */ -.highlight .ss { color: #51cc99 } /* Literal.String.Symbol */ -.highlight .fm { color: #6a6aff } /* Name.Function.Magic */ -.highlight .vc { color: #7AB4DB; font-weight: bold } /* Name.Variable.Class */ -.highlight .vg { color: #BE646C; font-weight: bold } /* Name.Variable.Global */ -.highlight .vi { color: #7AB4DB; font-weight: bold } /* Name.Variable.Instance */ -.highlight .vm { color: #7AB4DB; font-weight: bold } /* Name.Variable.Magic */ -.highlight .il { color: #4FB8CC } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/docs/_static/pygments/stata-light.css b/docs/_static/pygments/stata-light.css deleted file mode 100644 index 6e0609320..000000000 --- a/docs/_static/pygments/stata-light.css +++ /dev/null @@ -1,47 +0,0 @@ -.highlight .hll { background-color: #ffffcc } -.highlight { background: #ffffff; color: #111111 } -.highlight .c { color: #008800; font-style: italic } /* Comment */ -.highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */ -.highlight .k { color: #353580; font-weight: bold } /* Keyword */ -.highlight .ch { color: #008800; font-style: italic } /* Comment.Hashbang */ -.highlight .cm { color: #008800; font-style: italic } /* Comment.Multiline */ -.highlight .cp { color: #008800; font-style: italic } /* Comment.Preproc */ -.highlight .cpf { color: #008800; font-style: italic } /* Comment.PreprocFile */ -.highlight .c1 { color: #008800; font-style: italic } /* Comment.Single */ -.highlight .cs { color: #008800; font-style: italic } /* Comment.Special */ -.highlight .kc { color: #353580; font-weight: bold } /* Keyword.Constant */ -.highlight .kd { color: #353580; font-weight: bold } /* Keyword.Declaration */ -.highlight .kn { color: #353580; font-weight: bold } /* Keyword.Namespace */ -.highlight .kp { color: #353580; font-weight: bold } /* Keyword.Pseudo */ -.highlight .kr { color: #353580; font-weight: bold } /* Keyword.Reserved */ -.highlight .kt { color: #353580; font-weight: bold } /* Keyword.Type */ -.highlight .m { color: #2c2cff } /* Literal.Number */ -.highlight .s { color: #7a2424 } /* Literal.String */ -.highlight .nf { color: #2c2cff } /* Name.Function */ -.highlight .nx { color: #be646c } /* Name.Other */ -.highlight .nv { color: #35baba; font-weight: bold } /* Name.Variable */ -.highlight .w { color: #bbbbbb } /* Text.Whitespace */ -.highlight .mb { color: #2c2cff } /* Literal.Number.Bin */ -.highlight .mf { color: #2c2cff } /* Literal.Number.Float */ -.highlight .mh { color: #2c2cff } /* Literal.Number.Hex */ -.highlight .mi { color: #2c2cff } /* Literal.Number.Integer */ -.highlight .mo { color: #2c2cff } /* Literal.Number.Oct */ -.highlight .sa { color: #7a2424 } /* Literal.String.Affix */ -.highlight .sb { color: #7a2424 } /* Literal.String.Backtick */ -.highlight .sc { color: #7a2424 } /* Literal.String.Char */ -.highlight .dl { color: #7a2424 } /* Literal.String.Delimiter */ -.highlight .sd { color: #7a2424 } /* Literal.String.Doc */ -.highlight .s2 { color: #7a2424 } /* Literal.String.Double */ -.highlight .se { color: #7a2424 } /* Literal.String.Escape */ -.highlight .sh { color: #7a2424 } /* Literal.String.Heredoc */ -.highlight .si { color: #7a2424 } /* Literal.String.Interpol */ -.highlight .sx { color: #7a2424 } /* Literal.String.Other */ -.highlight .sr { color: #7a2424 } /* Literal.String.Regex */ -.highlight .s1 { color: #7a2424 } /* Literal.String.Single */ -.highlight .ss { color: #7a2424 } /* Literal.String.Symbol */ -.highlight .fm { color: #2c2cff } /* Name.Function.Magic */ -.highlight .vc { color: #35baba; font-weight: bold } /* Name.Variable.Class */ -.highlight .vg { color: #b5565e; font-weight: bold } /* Name.Variable.Global */ -.highlight .vi { color: #35baba; font-weight: bold } /* Name.Variable.Instance */ -.highlight .vm { color: #35baba; font-weight: bold } /* Name.Variable.Magic */ -.highlight .il { color: #2c2cff } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/docs/_static/pygments/stata.css b/docs/_static/pygments/stata.css deleted file mode 100644 index 6e0609320..000000000 --- a/docs/_static/pygments/stata.css +++ /dev/null @@ -1,47 +0,0 @@ -.highlight .hll { background-color: #ffffcc } -.highlight { background: #ffffff; color: #111111 } -.highlight .c { color: #008800; font-style: italic } /* Comment */ -.highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */ -.highlight .k { color: #353580; font-weight: bold } /* Keyword */ -.highlight .ch { color: #008800; font-style: italic } /* Comment.Hashbang */ -.highlight .cm { color: #008800; font-style: italic } /* Comment.Multiline */ -.highlight .cp { color: #008800; font-style: italic } /* Comment.Preproc */ -.highlight .cpf { color: #008800; font-style: italic } /* Comment.PreprocFile */ -.highlight .c1 { color: #008800; font-style: italic } /* Comment.Single */ -.highlight .cs { color: #008800; font-style: italic } /* Comment.Special */ -.highlight .kc { color: #353580; font-weight: bold } /* Keyword.Constant */ -.highlight .kd { color: #353580; font-weight: bold } /* Keyword.Declaration */ -.highlight .kn { color: #353580; font-weight: bold } /* Keyword.Namespace */ -.highlight .kp { color: #353580; font-weight: bold } /* Keyword.Pseudo */ -.highlight .kr { color: #353580; font-weight: bold } /* Keyword.Reserved */ -.highlight .kt { color: #353580; font-weight: bold } /* Keyword.Type */ -.highlight .m { color: #2c2cff } /* Literal.Number */ -.highlight .s { color: #7a2424 } /* Literal.String */ -.highlight .nf { color: #2c2cff } /* Name.Function */ -.highlight .nx { color: #be646c } /* Name.Other */ -.highlight .nv { color: #35baba; font-weight: bold } /* Name.Variable */ -.highlight .w { color: #bbbbbb } /* Text.Whitespace */ -.highlight .mb { color: #2c2cff } /* Literal.Number.Bin */ -.highlight .mf { color: #2c2cff } /* Literal.Number.Float */ -.highlight .mh { color: #2c2cff } /* Literal.Number.Hex */ -.highlight .mi { color: #2c2cff } /* Literal.Number.Integer */ -.highlight .mo { color: #2c2cff } /* Literal.Number.Oct */ -.highlight .sa { color: #7a2424 } /* Literal.String.Affix */ -.highlight .sb { color: #7a2424 } /* Literal.String.Backtick */ -.highlight .sc { color: #7a2424 } /* Literal.String.Char */ -.highlight .dl { color: #7a2424 } /* Literal.String.Delimiter */ -.highlight .sd { color: #7a2424 } /* Literal.String.Doc */ -.highlight .s2 { color: #7a2424 } /* Literal.String.Double */ -.highlight .se { color: #7a2424 } /* Literal.String.Escape */ -.highlight .sh { color: #7a2424 } /* Literal.String.Heredoc */ -.highlight .si { color: #7a2424 } /* Literal.String.Interpol */ -.highlight .sx { color: #7a2424 } /* Literal.String.Other */ -.highlight .sr { color: #7a2424 } /* Literal.String.Regex */ -.highlight .s1 { color: #7a2424 } /* Literal.String.Single */ -.highlight .ss { color: #7a2424 } /* Literal.String.Symbol */ -.highlight .fm { color: #2c2cff } /* Name.Function.Magic */ -.highlight .vc { color: #35baba; font-weight: bold } /* Name.Variable.Class */ -.highlight .vg { color: #b5565e; font-weight: bold } /* Name.Variable.Global */ -.highlight .vi { color: #35baba; font-weight: bold } /* Name.Variable.Instance */ -.highlight .vm { color: #35baba; font-weight: bold } /* Name.Variable.Magic */ -.highlight .il { color: #2c2cff } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/docs/_static/pygments/tango.css b/docs/_static/pygments/tango.css deleted file mode 100644 index b0ec841f5..000000000 --- a/docs/_static/pygments/tango.css +++ /dev/null @@ -1,77 +0,0 @@ -.highlight .hll { background-color: #ffffcc } -.highlight { background: #f8f8f8; } -.highlight .c { color: #8f5902; font-style: italic } /* Comment */ -.highlight .err { color: #a40000; border: 1px solid #ef2929 } /* Error */ -.highlight .g { color: #000000 } /* Generic */ -.highlight .k { color: #204a87; font-weight: bold } /* Keyword */ -.highlight .l { color: #000000 } /* Literal */ -.highlight .n { color: #000000 } /* Name */ -.highlight .o { color: #ce5c00; font-weight: bold } /* Operator */ -.highlight .x { color: #000000 } /* Other */ -.highlight .p { color: #000000; font-weight: bold } /* Punctuation */ -.highlight .ch { color: #8f5902; font-style: italic } /* Comment.Hashbang */ -.highlight .cm { color: #8f5902; font-style: italic } /* Comment.Multiline */ -.highlight .cp { color: #8f5902; font-style: italic } /* Comment.Preproc */ -.highlight .cpf { color: #8f5902; font-style: italic } /* Comment.PreprocFile */ -.highlight .c1 { color: #8f5902; font-style: italic } /* Comment.Single */ -.highlight .cs { color: #8f5902; font-style: italic } /* Comment.Special */ -.highlight .gd { color: #a40000 } /* Generic.Deleted */ -.highlight .ge { color: #000000; font-style: italic } /* Generic.Emph */ -.highlight .gr { color: #ef2929 } /* Generic.Error */ -.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ -.highlight .gi { color: #00A000 } /* Generic.Inserted */ -.highlight .go { color: #000000; font-style: italic } /* Generic.Output */ -.highlight .gp { color: #8f5902 } /* Generic.Prompt */ -.highlight .gs { color: #000000; font-weight: bold } /* Generic.Strong */ -.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ -.highlight .gt { color: #a40000; font-weight: bold } /* Generic.Traceback */ -.highlight .kc { color: #204a87; font-weight: bold } /* Keyword.Constant */ -.highlight .kd { color: #204a87; font-weight: bold } /* Keyword.Declaration */ -.highlight .kn { color: #204a87; font-weight: bold } /* Keyword.Namespace */ -.highlight .kp { color: #204a87; font-weight: bold } /* Keyword.Pseudo */ -.highlight .kr { color: #204a87; font-weight: bold } /* Keyword.Reserved */ -.highlight .kt { color: #204a87; font-weight: bold } /* Keyword.Type */ -.highlight .ld { color: #000000 } /* Literal.Date */ -.highlight .m { color: #0000cf; font-weight: bold } /* Literal.Number */ -.highlight .s { color: #4e9a06 } /* Literal.String */ -.highlight .na { color: #c4a000 } /* Name.Attribute */ -.highlight .nb { color: #204a87 } /* Name.Builtin */ -.highlight .nc { color: #000000 } /* Name.Class */ -.highlight .no { color: #000000 } /* Name.Constant */ -.highlight .nd { color: #5c35cc; font-weight: bold } /* Name.Decorator */ -.highlight .ni { color: #ce5c00 } /* Name.Entity */ -.highlight .ne { color: #cc0000; font-weight: bold } /* Name.Exception */ -.highlight .nf { color: #000000 } /* Name.Function */ -.highlight .nl { color: #f57900 } /* Name.Label */ -.highlight .nn { color: #000000 } /* Name.Namespace */ -.highlight .nx { color: #000000 } /* Name.Other */ -.highlight .py { color: #000000 } /* Name.Property */ -.highlight .nt { color: #204a87; font-weight: bold } /* Name.Tag */ -.highlight .nv { color: #000000 } /* Name.Variable */ -.highlight .ow { color: #204a87; font-weight: bold } /* Operator.Word */ -.highlight .w { color: #f8f8f8; text-decoration: underline } /* Text.Whitespace */ -.highlight .mb { color: #0000cf; font-weight: bold } /* Literal.Number.Bin */ -.highlight .mf { color: #0000cf; font-weight: bold } /* Literal.Number.Float */ -.highlight .mh { color: #0000cf; font-weight: bold } /* Literal.Number.Hex */ -.highlight .mi { color: #0000cf; font-weight: bold } /* Literal.Number.Integer */ -.highlight .mo { color: #0000cf; font-weight: bold } /* Literal.Number.Oct */ -.highlight .sa { color: #4e9a06 } /* Literal.String.Affix */ -.highlight .sb { color: #4e9a06 } /* Literal.String.Backtick */ -.highlight .sc { color: #4e9a06 } /* Literal.String.Char */ -.highlight .dl { color: #4e9a06 } /* Literal.String.Delimiter */ -.highlight .sd { color: #8f5902; font-style: italic } /* Literal.String.Doc */ -.highlight .s2 { color: #4e9a06 } /* Literal.String.Double */ -.highlight .se { color: #4e9a06 } /* Literal.String.Escape */ -.highlight .sh { color: #4e9a06 } /* Literal.String.Heredoc */ -.highlight .si { color: #4e9a06 } /* Literal.String.Interpol */ -.highlight .sx { color: #4e9a06 } /* Literal.String.Other */ -.highlight .sr { color: #4e9a06 } /* Literal.String.Regex */ -.highlight .s1 { color: #4e9a06 } /* Literal.String.Single */ -.highlight .ss { color: #4e9a06 } /* Literal.String.Symbol */ -.highlight .bp { color: #3465a4 } /* Name.Builtin.Pseudo */ -.highlight .fm { color: #000000 } /* Name.Function.Magic */ -.highlight .vc { color: #000000 } /* Name.Variable.Class */ -.highlight .vg { color: #000000 } /* Name.Variable.Global */ -.highlight .vi { color: #000000 } /* Name.Variable.Instance */ -.highlight .vm { color: #000000 } /* Name.Variable.Magic */ -.highlight .il { color: #0000cf; font-weight: bold } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/docs/_static/pygments/trac.css b/docs/_static/pygments/trac.css deleted file mode 100644 index 05fa84e06..000000000 --- a/docs/_static/pygments/trac.css +++ /dev/null @@ -1,67 +0,0 @@ -.highlight .hll { background-color: #ffffcc } -.highlight { background: #ffffff; } -.highlight .c { color: #999988; font-style: italic } /* Comment */ -.highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */ -.highlight .k { font-weight: bold } /* Keyword */ -.highlight .o { font-weight: bold } /* Operator */ -.highlight .ch { color: #999988; font-style: italic } /* Comment.Hashbang */ -.highlight .cm { color: #999988; font-style: italic } /* Comment.Multiline */ -.highlight .cp { color: #999999; font-weight: bold } /* Comment.Preproc */ -.highlight .cpf { color: #999988; font-style: italic } /* Comment.PreprocFile */ -.highlight .c1 { color: #999988; font-style: italic } /* Comment.Single */ -.highlight .cs { color: #999999; font-weight: bold; font-style: italic } /* Comment.Special */ -.highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */ -.highlight .ge { font-style: italic } /* Generic.Emph */ -.highlight .gr { color: #aa0000 } /* Generic.Error */ -.highlight .gh { color: #999999 } /* Generic.Heading */ -.highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */ -.highlight .go { color: #888888 } /* Generic.Output */ -.highlight .gp { color: #555555 } /* Generic.Prompt */ -.highlight .gs { font-weight: bold } /* Generic.Strong */ -.highlight .gu { color: #aaaaaa } /* Generic.Subheading */ -.highlight .gt { color: #aa0000 } /* Generic.Traceback */ -.highlight .kc { font-weight: bold } /* Keyword.Constant */ -.highlight .kd { font-weight: bold } /* Keyword.Declaration */ -.highlight .kn { font-weight: bold } /* Keyword.Namespace */ -.highlight .kp { font-weight: bold } /* Keyword.Pseudo */ -.highlight .kr { font-weight: bold } /* Keyword.Reserved */ -.highlight .kt { color: #445588; font-weight: bold } /* Keyword.Type */ -.highlight .m { color: #009999 } /* Literal.Number */ -.highlight .s { color: #bb8844 } /* Literal.String */ -.highlight .na { color: #008080 } /* Name.Attribute */ -.highlight .nb { color: #999999 } /* Name.Builtin */ -.highlight .nc { color: #445588; font-weight: bold } /* Name.Class */ -.highlight .no { color: #008080 } /* Name.Constant */ -.highlight .ni { color: #800080 } /* Name.Entity */ -.highlight .ne { color: #990000; font-weight: bold } /* Name.Exception */ -.highlight .nf { color: #990000; font-weight: bold } /* Name.Function */ -.highlight .nn { color: #555555 } /* Name.Namespace */ -.highlight .nt { color: #000080 } /* Name.Tag */ -.highlight .nv { color: #008080 } /* Name.Variable */ -.highlight .ow { font-weight: bold } /* Operator.Word */ -.highlight .w { color: #bbbbbb } /* Text.Whitespace */ -.highlight .mb { color: #009999 } /* Literal.Number.Bin */ -.highlight .mf { color: #009999 } /* Literal.Number.Float */ -.highlight .mh { color: #009999 } /* Literal.Number.Hex */ -.highlight .mi { color: #009999 } /* Literal.Number.Integer */ -.highlight .mo { color: #009999 } /* Literal.Number.Oct */ -.highlight .sa { color: #bb8844 } /* Literal.String.Affix */ -.highlight .sb { color: #bb8844 } /* Literal.String.Backtick */ -.highlight .sc { color: #bb8844 } /* Literal.String.Char */ -.highlight .dl { color: #bb8844 } /* Literal.String.Delimiter */ -.highlight .sd { color: #bb8844 } /* Literal.String.Doc */ -.highlight .s2 { color: #bb8844 } /* Literal.String.Double */ -.highlight .se { color: #bb8844 } /* Literal.String.Escape */ -.highlight .sh { color: #bb8844 } /* Literal.String.Heredoc */ -.highlight .si { color: #bb8844 } /* Literal.String.Interpol */ -.highlight .sx { color: #bb8844 } /* Literal.String.Other */ -.highlight .sr { color: #808000 } /* Literal.String.Regex */ -.highlight .s1 { color: #bb8844 } /* Literal.String.Single */ -.highlight .ss { color: #bb8844 } /* Literal.String.Symbol */ -.highlight .bp { color: #999999 } /* Name.Builtin.Pseudo */ -.highlight .fm { color: #990000; font-weight: bold } /* Name.Function.Magic */ -.highlight .vc { color: #008080 } /* Name.Variable.Class */ -.highlight .vg { color: #008080 } /* Name.Variable.Global */ -.highlight .vi { color: #008080 } /* Name.Variable.Instance */ -.highlight .vm { color: #008080 } /* Name.Variable.Magic */ -.highlight .il { color: #009999 } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/docs/_static/pygments/vim.css b/docs/_static/pygments/vim.css deleted file mode 100644 index afed707b5..000000000 --- a/docs/_static/pygments/vim.css +++ /dev/null @@ -1,78 +0,0 @@ -.highlight .hll { background-color: #222222 } -.highlight { background: #000000; color: #cccccc } -.highlight .c { color: #000080 } /* Comment */ -.highlight .err { color: #cccccc; border: 1px solid #FF0000 } /* Error */ -.highlight .esc { color: #cccccc } /* Escape */ -.highlight .g { color: #cccccc } /* Generic */ -.highlight .k { color: #cdcd00 } /* Keyword */ -.highlight .l { color: #cccccc } /* Literal */ -.highlight .n { color: #cccccc } /* Name */ -.highlight .o { color: #3399cc } /* Operator */ -.highlight .x { color: #cccccc } /* Other */ -.highlight .p { color: #cccccc } /* Punctuation */ -.highlight .ch { color: #000080 } /* Comment.Hashbang */ -.highlight .cm { color: #000080 } /* Comment.Multiline */ -.highlight .cp { color: #000080 } /* Comment.Preproc */ -.highlight .cpf { color: #000080 } /* Comment.PreprocFile */ -.highlight .c1 { color: #000080 } /* Comment.Single */ -.highlight .cs { color: #cd0000; font-weight: bold } /* Comment.Special */ -.highlight .gd { color: #cd0000 } /* Generic.Deleted */ -.highlight .ge { color: #cccccc; font-style: italic } /* Generic.Emph */ -.highlight .gr { color: #FF0000 } /* Generic.Error */ -.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ -.highlight .gi { color: #00cd00 } /* Generic.Inserted */ -.highlight .go { color: #888888 } /* Generic.Output */ -.highlight .gp { color: #000080; font-weight: bold } /* Generic.Prompt */ -.highlight .gs { color: #cccccc; font-weight: bold } /* Generic.Strong */ -.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ -.highlight .gt { color: #0044DD } /* Generic.Traceback */ -.highlight .kc { color: #cdcd00 } /* Keyword.Constant */ -.highlight .kd { color: #00cd00 } /* Keyword.Declaration */ -.highlight .kn { color: #cd00cd } /* Keyword.Namespace */ -.highlight .kp { color: #cdcd00 } /* Keyword.Pseudo */ -.highlight .kr { color: #cdcd00 } /* Keyword.Reserved */ -.highlight .kt { color: #00cd00 } /* Keyword.Type */ -.highlight .ld { color: #cccccc } /* Literal.Date */ -.highlight .m { color: #cd00cd } /* Literal.Number */ -.highlight .s { color: #cd0000 } /* Literal.String */ -.highlight .na { color: #cccccc } /* Name.Attribute */ -.highlight .nb { color: #cd00cd } /* Name.Builtin */ -.highlight .nc { color: #00cdcd } /* Name.Class */ -.highlight .no { color: #cccccc } /* Name.Constant */ -.highlight .nd { color: #cccccc } /* Name.Decorator */ -.highlight .ni { color: #cccccc } /* Name.Entity */ -.highlight .ne { color: #666699; font-weight: bold } /* Name.Exception */ -.highlight .nf { color: #cccccc } /* Name.Function */ -.highlight .nl { color: #cccccc } /* Name.Label */ -.highlight .nn { color: #cccccc } /* Name.Namespace */ -.highlight .nx { color: #cccccc } /* Name.Other */ -.highlight .py { color: #cccccc } /* Name.Property */ -.highlight .nt { color: #cccccc } /* Name.Tag */ -.highlight .nv { color: #00cdcd } /* Name.Variable */ -.highlight .ow { color: #cdcd00 } /* Operator.Word */ -.highlight .w { color: #cccccc } /* Text.Whitespace */ -.highlight .mb { color: #cd00cd } /* Literal.Number.Bin */ -.highlight .mf { color: #cd00cd } /* Literal.Number.Float */ -.highlight .mh { color: #cd00cd } /* Literal.Number.Hex */ -.highlight .mi { color: #cd00cd } /* Literal.Number.Integer */ -.highlight .mo { color: #cd00cd } /* Literal.Number.Oct */ -.highlight .sa { color: #cd0000 } /* Literal.String.Affix */ -.highlight .sb { color: #cd0000 } /* Literal.String.Backtick */ -.highlight .sc { color: #cd0000 } /* Literal.String.Char */ -.highlight .dl { color: #cd0000 } /* Literal.String.Delimiter */ -.highlight .sd { color: #cd0000 } /* Literal.String.Doc */ -.highlight .s2 { color: #cd0000 } /* Literal.String.Double */ -.highlight .se { color: #cd0000 } /* Literal.String.Escape */ -.highlight .sh { color: #cd0000 } /* Literal.String.Heredoc */ -.highlight .si { color: #cd0000 } /* Literal.String.Interpol */ -.highlight .sx { color: #cd0000 } /* Literal.String.Other */ -.highlight .sr { color: #cd0000 } /* Literal.String.Regex */ -.highlight .s1 { color: #cd0000 } /* Literal.String.Single */ -.highlight .ss { color: #cd0000 } /* Literal.String.Symbol */ -.highlight .bp { color: #cd00cd } /* Name.Builtin.Pseudo */ -.highlight .fm { color: #cccccc } /* Name.Function.Magic */ -.highlight .vc { color: #00cdcd } /* Name.Variable.Class */ -.highlight .vg { color: #00cdcd } /* Name.Variable.Global */ -.highlight .vi { color: #00cdcd } /* Name.Variable.Instance */ -.highlight .vm { color: #00cdcd } /* Name.Variable.Magic */ -.highlight .il { color: #cd00cd } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/docs/_static/pygments/vs.css b/docs/_static/pygments/vs.css deleted file mode 100644 index af4b2a094..000000000 --- a/docs/_static/pygments/vs.css +++ /dev/null @@ -1,38 +0,0 @@ -.highlight .hll { background-color: #ffffcc } -.highlight { background: #ffffff; } -.highlight .c { color: #008000 } /* Comment */ -.highlight .err { border: 1px solid #FF0000 } /* Error */ -.highlight .k { color: #0000ff } /* Keyword */ -.highlight .ch { color: #008000 } /* Comment.Hashbang */ -.highlight .cm { color: #008000 } /* Comment.Multiline */ -.highlight .cp { color: #0000ff } /* Comment.Preproc */ -.highlight .cpf { color: #008000 } /* Comment.PreprocFile */ -.highlight .c1 { color: #008000 } /* Comment.Single */ -.highlight .cs { color: #008000 } /* Comment.Special */ -.highlight .ge { font-style: italic } /* Generic.Emph */ -.highlight .gh { font-weight: bold } /* Generic.Heading */ -.highlight .gp { font-weight: bold } /* Generic.Prompt */ -.highlight .gs { font-weight: bold } /* Generic.Strong */ -.highlight .gu { font-weight: bold } /* Generic.Subheading */ -.highlight .kc { color: #0000ff } /* Keyword.Constant */ -.highlight .kd { color: #0000ff } /* Keyword.Declaration */ -.highlight .kn { color: #0000ff } /* Keyword.Namespace */ -.highlight .kp { color: #0000ff } /* Keyword.Pseudo */ -.highlight .kr { color: #0000ff } /* Keyword.Reserved */ -.highlight .kt { color: #2b91af } /* Keyword.Type */ -.highlight .s { color: #a31515 } /* Literal.String */ -.highlight .nc { color: #2b91af } /* Name.Class */ -.highlight .ow { color: #0000ff } /* Operator.Word */ -.highlight .sa { color: #a31515 } /* Literal.String.Affix */ -.highlight .sb { color: #a31515 } /* Literal.String.Backtick */ -.highlight .sc { color: #a31515 } /* Literal.String.Char */ -.highlight .dl { color: #a31515 } /* Literal.String.Delimiter */ -.highlight .sd { color: #a31515 } /* Literal.String.Doc */ -.highlight .s2 { color: #a31515 } /* Literal.String.Double */ -.highlight .se { color: #a31515 } /* Literal.String.Escape */ -.highlight .sh { color: #a31515 } /* Literal.String.Heredoc */ -.highlight .si { color: #a31515 } /* Literal.String.Interpol */ -.highlight .sx { color: #a31515 } /* Literal.String.Other */ -.highlight .sr { color: #a31515 } /* Literal.String.Regex */ -.highlight .s1 { color: #a31515 } /* Literal.String.Single */ -.highlight .ss { color: #a31515 } /* Literal.String.Symbol */ \ No newline at end of file diff --git a/docs/_static/pygments/xcode.css b/docs/_static/pygments/xcode.css deleted file mode 100644 index 1b11b1e53..000000000 --- a/docs/_static/pygments/xcode.css +++ /dev/null @@ -1,63 +0,0 @@ -.highlight .hll { background-color: #ffffcc } -.highlight { background: #ffffff; } -.highlight .c { color: #177500 } /* Comment */ -.highlight .err { color: #000000 } /* Error */ -.highlight .k { color: #A90D91 } /* Keyword */ -.highlight .l { color: #1C01CE } /* Literal */ -.highlight .n { color: #000000 } /* Name */ -.highlight .o { color: #000000 } /* Operator */ -.highlight .ch { color: #177500 } /* Comment.Hashbang */ -.highlight .cm { color: #177500 } /* Comment.Multiline */ -.highlight .cp { color: #633820 } /* Comment.Preproc */ -.highlight .cpf { color: #177500 } /* Comment.PreprocFile */ -.highlight .c1 { color: #177500 } /* Comment.Single */ -.highlight .cs { color: #177500 } /* Comment.Special */ -.highlight .kc { color: #A90D91 } /* Keyword.Constant */ -.highlight .kd { color: #A90D91 } /* Keyword.Declaration */ -.highlight .kn { color: #A90D91 } /* Keyword.Namespace */ -.highlight .kp { color: #A90D91 } /* Keyword.Pseudo */ -.highlight .kr { color: #A90D91 } /* Keyword.Reserved */ -.highlight .kt { color: #A90D91 } /* Keyword.Type */ -.highlight .ld { color: #1C01CE } /* Literal.Date */ -.highlight .m { color: #1C01CE } /* Literal.Number */ -.highlight .s { color: #C41A16 } /* Literal.String */ -.highlight .na { color: #836C28 } /* Name.Attribute */ -.highlight .nb { color: #A90D91 } /* Name.Builtin */ -.highlight .nc { color: #3F6E75 } /* Name.Class */ -.highlight .no { color: #000000 } /* Name.Constant */ -.highlight .nd { color: #000000 } /* Name.Decorator */ -.highlight .ni { color: #000000 } /* Name.Entity */ -.highlight .ne { color: #000000 } /* Name.Exception */ -.highlight .nf { color: #000000 } /* Name.Function */ -.highlight .nl { color: #000000 } /* Name.Label */ -.highlight .nn { color: #000000 } /* Name.Namespace */ -.highlight .nx { color: #000000 } /* Name.Other */ -.highlight .py { color: #000000 } /* Name.Property */ -.highlight .nt { color: #000000 } /* Name.Tag */ -.highlight .nv { color: #000000 } /* Name.Variable */ -.highlight .ow { color: #000000 } /* Operator.Word */ -.highlight .mb { color: #1C01CE } /* Literal.Number.Bin */ -.highlight .mf { color: #1C01CE } /* Literal.Number.Float */ -.highlight .mh { color: #1C01CE } /* Literal.Number.Hex */ -.highlight .mi { color: #1C01CE } /* Literal.Number.Integer */ -.highlight .mo { color: #1C01CE } /* Literal.Number.Oct */ -.highlight .sa { color: #C41A16 } /* Literal.String.Affix */ -.highlight .sb { color: #C41A16 } /* Literal.String.Backtick */ -.highlight .sc { color: #2300CE } /* Literal.String.Char */ -.highlight .dl { color: #C41A16 } /* Literal.String.Delimiter */ -.highlight .sd { color: #C41A16 } /* Literal.String.Doc */ -.highlight .s2 { color: #C41A16 } /* Literal.String.Double */ -.highlight .se { color: #C41A16 } /* Literal.String.Escape */ -.highlight .sh { color: #C41A16 } /* Literal.String.Heredoc */ -.highlight .si { color: #C41A16 } /* Literal.String.Interpol */ -.highlight .sx { color: #C41A16 } /* Literal.String.Other */ -.highlight .sr { color: #C41A16 } /* Literal.String.Regex */ -.highlight .s1 { color: #C41A16 } /* Literal.String.Single */ -.highlight .ss { color: #C41A16 } /* Literal.String.Symbol */ -.highlight .bp { color: #5B269A } /* Name.Builtin.Pseudo */ -.highlight .fm { color: #000000 } /* Name.Function.Magic */ -.highlight .vc { color: #000000 } /* Name.Variable.Class */ -.highlight .vg { color: #000000 } /* Name.Variable.Global */ -.highlight .vi { color: #000000 } /* Name.Variable.Instance */ -.highlight .vm { color: #000000 } /* Name.Variable.Magic */ -.highlight .il { color: #1C01CE } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/docs/_templates/breadcrumbs.html b/docs/_templates/breadcrumbs.html deleted file mode 100644 index 997b4c9cb..000000000 --- a/docs/_templates/breadcrumbs.html +++ /dev/null @@ -1,21 +0,0 @@ -{% extends "!breadcrumbs.html" %} - -{% block breadcrumbs %} - -
  • - -
  • - {{ super() }} -{% endblock %} diff --git a/docs/_templates/layout.html b/docs/_templates/layout.html deleted file mode 100644 index bb8f17002..000000000 --- a/docs/_templates/layout.html +++ /dev/null @@ -1,24 +0,0 @@ -{% extends "!layout.html" %} - -{% block extrahead %} - - - {{ super() }} -{% endblock %} -{% block footer %} - - {{ super() }} -{% endblock %} diff --git a/docs/api.rst b/docs/api.rst index 0d5ce0fbd..5206fc0a4 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1,25 +1,45 @@ +.. _api: + ============= API reference ============= -Top-level functions -=================== +The comprehensive API reference. All of the below objects are imported +into the top-level namespace. Use ``help(pplt.object)`` to read +the docs during a python session. + +Please note that proplot removes the associated documentation when functionality +is deprecated (see :ref:`What's New `). However, proplot adheres to +`semantic versioning `__, which means old code that uses +deprecated functionality will still work and issue warnings rather than errors +until the first major release (version 1.0.0). + +.. important:: + + The documentation for "wrapper" functions like `standardize_1d` and `cmap_changer` + from proplot < 0.8.0 can now be found under individual `~proplot.axes.PlotAxes` + methods like `~proplot.axes.PlotAxes.plot` and `~proplot.axes.PlotAxes.pcolor`. Note + that calling ``help(ax.method)`` in a python session will show both the proplot + documentation and the original matplotlib documentation. -Primary functions used to interactively work with figures. Modeled after the -`~matplotlib.pyplot` versions. +Figure class +============ + +.. automodule:: proplot.figure -.. automodsumm:: proplot.subplots - :functions-only: +.. automodsumm:: proplot.figure :toctree: api -Figure classes -============== -The figure class and a couple related classes. +Grid classes +============ + +.. automodule:: proplot.gridspec -.. automodsumm:: proplot.subplots - :classes-only: +.. automodsumm:: proplot.gridspec :toctree: api + :skip: SubplotsContainer + Axes classes ============ @@ -28,99 +48,88 @@ Axes classes .. automodsumm:: proplot.axes :toctree: api - :classes-only: -Plotting wrappers -================= -.. automodule:: proplot.wrappers +Top-level functions +=================== + +.. automodule:: proplot.ui -.. automodsumm:: proplot.wrappers +.. automodsumm:: proplot.ui :toctree: api - :functions-only: -Projection tools -================ -.. automodule:: proplot.projs +Configuration tools +=================== -.. rubric:: Variables +.. automodule:: proplot.config -.. automodsumm:: proplot.projs - :variables-only: +.. automodsumm:: proplot.config :toctree: api + :skip: inline_backend_fmt, RcConfigurator -.. rubric:: Functions -.. automodsumm:: proplot.projs - :functions-only: - :toctree: api +Constructor functions +===================== -.. rubric:: Classes +.. automodule:: proplot.constructor -.. automodsumm:: proplot.projs - :classes-only: +.. automodsumm:: proplot.constructor :toctree: api + :skip: Colors -Axis tools -========== -.. automodule:: proplot.axistools +Locators and formatters +======================= -.. rubric:: Variables +.. automodule:: proplot.ticker -.. automodsumm:: proplot.axistools +.. automodsumm:: proplot.ticker :toctree: api - :variables-only: -.. rubric:: Functions -.. automodsumm:: proplot.axistools - :toctree: api - :functions-only: +Axis scale classes +================== -.. rubric:: Classes +.. automodule:: proplot.scale -.. automodsumm:: proplot.axistools +.. automodsumm:: proplot.scale :toctree: api - :classes-only: -Color and font tools -==================== -.. automodule:: proplot.styletools +Colormaps and normalizers +========================= -.. rubric:: Variables +.. automodule:: proplot.colors -.. automodsumm:: proplot.styletools - :variables-only: +.. automodsumm:: proplot.colors :toctree: api + :skip: ListedColormap, LinearSegmentedColormap, PerceptuallyUniformColormap, LinearSegmentedNorm -.. rubric:: Functions -.. automodsumm:: proplot.styletools - :functions-only: - :toctree: api +Projection classes +================== -.. rubric:: Classes +.. automodule:: proplot.proj -.. automodsumm:: proplot.styletools - :classes-only: +.. automodsumm:: proplot.proj :toctree: api -Rc configuration tools -====================== -.. automodule:: proplot.rctools +Demo functions +============== + +.. automodule:: proplot.demos -.. automodsumm:: proplot.rctools +.. automodsumm:: proplot.demos :toctree: api -Miscellaneous tools -=================== + +Miscellaneous functions +======================= .. automodule:: proplot.utils .. automodsumm:: proplot.utils - :functions-only: :toctree: api + :skip: shade, saturate diff --git a/docs/authors.rst b/docs/authors.rst index 5783548e1..98164b0a8 100644 --- a/docs/authors.rst +++ b/docs/authors.rst @@ -1,22 +1,57 @@ +.. _authors: + About the authors ================= -Main authors ------------- +Creator +------- + * `Luke Davis`_ Contributors ------------ + +* `Luke Davis`_ * `Riley Brady`_ +* `Mark Harfouche`_ +* `Stephane Raynaud`_ +* `Mickaël Lalande`_ +* `Pratiman Patel`_ +* `Zachary Moon`_ Bios ---- -`Luke Davis`_ is the sole developer as of December 2019. He is a graduate student in climate science at Colorado State University who has always been frustrated by repetitive and cumbersome plotting code. As an undergraduate, he developed an extensive set of `MATLAB plotting utilities `__ for personal use. When he switched to python in graduate school, he replicated most of these utilities in python, learned more about the language, began to rapidly develop them, and decided to share them with the rest of the scientific community. Luke is also working on a project called `climpy `__, a companion to `metpy `__ for carrying out data analysis tasks related to *climate science*, and has authored `a number of vim plugins `__ as an avid vim user. - -`Riley Brady`_ is the biggest contributor. He helped Luke set up automatic testing, deploy ProPlot to PyPi, and make ProPlot more suitable for collaboration. He is also ProPlot's earliest user and helped fix `a lot of the early bugs `__. He is the lead developer on a climate prediction package called `climpred `__. - +`Luke Davis`_ is the project creator and primary contributor as of 2021. +He is a PhD candidate at Colorado State University's +`Department of Atmospheric Science `__ +who has always been frustrated by repetitive and cumbersome +plotting code. As an undergraduate, he developed a set of +`MATLAB plotting utilities `__ for personal use. +When he switched to python in graduate school, he replicated most of these utilities in +python, learned more about the language, began to rapidly develop them, and decided to +share them with the rest of the scientific community. Luke is also working on a project +called `climopy `__, a companion to +`metpy `__ for carrying out data analysis tasks +related to climate science, and has authored a number of +`vim plugins `__ +as an avid vim user. + +`Riley Brady`_ is the next-biggest contributor. He helped Luke set up automatic +testing, deploy this project to PyPi, and improve new user experience. He is +also proplot's earliest user and helped fix `lots of early bugs +`__. .. _Luke Davis: https://github.com/lukelbd .. _Riley Brady: https://github.com/bradyrx + +.. _Mark Harfouche: https://github.com/hmaarrfk + +.. _Stephane Raynaud: https://github.com/stefraynaud + +.. _Pratiman Patel: https://github.com/pratiman-91 + +.. _Mickaël Lalande: https://github.com/mickaellalande + +.. _Zachary Moon: https://github.com/zmoon diff --git a/docs/axis.ipynb b/docs/axis.ipynb deleted file mode 100644 index 38f40eb38..000000000 --- a/docs/axis.ipynb +++ /dev/null @@ -1,649 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# X and Y axis settings\n", - "\n" - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "This section documents features used for modifying *x* and *y* axis settings, including axis scales, tick locations, and tick label formatting. It also documents a handy \"dual axes\" feature." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Axis tick locations" - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "\"Axis locators\" are used to automatically select sensible tick locations based on the axis data limits. In ProPlot, you can change the axis locator using the `~proplot.axes.Axes.format` keyword arguments `xlocator`, `ylocator`, `xminorlocator`, and `yminorlocator` (or their aliases, `xticks`, `yticks`, `xminorticks`, and `yminorticks`). This is powered by the `~proplot.axistools.Locator` constructor function.\n", - "\n", - "These keyword arguments can be used to apply built-in matplotlib `~matplotlib.ticker.Locator`\\ s by their \"registered\" names (e.g. ``xlocator='log'``), to draw ticks every ``N`` data values with `~matplotlib.ticker.MultipleLocator` (e.g. ``xlocator=2``), or to tick the specific locations in a list using `~matplotlib.ticker.FixedLocator` (just like `~matplotlib.axes.Axes.set_xticks` and `~matplotlib.axes.Axes.set_yticks`). See `~proplot.axes.XYAxes.format` and `~proplot.axistools.Locator` for details.\n", - "\n", - "To generate lists of tick locations, we recommend using ProPlot's `~proplot.utils.arange` function -- it’s basically an *endpoint-inclusive* version of `numpy.arange`, which is usually what you'll want in this context." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", - "import numpy as np\n", - "state = np.random.RandomState(51423)\n", - "plot.rc.facecolor = plot.shade('powder blue', 1.15)\n", - "plot.rc.update(\n", - " linewidth=1,\n", - " small=10, large=12,\n", - " color='dark blue', suptitlecolor='dark blue',\n", - " titleloc='upper center', titlecolor='dark blue', titleborder=False,\n", - ")\n", - "f, axs = plot.subplots(nrows=5, axwidth=5, aspect=(8, 1), share=0)\n", - "axs.format(suptitle='Tick locators demo')\n", - "\n", - "# Manual locations\n", - "axs[0].format(\n", - " xlim=(0, 200), xminorlocator=10, xlocator=30,\n", - " title='MultipleLocator'\n", - ")\n", - "axs[1].format(\n", - " xlim=(0, 10), xminorlocator=0.1,\n", - " xlocator=[0, 0.3, 0.8, 1.6, 4.4, 8, 8.8, 10],\n", - " title='FixedLocator',\n", - ")\n", - "\n", - "# Approx number of ticks you want, but not exact locations\n", - "axs[3].format(\n", - " xlim=(1, 10), xlocator=('maxn', 20),\n", - " title='MaxNLocator',\n", - ")\n", - "\n", - "# Log minor locator, automatically applied for log scale plots\n", - "axs[2].format(\n", - " xlim=(1, 100), xlocator='log', xminorlocator='logminor',\n", - " title='LogLocator',\n", - ")\n", - "\n", - "# Index locator, only draws ticks where data is plotted\n", - "axs[4].plot(np.arange(10) - 5, state.rand(10), alpha=0)\n", - "axs[4].format(\n", - " xlim=(0, 6), ylim=(0, 1), xlocator='index',\n", - " xformatter=[r'$\\alpha$', r'$\\beta$', r'$\\gamma$', r'$\\delta$', r'$\\epsilon$'],\n", - " title='IndexLocator',\n", - ")\n", - "plot.rc.reset()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Axis tick labels" - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "\"Axis formatters\" are used to convert floating point numbers to nicely-formatted tick labels. In ProPlot, you can change the axis formatter using the `~proplot.axes.Axes.format` keyword arguments `xformatter` and `yformatter` (or their aliases, `xticklabels` and `yticklabels`). This is powered by the `~proplot.axistools.Formatter` constructor function.\n", - "\n", - "These keyword arguments can be used to apply built-in matplotlib `~matplotlib.ticker.Formatter`\\ s by their \"registered\" names (e.g. ``xformatter='log'``), to apply new \"preset\" axis formatters (e.g. ``xformatter='deglat'`` to label ticks as the geographic latitude or ``xformatter='pi'`` to label ticks as fractions of :math:`\\pi`), to apply a ``%``-style format directive with `~matplotlib.ticker.FormatStrFormatter` (e.g. ``xformatter='%.0f'``), or to apply custom tick labels with `~matplotlib.ticker.FixedFormatter` (just like `~matplotlib.axes.Axes.set_xticklabels` and `~matplotlib.axes.Axes.set_yticklabels`). See `~proplot.axes.XYAxes.format` and `~proplot.axistools.Formatter` for details." - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "ProPlot also changes the default axis formatter to `~proplot.axistools.AutoFormatter`. This class trims trailing zeros by default, can be used to *omit tick labels* outside of some data range, and can add arbitrary prefixes and suffixes to each label. See `~proplot.axistools.AutoFormatter` for details." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", - "import numpy as np\n", - "plot.rc.update(\n", - " linewidth=1.2, small=10, large=12, facecolor='gray8', figurefacecolor='gray8',\n", - " suptitlecolor='w', gridcolor='w', color='w',\n", - " titleloc='upper center', titlecolor='w', titleborder=False,\n", - ")\n", - "f, axs = plot.subplots(nrows=6, axwidth=5, aspect=(8, 1), share=0)\n", - "\n", - "# Fraction formatters\n", - "axs[0].format(\n", - " xlim=(0, 3*np.pi), xlocator=plot.arange(0, 4, 0.25) * np.pi,\n", - " xformatter='pi', title='FracFormatter',\n", - ")\n", - "axs[1].format(\n", - " xlim=(0, 2*np.e), xlocator=plot.arange(0, 2, 0.5) * np.e,\n", - " xticklabels='e', title='FracFormatter',\n", - ")\n", - "\n", - "# Geographic formatter\n", - "axs[2].format(\n", - " xlim=(-90, 90), xlocator=plot.arange(-90, 90, 30),\n", - " xformatter='deglat', title='Geographic preset'\n", - ")\n", - "\n", - "# User input labels\n", - "axs[3].format(\n", - " xlim=(-1.01, 1), xlocator=0.5,\n", - " xticklabels=['a', 'b', 'c', 'd', 'e'], title='FixedFormatter',\n", - ")\n", - "\n", - "# Custom style labels\n", - "axs[4].format(\n", - " xlim=(0, 0.001), xlocator=0.0001,\n", - " xformatter='%.E', title='FormatStrFormatter',\n", - ")\n", - "axs[5].format(\n", - " xlim=(0, 100), xtickminor=False, xlocator=20,\n", - " xformatter='{x:.1f}', title='StrMethodFormatter',\n", - ")\n", - "axs.format(ylocator='null', suptitle='Tick formatters demo')\n", - "plot.rc.reset()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", - "plot.rc.linewidth = 2\n", - "plot.rc.small = plot.rc.large = 11\n", - "locator = [0, 0.25, 0.5, 0.75, 1]\n", - "f, axs = plot.subplots([[1, 1, 2, 2], [0, 3, 3, 0]], axwidth=1.5, share=0)\n", - "\n", - "# Formatter comparison\n", - "axs[0].format(\n", - " xformatter='scalar', yformatter='scalar', title='Matplotlib formatter'\n", - ")\n", - "axs[1].format(yticklabelloc='both', title='ProPlot formatter')\n", - "axs[:2].format(xlocator=locator, ylocator=locator)\n", - "\n", - "# Limiting the formatter tick range\n", - "axs[2].format(\n", - " title='Omitting tick labels', ticklen=5, xlim=(0, 5), ylim=(0, 5),\n", - " xtickrange=(0, 2), ytickrange=(0, 2), xlocator=1, ylocator=1\n", - ")\n", - "axs.format(\n", - " ytickloc='both', yticklabelloc='both',\n", - " titlepad='0.5em', suptitle='Default formatters demo'\n", - ")\n", - "plot.rc.reset()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Datetime axes" - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "ProPlot can also be used to customize the tick locations and tick label format of \"datetime\" axes. To draw ticks on some particular time unit, just use a unit string (e.g. ``xlocator='month'``). To draw ticks every ``N`` time units, just use a (unit, N) tuple (e.g. ``xlocator=('day', 5)``). For `% style formatting `__ of datetime tick labels, just use a string containing ``'%'`` (e.g. ``xformatter='%Y-%m-%d'``). See `~proplot.axes.XYAxes.format`, `~proplot.axistools.Locator`, and `~proplot.axistools.Formatter` for details." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", - "import numpy as np\n", - "plot.rc.update(\n", - " linewidth=1.2, small=10, large=12, ticklenratio=0.7,\n", - " figurefacecolor='w', facecolor=plot.shade('C0', 2.7),\n", - " titleloc='upper center', titleborder=False,\n", - ")\n", - "f, axs = plot.subplots(nrows=5, axwidth=6, aspect=(8, 1), share=0)\n", - "axs[:4].format(xrotation=0) # no rotation for these examples\n", - "\n", - "# Default date locator\n", - "# This is enabled if you plot datetime data or set datetime limits\n", - "axs[0].format(\n", - " xlim=(np.datetime64('2000-01-01'), np.datetime64('2001-01-02')),\n", - " title='Auto date locator and formatter'\n", - ")\n", - "\n", - "# Concise date formatter introduced in matplotlib 3.1\n", - "axs[1].format(\n", - " xlim=(np.datetime64('2000-01-01'), np.datetime64('2001-01-01')),\n", - " xformatter='concise', title='Concise date formatter',\n", - ")\n", - "\n", - "# Minor ticks every year, major every 10 years\n", - "axs[2].format(\n", - " xlim=(np.datetime64('2000-01-01'), np.datetime64('2050-01-01')),\n", - " xlocator=('year', 10), xformatter='\\'%y', title='Ticks every N units',\n", - ")\n", - "\n", - "# Minor ticks every 10 minutes, major every 2 minutes\n", - "axs[3].format(\n", - " xlim=(np.datetime64('2000-01-01T00:00:00'), np.datetime64('2000-01-01T12:00:00')),\n", - " xlocator=('hour', range(0, 24, 2)), xminorlocator=('minute', range(0, 60, 10)),\n", - " xformatter='T%H:%M:%S', title='Ticks at specific intervals',\n", - ")\n", - "\n", - "# Month and year labels, with default tick label rotation\n", - "axs[4].format(\n", - " xlim=(np.datetime64('2000-01-01'), np.datetime64('2008-01-01')),\n", - " xlocator='year', xminorlocator='month', # minor ticks every month\n", - " xformatter='%b %Y', title='Ticks with default rotation',\n", - ")\n", - "axs.format(\n", - " ylocator='null', suptitle='Datetime locators and formatters demo'\n", - ")\n", - "plot.rc.reset()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Axis scales" - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "\"Axis scales\" like ``'linear'`` and ``'log'`` control the *x* and *y* axis coordinate system. To change the axis scale, simply pass e.g. ``xscale='log'`` or ``yscale='log'`` to `~proplot.axes.Axes.format`. This is powered by the `~proplot.axistools.Scale` constructor function.\n", - "\n", - "ProPlot also makes several changes to the axis scale API:\n", - "\n", - "* By default, the `~proplot.axistools.AutoFormatter` formatter is used for all axis scales instead of e.g. `~matplotlib.ticker.LogFormatter` for `~matplotlib.scale.LogScale` scales. This can be changed e.g. by passing ``xformatter='log'`` or ``yformatter='log'`` to `~proplot.axes.XYAxes.format`.\n", - "* To make its behavior consistent with `~proplot.axistools.Locator` and `~proplot.axistools.Formatter`, the `~proplot.axistools.Scale` constructor function returns instances of `~matplotlib.scale.ScaleBase`, and `~matplotlib.axes.Axes.set_xscale` and `~matplotlib.axes.Axes.set_yscale` now accept these class instances in addition to string names like ``'log'``.\n", - "* While matplotlib axis scales must be instantiated with an `~matplotlib.axis.Axis` instance (for backward compatibility reasons), ProPlot axis scales can be instantiated without the axis instance (e.g. ``plot.LogScale()`` instead of ``plot.LogScale(ax.xaxis)``). \n", - "* The ``'log'`` and ``'symlog'`` axis scales now accept the more sensible `base`, `linthresh`, `linscale`, and `subs` keyword arguments, rather than `basex`, `basey`, `linthreshx`, `linthreshy`, `linscalex`, `linscaley`, `subsx`, and `subsy`. Also, the default `subs` for the ``'symlog'`` axis scale is now ``np.arange(1, 10)``, and the default `linthresh` is now ``1``." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", - "import numpy as np\n", - "N = 200\n", - "lw = 3\n", - "plot.rc.update({\n", - " 'linewidth': 1, 'ticklabelweight': 'bold', 'axeslabelweight': 'bold'\n", - "})\n", - "f, axs = plot.subplots(ncols=2, nrows=2, axwidth=1.8, share=0)\n", - "axs.format(suptitle='Axis scales demo', ytickminor=True)\n", - "\n", - "# Linear and log scales\n", - "axs[0].format(yscale='linear', ylabel='linear scale')\n", - "axs[1].format(ylim=(1e-3, 1e3), yscale='log', ylabel='log scale')\n", - "axs[:2].plot(np.linspace(0, 1, N), np.linspace(0, 1000, N), lw=lw)\n", - "\n", - "# Symlog scale\n", - "ax = axs[2]\n", - "ax.format(yscale='symlog', ylabel='symlog scale')\n", - "ax.plot(np.linspace(0, 1, N), np.linspace(-1000, 1000, N), lw=lw)\n", - "\n", - "# Logit scale\n", - "ax = axs[3]\n", - "ax.format(yscale='logit', ylabel='logit scale')\n", - "ax.plot(np.linspace(0, 1, N), np.linspace(0.01, 0.99, N), lw=lw)\n", - "plot.rc.reset()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## New axis scales" - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "ProPlot introduces several new axis scales. The ``'cutoff'`` scale (see `~proplot.axistools.CutoffScale`) is useful when the statistical distribution of your data is very unusual. The ``'sine'`` scale (see `~proplot.axistools.SineLatitudeScale`) scales the axis with a sine function, resulting in an *area weighted* spherical latitude coordinate, and the ``'mercator'`` scale (see `~proplot.axistools.MercatorLatitudeScale`) scales the axis with the Mercator projection latitude coordinate. The ``'inverse'`` scale (see `~proplot.axistools.InverseScale`) can be useful when working with spectral data, especially with :ref:`\"dual\" unit axes `." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", - "import numpy as np\n", - "f, axs = plot.subplots(width=6, nrows=4, aspect=(5, 1), sharex=False)\n", - "ax = axs[0]\n", - "\n", - "# Sample data\n", - "x = np.linspace(0, 4*np.pi, 100)\n", - "dy = np.linspace(-1, 1, 5)\n", - "y1 = np.sin(x)\n", - "y2 = np.cos(x)\n", - "state = np.random.RandomState(51423)\n", - "data = state.rand(len(dy)-1, len(x)-1)\n", - "\n", - "# Loop through various cutoff scale options\n", - "titles = ('Zoom out of left', 'Zoom into left', 'Discrete jump', 'Fast jump')\n", - "args = [\n", - " (np.pi, 3), # speed up\n", - " (3*np.pi, 1/3), # slow down\n", - " (np.pi, np.inf, 3*np.pi), # discrete jump\n", - " (np.pi, 5, 3*np.pi) # fast jump\n", - "]\n", - "locators = (\n", - " 2*[np.pi/3]\n", - " + 2*[[*np.linspace(0, 1, 4) * np.pi, *(np.linspace(0, 1, 4) * np.pi + 3*np.pi)]]\n", - ")\n", - "for ax, iargs, title, locator in zip(axs, args, titles, locators):\n", - " ax.pcolormesh(x, dy, data, cmap='grays', cmap_kw={'right': 0.8})\n", - " for y, color in zip((y1, y2), ('coral', 'sky blue')):\n", - " ax.plot(x, y, lw=4, color=color)\n", - " ax.format(\n", - " xscale=('cutoff', *iargs), title=title,\n", - " xlim=(0, 4*np.pi), ylabel='wave amplitude',\n", - " xformatter='pi', xlocator=locator,\n", - " xtickminor=False, xgrid=True, ygrid=False, suptitle='Cutoff axis scales demo'\n", - " )" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", - "import numpy as np\n", - "plot.rc.reset()\n", - "f, axs = plot.subplots(nrows=2, ncols=3, axwidth=1.7, share=0, order='F')\n", - "axs.format(\n", - " collabels=['Power scales', 'Exponential scales', 'Cartographic scales'],\n", - " suptitle='Additional axis scales demo'\n", - ")\n", - "x = np.linspace(0, 1, 50)\n", - "y = 10*x\n", - "state = np.random.RandomState(51423)\n", - "data = state.rand(len(y) - 1, len(x) - 1)\n", - "\n", - "# Power scales\n", - "colors = ('coral', 'sky blue')\n", - "for ax, power, color in zip(axs[:2], (2, 1/4), colors):\n", - " ax.pcolormesh(x, y, data, cmap='grays', cmap_kw={'right': 0.8})\n", - " ax.plot(x, y, lw=4, color=color)\n", - " ax.format(\n", - " ylim=(0.1, 10), yscale=('power', power),\n", - " title=f'$x^{{{power}}}$'\n", - " )\n", - " \n", - "# Exp scales\n", - "for ax, a, c, color in zip(axs[2:4], (np.e, 2), (0.5, -1), colors):\n", - " ax.pcolormesh(x, y, data, cmap='grays', cmap_kw={'right': 0.8})\n", - " ax.plot(x, y, lw=4, color=color)\n", - " ax.format(\n", - " ylim=(0.1, 10), yscale=('exp', a, c),\n", - " title=f'${(a,\"e\")[a==np.e]}^{{{(c,\"-\")[c==-1]}x}}$'\n", - " )\n", - " \n", - "# Geographic scales\n", - "n = 20\n", - "x = np.linspace(-180, 180, n)\n", - "y = np.linspace(-85, 85, n)\n", - "y2 = np.linspace(-85, 85, n)\n", - "data = state.rand(len(x), len(y2))\n", - "for ax, scale, color in zip(axs[4:], ('sine', 'mercator'), ('coral', 'sky blue')):\n", - " ax.plot(x, y, '-', color=color, lw=4)\n", - " ax.pcolormesh(x, y2, data, cmap='grays', cmap_kw={'right': 0.8})\n", - " ax.format(\n", - " title=scale.title() + ' y-axis', yscale=scale, ytickloc='left',\n", - " yformatter='deg', grid=False, ylocator=20,\n", - " xscale='linear', xlim=None, ylim=(-85, 85)\n", - " )" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Dual unit axes" - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "The `~proplot.axes.XYAxes.dualx` and `~proplot.axes.XYAxes.dualy` methods can be used to draw duplicate *x* and *y* axes meant to represent *alternate units* in the same coordinate range as the \"parent\" axis. This feature is powered by the `~proplot.axistools.FuncScale` class.\n", - "\n", - "`~proplot.axes.XYAxes.dualx` and `~proplot.axes.XYAxes.dualy` accept either (1) a single linear forward function, (2) a pair of arbitrary forward and inverse functions, or (3) a scale name or scale class instance. In the latter case, the scale's transforms are used for the forward and inverse functions, and the scale's default locators and formatters are used for the default `~proplot.axistools.FuncScale` locators and formatters.\n", - "\n", - "Notably, the \"parent\" axis scale is now *arbitrary* -- in the first example shown below, we create a `~proplot.axes.XYAxes.dualx` axis for an axis scaled by the ``'symlog'`` scale." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", - "plot.rc.update({'grid.alpha': 0.4, 'linewidth': 1, 'grid.linewidth': 1})\n", - "c1 = plot.shade('cerulean', 0.5)\n", - "c2 = plot.shade('red', 0.5)\n", - "f, axs = plot.subplots(\n", - " [[1, 1, 2, 2], [0, 3, 3, 0]],\n", - " share=0, aspect=2.2, axwidth=3\n", - ")\n", - "axs.format(\n", - " suptitle='Duplicate axes with custom transformations',\n", - " xcolor=c1, gridcolor=c1,\n", - " ylocator=[], yformatter=[]\n", - ")\n", - "\n", - "# Meters and kilometers\n", - "ax = axs[0]\n", - "ax.format(xlim=(0, 5000), xlabel='meters')\n", - "ax.dualx(\n", - " lambda x: x*1e-3,\n", - " label='kilometers', grid=True, color=c2, gridcolor=c2\n", - ")\n", - "\n", - "# Kelvin and Celsius\n", - "ax = axs[1]\n", - "ax.format(xlim=(200, 300), xlabel='temperature (K)')\n", - "ax.dualx(\n", - " lambda x: x - 273.15,\n", - " label='temperature (\\N{DEGREE SIGN}C)', grid=True, color=c2, gridcolor=c2\n", - ")\n", - "\n", - "# With symlog parent\n", - "ax = axs[2]\n", - "ax.format(xlim=(-100, 100), xscale='symlog', xlabel='MegaJoules')\n", - "ax.dualx(\n", - " lambda x: x*1e6,\n", - " label='Joules', formatter='log', grid=True, color=c2, gridcolor=c2\n", - ")\n", - "plot.rc.reset()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", - "plot.rc.update({'grid.alpha': 0.4, 'linewidth': 1, 'grid.linewidth': 1})\n", - "f, axs = plot.subplots(ncols=2, share=0, aspect=0.4, axwidth=1.8)\n", - "axs.format(suptitle='Duplicate axes with special transformations')\n", - "c1, c2 = plot.shade('cerulean', 0.5), plot.shade('red', 0.5)\n", - "\n", - "# Pressure as the linear scale, height on opposite axis (scale height 7km)\n", - "ax = axs[0]\n", - "ax.format(\n", - " xformatter='null', ylabel='pressure (hPa)',\n", - " ylim=(1000, 10), xlocator=[], ycolor=c1, gridcolor=c1\n", - ")\n", - "scale = plot.Scale('height')\n", - "ax.dualy(\n", - " scale, label='height (km)', ticks=2.5, color=c2, gridcolor=c2, grid=True\n", - ")\n", - "\n", - "# Height as the linear scale, pressure on opposite axis (scale height 7km)\n", - "ax = axs[1] # span\n", - "ax.format(\n", - " xformatter='null', ylabel='height (km)', ylim=(0, 20), xlocator='null',\n", - " grid=True, gridcolor=c2, ycolor=c2\n", - ")\n", - "scale = plot.Scale('pressure')\n", - "ax.dualy(\n", - " scale, label='pressure (hPa)', locator=100,\n", - " color=c1, gridcolor=c1, grid=True\n", - ")\n", - "plot.rc.reset()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", - "import numpy as np\n", - "plot.rc['axes.ymargin'] = 0\n", - "f, ax = plot.subplots(aspect=(3, 1), width=6)\n", - "\n", - "# Sample data\n", - "cutoff = 1/5\n", - "c1, c2 = plot.shade('cerulean', 0.5), plot.shade('red', 0.5)\n", - "x = np.linspace(0.01, 0.5, 1000) # in wavenumber days\n", - "response = (np.tanh(-((x - cutoff)/0.03)) + 1) / 2 # response func\n", - "ax.axvline(cutoff, lw=2, ls='-', color=c2)\n", - "ax.fill_between([cutoff - 0.03, cutoff + 0.03], 0, 1, color=c2, alpha=0.3)\n", - "ax.plot(x, response, color=c1, lw=2)\n", - "\n", - "# Add inverse scale to top\n", - "scale = plot.Scale('inverse')\n", - "ax.format(xlabel='wavenumber (days$^{-1}$)', ylabel='response', grid=False)\n", - "ax = ax.dualx(scale, locator='log', locator_kw={'subs': (1, 2, 5)}, label='period (days)')\n", - "ax.format(\n", - " title='Imaginary response function',\n", - " suptitle='Duplicate axes with wavenumber and period'\n", - ")\n", - "plot.rc.reset()" - ] - } - ], - "metadata": { - "celltoolbar": "Raw Cell Format", - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.3" - }, - "toc": { - "colors": { - "hover_highlight": "#ece260", - "navigate_num": "#000000", - "navigate_text": "#000000", - "running_highlight": "#FF0000", - "selected_highlight": "#fff968", - "sidebar_border": "#ffffff", - "wrapper_background": "#ffffff" - }, - "moveMenuLeft": false, - "nav_menu": { - "height": "12px", - "width": "250px" - }, - "navigate_menu": true, - "number_sections": true, - "sideBar": true, - "threshold": 4, - "toc_cell": false, - "toc_section_display": "block", - "toc_window_display": true, - "widenNotebook": false - }, - "varInspector": { - "cols": { - "lenName": 16, - "lenType": 16, - "lenVar": 40 - }, - "kernels_config": { - "python": { - "delete_cmd_postfix": "", - "delete_cmd_prefix": "del ", - "library": "var_list.py", - "varRefreshCmd": "print(var_dic_list())" - }, - "r": { - "delete_cmd_postfix": ") ", - "delete_cmd_prefix": "rm(", - "library": "var_list.r", - "varRefreshCmd": "cat(var_dic_list()) " - } - }, - "types_to_exclude": [ - "module", - "function", - "builtin_function_or_method", - "instance", - "_Feature" - ], - "window_display": false - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/docs/basics.ipynb b/docs/basics.ipynb deleted file mode 100644 index 824211916..000000000 --- a/docs/basics.ipynb +++ /dev/null @@ -1,321 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# The basics" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Figures and subplots" - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "ProPlot works by subclassing the matplotlib `~matplotlib.figure.Figure` and `~matplotlib.axes.Axes` classes. You can generate grids of proplot `~proplot.axes.Axes` axes on a proplot `~proplot.subplots.Figure` using the `~proplot.subplots.subplots` command.\n", - "\n", - ".. code-block:: python\n", - "\n", - " import proplot as plot\n", - " f, axs = plot.subplots(...)\n", - " \n", - "* Just like `matplotlib.pyplot.subplots`, you can use `~proplot.subplots.subplots` without arguments to generate a single-axes subplot or with `ncols` or `nrows` to set up simple grids of subplots.\n", - "* Unlike `matplotlib.pyplot.subplots`, you can *also* use `~proplot.subplots.subplots` to draw arbitrarily complex grids using a 2D array of integers. Just think of this array as a \"picture\" of your figure, where each unique integer corresponds to a unique axes and ``0`` corresponds to an empty space.\n", - "\n", - "In the below examples, we create subplot grids with `~proplot.subplots.subplots` and modify the axes using `~proplot.axes.Axes.format` and `~proplot.subplots.subplot_grid`. See :ref:`Formatting subplots` and :ref:`Subplot grids` for details." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", - "import numpy as np\n", - "state = np.random.RandomState(51423)\n", - "data = 2*(state.rand(100, 5) - 0.5).cumsum(axis=0)\n", - "\n", - "# Simple plot\n", - "f, axs = plot.subplots(ncols=2)\n", - "axs[0].plot(data, lw=2)\n", - "axs[0].format(xticks=20, xtickminor=False)\n", - "axs.format(\n", - " suptitle='Simple subplot grid', title='Title',\n", - " xlabel='x axis', ylabel='y axis'\n", - ")\n", - "\n", - "# Complex grid\n", - "array = [ # the \"picture\"; 1 == subplot a, 2 == subplot b, etc.\n", - " [1, 1, 2, 2],\n", - " [0, 3, 3, 0],\n", - "]\n", - "f, axs = plot.subplots(array, axwidth=1.8)\n", - "axs.format(\n", - " abc=True, abcloc='ul', suptitle='Complex subplot grid',\n", - " xlabel='xlabel', ylabel='ylabel'\n", - ")\n", - "axs[2].plot(data, lw=2)\n", - "\n", - "# Really complex grid\n", - "array = [ # the \"picture\" \n", - " [1, 1, 2],\n", - " [1, 1, 6],\n", - " [3, 4, 4],\n", - " [3, 5, 5],\n", - "]\n", - "f, axs = plot.subplots(array, width=5, span=False)\n", - "axs.format(\n", - " suptitle='Really complex subplot grid',\n", - " xlabel='xlabel', ylabel='ylabel', abc=True\n", - ")\n", - "axs[0].plot(data, lw=2)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Formatting subplots" - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "Every `~matplotlib.axes.Axes` returned by `~proplot.subplots.subplots` has a ``format`` method. This is your one-stop-shop for changing axes settings. Keyword args passed to ``format`` are interpreted as follows:\n", - "\n", - "1. Any keyword arg matching the name of an `~proplot.rctools.rc` setting will be applied to the axes using `~proplot.rctools.rc_configurator.context`. If the name has \"dots\", simply omit them. See :ref:`Configuring proplot` for details.\n", - "2. Remaining keyword args are passed to the class-specific `~proplot.axes.XYAxes` `~proplot.axes.XYAxes.format`, `~proplot.axes.ProjAxes` `~proplot.axes.ProjAxes.format`, or `~proplot.axes.PolarAxes` `~proplot.axes.PolarAxes.format` methods.\n", - "3. Still remaining keyword args are passed to the `~proplot.axes.Axes` `~proplot.axes.Axes.format` method. `~proplot.axes.Axes` is the base class for all other axes classes. This changes axes titles, a-b-c subplot labeling, and figure titles.\n", - "\n", - "``format`` lets you use simple shorthands for changing all kinds of axes settings at once, instead of the verbose, one-liner setter methods like ``ax.set_title()``, ``ax.set_xlabel()``, and ``ax.xaxis.tick_params()``. It is also integrated with the `~proplot.axistools.Locator`, `~proplot.axistools.Formatter`, and `~proplot.axistools.Scale` constructor functions (see :ref:`X and Y axis settings` for details).\n", - "\n", - "The below example shows the many different keyword arguments accepted by ``format``, and demonstrates how ``format`` can be used to succinctly and efficiently customize your plots." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", - "import numpy as np\n", - "f, axs = plot.subplots(ncols=2, nrows=2, share=0, tight=True, axwidth=1.7)\n", - "state = np.random.RandomState(51423)\n", - "axs[0].plot(np.linspace(1, 10, 80), (state.rand(80, 5) - 0.5).cumsum(axis=0))\n", - "axs.format(\n", - " suptitle='Format command demo',\n", - " abc=True, abcloc='ul', abcstyle='a.',\n", - " title='Main', ltitle='Left', rtitle='Right', # different titles\n", - " urtitle='Title A', lltitle='Title B', lrtitle='Title C', # extra titles\n", - " collabels=['Column label 1', 'Column label 2'], rowlabels=['Row label 1', 'Row label 2'],\n", - " xlabel='x-axis', ylabel='y-axis',\n", - " xlim=(1, 10), xticks=1, xscale='log',\n", - " ylim=(-2, 2), yticks=plot.arange(-2, 2), yticklabels=('a', 'bb', 'c', 'dd', 'e'),\n", - " xtickdir='inout', xtickminor=False,\n", - " ytickloc='both', yticklabelloc='both', ygridminor=True,\n", - " linewidth=0.8, gridlinewidth=0.8, gridminorlinewidth=0.5,\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Changing rc settings" - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "A special object named `~proplot.rctools.rc` is created whenever you import ProPlot. `~proplot.rctools.rc` is similar to the matplotlib `~matplotlib.rcParams` dictionary, but can be used to change (1) matplotlib `rcParams `__ settings, (2) custom ProPlot :ref:`rcParamsLong` settings, or (3) special :ref:`rcParamsShort` meta-settings, which change lots of `rcParams `__ and :ref:`rcParamsLong` settings all at once. See :ref:`Configuring proplot` for details.\n", - "\n", - "To modify a setting for just one subplot, you can pass it to the `~proplot.axes.Axes` `~proplot.axes.Axes.format` method. To temporarily modify setting(s) for a block of code, use `~proplot.rctools.rc_configurator.context`. To modify setting(s) for the entire python session, just assign it to the `~proplot.rctools.rc` object or use `~proplot.rctools.rc_configurator.update`. To reset everything to the default state, use `~proplot.rctools.rc_configurator.reset`. See the below example." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", - "import numpy as np\n", - "\n", - "# Update global settings in several different ways\n", - "plot.rc.cycle = 'colorblind'\n", - "plot.rc.color = 'gray6'\n", - "plot.rc.update({'fontname': 'Noto Sans'})\n", - "plot.rc['figure.facecolor'] = 'gray3'\n", - "plot.rc.axesfacecolor = 'gray4'\n", - "\n", - "# Apply settings to figure with context()\n", - "with plot.rc.context({'suptitle.size': 11}, toplabelcolor='gray6', linewidth=1.5):\n", - " f, axs = plot.subplots(ncols=2, aspect=1, width=6, span=False, sharey=2)\n", - " \n", - "# Plot lines \n", - "N, M = 100, 6\n", - "state = np.random.RandomState(51423)\n", - "values = np.arange(1, M+1)\n", - "for i, ax in enumerate(axs):\n", - " data = np.cumsum(state.rand(N, M) - 0.5, axis=0)\n", - " lines = ax.plot(data, linewidth=3, cycle='Grays')\n", - " \n", - "# Apply settings to axes with format()\n", - "axs.format(\n", - " grid=False, xlabel='x label', ylabel='y label',\n", - " collabels=['Column label 1', 'Column label 2'],\n", - " suptitle='Rc settings demo',\n", - " suptitlecolor='gray7',\n", - " abc=True, abcloc='l', abcstyle='A)',\n", - " title='Title', titleloc='r', titlecolor='gray7'\n", - ")\n", - "ay = axs[-1].twinx()\n", - "ay.format(ycolor='red', linewidth=1.5, ylabel='secondary axis')\n", - "ay.plot((state.rand(100) - 0.2).cumsum(), color='r', lw=3)\n", - "\n", - "# Reset persistent modifications from head of cell\n", - "plot.rc.reset()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Subplot grids" - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "Instead of an `~numpy.ndarray` of axes, `~proplot.subplots.subplots` returns a special `~proplot.subplots.subplot_grid` container. This container behaves like a *python list*, but lets you call any arbitrary method on multiple axes at once. It supports both 2D indexing (e.g. ``axs[0,1]``) and 1D indexing (e.g. ``axs[2]``), and is row-major by default. Further, slicing a subplot grid (e.g. ``axs[:,0]``) returns another subplot grid.\n", - "\n", - "In the below example, the `~proplot.subplots.subplot_grid` returned by `~proplot.subplots.subplots` is used to call `~proplot.axes.Axes.format` on several axes at once. Note that you can make your own subplot grid simply by passing a list of axes to `~proplot.subplots.subplot_grid`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", - "import numpy as np\n", - "state = np.random.RandomState(51423)\n", - "f, axs = plot.subplots(ncols=4, nrows=4, axwidth=1.2)\n", - "axs.format(\n", - " xlabel='xlabel', ylabel='ylabel', suptitle='Subplot grid demo',\n", - " grid=False, xlim=(0, 50), ylim=(-4, 4)\n", - ")\n", - "\n", - "# Various ways to select subplots in the subplot grid\n", - "axs[:, 0].format(color='gray7', facecolor='gray3', linewidth=1)\n", - "axs[0, :].format(color='red', facecolor='gray3', linewidth=1)\n", - "axs[0].format(color='black', facecolor='gray5', linewidth=1.4)\n", - "axs[1:, 1:].format(facecolor='gray1')\n", - "for ax in axs[1:, 1:]:\n", - " ax.plot((state.rand(50, 5) - 0.5).cumsum(axis=0), cycle='Grays', lw=2)" - ] - } - ], - "metadata": { - "celltoolbar": "Raw Cell Format", - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.3" - }, - "toc": { - "colors": { - "hover_highlight": "#ece260", - "navigate_num": "#000000", - "navigate_text": "#000000", - "running_highlight": "#FF0000", - "selected_highlight": "#fff968", - "sidebar_border": "#ffffff", - "wrapper_background": "#ffffff" - }, - "moveMenuLeft": false, - "nav_menu": { - "height": "84px", - "width": "252px" - }, - "navigate_menu": true, - "number_sections": true, - "sideBar": true, - "threshold": 4, - "toc_cell": false, - "toc_position": { - "height": "723px", - "left": "0px", - "right": "1198px", - "top": "46px", - "width": "205px" - }, - "toc_section_display": "block", - "toc_window_display": true, - "widenNotebook": false - }, - "varInspector": { - "cols": { - "lenName": 16, - "lenType": 16, - "lenVar": 40 - }, - "kernels_config": { - "python": { - "delete_cmd_postfix": "", - "delete_cmd_prefix": "del ", - "library": "var_list.py", - "varRefreshCmd": "print(var_dic_list())" - }, - "r": { - "delete_cmd_postfix": ") ", - "delete_cmd_prefix": "rm(", - "library": "var_list.r", - "varRefreshCmd": "cat(var_dic_list()) " - } - }, - "types_to_exclude": [ - "module", - "function", - "builtin_function_or_method", - "instance", - "_Feature" - ], - "window_display": false - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/docs/basics.py b/docs/basics.py new file mode 100644 index 000000000..57b7f3b63 --- /dev/null +++ b/docs/basics.py @@ -0,0 +1,527 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.11.4 +# kernelspec: +# display_name: Python 3 +# language: python +# name: python3 +# --- + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_basics: +# +# The basics +# ========== + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_intro: +# +# Creating figures +# ---------------- +# +# Proplot works by `subclassing +# `__ +# three fundamental matplotlib classes: `proplot.figure.Figure` replaces +# `matplotlib.figure.Figure`, `proplot.axes.Axes` replaces `matplotlib.axes.Axes`, +# and `proplot.gridspec.GridSpec` replaces `matplotlib.gridspec.GridSpec` +# (see this `tutorial +# `__ +# for more on gridspecs). +# +# To make plots with these classes, you must start with the top-level commands +# `~proplot.ui.figure`, `~proplot.ui.subplot`, or `~proplot.ui.subplots`. These are +# modeled after the `~matplotlib.pyplot` commands of the same name. As in +# `~matplotlib.pyplot`, `~proplot.ui.subplot` creates a figure and a single +# subplot, `~proplot.ui.subplots` creates a figure and a grid of subplots, and +# `~proplot.ui.figure` creates an empty figure that can be subsequently filled +# with subplots. A minimal example with just one subplot is shown below. +# +# %% [raw] raw_mimetype="text/restructuredtext" +# .. note:: +# +# Proplot changes the default :rcraw:`figure.facecolor` +# so that the figure backgrounds shown by the `matplotlib backend +# `__ are light gray +# (the :rcraw:`savefig.facecolor` applied to saved figures is still white). +# Proplot also controls the appearance of figures in Jupyter notebooks +# using the new :rcraw:`inlineformat` setting, which is passed to +# `~proplot.config.config_inline_backend` on import. This +# imposes a higher-quality default `"inline" format +# `__ +# and disables the backend-specific settings ``InlineBackend.rc`` and +# ``InlineBackend.print_figure_kwargs``, ensuring that the figures you save +# look like the figures displayed by the backend. +# +# Proplot also changes the default :rcraw:`savefig.format` +# from PNG to PDF for the following reasons: +# +# #. Vector graphic formats are infinitely scalable. +# #. Vector graphic formats are preferred by academic journals. +# #. Nearly all academic journals accept figures in the PDF format alongside +# the `EPS `__ format. +# #. The EPS format is outdated and does not support transparent graphic +# elements. +# +# In case you *do* need a raster format like PNG, proplot increases the +# default :rcraw:`savefig.dpi` to 1000 dots per inch, which is +# `recommended `__ by most journals +# as the minimum resolution for figures containing lines and text. See the +# :ref:`configuration section ` for how to change these settings. +# + +# %% +# Simple subplot +import numpy as np +import proplot as pplt +state = np.random.RandomState(51423) +data = 2 * (state.rand(100, 5) - 0.5).cumsum(axis=0) +fig, ax = pplt.subplot(suptitle='Single subplot', xlabel='x axis', ylabel='y axis') +# fig = pplt.figure(suptitle='Single subplot') # equivalent to above +# ax = fig.subplot(xlabel='x axis', ylabel='y axis') +ax.plot(data, lw=2) + + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_subplot: +# +# Creating subplots +# ----------------- +# +# Similar to matplotlib, subplots can be added to figures one-by-one +# or all at once. Each subplot will be an instance of +# `proplot.axes.Axes`. To add subplots all at once, use +# `proplot.figure.Figure.add_subplots` (or its shorthand, +# `proplot.figure.Figure.subplots`). Note that under the hood, the top-level +# proplot command `~proplot.ui.subplots` simply calls `~proplot.ui.figure` +# followed by `proplot.figure.Figure.add_subplots`. +# +# * With no arguments, `~proplot.figure.Figure.add_subplots` returns a subplot +# generated from a 1-row, 1-column `~proplot.gridspec.GridSpec`. +# * With `ncols` or `nrows`, `~proplot.figure.Figure.add_subplots` returns a +# simple grid of subplots from a `~proplot.gridspec.GridSpec` with +# matching geometry in either row-major or column-major `order`. +# * With `array`, `~proplot.figure.Figure.add_subplots` returns an arbitrarily +# complex grid of subplots from a `~proplot.gridspec.GridSpec` with matching +# geometry. Here `array` is a 2D array representing a "picture" of the subplot +# layout, where each unique integer indicates a `~matplotlib.gridspec.GridSpec` +# slot occupied by the corresponding subplot and ``0`` indicates an empty space. +# The returned subplots are contained in a `~proplot.gridspec.SubplotGrid` +# (:ref:`see below ` for details). +# +# To add subplots one-by-one, use the `proplot.figure.Figure.add_subplot` +# command (or its shorthand `proplot.figure.Figure.subplot`). +# +# * With no arguments, `~proplot.figure.Figure.add_subplot` returns a subplot +# generated from a 1-row, 1-column `~proplot.gridspec.GridSpec`. +# * With integer arguments, `~proplot.figure.Figure.add_subplot` returns +# a subplot matching the corresponding `~proplot.gridspec.GridSpec` geometry, +# as in matplotlib. Note that unlike matplotlib, the geometry must be compatible +# with the geometry implied by previous `~proplot.figure.Figure.add_subplot` calls. +# * With a `~matplotlib.gridspec.SubplotSpec` generated by indexing a +# `proplot.gridspec.GridSpec`, `~proplot.figure.Figure.add_subplot` returns a +# subplot at the corresponding location. Note that unlike matplotlib, only +# one `~proplot.figure.Figure.gridspec` can be used with each figure. +# +# As in matplotlib, to save figures, use `~matplotlib.figure.Figure.savefig` (or its +# shorthand `proplot.figure.Figure.save`). User paths in the filename are expanded +# with `os.path.expanduser`. In the following examples, we add subplots to figures +# with a variety of methods and then save the results to the home directory. +# +# .. warning:: +# +# Proplot employs :ref:`automatic axis sharing ` by default. This lets +# subplots in the same row or column share the same axis limits, scales, ticks, +# and labels. This is often convenient, but may be annoying for some users. To +# keep this feature turned off, simply :ref:`change the default settings ` +# with e.g. ``pplt.rc.update('subplots', share=False, span=False)``. See the +# :ref:`axis sharing section ` for details. + +# %% +# Simple subplot grid +import numpy as np +import proplot as pplt +state = np.random.RandomState(51423) +data = 2 * (state.rand(100, 5) - 0.5).cumsum(axis=0) +fig = pplt.figure() +ax = fig.subplot(121) +ax.plot(data, lw=2) +ax = fig.subplot(122) +fig.format( + suptitle='Simple subplot grid', title='Title', + xlabel='x axis', ylabel='y axis' +) +# fig.save('~/example1.png') # save the figure +# fig.savefig('~/example1.png') # alternative + + +# %% +# Complex grid +import numpy as np +import proplot as pplt +state = np.random.RandomState(51423) +data = 2 * (state.rand(100, 5) - 0.5).cumsum(axis=0) +array = [ # the "picture" (0 == nothing, 1 == subplot A, 2 == subplot B, etc.) + [1, 1, 2, 2], + [0, 3, 3, 0], +] +fig = pplt.figure(refwidth=1.8) +axs = fig.subplots(array) +axs.format( + abc=True, abcloc='ul', suptitle='Complex subplot grid', + xlabel='xlabel', ylabel='ylabel' +) +axs[2].plot(data, lw=2) +# fig.save('~/example2.png') # save the figure +# fig.savefig('~/example2.png') # alternative + + +# %% +# Really complex grid +import numpy as np +import proplot as pplt +state = np.random.RandomState(51423) +data = 2 * (state.rand(100, 5) - 0.5).cumsum(axis=0) +array = [ # the "picture" (1 == subplot A, 2 == subplot B, etc.) + [1, 1, 2], + [1, 1, 6], + [3, 4, 4], + [3, 5, 5], +] +fig, axs = pplt.subplots(array, figwidth=5, span=False) +axs.format( + suptitle='Really complex subplot grid', + xlabel='xlabel', ylabel='ylabel', abc=True +) +axs[0].plot(data, lw=2) +# fig.save('~/example3.png') # save the figure +# fig.savefig('~/example3.png') # alternative + +# %% +# Using a GridSpec +import numpy as np +import proplot as pplt +state = np.random.RandomState(51423) +data = 2 * (state.rand(100, 5) - 0.5).cumsum(axis=0) +gs = pplt.GridSpec(nrows=2, ncols=2, pad=1) +fig = pplt.figure(span=False, refwidth=2) +ax = fig.subplot(gs[:, 0]) +ax.plot(data, lw=2) +ax = fig.subplot(gs[0, 1]) +ax = fig.subplot(gs[1, 1]) +fig.format( + suptitle='Subplot grid with a GridSpec', + xlabel='xlabel', ylabel='ylabel', abc=True +) +# fig.save('~/example4.png') # save the figure +# fig.savefig('~/example4.png') # alternative + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_subplotgrid: +# +# Multiple subplots +# ----------------- +# +# If you create subplots all-at-once with e.g. `~proplot.ui.subplots`, +# proplot returns a `~proplot.gridspec.SubplotGrid` of subplots. This list-like, +# array-like object provides some useful features and unifies the behavior of the +# three possible return types used by `matplotlib.pyplot.subplots`: +# +# * `~proplot.gridspec.SubplotGrid` behaves like a scalar when it is singleton. +# In other words, if you make a single subplot with ``fig, axs = pplt.subplots()``, +# then ``axs[0].method(...)`` is equivalent to ``axs.method(...)``. +# * `~proplot.gridspec.SubplotGrid` permits list-like 1D indexing, e.g. ``axs[1]`` +# to return the second subplot. The subplots in the grid are sorted by +# `~proplot.axes.Axes.number` (see :ref:`this page ` for details +# on changing the `~proplot.axes.Axes.number` order). +# * `~proplot.gridspec.SubplotGrid` permits array-like 2D indexing, e.g. +# ``axs[1, 0]`` to return the subplot in the second row, first column, or +# ``axs[:, 0]`` to return a `~proplot.gridspec.SubplotGrid` of every subplot +# in the first column. The 2D indexing is powered by the underlying +# `~proplot.gridspec.SubplotGrid.gridspec`. +# +# `~proplot.gridspec.SubplotGrid` includes methods for working +# simultaneously with different subplots. Currently, this includes +# the commands `~proplot.gridspec.SubplotGrid.format`, +# `~proplot.gridspec.SubplotGrid.panel_axes`, +# `~proplot.gridspec.SubplotGrid.inset_axes`, +# `~proplot.gridspec.SubplotGrid.altx`, and `~proplot.gridspec.SubplotGrid.alty`. +# In the below example, we use `proplot.gridspec.SubplotGrid.format` on the grid +# returned by `~proplot.ui.subplots` to format different subgroups of subplots +# (:ref:`see below ` for more on the format command). +# +# .. note:: +# +# If you create subplots one-by-one with `~proplot.figure.Figure.subplot` or +# `~proplot.figure.Figure.add_subplot`, a `~proplot.gridspec.SubplotGrid` +# containing the numbered subplots is available via the +# `proplot.figure.Figure.subplotgrid` property. As with subplots made +# all-at-once, the subplots in the grid are sorted by `~proplot.axes.Axes.number`. + +# %% +import proplot as pplt +import numpy as np +state = np.random.RandomState(51423) + +# Selected subplots in a simple grid +fig, axs = pplt.subplots(ncols=4, nrows=4, refwidth=1.2, span=True) +axs.format(xlabel='xlabel', ylabel='ylabel', suptitle='Simple SubplotGrid') +axs.format(grid=False, xlim=(0, 50), ylim=(-4, 4)) +axs[:, 0].format(facecolor='blush', edgecolor='gray7', linewidth=1) # eauivalent +axs[:, 0].format(fc='blush', ec='gray7', lw=1) +axs[0, :].format(fc='sky blue', ec='gray7', lw=1) +axs[0].format(ec='black', fc='gray5', lw=1.4) +axs[1:, 1:].format(fc='gray1') +for ax in axs[1:, 1:]: + ax.plot((state.rand(50, 5) - 0.5).cumsum(axis=0), cycle='Grays', lw=2) + +# Selected subplots in a complex grid +fig = pplt.figure(refwidth=1, refnum=5, span=False) +axs = fig.subplots([[1, 1, 2], [3, 4, 2], [3, 4, 5]], hratios=[2.2, 1, 1]) +axs.format(xlabel='xlabel', ylabel='ylabel', suptitle='Complex SubplotGrid') +axs[0].format(ec='black', fc='gray1', lw=1.4) +axs[1, 1:].format(fc='blush') +axs[1, :1].format(fc='sky blue') +axs[-1, -1].format(fc='gray4', grid=False) +axs[0].plot((state.rand(50, 10) - 0.5).cumsum(axis=0), cycle='Grays_r', lw=2) + + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_plots: +# +# Plotting stuff +# -------------- +# +# Matplotlib includes `two different interfaces +# `__ for plotting stuff: +# a python-style object-oriented interface with axes-level commands +# like `matplotlib.axes.Axes.plot`, and a MATLAB-style `~matplotlib.pyplot` interface +# with global commands like `matplotlib.pyplot.plot` that track the "current" axes. +# Proplot builds upon the python-style interface using the `proplot.axes.PlotAxes` +# class. Since every axes used by proplot is a child of `~proplot.axes.PlotAxes`, we +# are able to add features directly to the axes-level commands rather than relying +# on a separate library of commands (note that while some of these features may be +# accessible via `~matplotlib.pyplot` commands, this is not officially supported). +# +# For the most part, the features added by `~proplot.axes.PlotAxes` represent +# a *superset* of matplotlib. If you are not interested, you can use the plotting +# commands just like you would in matplotlib. Some of the core added features include +# more flexible treatment of :ref:`data arguments `, recognition of +# :ref:`xarray and pandas ` data structures, integration with +# proplot's :ref:`colormap ` and :ref:`color cycle ` +# tools, and on-the-fly :ref:`legend and colorbar generation `. +# In the below example, we create a 4-panel figure with the +# familiar "1D" plotting commands `~proplot.axes.PlotAxes.plot` and +# `~proplot.axes.PlotAxes.scatter`, along with the "2D" plotting commands +# `~proplot.axes.PlotAxes.pcolormesh` and `~proplot.axes.PlotAxes.contourf`. +# See the :ref:`1D plotting ` and :ref:`2D plotting ` +# sections for details on the features added by proplot. + + +# %% +import proplot as pplt +import numpy as np + +# Sample data +N = 20 +state = np.random.RandomState(51423) +data = N + (state.rand(N, N) - 0.55).cumsum(axis=0).cumsum(axis=1) + +# Example plots +cycle = pplt.Cycle('greys', left=0.2, N=5) +fig, axs = pplt.subplots(ncols=2, nrows=2, figwidth=5, share=False) +axs[0].plot(data[:, :5], linewidth=2, linestyle='--', cycle=cycle) +axs[1].scatter(data[:, :5], marker='x', cycle=cycle) +axs[2].pcolormesh(data, cmap='greys') +m = axs[3].contourf(data, cmap='greys') +axs.format( + abc='a.', titleloc='l', title='Title', + xlabel='xlabel', ylabel='ylabel', suptitle='Quick plotting demo' +) +fig.colorbar(m, loc='b', label='label') + + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_format: +# +# Formatting stuff +# ---------------- +# +# Matplotlib includes `two different interfaces +# `__ for formatting stuff: +# a "python-style" object-oriented interface with instance-level commands +# like `matplotlib.axes.Axes.set_title`, and a "MATLAB-style" interface +# that tracks current axes and provides global commands like +# `matplotlib.pyplot.title`. +# +# Proplot provides the ``format`` command as an +# alternative "python-style" command for formatting a variety of plot elements. +# While matplotlib's one-liner commands still work, ``format`` only needs to be +# called once and tends to cut down on boilerplate code. You can call +# ``format`` manually or pass ``format`` parameters to axes-creation commands +# like `~proplot.figure.Figure.subplots`, `~proplot.figure.Figure.add_subplot`, +# `~proplot.axes.Axes.inset_axes`, `~proplot.axes.Axes.panel_axes`, and +# `~proplot.axes.CartesianAxes.altx` or `~proplot.axes.CartesianAxes.alty`. The +# keyword arguments accepted by ``format`` can be grouped as follows: +# +# * Figure settings. These are related to row labels, column labels, and +# figure "super" titles -- for example, ``fig.format(suptitle='Super title')``. +# See `proplot.figure.Figure.format` for details. +# +# * General axes settings. These are related to background patches, +# a-b-c labels, and axes titles -- for example, ``ax.format(title='Title')`` +# See `proplot.axes.Axes.format` for details. +# +# * Cartesian axes settings (valid only for `~proplot.axes.CartesianAxes`). +# These are related to *x* and *y* axis ticks, spines, bounds, and labels -- +# for example, ``ax.format(xlim=(0, 5))`` changes the x axis bounds. +# See `proplot.axes.CartesianAxes.format` and +# :ref:`this section ` for details. +# +# * Polar axes settings (valid only for `~proplot.axes.PolarAxes`). +# These are related to azimuthal and radial grid lines, bounds, and labels -- +# for example, ``ax.format(rlim=(0, 10))`` changes the radial bounds. +# See `proplot.axes.PolarAxes.format` +# and :ref:`this section ` for details. +# +# * Geographic axes settings (valid only for `~proplot.axes.GeoAxes`). +# These are related to map bounds, meridian and parallel lines and labels, +# and geographic features -- for example, ``ax.format(latlim=(0, 90))`` +# changes the meridional bounds. See `proplot.axes.GeoAxes.format` +# and :ref:`this section ` for details. +# +# * `~proplot.config.rc` settings. Any keyword matching the name +# of an rc setting is locally applied to the figure and axes. +# If the name has "dots", you can pass it as a keyword argument with +# the "dots" omitted, or pass it to `rc_kw` in a dictionary. For example, the +# default a-b-c label location is controlled by :rcraw:`abc.loc`. To change +# this for an entire figure, you can use ``fig.format(abcloc='right')`` +# or ``fig.format(rc_kw={'abc.loc': 'right'})``. +# See :ref:`this section ` for more on rc settings. +# +# A ``format`` command is available on every figure and axes. +# `proplot.figure.Figure.format` accepts both figure and axes +# settings (applying them to each numbered subplot by default). +# Similarly, `proplot.axes.Axes.format` accepts both axes and figure +# settings. There is also a `proplot.gridspec.SubplotGrid.format` +# command that can be used to change settings for a subset of +# subplots -- for example, ``axs[:2].format(xtickminor=True)`` +# turns on minor ticks for the first two subplots (see +# :ref:`this section ` for more on subplot grids). +# The below example shows the many keyword arguments accepted +# by ``format``, and demonstrates how ``format`` can be +# used to succinctly and efficiently customize plots. + +# %% +import proplot as pplt +import numpy as np +fig, axs = pplt.subplots(ncols=2, nrows=2, refwidth=2, share=False) +state = np.random.RandomState(51423) +N = 60 +x = np.linspace(1, 10, N) +y = (state.rand(N, 5) - 0.5).cumsum(axis=0) +axs[0].plot(x, y, linewidth=1.5) +axs.format( + suptitle='Format command demo', + abc='A.', abcloc='ul', + title='Main', ltitle='Left', rtitle='Right', # different titles + ultitle='Title 1', urtitle='Title 2', lltitle='Title 3', lrtitle='Title 4', + toplabels=('Column 1', 'Column 2'), + leftlabels=('Row 1', 'Row 2'), + xlabel='xaxis', ylabel='yaxis', + xscale='log', + xlim=(1, 10), xticks=1, + ylim=(-3, 3), yticks=pplt.arange(-3, 3), + yticklabels=('a', 'bb', 'c', 'dd', 'e', 'ff', 'g'), + ytickloc='both', yticklabelloc='both', + xtickdir='inout', xtickminor=False, ygridminor=True, +) + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_rc: +# +# Settings and styles +# ------------------- +# +# A dictionary-like object named `~proplot.config.rc` is created when you import +# proplot. `~proplot.config.rc` is similar to the matplotlib `~matplotlib.rcParams` +# dictionary, but can be used to change both `matplotlib settings +# `__ and +# :ref:`proplot settings `. The matplotlib-specific settings are +# stored in `~proplot.config.rc_matplotlib` (our name for `matplotlib.rcParams`) and +# the proplot-specific settings are stored in `~proplot.config.rc_proplot`. +# Proplot also includes a :rcraw:`style` setting that can be used to +# switch between `matplotlib stylesheets +# `__. +# See the :ref:`configuration section ` for details. +# +# To modify a setting for just one subplot or figure, you can pass it to +# `proplot.axes.Axes.format` or `proplot.figure.Figure.format`. To temporarily +# modify setting(s) for a block of code, use `~proplot.config.Configurator.context`. +# To modify setting(s) for the entire python session, just assign it to the +# `~proplot.config.rc` dictionary or use `~proplot.config.Configurator.update`. +# To reset everything to the default state, use `~proplot.config.Configurator.reset`. +# See the below example. + + +# %% +import proplot as pplt +import numpy as np + +# Update global settings in several different ways +pplt.rc.metacolor = 'gray6' +pplt.rc.update({'fontname': 'Source Sans Pro', 'fontsize': 11}) +pplt.rc['figure.facecolor'] = 'gray3' +pplt.rc.axesfacecolor = 'gray4' +# pplt.rc.save() # save the current settings to ~/.proplotrc + +# Apply settings to figure with context() +with pplt.rc.context({'suptitle.size': 13}, toplabelcolor='gray6', metawidth=1.5): + fig = pplt.figure(figwidth=6, sharey='limits', span=False) + axs = fig.subplots(ncols=2) + +# Plot lines with a custom cycler +N, M = 100, 7 +state = np.random.RandomState(51423) +values = np.arange(1, M + 1) +cycle = pplt.get_colors('grays', M - 1) + ['red'] +for i, ax in enumerate(axs): + data = np.cumsum(state.rand(N, M) - 0.5, axis=0) + lines = ax.plot(data, linewidth=3, cycle=cycle) + +# Apply settings to axes with format() +axs.format( + grid=False, xlabel='xlabel', ylabel='ylabel', + toplabels=('Column 1', 'Column 2'), + suptitle='Rc settings demo', + suptitlecolor='gray7', + abc='[A]', abcloc='l', + title='Title', titleloc='r', titlecolor='gray7' +) + +# Reset persistent modifications from head of cell +pplt.rc.reset() + + +# %% +import proplot as pplt +import numpy as np +# pplt.rc.style = 'style' # set the style everywhere + +# Sample data +state = np.random.RandomState(51423) +data = state.rand(10, 5) + +# Set up figure +fig, axs = pplt.subplots(ncols=2, nrows=2, span=False, share=False) +axs.format(suptitle='Stylesheets demo') +styles = ('ggplot', 'seaborn', '538', 'bmh') + +# Apply different styles to different axes with format() +for ax, style in zip(axs, styles): + ax.format(style=style, xlabel='xlabel', ylabel='ylabel', title=style) + ax.plot(data, linewidth=3) diff --git a/docs/cartesian.py b/docs/cartesian.py new file mode 100644 index 000000000..eb6640397 --- /dev/null +++ b/docs/cartesian.py @@ -0,0 +1,665 @@ +# -*- coding: utf-8 -*- +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.11.4 +# kernelspec: +# display_name: Python 3 +# language: python +# name: python3 +# --- + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_cartesian: +# +# Cartesian axes +# ============== +# +# This section documents features used for modifying Cartesian *x* and *y* +# axes, including axis scales, tick locations, tick label formatting, and +# several twin and dual axes commands. + + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_locators: +# +# Tick locations +# -------------- +# +# Matplotlib `tick locators +# `__ +# select sensible tick locations based on the axis data limits. In proplot, you can +# change the tick locator using the `~proplot.axes.CartesianAxes.format` keyword +# arguments `xlocator`, `ylocator`, `xminorlocator`, and `yminorlocator` (or their +# aliases, `xticks`, `yticks`, `xminorticks`, and `yminorticks`). This is powered by +# the `~proplot.constructor.Locator` :ref:`constructor function `. +# +# You can use these keyword arguments to apply built-in matplotlib +# `~matplotlib.ticker.Locator`\ s by their "registered" names +# (e.g., ``xlocator='log'``), to draw ticks every ``N`` data values with +# `~matplotlib.ticker.MultipleLocator` (e.g., ``xlocator=2``), or to tick the +# specific locations in a list using `~matplotlib.ticker.FixedLocator` (just +# like `~matplotlib.axes.Axes.set_xticks` and `~matplotlib.axes.Axes.set_yticks`). +# If you want to work with the locator classes directly, they are available in the +# top-level namespace (e.g., ``xlocator=pplt.MultipleLocator(...)`` is allowed). +# +# To generate lists of tick locations, we recommend using proplot's +# `~proplot.utils.arange` function -- it’s basically an endpoint-inclusive +# version of `numpy.arange`, which is usually what you'll want in this context. + +# %% +import proplot as pplt +import numpy as np +state = np.random.RandomState(51423) +pplt.rc.update( + metawidth=1, fontsize=10, + metacolor='dark blue', suptitlecolor='dark blue', + titleloc='upper center', titlecolor='dark blue', titleborder=False, + axesfacecolor=pplt.scale_luminance('powderblue', 1.15), +) +fig = pplt.figure(share=False, refwidth=5, refaspect=(8, 1)) +fig.format(suptitle='Tick locators demo') + +# Step size for tick locations +ax = fig.subplot(711, title='MultipleLocator') +ax.format(xlim=(0, 200), xminorlocator=10, xlocator=30) + +# Specific list of locations +ax = fig.subplot(712, title='FixedLocator') +ax.format(xlim=(0, 10), xminorlocator=0.1, xlocator=[0, 0.3, 0.8, 1.6, 4.4, 8, 8.8]) + +# Ticks at numpy.linspace(xmin, xmax, N) +ax = fig.subplot(713, title='LinearLocator') +ax.format(xlim=(0, 10), xlocator=('linear', 21)) + +# Logarithmic locator, used automatically for log scale plots +ax = fig.subplot(714, title='LogLocator') +ax.format(xlim=(1, 100), xlocator='log', xminorlocator='logminor') + +# Maximum number of ticks, but at "nice" locations +ax = fig.subplot(715, title='MaxNLocator') +ax.format(xlim=(1, 7), xlocator=('maxn', 11)) + +# Hide all ticks +ax = fig.subplot(716, title='NullLocator') +ax.format(xlim=(-10, 10), xlocator='null') + +# Tick locations that cleanly divide 60 minute/60 second intervals +ax = fig.subplot(717, title='Degree-Minute-Second Locator (requires cartopy)') +ax.format(xlim=(0, 2), xlocator='dms', xformatter='dms') + +pplt.rc.reset() + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_formatters: +# +# Tick formatting +# --------------- +# +# Matplotlib `tick formatters +# `__ +# convert floating point numbers to nicely-formatted tick labels. In proplot, you can +# change the tick formatter using the `~proplot.axes.CartesianAxes.format` keyword +# arguments `xformatter` and `yformatter` (or their aliases, `xticklabels` and +# `yticklabels`). This is powered by the `~proplot.constructor.Formatter` +# :ref:`constructor function `. +# +# You can use these keyword arguments to apply built-in matplotlib +# `~matplotlib.ticker.Formatter`\ s by their "registered" names +# (e.g., ``xformatter='log'``), to apply a ``%``-style format directive with +# `~matplotlib.ticker.FormatStrFormatter` (e.g., ``xformatter='%.0f'``), or +# to apply custom tick labels with `~matplotlib.ticker.FixedFormatter` (just +# like `~matplotlib.axes.Axes.set_xticklabels`). You can also apply one of proplot's +# new tick formatters -- for example, ``xformatter='deglat'`` to label ticks +# as geographic latitude coordinates, ``xformatter='pi'`` to label ticks as +# fractions of :math:`\pi`, or ``xformatter='sci'`` to label ticks with +# scientific notation. If you want to work with the formatter classes +# directly, they are available in the top-level namespace +# (e.g., ``xformatter=pplt.SciFormatter(...)`` is allowed). +# +# Proplot also changes the default tick formatter to +# `~proplot.ticker.AutoFormatter`. This class trims trailing zeros by +# default, can optionally omit or wrap tick values within particular +# number ranges, and can add prefixes and suffixes to each label. See +# `~proplot.ticker.AutoFormatter` for details. To disable the trailing +# zero-trimming feature, set :rcraw:`formatter.zerotrim` to ``False``. + +# %% +import proplot as pplt +pplt.rc.fontsize = 11 +pplt.rc.metawidth = 1.5 +pplt.rc.gridwidth = 1 + +# Create the figure +fig, axs = pplt.subplots(ncols=2, nrows=2, refwidth=1.5, share=False) +axs.format( + ytickloc='both', yticklabelloc='both', + titlepad='0.5em', suptitle='Default formatters demo' +) + +# Formatter comparison +locator = [0, 0.25, 0.5, 0.75, 1] +axs[0].format(xformatter='scalar', yformatter='scalar', title='Matplotlib formatter') +axs[1].format(title='Proplot formatter') +axs[:2].format(xlocator=locator, ylocator=locator) + +# Limiting the tick range +axs[2].format( + title='Omitting tick labels', ticklen=5, xlim=(0, 5), ylim=(0, 5), + xtickrange=(0, 2), ytickrange=(0, 2), xlocator=1, ylocator=1 +) + +# Setting the wrap range +axs[3].format( + title='Wrapping the tick range', ticklen=5, xlim=(0, 7), ylim=(0, 6), + xwraprange=(0, 5), ywraprange=(0, 3), xlocator=1, ylocator=1 +) +pplt.rc.reset() + + +# %% +import proplot as pplt +import numpy as np +pplt.rc.update( + metawidth=1.2, fontsize=10, axesfacecolor='gray0', figurefacecolor='gray2', + metacolor='gray8', gridcolor='gray8', titlecolor='gray8', suptitlecolor='gray8', + titleloc='upper center', titleborder=False, +) +fig = pplt.figure(refwidth=5, refaspect=(8, 1), share=False) + +# Scientific notation +ax = fig.subplot(911, title='SciFormatter') +ax.format(xlim=(0, 1e20), xformatter='sci') + +# N significant figures for ticks at specific values +ax = fig.subplot(912, title='SigFigFormatter') +ax.format( + xlim=(0, 20), xlocator=(0.0034, 3.233, 9.2, 15.2344, 7.2343, 19.58), + xformatter=('sigfig', 2), # 2 significant digits +) + +# Fraction formatters +ax = fig.subplot(913, title='FracFormatter') +ax.format(xlim=(0, 3 * np.pi), xlocator=np.pi / 4, xformatter='pi') +ax = fig.subplot(914, title='FracFormatter') +ax.format(xlim=(0, 2 * np.e), xlocator=np.e / 2, xticklabels='e') + +# Geographic formatters +ax = fig.subplot(915, title='Latitude Formatter') +ax.format(xlim=(-90, 90), xlocator=30, xformatter='deglat') +ax = fig.subplot(916, title='Longitude Formatter') +ax.format(xlim=(0, 360), xlocator=60, xformatter='deglon') + +# User input labels +ax = fig.subplot(917, title='FixedFormatter') +ax.format( + xlim=(0, 5), xlocator=np.arange(5), + xticklabels=['a', 'b', 'c', 'd', 'e'], +) + +# Custom style labels +ax = fig.subplot(918, title='FormatStrFormatter') +ax.format(xlim=(0, 0.001), xlocator=0.0001, xformatter='%.E') +ax = fig.subplot(919, title='StrMethodFormatter') +ax.format(xlim=(0, 100), xtickminor=False, xlocator=20, xformatter='{x:.1f}') +fig.format(ylocator='null', suptitle='Tick formatters demo') +pplt.rc.reset() + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _pandas: https://pandas.pydata.org +# +# .. _ug_datetime: +# +# Datetime ticks +# -------------- +# +# The above examples all assumed typical "numeric" axes. However +# `~proplot.axes.CartesianAxes.format` can also modify the tick locations and tick +# labels for "datetime" axes. To draw ticks on each occurence of some particular time +# unit, use a unit string (e.g., ``xlocator='month'``). To draw ticks every ``N`` time +# units, use a (unit, N) tuple (e.g., ``xlocator=('day', 5)``). For `% style formatting +# `__ +# of datetime tick labels with `~datetime.datetime.strftime`, you can use a string +# containing ``'%'`` (e.g. ``xformatter='%Y-%m-%d'``). By default, *x* axis datetime +# axis labels are rotated 90 degrees, like in `pandas`_. This can be disabled by +# passing ``xrotation=0`` to `~proplot.axes.CartesianAxes.format` or by setting +# :rcraw:`formatter.timerotation` to ``0``. See `~proplot.constructor.Locator` +# and `~proplot.constructor.Formatter` for details. + +# %% +import proplot as pplt +import numpy as np +pplt.rc.update( + metawidth=1.2, fontsize=10, ticklenratio=0.7, + figurefacecolor='w', axesfacecolor='pastel blue', + titleloc='upper center', titleborder=False, +) +fig, axs = pplt.subplots(nrows=5, refwidth=6, refaspect=(8, 1), share=False) + +# Default date locator +# This is enabled if you plot datetime data or set datetime limits +ax = axs[0] +ax.format( + xlim=(np.datetime64('2000-01-01'), np.datetime64('2001-01-02')), + title='Auto date locator and formatter' +) + +# Concise date formatter introduced in matplotlib 3.1 +ax = axs[1] +ax.format( + xlim=(np.datetime64('2000-01-01'), np.datetime64('2001-01-01')), + xformatter='concise', title='Concise date formatter', +) + +# Minor ticks every year, major every 10 years +ax = axs[2] +ax.format( + xlim=(np.datetime64('2000-01-01'), np.datetime64('2050-01-01')), + xlocator=('year', 10), xformatter='\'%y', title='Ticks every N units', +) + +# Minor ticks every 10 minutes, major every 2 minutes +ax = axs[3] +ax.format( + xlim=(np.datetime64('2000-01-01T00:00:00'), np.datetime64('2000-01-01T12:00:00')), + xlocator=('hour', range(0, 24, 2)), xminorlocator=('minute', range(0, 60, 10)), + xformatter='T%H:%M:%S', title='Ticks at specific intervals', +) + +# Month and year labels, with default tick label rotation +ax = axs[4] +ax.format( + xlim=(np.datetime64('2000-01-01'), np.datetime64('2008-01-01')), + xlocator='year', xminorlocator='month', # minor ticks every month + xformatter='%b %Y', title='Ticks with default rotation', +) +axs[:4].format(xrotation=0) # no rotation for the first four examples +fig.format(ylocator='null', suptitle='Datetime locators and formatters demo') +pplt.rc.reset() + + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_loc: +# +# Axis positions +# -------------- +# +# The locations of `axis spines +# `__, +# tick marks, tick labels, and axis labels can be controlled with +# `proplot.axes.CartesianAxes.format` keyword arguments like `xspineloc` +# (shorthand `xloc`), `xtickloc`, `xticklabelloc`, and `xlabelloc`. Valid +# locations include ``'left'``, ``'right'``, ``'top'``, ``'bottom'``, ``'neither'``, +# ``'none'``, or ``'both'``. Spine locations can also be set to a valid +# `~matplotlib.spines.Spine.set_position` value, e.g. ``'zero'`` or +# ``('axes', 1.5)``. The top or right spine is used when the coordinate is +# more than halfway across the axes. This is often convenient when passing +# e.g. `loc` to :ref:`"alternate" axes commands `. These keywords +# provide the functionality of matplotlib's `~matplotlib.axis.YAxis.tick_left`, +# `~matplotlib.axis.YAxis.tick_right`, `~matplotlib.axis.XAxis.tick_top`, and +# `~matplotlib.axis.XAxis.tick_bottom`, and `~matplotlib.spines.Spine.set_position`, +# but with additional flexibility. + +# %% +import proplot as pplt +pplt.rc.update( + metawidth=1.2, fontsize=10, gridcolor='coral', + axesedgecolor='deep orange', figurefacecolor='white', +) +fig = pplt.figure(share=False, refwidth=2, suptitle='Axis locations demo') + +# Spine location demonstration +ax = fig.subplot(121, title='Various locations') +ax.format(xloc='top', xlabel='original axis') +ax.twiny(xloc='bottom', xcolor='black', xlabel='locked twin') +ax.twiny(xloc=('axes', 1.25), xcolor='black', xlabel='offset twin') +ax.twiny(xloc=('axes', -0.25), xcolor='black', xlabel='offset twin') +ax.format(ytickloc='both', yticklabelloc='both') +ax.format(ylabel='labels on both sides') + +# Other locations locations +ax = fig.subplot(122, title='Zero-centered spines', titlepad='1em') +ax.format(xlim=(-10, 10), ylim=(-3, 3), yticks=1) +ax.format(xloc='zero', yloc='zero') +pplt.rc.reset() + + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_scales: +# +# Axis scales +# ----------- +# +# "Axis scales" like ``'linear'`` and ``'log'`` control the *x* and *y* axis +# coordinate system. To change the axis scale, pass e.g. ``xscale='log'`` or +# ``yscale='log'`` to `~proplot.axes.Axes.format`. This is powered by the +# `~proplot.constructor.Scale` :ref:`constructor function `. +# Proplot makes several changes to the axis scale API: +# +# * The `~proplot.ticker.AutoFormatter` formatter is now used for all axis scales +# by default, including ``'log'`` and ``'symlog'``. Matplotlib's behavior can +# be restored by passing e.g. ``xformatter='log'`` or ``yformatter='log'`` to +# `~proplot.axes.CartesianAxes.format`. +# * To make its behavior consistent with `~proplot.constructor.Locator` and +# `~proplot.constructor.Formatter`, the `~proplot.constructor.Scale` +# constructor function returns instances of `~matplotlib.scale.ScaleBase`, +# and `~matplotlib.axes.Axes.set_xscale` and +# `~matplotlib.axes.Axes.set_yscale` now accept these class instances in +# addition to "registered" names like ``'log'``. +# * While matplotlib axis scales must be instantiated with an +# `~matplotlib.axis.Axis` instance (for backwards compatibility reasons), +# proplot axis scales can be instantiated without the axis instance +# (e.g., ``pplt.LogScale()`` instead of ``pplt.LogScale(ax.xaxis)``). +# * The default `subs` for the ``'symlog'`` axis scale is now ``np.arange(1, 10)``, +# and the default `linthresh` is now ``1``. Also the ``'log'`` and ``'symlog'`` +# axis scales now accept the keywords `base`, `linthresh`, `linscale`, and +# `subs` rather than keywords with trailing ``x`` or ``y``. +# +# Proplot also includes a few new axis scales. The ``'cutoff'`` scale +# `~proplot.scale.CutoffScale` is useful when the statistical distribution +# of your data is very unusual. The ``'sine'`` scale `~proplot.scale.SineLatitudeScale` +# scales the axis with a sine function (resulting in an area-weighted spherical latitude +# coordinate) and the ``'mercator'`` scale `~proplot.scale.MercatorLatitudeScale` +# scales the axis with the Mercator projection latitude coordinate. The +# ``'inverse'`` scale `~proplot.scale.InverseScale` can be useful when +# working with spectral data, especially with :ref:`"dual" unit axes `. +# If you want to work with the axis scale classes directly, they are available +# in the top-level namespace (e.g., ``xscale=pplt.CutoffScale(...)`` is allowed). + +# %% +import proplot as pplt +import numpy as np +N = 200 +lw = 3 +pplt.rc.update({'meta.width': 1, 'label.weight': 'bold', 'tick.labelweight': 'bold'}) +fig = pplt.figure(refwidth=1.8, share=False) + +# Linear and log scales +ax1 = fig.subplot(221) +ax1.format(yscale='linear', ylabel='linear scale') +ax2 = fig.subplot(222) +ax2.format(ylim=(1e-3, 1e3), yscale='log', ylabel='log scale') +for ax in (ax1, ax2): + ax.plot(np.linspace(0, 1, N), np.linspace(0, 1000, N), lw=lw) + +# Symlog scale +ax = fig.subplot(223) +ax.format(yscale='symlog', ylabel='symlog scale') +ax.plot(np.linspace(0, 1, N), np.linspace(-1000, 1000, N), lw=lw) + +# Logit scale +ax = fig.subplot(224) +ax.format(yscale='logit', ylabel='logit scale') +ax.plot(np.linspace(0, 1, N), np.linspace(0.01, 0.99, N), lw=lw) + +fig.format(suptitle='Axis scales demo', ytickminor=True) +pplt.rc.reset() + + +# %% +import proplot as pplt +import numpy as np + +# Create figure +x = np.linspace(0, 4 * np.pi, 100) +dy = np.linspace(-1, 1, 5) +ys = (np.sin(x), np.cos(x)) +state = np.random.RandomState(51423) +data = state.rand(len(dy) - 1, len(x) - 1) +colors = ('coral', 'sky blue') +cmap = pplt.Colormap('grays', right=0.8) +fig, axs = pplt.subplots(nrows=4, refaspect=(5, 1), figwidth=5.5, sharex=False) + +# Loop through various cutoff scale options +titles = ('Zoom out of left', 'Zoom into left', 'Discrete jump', 'Fast jump') +args = ( + (np.pi, 3), # speed up + (3 * np.pi, 1 / 3), # slow down + (np.pi, np.inf, 3 * np.pi), # discrete jump + (np.pi, 5, 3 * np.pi) # fast jump +) +locators = ( + np.pi / 3, + np.pi / 3, + np.pi * np.append(np.linspace(0, 1, 4), np.linspace(3, 4, 4)), + np.pi * np.append(np.linspace(0, 1, 4), np.linspace(3, 4, 4)), +) +for ax, iargs, title, locator in zip(axs, args, titles, locators): + ax.pcolormesh(x, dy, data, cmap=cmap) + for y, color in zip(ys, colors): + ax.plot(x, y, lw=4, color=color) + ax.format( + xscale=('cutoff', *iargs), xlim=(0, 4 * np.pi), + xlocator=locator, xformatter='pi', xtickminor=False, + ygrid=False, ylabel='wave amplitude', + title=title, suptitle='Cutoff axis scales demo' + ) + +# %% +import proplot as pplt +import numpy as np + +# Create figure +n = 30 +state = np.random.RandomState(51423) +data = state.rand(n - 1, n - 1) +colors = ('coral', 'sky blue') +cmap = pplt.Colormap('grays', right=0.8) +gs = pplt.GridSpec(nrows=2, ncols=2) +fig = pplt.figure(refwidth=2.3, share=False) +fig.format(grid=False, suptitle='Other axis scales demo') + +# Geographic scales +x = np.linspace(-180, 180, n) +y = np.linspace(-85, 85, n) +for i, scale in enumerate(('sine', 'mercator')): + ax = fig.subplot(gs[i, 0]) + ax.plot(x, y, '-', color=colors[i], lw=4) + ax.pcolormesh(x, y, data, cmap='grays', cmap_kw={'right': 0.8}) + ax.format( + yscale=scale, title=scale.title() + ' scale', + ylim=(-85, 85), ylocator=20, yformatter='deg', + ) + +# Exponential scale +n = 50 +x = np.linspace(0, 1, n) +y = 3 * np.linspace(0, 1, n) +data = state.rand(len(y) - 1, len(x) - 1) +ax = fig.subplot(gs[0, 1]) +title = 'Exponential $e^x$ scale' +ax.pcolormesh(x, y, data, cmap='grays', cmap_kw={'right': 0.8}) +ax.plot(x, y, lw=4, color=colors[0]) +ax.format(ymin=0.05, yscale=('exp', np.e), title=title) + +# Power scale +ax = fig.subplot(gs[1, 1]) +title = 'Power $x^{0.5}$ scale' +ax.pcolormesh(x, y, data, cmap='grays', cmap_kw={'right': 0.8}) +ax.plot(x, y, lw=4, color=colors[1]) +ax.format(ymin=0.05, yscale=('power', 0.5), title=title) + + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_alt: +# +# Alternate axes +# -------------- +# +# The `matplotlib.axes.Axes` class includes `~matplotlib.axes.Axes.twinx` +# and `~matplotlib.axes.Axes.twiny` commands for drawing "twin" *x* and +# *y* axes in the same subplot. Proplot expands on these commands and adds +# the arguably more intuitive `~proplot.axes.CartesianAxes.altx` and +# `~proplot.axes.CartesianAxes.alty` options. Here `~proplot.axes.CartesianAxes.altx` +# is equivalent to `~proplot.axes.CartesianAxes.twiny` (makes an alternate *x* +# axes and an identical twin *y* axes) and `~proplot.axes.CartesianAxes.alty` +# is equivalent to `~proplot.axes.CartesianAxes.twinx` (makes an alternate *y* +# axes and an identical twin *x* axes). The proplot versions can be quickly +# formatted by passing `proplot.axes.CartesianAxes.format` keyword arguments +# to the commands (e.g., ``ax.alty(ycolor='red')`` or, since the ``y`` prefix in +# this context is redundant, just ``ax.alty(color='red')``). They also enforce +# sensible default locations for the spines, ticks, and labels, and disable +# the twin axes background patch and gridlines by default. +# +# .. note:: +# +# Unlike matplotlib, proplot adds alternate axes as `children +# `__ +# of the original axes. This helps simplify the :ref:`tight layout algorithm +# ` but means that the drawing order is controlled by the difference +# between the zorders of the alternate axes and the content *inside* the original +# axes rather than the zorder of the original axes itself (see `this issue page +# `__ for details). + +# %% +import proplot as pplt +import numpy as np +state = np.random.RandomState(51423) +c0 = 'gray5' +c1 = 'red8' +c2 = 'blue8' +N, M = 50, 10 + +# Alternate y axis +data = state.rand(M) + (state.rand(N, M) - 0.48).cumsum(axis=0) +altdata = 5 * (state.rand(N) - 0.45).cumsum(axis=0) +fig = pplt.figure(share=False) +ax = fig.subplot(121, title='Alternate y twin x') +ax.line(data, color=c0, ls='--') +ox = ax.alty(color=c2, label='alternate ylabel', linewidth=1) +ox.line(altdata, color=c2) + +# Alternate x axis +data = state.rand(M) + (state.rand(N, M) - 0.48).cumsum(axis=0) +altdata = 5 * (state.rand(N) - 0.45).cumsum(axis=0) +ax = fig.subplot(122, title='Alternate x twin y') +ax.linex(data, color=c0, ls='--') +ox = ax.altx(color=c1, label='alternate xlabel', linewidth=1) +ox.linex(altdata, color=c1) +fig.format(xlabel='xlabel', ylabel='ylabel', suptitle='Alternate axes demo') + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_dual: +# +# Dual unit axes +# -------------- +# +# The `~proplot.axes.CartesianAxes.dualx` and +# `~proplot.axes.CartesianAxes.dualy` methods can be used to draw duplicate *x* and +# *y* axes meant to represent *alternate units* in the same coordinate range as the +# "parent" axis. This feature is powered by the `~proplot.scale.FuncScale` class. +# `~proplot.axes.CartesianAxes.dualx` and `~proplot.axes.CartesianAxes.dualy` accept +# the same axis formatting keyword arguments as `~proplot.axes.CartesianAxes.altx` +# and `~proplot.axes.CartesianAxes.alty`. The alternate units are specified with +# either of the following three positional arguments: +# +# #. A single linear forward function. +# #. A 2-tuple of arbitrary forward and inverse functions. +# #. An :ref:`axis scale ` name or class instance. +# +# In the third case, the axis scale transforms are used for the forward and +# inverse functions, and the default axis scale locators and formatters are used +# for the default dual axis locators and formatters. In the below examples, +# we generate dual axes with each of these three methods. Note that the +# "parent" axis scale is arbitrary -- in the first example, we create +# a `~proplot.axes.CartesianAxes.dualx` axis for a `symlog-scaled +# `__ axis. + +# %% +import proplot as pplt +pplt.rc.update({'grid.alpha': 0.4, 'meta.width': 1, 'grid.linewidth': 1}) +c1 = pplt.scale_luminance('cerulean', 0.5) +c2 = pplt.scale_luminance('red', 0.5) +fig = pplt.figure(refaspect=2.2, refwidth=3, share=False) +axs = fig.subplots( + [[1, 1, 2, 2], [0, 3, 3, 0]], + suptitle='Duplicate axes with simple transformations', + ylocator=[], yformatter=[], xcolor=c1, gridcolor=c1, +) + +# Meters and kilometers +ax = axs[0] +ax.format(xlim=(0, 5000), xlabel='meters') +ax.dualx( + lambda x: x * 1e-3, + label='kilometers', grid=True, color=c2, gridcolor=c2 +) + +# Kelvin and Celsius +ax = axs[1] +ax.format(xlim=(200, 300), xlabel='temperature (K)') +ax.dualx( + lambda x: x - 273.15, + label='temperature (\N{DEGREE SIGN}C)', grid=True, color=c2, gridcolor=c2 +) + +# With symlog parent +ax = axs[2] +ax.format(xlim=(-100, 100), xscale='symlog', xlabel='MegaJoules') +ax.dualx( + lambda x: x * 1e6, + label='Joules', formatter='log', grid=True, color=c2, gridcolor=c2 +) +pplt.rc.reset() + +# %% +import proplot as pplt +pplt.rc.update({'grid.alpha': 0.4, 'meta.width': 1, 'grid.linewidth': 1}) +c1 = pplt.scale_luminance('cerulean', 0.5) +c2 = pplt.scale_luminance('red', 0.5) +fig = pplt.figure( + share=False, refaspect=0.4, refwidth=1.8, + suptitle='Duplicate axes with pressure and height' +) + +# Pressure as the linear scale, height on opposite axis (scale height 7km) +ax = fig.subplot(121) +ax.format( + xformatter='null', ylabel='pressure (hPa)', + ylim=(1000, 10), xlocator=[], ycolor=c1, gridcolor=c1 +) +ax.dualy( + 'height', label='height (km)', ticks=2.5, color=c2, gridcolor=c2, grid=True +) + +# Height as the linear scale, pressure on opposite axis (scale height 7km) +ax = fig.subplot(122) +ax.format( + xformatter='null', ylabel='height (km)', ylim=(0, 20), xlocator='null', + grid=True, gridcolor=c2, ycolor=c2 +) +ax.dualy( + 'pressure', label='pressure (hPa)', locator=100, color=c1, gridcolor=c1, grid=True +) +pplt.rc.reset() + +# %% +import proplot as pplt +import numpy as np +pplt.rc.margin = 0 +c1 = pplt.scale_luminance('cerulean', 0.5) +c2 = pplt.scale_luminance('red', 0.5) +fig, ax = pplt.subplots(refaspect=(3, 1), figwidth=6) + +# Sample data +cutoff = 1 / 5 +x = np.linspace(0.01, 0.5, 1000) # in wavenumber days +response = (np.tanh(-((x - cutoff) / 0.03)) + 1) / 2 # response func +ax.axvline(cutoff, lw=2, ls='-', color=c2) +ax.fill_between([cutoff - 0.03, cutoff + 0.03], 0, 1, color=c2, alpha=0.3) +ax.plot(x, response, color=c1, lw=2) + +# Add inverse scale to top +ax.format( + title='Imaginary response function', + suptitle='Duplicate axes with wavenumber and period', + xlabel='wavenumber (days$^{-1}$)', ylabel='response', grid=False, +) +ax = ax.dualx( + 'inverse', locator='log', locator_kw={'subs': (1, 2, 5)}, label='period (days)' +) +pplt.rc.reset() diff --git a/docs/changelog.rst b/docs/changelog.rst deleted file mode 100644 index 565b0521d..000000000 --- a/docs/changelog.rst +++ /dev/null @@ -1 +0,0 @@ -.. include:: ../CHANGELOG.rst diff --git a/docs/colorbars_legends.ipynb b/docs/colorbars_legends.ipynb deleted file mode 100644 index d6a4357ef..000000000 --- a/docs/colorbars_legends.ipynb +++ /dev/null @@ -1,371 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Colorbars and legends" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Axes colorbars and legends" - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "ProPlot includes some useful improvements to the matplotlib API that make working with colorbars and legends much easier.\n", - "\n", - "To draw a colorbar or legend along the *outside edge* of a subplot, simply pass an \"edge\" location (e.g. ``loc='right'``) to the `~proplot.axes.Axes.colorbar` or `~proplot.axes.Axes.legend` `~proplot.axes.Axes` methods. If you draw multiple colorbars or legends on the same side, they are stacked on top of each other. To preserve subplot aspect ratios and visual symmetry between subplots, the space for outer colorbars and legends is not \"stolen\" from subplots, but separately allocated within the figure `~proplot.subplots.GridSpec`.\n", - "\n", - "ProPlot can also be used to draw colorbars and legends on-the-fly. To plot data and draw a colorbar in one go, pass a location (e.g. ``colorbar='r'``) to methods that accept a `cmap` argument (e.g. `~matplotlib.axes.Axes.contourf`). To draw a legend or colorbar-legend in one go, pass a location (e.g. ``legend='r'`` or ``colorbar='r'``) to methods that accept a `cycle` argument (e.g. `~matplotlib.axes.Axes.plot`). This feature is powered by the `~proplot.wrappers.cmap_changer` and `~proplot.wrappers.cycle_changer` wrappers.\n", - "\n", - "Finally, just like matplotlib \"inset\" legends, ProPlot also supports \"inset\" *colorbars*. To draw an inset colorbar, simply pass an inset location to `~proplot.axes.Axes.colorbar` (e.g. ``loc='upper right'`` or ``loc='ur'``). These colorbars have optional background \"frames\" that can be configured with various `~proplot.axes.Axes.colorbar` keyword arguments." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", - "import numpy as np\n", - "with plot.rc.context(abc=True):\n", - " f, axs = plot.subplots(ncols=2, share=0)\n", - "\n", - "# Colorbars\n", - "ax = axs[0]\n", - "state = np.random.RandomState(51423)\n", - "m = ax.heatmap(state.rand(10, 10), colorbar='t', cmap='dusk')\n", - "ax.colorbar(m, loc='r')\n", - "ax.colorbar(m, loc='ll', label='colorbar label')\n", - "ax.format(title='Axes colorbars', suptitle='Axes colorbars and legends demo')\n", - "\n", - "# Legends\n", - "ax = axs[1]\n", - "ax.format(title='Axes legends', titlepad='0em')\n", - "hs = ax.plot(\n", - " (state.rand(10, 5) - 0.5).cumsum(axis=0), linewidth=3,\n", - " cycle='ggplot', legend='t',\n", - " labels=list('abcde'), legend_kw={'ncols': 5, 'frame': False}\n", - ")\n", - "ax.legend(hs, loc='r', ncols=1, frame=False)\n", - "ax.legend(hs, loc='ll', label='legend label')\n", - "axs.format(xlabel='xlabel', ylabel='ylabel')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", - "import numpy as np\n", - "f, axs = plot.subplots(nrows=2, share=0, axwidth='55mm', panelpad='1em')\n", - "axs.format(suptitle='Stacked colorbars demo')\n", - "state = np.random.RandomState(51423)\n", - "N = 10\n", - "# Repeat for both axes\n", - "for j, ax in enumerate(axs):\n", - " ax.format(\n", - " xlabel='data', xlocator=np.linspace(0, 0.8, 5),\n", - " title=f'Subplot #{j+1}'\n", - " )\n", - " for i, (x0, y0, x1, y1, cmap, scale) in enumerate((\n", - " (0, 0.5, 1, 1, 'grays', 0.5),\n", - " (0, 0, 0.5, 0.5, 'reds', 1),\n", - " (0.5, 0, 1, 0.5, 'blues', 2)\n", - " )):\n", - " if j == 1 and i == 0:\n", - " continue\n", - " data = state.rand(N, N)*scale\n", - " x, y = np.linspace(x0, x1, N + 1), np.linspace(y0, y1, N + 1)\n", - " m = ax.pcolormesh(\n", - " x, y, data, cmap=cmap,\n", - " levels=np.linspace(0, scale, 11)\n", - " )\n", - " ax.colorbar(m, loc='l', label=f'dataset #{i+1}')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Figure colorbars and legends" - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "To draw a colorbar or legend along the *edge of the figure*, simply use the `~proplot.subplots.Figure.colorbar` or `~proplot.subplots.Figure.legend` `~proplot.subplots.Figure` methods. Figure colorbars and legends are aligned between the edges of the subplot grid. As with :ref:`axes colorbars and legends `, if you draw multiple colorbars or legends on the same side, they are stacked on top of each other.\n", - "\n", - "To draw a colorbar or legend alongside *particular row(s) or column(s)* of the subplot grid, use the `row`, `rows`, `col`, or `cols` keyword arguments. Pass an integer to draw the colorbar or legend beside a single row or column, or pass a tuple to draw it beside a range of rows or columns." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", - "import numpy as np\n", - "f, axs = plot.subplots(ncols=3, nrows=3, axwidth=1.4)\n", - "state = np.random.RandomState(51423)\n", - "m = axs.pcolormesh(\n", - " state.rand(20, 20), cmap='grays',\n", - " levels=np.linspace(0, 1, 11), extend='both'\n", - ")[0]\n", - "axs.format(\n", - " suptitle='Figure colorbars and legends demo', abc=True,\n", - " abcloc='l', abcstyle='a.', xlabel='xlabel', ylabel='ylabel'\n", - ")\n", - "f.colorbar(m, label='column 1', ticks=0.5, loc='b', col=1)\n", - "f.colorbar(m, label='columns 2-3', ticks=0.2, loc='b', cols=(2, 3))\n", - "f.colorbar(m, label='stacked colorbar', ticks=0.1, loc='b', minorticks=0.05)\n", - "f.colorbar(m, label='colorbar with length <1', ticks=0.1, loc='r', length=0.7)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", - "import numpy as np\n", - "f, axs = plot.subplots(\n", - " ncols=2, nrows=2, axwidth=1.7,\n", - " share=0, wspace=0.3, order='F'\n", - ")\n", - "\n", - "# Plot data\n", - "data = (np.random.rand(50, 50) - 0.1).cumsum(axis=0)\n", - "m = axs[:2].contourf(data, cmap='grays', extend='both')\n", - "colors = plot.Colors('grays', 5)\n", - "hs = []\n", - "state = np.random.RandomState(51423)\n", - "for abc, color in zip('ABCDEF', colors):\n", - " h = axs[2:].plot(state.rand(10), lw=3, color=color, label=f'line {abc}')\n", - " hs.extend(h[0])\n", - "\n", - "# Add colorbars and legends\n", - "f.colorbar(m[0], length=0.8, label='colorbar label', loc='b', col=1, locator=5)\n", - "f.colorbar(m[0], label='colorbar label', loc='l')\n", - "f.legend(hs, ncols=2, center=True, frame=False, loc='b', col=2)\n", - "f.legend(hs, ncols=1, label='legend label', frame=False, loc='r')\n", - "axs.format(\n", - " suptitle='Figure colorbars and legends demo',\n", - " abc=True, abcloc='ul', abcstyle='A'\n", - ")\n", - "for ax, title in zip(axs, ['2D dataset #1', '2D dataset #2', 'Line set #1', 'Line set #2']):\n", - " ax.format(xlabel='xlabel', title=title)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## New colorbar features" - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "The `~proplot.subplots.Figure` `~proplot.subplots.Figure.colorbar` and `~proplot.axes.Axes` `~proplot.axes.Axes.colorbar` methods are wrapped by `~proplot.wrappers.colorbar_wrapper`, which adds several new features.\n", - "\n", - "You can now draw colorbars from *lists of colors* or *lists of artists* by passing a list instead of a mappable object. Colorbar minor ticks are now much more robust, and the tick location and formatter arguments are passed through `~proplot.axistools.Locator` and `~proplot.axistools.Formatter`. The colorbar width and length can be changed with the `width` and `length` keyword args. Note that colorbar widths are now specified in *physical units*, which helps prevent drawing colorbars that look \"too skinny\" or \"too fat\"." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", - "import numpy as np\n", - "f, axs = plot.subplots(share=0, ncols=2, axwidth=2)\n", - "\n", - "# Colorbars from lines\n", - "ax = axs[0]\n", - "state = np.random.RandomState(51423)\n", - "data = 1 + (state.rand(12, 10) - 0.45).cumsum(axis=0)\n", - "cycle = plot.Cycle('algae')\n", - "hs = ax.plot(\n", - " data, lw=4, cycle=cycle, colorbar='lr',\n", - " colorbar_kw={'length': '8em', 'label': 'from lines'}\n", - ")\n", - "ax.colorbar(\n", - " hs, loc='t', values=np.arange(0, 10),\n", - " label='from lines', length=0.7, ticks=2\n", - ")\n", - "\n", - "# Colorbars from a mappable\n", - "ax = axs[1]\n", - "m = ax.contourf(\n", - " data.T, extend='both', cmap='algae',\n", - " levels=plot.arange(0, 3, 0.5)\n", - ")\n", - "f.colorbar(\n", - " m, length=1, loc='r', label='inside ticks',\n", - " tickloc='left'\n", - ")\n", - "ax.colorbar(\n", - " m, loc='ul', length=1, tickminor=True,\n", - " label='inset colorbar', alpha=0.5\n", - ")\n", - "axs.format(\n", - " suptitle='Colorbar formatting demo',\n", - " xlabel='xlabel', ylabel='ylabel', abovetop=False\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## New legend features" - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "The `~proplot.subplots.Figure` `~proplot.subplots.Figure.legend` and `~proplot.axes.Axes` `~proplot.axes.Axes.legend` methods are wrapped by `~proplot.wrappers.legend_wrapper`, which adds several new features.\n", - "\n", - "You can draw legends with *centered legend rows*, either by passing ``center=True`` or by passing *list of lists* of plot handles. This is accomplished by stacking multiple single-row, horizontally centered legends, then manually adding an encompassing legend frame. You can also modify legend *text and handle properties* with several keyword args, and switch between row-major and column-major order for legend entries with the `order` keyword arg (default is row-major)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", - "import numpy as np\n", - "plot.rc.cycle = '538'\n", - "labels = ['a', 'bb', 'ccc', 'dddd', 'eeeee']\n", - "f, axs = plot.subplots(ncols=2, span=False, share=1, axwidth=2)\n", - "hs1, hs2 = [], []\n", - "\n", - "# On-the-fly legends\n", - "state = np.random.RandomState(51423)\n", - "for i, label in enumerate(labels):\n", - " data = (state.rand(20) - 0.45).cumsum(axis=0)\n", - " h1 = axs[0].plot(\n", - " data, lw=4, label=label, legend='ul',\n", - " legend_kw={'order': 'F', 'title': 'column major'}\n", - " )\n", - " hs1.extend(h1)\n", - " h2 = axs[1].plot(\n", - " data, lw=4, label=label, legend='r', cycle='Set3',\n", - " legend_kw={'ncols': 1, 'frame': False, 'title': 'no frame'}\n", - " )\n", - " hs2.extend(h2)\n", - " \n", - "# Outer legends\n", - "ax = axs[0]\n", - "ax.legend(\n", - " hs1, loc='b', ncols=3, title='row major', order='C',\n", - " facecolor='gray2'\n", - ")\n", - "ax = axs[1]\n", - "ax.legend(hs2, loc='b', ncols=3, center=True, title='centered rows')\n", - "axs.format(xlabel='xlabel', ylabel='ylabel', suptitle='Legend formatting demo')" - ] - } - ], - "metadata": { - "celltoolbar": "Raw Cell Format", - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.3" - }, - "toc": { - "colors": { - "hover_highlight": "#ece260", - "navigate_num": "#000000", - "navigate_text": "#000000", - "running_highlight": "#FF0000", - "selected_highlight": "#fff968", - "sidebar_border": "#ffffff", - "wrapper_background": "#ffffff" - }, - "moveMenuLeft": false, - "nav_menu": { - "height": "66px", - "width": "252px" - }, - "navigate_menu": true, - "number_sections": true, - "sideBar": true, - "threshold": 4, - "toc_cell": false, - "toc_section_display": "block", - "toc_window_display": true, - "widenNotebook": false - }, - "varInspector": { - "cols": { - "lenName": 16, - "lenType": 16, - "lenVar": 40 - }, - "kernels_config": { - "python": { - "delete_cmd_postfix": "", - "delete_cmd_prefix": "del ", - "library": "var_list.py", - "varRefreshCmd": "print(var_dic_list())" - }, - "r": { - "delete_cmd_postfix": ") ", - "delete_cmd_prefix": "rm(", - "library": "var_list.r", - "varRefreshCmd": "cat(var_dic_list()) " - } - }, - "types_to_exclude": [ - "module", - "function", - "builtin_function_or_method", - "instance", - "_Feature" - ], - "window_display": false - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/docs/colorbars_legends.py b/docs/colorbars_legends.py new file mode 100644 index 000000000..a29be7040 --- /dev/null +++ b/docs/colorbars_legends.py @@ -0,0 +1,428 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.11.4 +# kernelspec: +# display_name: Python 3 +# language: python +# name: python3 +# --- + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_guides: +# +# Colorbars and legends +# ===================== +# +# Proplot includes some useful changes to the matplotlib API that make +# working with colorbars and legends :ref:`easier `. +# Notable features include "inset" colorbars, "outer" legends, +# on-the-fly colorbars and legends, colorbars built from artists, +# and row-major and centered-row legends. + + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_guides_loc: +# +# Outer and inset locations +# ------------------------- +# +# Matplotlib supports drawing "inset" legends and "outer" colorbars using the `loc` +# and `location` keyword arguments. However, "outer" legends are only +# posssible using the somewhat opaque `bbox_to_anchor` keyword (see `here +# `__) +# and "inset" colorbars are not possible without manually creating and positioning +# the associated axes. Proplot tries to improve this behavior: +# +# * `proplot.axes.Axes.legend` can draw both "inset" legends when you request an inset +# location (e.g., ``loc='upper right'`` or the shorthand ``loc='ur'``) and "outer" +# legends along a subplot edge when you request a :ref:`side location ` +# (e.g., ``loc='right'`` or the shorthand ``loc='r'``). If you draw multiple legends +# or colorbars on one side, they are "stacked" on top of each other. Unlike using +# `bbox_to_anchor`, the "outer" legend position is adjusted automatically when the +# :ref:`tight layout algorithm ` is active. +# * Proplot adds the axes command `proplot.axes.Axes.colorbar`, +# analogous to `proplot.axes.Axes.legend` and equivalent to +# calling `proplot.figure.Figure.colorbar` with an `ax` keyword. +# `~proplot.axes.Axes.colorbar` can draw both "outer" colorbars when you request +# a side location (e.g., ``loc='right'`` or the shorthand ``loc='r'``) and "inset" +# colorbars when you request an :ref:`inset location ` +# (e.g., ``loc='upper right'`` or the shorthand ``loc='ur'``). Inset +# colorbars have optional background "frames" that can be configured +# with various `~proplot.axes.Axes.colorbar` keywords. + +# `~proplot.axes.Axes.colorbar` and `~proplot.axes.Axes.legend` also both accept +# `space` and `pad` keywords. `space` controls the absolute separation of the +# "outer" colorbar or legend from the parent subplot edge and `pad` controls the +# :ref:`tight layout ` padding relative to the subplot's tick and axis labels +# (or, for "inset" locations, the padding between the subplot edge and the inset frame). +# The below example shows a variety of arrangements of "outer" and "inset" +# colorbars and legends. +# +# .. important:: +# +# Unlike matplotlib, proplot adds "outer" colorbars and legends by allocating +# new rows and columns in the `~proplot.gridspec.GridSpec` rather than +# "stealing" space from the parent subplot (note that subsequently indexing +# the `~proplot.gridspec.GridSpec` will ignore the slots allocated for +# colorbars and legends). This approach means that "outer" colorbars and +# legends :ref:`do not affect subplot aspect ratios ` +# and :ref:`do not affect subplot spacing `, which lets +# proplot avoid relying on complicated `"constrained layout" algorithms +# `__ +# and tends to improve the appearance of figures with even the most +# complex arrangements of subplots, colorbars, and legends. + +# %% +import proplot as pplt +import numpy as np +state = np.random.RandomState(51423) +fig = pplt.figure(share=False, refwidth=2.3) + +# Colorbars +ax = fig.subplot(121, title='Axes colorbars') +data = state.rand(10, 10) +m = ax.heatmap(data, cmap='dusk') +ax.colorbar(m, loc='r') +ax.colorbar(m, loc='t') # title is automatically adjusted +ax.colorbar(m, loc='ll', label='colorbar label') # inset colorbar demonstration + +# Legends +ax = fig.subplot(122, title='Axes legends', titlepad='0em') +data = (state.rand(10, 5) - 0.5).cumsum(axis=0) +hs = ax.plot(data, lw=3, cycle='ggplot', labels=list('abcde')) +ax.legend(loc='ll', label='legend label') # automatically infer handles and labels +ax.legend(hs, loc='t', ncols=5, frame=False) # automatically infer labels from handles +ax.legend(hs, list('jklmn'), loc='r', ncols=1, frame=False) # manually override labels +fig.format( + abc=True, xlabel='xlabel', ylabel='ylabel', + suptitle='Colorbar and legend location demo' +) + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_guides_plot: +# +# On-the-fly colorbars and legends +# -------------------------------- +# +# In proplot, you can add colorbars and legends on-the-fly by supplying keyword +# arguments to various `~proplot.axes.PlotAxes` commands. To plot data and +# draw a colorbar or legend in one go, pass a location (e.g., ``colorbar='r'`` +# or ``legend='b'``) to the plotting command (e.g., `~proplot.axes.PlotAxes.plot` +# or `~proplot.axes.PlotAxes.contour`). To pass keyword arguments to the colorbar +# and legend commands, use the `legend_kw` and `colorbar_kw` arguments (e.g., +# ``legend_kw={'ncol': 3}``). Note that `~proplot.axes.Axes.colorbar` can also +# build colorbars from lists of arbitrary matplotlib artists, for example the +# lines generated by `~proplot.axes.PlotAxes.plot` or `~proplot.axes.PlotAxes.line` +# (see :ref:`below `). +# +# .. note:: +# +# Specifying the same `colorbar` location with multiple plotting calls will have +# a different effect depending on the plotting command. For :ref:`1D commands +# `, this will add each item to a "queue" used to build colorbars +# from a list of artists. For :ref:`2D commands `, this will "stack" +# colorbars in outer locations, or replace existing colorbars in inset locations. +# By contrast, specifying the same `legend` location will always add items to +# the same legend rather than creating "stacks". + +# %% +import proplot as pplt +labels = list('xyzpq') +state = np.random.RandomState(51423) +fig = pplt.figure(share=0, refwidth=2.3, suptitle='On-the-fly colorbar and legend demo') + +# Legends +data = (state.rand(30, 10) - 0.5).cumsum(axis=0) +ax = fig.subplot(121, title='On-the-fly legend') +ax.plot( # add all at once + data[:, :5], lw=2, cycle='Reds1', cycle_kw={'ls': ('-', '--'), 'left': 0.1}, + labels=labels, legend='b', legend_kw={'title': 'legend title'} +) +for i in range(5): + ax.plot( # add one-by-one + data[:, 5 + i], label=labels[i], linewidth=2, + cycle='Blues1', cycle_kw={'N': 5, 'ls': ('-', '--'), 'left': 0.1}, + colorbar='ul', colorbar_kw={'label': 'colorbar from lines'} + ) + +# Colorbars +ax = fig.subplot(122, title='On-the-fly colorbar') +data = state.rand(8, 8) +ax.contourf( + data, cmap='Reds1', extend='both', colorbar='b', + colorbar_kw={'length': 0.8, 'label': 'colorbar label'}, +) +ax.contour( + data, color='gray7', lw=1.5, + label='contour', legend='ul', legend_kw={'label': 'legend from contours'}, +) + +# %% +import proplot as pplt +import numpy as np +N = 10 +state = np.random.RandomState(51423) +fig, axs = pplt.subplots( + nrows=2, share=False, + refwidth='55mm', panelpad='1em', + suptitle='Stacked colorbars demo' +) + +# Repeat for both axes +args1 = (0, 0.5, 1, 1, 'grays', 0.5) +args2 = (0, 0, 0.5, 0.5, 'reds', 1) +args3 = (0.5, 0, 1, 0.5, 'blues', 2) +for j, ax in enumerate(axs): + ax.format(xlabel='data', xlocator=np.linspace(0, 0.8, 5), title=f'Subplot #{j+1}') + for i, (x0, y0, x1, y1, cmap, scale) in enumerate((args1, args2, args3)): + if j == 1 and i == 0: + continue + data = state.rand(N, N) * scale + x, y = np.linspace(x0, x1, N + 1), np.linspace(y0, y1, N + 1) + m = ax.pcolormesh(x, y, data, cmap=cmap, levels=np.linspace(0, scale, 11)) + ax.colorbar(m, loc='l', label=f'dataset #{i + 1}') + + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_guides_multi: +# +# Figure-wide colorbars and legends +# --------------------------------- +# +# In proplot, colorbars and legends can be added to the edge of figures using the +# figure methods `proplot.figure.Figure.colorbar` and `proplot.figure.Figure.legend`. +# These methods align colorbars and legends between the edges +# of the `~proplot.figure.Figure.gridspec` rather than the figure. +# As with :ref:`axes colorbars and legends `, if you +# draw multiple colorbars or legends on the same side, they are stacked on +# top of each other. To draw a colorbar or legend alongside particular row(s) or +# column(s) of the subplot grid, use the `row`, `rows`, `col`, or `cols` keyword +# arguments. You can pass an integer to draw the colorbar or legend beside a +# single row or column (e.g., ``fig.colorbar(m, row=1)``), or pass a tuple to +# draw the colorbar or legend along a range of rows or columns +# (e.g., ``fig.colorbar(m, rows=(1, 2))``). The space separation between the subplot +# grid edge and the colorbars or legends can be controlled with the `space` keyword, +# and the tight layout padding can be controlled with the `pad` keyword. + +# %% +import proplot as pplt +import numpy as np +state = np.random.RandomState(51423) +fig, axs = pplt.subplots(ncols=3, nrows=3, refwidth=1.4) +for ax in axs: + m = ax.pcolormesh( + state.rand(20, 20), cmap='grays', + levels=np.linspace(0, 1, 11), extend='both' + ) +fig.format( + suptitle='Figure colorbars and legends demo', + abc='a.', abcloc='l', xlabel='xlabel', ylabel='ylabel' +) +fig.colorbar(m, label='column 1', ticks=0.5, loc='b', col=1) +fig.colorbar(m, label='columns 2 and 3', ticks=0.2, loc='b', cols=(2, 3)) +fig.colorbar(m, label='stacked colorbar', ticks=0.1, loc='b', minorticks=0.05) +fig.colorbar(m, label='colorbar with length <1', ticks=0.1, loc='r', length=0.7) + +# %% +import proplot as pplt +import numpy as np +state = np.random.RandomState(51423) +fig, axs = pplt.subplots( + ncols=2, nrows=2, order='F', refwidth=1.7, wspace=2.5, share=False +) + +# Plot data +data = (state.rand(50, 50) - 0.1).cumsum(axis=0) +for ax in axs[:2]: + m = ax.contourf(data, cmap='grays', extend='both') +hs = [] +colors = pplt.get_colors('grays', 5) +for abc, color in zip('ABCDEF', colors): + data = state.rand(10) + for ax in axs[2:]: + h, = ax.plot(data, color=color, lw=3, label=f'line {abc}') + hs.append(h) + +# Add colorbars and legends +fig.colorbar(m, length=0.8, label='colorbar label', loc='b', col=1, locator=5) +fig.colorbar(m, label='colorbar label', loc='l') +fig.legend(hs, ncols=2, center=True, frame=False, loc='b', col=2) +fig.legend(hs, ncols=1, label='legend label', frame=False, loc='r') +fig.format(abc='A', abcloc='ul', suptitle='Figure colorbars and legends demo') +for ax, title in zip(axs, ('2D {} #1', '2D {} #2', 'Line {} #1', 'Line {} #2')): + ax.format(xlabel='xlabel', title=title.format('dataset')) + + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_colorbars: +# +# Added colorbar features +# ----------------------- +# +# The `proplot.axes.Axes.colorbar` and `proplot.figure.Figure.colorbar` commands are +# somehwat more flexible than their matplotlib counterparts. The following core +# features are unique to proplot: + +# * Calling ``colorbar`` with a list of `~matplotlib.artist.Artist`\ s, +# a `~matplotlib.colors.Colormap` name or object, or a list of colors +# will build the required `~matplotlib.cm.ScalarMappable` on-the-fly. Lists +# of `~matplotlib.artist.Artists`\ s are used when you use the `colorbar` +# keyword with :ref:`1D commands ` like `~proplot.axes.PlotAxes.plot`. +# * The associated :ref:`colormap normalizer ` can be specified with the +# `vmin`, `vmax`, `norm`, and `norm_kw` keywords. The `~proplot.colors.DiscreteNorm` +# levels can be specified with `values`, or proplot will infer them from the +# `~matplotlib.artist.Artist` labels (non-numeric labels will be applied to +# the colorbar as tick labels). This can be useful for labeling discrete plot +# elements that bear some numeric relationship to each other. +# +# Proplot also includes improvements for adding ticks and tick labels to colorbars. +# Similar to `proplot.axes.CartesianAxes.format`, you can flexibly specify +# major tick locations, minor tick locations, and major tick labels using the +# `locator`, `minorlocator`, `formatter`, `ticks`, `minorticks`, and `ticklabels` +# keywords. These arguments are passed through the `~proplot.constructor.Locator` and +# `~proplot.constructor.Formatter` :ref:`constructor functions `. +# Unlike matplotlib, the default ticks for :ref:`discrete colormaps ` +# are restricted based on the axis length using `~proplot.ticker.DiscreteLocator`. +# You can easily toggle minor ticks using ``tickminor=True``. +# +# Similar to :ref:`axes panels `, the geometry of proplot colorbars is +# specified with :ref:`physical units ` (this helps avoid the common issue +# where colorbars appear "too skinny" or "too fat" and preserves their appearance +# when the figure size changes). You can specify the colorbar width locally using the +# `width` keyword or globally using the :rcraw:`colorbar.width` setting (for outer +# colorbars) and the :rcraw:`colorbar.insetwidth` setting (for inset colorbars). +# Similarly, you can specify the colorbar length locally with the `length` keyword or +# globally using the :rcraw:`colorbar.insetlength` setting. The outer colorbar length +# is always relative to the subplot grid and always has a default of ``1``. You +# can also specify the size of the colorbar "extensions" in physical units rather +# than relative units using the `extendsize` keyword rather than matplotlib's +# `extendfrac`. The default `extendsize` values are :rcraw:`colorbar.extend` (for +# outer colorbars) and :rcraw:`colorbar.insetextend` (for inset colorbars). +# See `~proplot.axes.Axes.colorbar` for details. + +# %% +import proplot as pplt +import numpy as np +fig = pplt.figure(share=False, refwidth=2) + +# Colorbars from lines +ax = fig.subplot(121) +state = np.random.RandomState(51423) +data = 1 + (state.rand(12, 10) - 0.45).cumsum(axis=0) +cycle = pplt.Cycle('algae') +hs = ax.line( + data, lw=4, cycle=cycle, colorbar='lr', + colorbar_kw={'length': '8em', 'label': 'line colorbar'} +) +ax.colorbar( + hs, loc='t', values=np.arange(0, 10), + label='line colorbar', ticks=2 +) + +# Colorbars from a mappable +ax = fig.subplot(122) +m = ax.contourf( + data.T, extend='both', cmap='algae', + levels=pplt.arange(0, 3, 0.5) +) +fig.colorbar( + m, loc='r', length=1, # length is relative + label='interior ticks', tickloc='left' +) +ax.colorbar( + m, loc='ul', length=6, # length is em widths + label='inset colorbar', tickminor=True, alpha=0.5, +) +fig.format( + suptitle='Colorbar formatting demo', + xlabel='xlabel', ylabel='ylabel', titleabove=False +) + + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_legends: +# +# Added legend features +# --------------------- +# +# The `proplot.axes.Axes.legend` and `proplot.figure.Figure.legend` commands are +# somewhat more flexible than their matplotlib counterparts. The following core +# features are the same as matplotlib: + +# * Calling ``legend`` without positional arguments will +# automatically fill the legend with the labeled artist in the +# the parent axes (when using `proplot.axes.Axes.legend`) or +# or the parent figure (when using `proplot.figure.Figure.legend`). +# * Legend labels can be assigned early by calling plotting comamnds with +# the `label` keyword (e.g., ``ax.plot(..., label='label')``) or on-the-fly by +# passing two positional arguments to ``legend`` (where the first argument is the +# "handle" list and the second is the "label" list). + +# The following core features are unique to proplot: + +# * Legend labels can be assigned for each column of a +# :ref:`2D array passed to a 1D plotting command ` +# using the `labels` keyword (e.g., ``labels=['label1', 'label2', ...]``). +# * Legend labels can be assigned to `~matplotlib.contour.ContourSet`\ s by passing +# the `label` keyword to a contouring command (e.g., `~proplot.axes.PlotAxes.contour` +# or `~proplot.axes.PlotAxes.contourf`). +# * A "handle" list can be passed to ``legend`` as the sole +# positional argument and the labels will be automatically inferred +# using `~matplotlib.artist.Artist.get_label`. Valid "handles" include +# `~matplotlib.lines.Line2D`\ s returned by `~proplot.axes.PlotAxes.plot`, +# `~matplotlib.container.BarContainer`\ s returned by `~proplot.axes.PlotAxes.bar`, +# and `~matplotlib.collections.PolyCollection`\ s +# returned by `~proplot.axes.PlotAxes.fill_between`. +# * A composite handle can be created by grouping the "handle" +# list objects into tuples (see this `matplotlib guide +# `__ +# for more on tuple groups). The associated label will be automatically +# inferred from the objects in the group. If multiple distinct +# labels are found then the group is automatically expanded. +# +# `proplot.axes.Axes.legend` and `proplot.figure.Figure.legend` include a few other +# useful features. To draw legends with centered rows, pass ``center=True`` or +# a list of lists of "handles" to ``legend`` (this stacks several single-row, +# horizontally centered legends and adds an encompassing frame behind them). +# To switch between row-major and column-major order for legend entries, +# use the `order` keyword (the default ``order='C'`` is row-major, +# unlike matplotlib's column-major ``order='F'``). To alphabetize the legend +# entries, pass ``alphabetize=True`` to ``legend``. To modify the legend handles +# (e.g., `~proplot.axes.PlotAxes.plot` or `~proplot.axes.PlotAxes.scatter` handles) +# pass the relevant properties like `color`, `linewidth`, or `markersize` to ``legend`` +# (or use the `handle_kw` keyword). See `proplot.axes.Axes.legend` for details. + +# %% +import proplot as pplt +import numpy as np +pplt.rc.cycle = '538' +fig, axs = pplt.subplots(ncols=2, span=False, share='labels', refwidth=2.3) +labels = ['a', 'bb', 'ccc', 'dddd', 'eeeee'] +hs1, hs2 = [], [] + +# On-the-fly legends +state = np.random.RandomState(51423) +for i, label in enumerate(labels): + data = (state.rand(20) - 0.45).cumsum(axis=0) + h1 = axs[0].plot( + data, lw=4, label=label, legend='ul', + legend_kw={'order': 'F', 'title': 'column major'} + ) + hs1.extend(h1) + h2 = axs[1].plot( + data, lw=4, cycle='Set3', label=label, legend='r', + legend_kw={'lw': 8, 'ncols': 1, 'frame': False, 'title': 'modified\n handles'} + ) + hs2.extend(h2) + +# Outer legends +ax = axs[0] +ax.legend(hs1, loc='b', ncols=3, title='row major', order='C', facecolor='gray2') +ax = axs[1] +ax.legend(hs2, loc='b', ncols=3, center=True, title='centered rows') +axs.format(xlabel='xlabel', ylabel='ylabel', suptitle='Legend formatting demo') diff --git a/docs/colormaps.ipynb b/docs/colormaps.ipynb deleted file mode 100644 index 5dae96cc7..000000000 --- a/docs/colormaps.ipynb +++ /dev/null @@ -1,442 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Colormaps" - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "ProPlot defines **colormaps** as color palettes that sample some *continuous function* between two end colors. Colormaps are generally used to encode data values on a pseudo-third dimension. They are are implemented with the `~proplot.styletools.LinearSegmentedColormap` and `~proplot.styletools.PerceptuallyUniformColormap` classes, which are subclassed from `~matplotlib.colors.LinearSegmentedColormap` in matplotlib (see :ref:`Making new colormaps`).\n", - "\n", - "ProPlot adds several features to help you use colormaps effectively in your figures. This section documents the new registered colormaps, explains how to make and modify colormaps, and shows how to apply them to your plots." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Included colormaps" - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "On import, ProPlot registers a few sample `~proplot.styletools.PerceptuallyUniformColormap` colormaps (see :ref:`Perceptually uniform colormaps`) plus a ton of other colormaps from other online data viz projects. Use `~proplot.styletools.show_cmaps` to generate a table of registered maps. The figure is broken down into the following sections:\n", - "\n", - "* \"User\" colormaps, i.e. colormaps saved to your ``~/.proplot/cmaps`` folder. A great way to save colormaps to this folder is using the `~proplot.styletools.Colormap` function. See :ref:`Making new colormaps` for details.\n", - "* Matplotlib and seaborn original colormaps.\n", - "* ProPlot original :ref:`Perceptually uniform colormaps`.\n", - "* `cmOcean `__ colormaps designed for oceanographic data but useful for everyone.\n", - "* Fabio Crameri's `\"scientific colour maps\" `__.\n", - "* Cynthia Brewer's `ColorBrewer `__ colormaps, included with matplotlib by default.\n", - "* Colormaps from the `SciVisColor `__ online interactive tool. There are so many of these colormaps because they are intended to be *merged* with one another.\n", - "\n", - "ProPlot removes some default matplotlib colormaps with erratic color transitions. Note that colormap and color cycle identification is now flexible: Colormap names are *case-insensitive* (e.g. ``'Viridis'``, ``'viridis'``, and ``'ViRiDiS'`` are equivalent), diverging colormap names can be specified in their \"reversed\" form (e.g. ``'BuRd'`` is equivalent to ``'RdBu_r'``), and appending ``'_r'`` or ``'_shifted'`` to *any* colormap name will return the result of ``cmap.reversed()`` or ``cmap.shifted(180)``. See `~proplot.styletools.CmapDict` for more info." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", - "f = plot.show_cmaps()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Perceptually uniform colormaps" - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "ProPlot's custom colormaps are instances of the `~proplot.styletools.PerceptuallyUniformColormap` class. These colormaps generate colors by interpolating between coordinates in any of the following three colorspaces:\n", - "\n", - "* **HCL** (a.k.a. `CIELUV LCh `__): A purely perceptually uniform colorspace, where colors are broken down into “hue” (color, range 0-360), “chroma” (saturation, range 0-100), and “luminance” (brightness, range 0-100). This space is difficult to work with due to *impossible colors* -- colors that, when translated back from HCL to RGB, result in RGB channels greater than ``1``.\n", - "* **HPL** (a.k.a. `HPLuv `__): Hue and luminance are identical to HCL, but 100 saturation is set to the minimum maximum saturation *across all hues* for a given luminance. HPL restricts you to soft pastel colors, but is closer to HCL in terms of uniformity.\n", - "* **HSL** (a.k.a. `HSLuv `__): Hue and luminance are identical to HCL, but 100 saturation is set to the maximum saturation *for a given hue and luminance*. HSL gives you access to the entire RGB colorspace, but often results in sharp jumps in chroma.\n", - "\n", - "The colorspace used by each `~proplot.styletools.PerceptuallyUniformColormap` is set with the `space` keyword arg. To plot arbitrary cross-sections of these colorspaces, use `~proplot.styletools.show_colorspaces` (the black regions represent impossible colors). To see how colormaps vary with respect to each channel, use `~proplot.styletools.show_channels`. Some examples are shown below.\n", - "\n", - "In theory, \"uniform\" colormaps should have *straight* lines in hue, chroma, and luminance (bottom figure; top row) -- but in practice, this is difficult to accomplish due to impossible colors. Matplotlib and seaborn's ``'magma'`` and ``'Rocket'`` colormaps are fairly linear with respect to hue and luminance, but not chroma. ProPlot's ``'Fire'`` is linear in hue, luminance, and *HSL saturation* (bottom left), while ``'Dusk'`` is linear in hue, luminance, and *HPL saturation* (bottom left)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", - "f = plot.show_colorspaces(axwidth=1.6, luminance=50)\n", - "f = plot.show_colorspaces(axwidth=1.6, saturation=60)\n", - "f = plot.show_colorspaces(axwidth=1.6, hue=0)\n", - "f = plot.show_channels(\n", - " 'magma', 'rocket', 'fire', 'dusk',\n", - " axwidth=1.4, minhue=-180, maxsat=300, rgb=False\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Making new colormaps" - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "ProPlot doesn't just include new colormaps -- it provides tools for merging colormaps, modifying colormaps, making :ref:`Perceptually uniform colormaps` from scratch, and saving the results for future use. For your convenience, most of these features can be accessed via the `~proplot.styletools.Colormap` constructor function. Note that every plotting command that accepts a `cmap` keyword passes it through this function (see `~proplot.wrappers.cmap_changer`).\n", - "\n", - "To make `~proplot.styletools.PerceptuallyUniformColormap`\\ s from scratch, you have the following three options:\n", - "\n", - "* Pass a color name, hex string, or RGB tuple to `~proplot.styletools.Colormap`. This builds a *monochromatic* (single hue) colormap by calling the `~proplot.styletools.PerceptuallyUniformColormap.from_color` static method. The colormap colors will vary from the specified color to pure white or some shade *near* white (see the `fade` keyword arg).\n", - "* Pass a *list of colors* to `~proplot.styletools.Colormap`. This calls the `~proplot.styletools.PerceptuallyUniformColormap.from_list` static method, which linearly interpolates between each color in hue, saturation, and luminance.\n", - "* Pass a *dictionary* to `~proplot.styletools.Colormap`. This calls the `~proplot.styletools.PerceptuallyUniformColormap.from_hsl` static method, which draws lines between channel values specified by the keyword arguments `hue`, `saturation`, and `luminance`. The values can be numbers, color strings, or lists thereof. Numbers indicate the channel value. For color strings, the channel value is *inferred* from the specified color. You can end any color string with ``'+N'`` or ``'-N'`` to *offset* the channel value by the number ``N``.\n", - "\n", - "Below, we use all of these methods to make brand new `~proplot.styletools.PerceptuallyUniformColormap`\\ s in the ``'hsl'`` and ``'hpl'`` colorspaces." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", - "import numpy as np\n", - "state = np.random.RandomState(51423)\n", - "f, axs = plot.subplots(\n", - " [[0, 1, 1, 2, 2, 0], [3, 3, 4, 4, 5, 5]],\n", - " ncols=2, axwidth=2, aspect=1\n", - ")\n", - "\n", - "# Monochromatic colormaps\n", - "axs.format(\n", - " xlabel='x axis', ylabel='y axis', span=False,\n", - " suptitle='Building your own PerceptuallyUniformColormaps'\n", - ")\n", - "data = state.rand(30, 30).cumsum(axis=1)\n", - "axs[0].format(title='From single color')\n", - "m = axs[0].contourf(data, cmap='ocean blue', cmap_kw={'name': 'water'})\n", - "cmap1 = m.cmap\n", - "axs[1].format(title='From three colors')\n", - "cmap2 = plot.Colormap(\n", - " 'brick red_r', 'denim_r', 'warm gray_r',\n", - " fade=90, name='tricolor'\n", - ")\n", - "axs[1].contourf(data, cmap=cmap2, levels=12)\n", - "\n", - "# Colormaps from channel value dictionaries\n", - "axs[2:4].format(title='From channel values')\n", - "cmap3 = plot.Colormap({\n", - " 'hue': ['red-90', 'red+90'],\n", - " 'saturation': [50, 70, 30],\n", - " 'luminance': [20, 100]\n", - "}, name='Matter', space='hcl')\n", - "axs[2].pcolormesh(data, cmap=cmap3)\n", - "cmap4 = plot.Colormap({\n", - " 'hue': ['red', 'red-720'],\n", - " 'saturation': [80, 20],\n", - " 'luminance': [20, 100]\n", - "}, name='cubehelix', space='hpl')\n", - "axs[3].pcolormesh(data, cmap=cmap4)\n", - "\n", - "# Colormap from lists\n", - "m = axs[4].pcolormesh(\n", - " data, cmap=('maroon', 'desert sand'),\n", - " cmap_kw={'name': 'reddish'}\n", - ")\n", - "cmap5 = m.cmap\n", - "axs[4].format(title='From list of colors')\n", - "\n", - "# Test the channels\n", - "f = plot.show_channels(cmap1, cmap2, axwidth=1.4, rgb=False)\n", - "f = plot.show_channels(\n", - " cmap3, cmap4, cmap5, minhue=-180,\n", - " axwidth=1.4, rgb=False\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Merging colormaps" - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "To *merge* colormaps, simply pass multiple positional arguments to the `~proplot.styletools.Colormap` constructor. Each positional argument can be a colormap name, a colormap instance, or one of the argument types described in :ref:`Making new colormaps`. This lets you create new diverging colormaps and segmented `SciVisColor `__-style colormaps right inside ProPlot. Segmented colormaps are often desirable for complex datasets with complex statistical distributions.\n", - "\n", - "In the below example, we create a new divering colormap and reconstruct the colormap from `this SciVisColor example `__. We also *save* the results for future use by passing ``save=True`` to `~proplot.styletools.Colormap`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", - "import numpy as np\n", - "f, axs = plot.subplots([[0, 1, 1, 0], [2, 2, 3, 3]], axwidth=2, span=False)\n", - "state = np.random.RandomState(51423)\n", - "data = state.rand(30, 30).cumsum(axis=1)\n", - "\n", - "# Diverging colormap example\n", - "title1 = 'Custom diverging map'\n", - "cmap1 = plot.Colormap('Blue4_r', 'RedPurple3', name='Diverging', save=True)\n", - "\n", - "# SciVisColor examples\n", - "title2 = 'Custom complex map'\n", - "cmap2 = plot.Colormap(\n", - " 'Green1_r', 'Orange5', 'Blue1_r', 'Blue6',\n", - " name='Complex', save=True\n", - ")\n", - "title3 = 'SciVisColor example reproduction'\n", - "cmap3 = plot.Colormap(\n", - " 'Green1_r', 'Orange5', 'Blue1_r', 'Blue6',\n", - " ratios=(1, 3, 5, 10), name='SciVisColor', save=True\n", - ")\n", - "\n", - "# Plot examples\n", - "for ax, cmap, title in zip(axs, (cmap1, cmap2, cmap3), (title1, title2, title3)):\n", - " func = (ax.pcolormesh if cmap is cmap1 else ax.contourf)\n", - " m = func(data, cmap=cmap, levels=256)\n", - " ax.colorbar(m, loc='b', locator='null', label=cmap.name)\n", - " ax.format(title=title)\n", - "axs.format(\n", - " xlabel='xlabel', ylabel='ylabel',\n", - " suptitle='Merging existing colormaps'\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Modifying colormaps" - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "ProPlot allows you to create modified versions of *existing* colormaps using the `~proplot.styletools.Colormap` constructor and the new `~proplot.styletools.LinearSegmentedColormap` and `~proplot.styletools.ListedColormap` classes, which are used to replace the native matplotlib colormap classes. They can be modified in the following ways:\n", - "\n", - "* To remove colors from the left or right ends of a colormap, pass `left` or `right` to `~proplot.styletools.Colormap`. This calls the `~proplot.styletools.LinearSegmentedColormap.truncated` method, and can be useful when you want to use colormaps as :ref:`Color cycles` and need to remove the \"white\" part so that your lines stand out against the background.\n", - "* To remove central colors from a diverging colormap, pass `cut` to `~proplot.styletools.Colormap`. This calls the `~proplot.styletools.LinearSegmentedColormap.punched` method, and can be used to create a sharper cutoff between negative and positive values. This should generally be used *without* a central level.\n", - "* To rotate a cyclic colormap, pass `shift` to `~proplot.styletools.Colormap`. This calls the `~proplot.styletools.LinearSegmentedColormap.shifted` method. ProPlot ensures the colors at the ends of \"shifted\" colormaps are *distinct* so that levels never blur together.\n", - "* To change the transparency of an entire colormap, pass `alpha` to `~proplot.styletools.Colormap`. This calls the `~proplot.styletools.LinearSegmentedColormap.set_alpha` method, and can be useful when *layering* filled contour or mesh elements.\n", - "* To change the \"gamma\" of a `~proplot.styletools.PerceptuallyUniformColormap`, pass `gamma` to `~proplot.styletools.Colormap`. This calls the `~proplot.styletools.PerceptuallyUniformColormap.set_gamma` method, and controls how the luminance and saturation channels vary between colormap segments. ``gamma > 1`` emphasizes high luminance, low saturation colors, while ``gamma < 1`` emphasizes low luminance, high saturation colors." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", - "import numpy as np\n", - "f, axs = plot.subplots(\n", - " [[1, 1, 2, 2, 3, 3], [0, 4, 4, 5, 5, 0], [0, 6, 6, 7, 7, 0]],\n", - " axwidth=1.7, span=False\n", - ")\n", - "state = np.random.RandomState(51423)\n", - "data = state.rand(40, 40).cumsum(axis=0) - 12\n", - "\n", - "# Cutting left and right\n", - "for ax, coord in zip(axs[:3], (None, 0.3, 0.7)):\n", - " cmap = 'grays'\n", - " if coord is None:\n", - " title, cmap_kw = 'Original', {}\n", - " elif coord < 0.5:\n", - " title, cmap_kw = f'left={coord}', {'left': coord}\n", - " else:\n", - " title, cmap_kw = f'right={coord}', {'right': coord}\n", - " ax.pcolormesh(\n", - " data, cmap=cmap, cmap_kw=cmap_kw,\n", - " colorbar='b', colorbar_kw={'locator': 'null'}\n", - " )\n", - " ax.format(xlabel='x axis', ylabel='y axis', title=title)\n", - " \n", - "# Cutting central colors\n", - "levels = plot.arange(-10, 10, 2)\n", - "for i, (ax, cut) in enumerate(zip(axs[3:], (None, None, 0.1, 0.2))):\n", - " if i == 0:\n", - " title = 'With central level'\n", - " levels = plot.edges(plot.arange(-10, 10, 2))\n", - " else:\n", - " title = 'Without central level'\n", - " levels = plot.arange(-10, 10, 2)\n", - " if cut is not None:\n", - " title = f'cut = {cut}'\n", - " m = ax.contourf(\n", - " data, cmap='Div', cmap_kw={'cut': cut},\n", - " extend='both', levels=levels\n", - " )\n", - " ax.format(\n", - " xlabel='x axis', ylabel='y axis', title=title,\n", - " suptitle='Truncating sequential and diverging colormaps'\n", - " )\n", - " ax.colorbar(m, loc='b', locator='null')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", - "import numpy as np\n", - "state = np.random.RandomState(51423)\n", - "\n", - "# Rotating cyclic colormaps\n", - "f, axs = plot.subplots(ncols=3, axwidth=1.7)\n", - "data = (state.rand(50, 50) - 0.48).cumsum(axis=1).cumsum(axis=0) - 50\n", - "for ax, shift in zip(axs, (0, 90, 180)):\n", - " m = ax.contourf(data, cmap='twilight', cmap_kw={'shift': shift}, levels=12)\n", - " ax.format(\n", - " xlabel='x axis', ylabel='y axis', title=f'shift = {shift}',\n", - " suptitle='Rotating cyclic colormaps'\n", - " )\n", - " ax.colorbar(m, loc='b', locator='null')\n", - " \n", - "# Changing the colormap gamma\n", - "f, axs = plot.subplots(ncols=3, axwidth=1.7, aspect=1)\n", - "data = state.rand(10, 10).cumsum(axis=1)\n", - "for ax, gamma in zip(axs, (0.7, 1.0, 1.4)):\n", - " cmap = plot.Colormap('boreal', name=str(gamma), gamma=gamma)\n", - " m = ax.pcolormesh(data, cmap=cmap, levels=10, extend='both')\n", - " ax.colorbar(m, loc='b', locator='none')\n", - " ax.format(\n", - " title=f'gamma = {gamma}', xlabel='x axis', ylabel='y axis',\n", - " suptitle='Changing the colormap gamma'\n", - " )" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Downloading colormaps" - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "There are plenty of online interactive tools for generating perceptually uniform colormaps, including `Chroma.js `__, `HCLWizard `__, `HCL picker `__, the `CCC-tool `__, and `SciVisColor `__.\n", - "\n", - "To add colormaps downloaded from any of these sources, save the colormap data to a file in your ``~/.proplot/cmaps`` folder and call `~proplot.styletools.register_cmaps` (or restart your python session), or use `~proplot.styletools.LinearSegmentedColormap.from_file`. The file name is used as the registered colormap name. See `~proplot.styletools.LinearSegmentedColormap.from_file` for a table of valid file extensions." - ] - } - ], - "metadata": { - "celltoolbar": "Raw Cell Format", - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.3" - }, - "toc": { - "colors": { - "hover_highlight": "#ece260", - "navigate_num": "#000000", - "navigate_text": "#000000", - "running_highlight": "#FF0000", - "selected_highlight": "#fff968", - "sidebar_border": "#ffffff", - "wrapper_background": "#ffffff" - }, - "moveMenuLeft": false, - "nav_menu": { - "height": "12px", - "width": "250px" - }, - "navigate_menu": true, - "number_sections": true, - "sideBar": true, - "threshold": 4, - "toc_cell": false, - "toc_section_display": "block", - "toc_window_display": true, - "widenNotebook": false - }, - "varInspector": { - "cols": { - "lenName": 16, - "lenType": 16, - "lenVar": 40 - }, - "kernels_config": { - "python": { - "delete_cmd_postfix": "", - "delete_cmd_prefix": "del ", - "library": "var_list.py", - "varRefreshCmd": "print(var_dic_list())" - }, - "r": { - "delete_cmd_postfix": ") ", - "delete_cmd_prefix": "rm(", - "library": "var_list.r", - "varRefreshCmd": "cat(var_dic_list()) " - } - }, - "types_to_exclude": [ - "module", - "function", - "builtin_function_or_method", - "instance", - "_Feature" - ], - "window_display": false - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/docs/colormaps.py b/docs/colormaps.py new file mode 100644 index 000000000..5ae2ecb03 --- /dev/null +++ b/docs/colormaps.py @@ -0,0 +1,482 @@ +# -*- coding: utf-8 -*- +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.11.4 +# kernelspec: +# display_name: Python 3 +# language: python +# name: python3 +# --- + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _cmocean: https://matplotlib.org/cmocean/ +# +# .. _fabio: http://www.fabiocrameri.ch/colourmaps.php +# +# .. _brewer: http://colorbrewer2.org/ +# +# .. _sciviscolor: https://sciviscolor.org/home/colormoves/ +# +# .. _matplotlib: https://matplotlib.org/stable/tutorials/colors/colormaps.html +# +# .. _seaborn: https://seaborn.pydata.org/tutorial/color_palettes.html +# +# .. _ug_cmaps: +# +# Colormaps +# ========= +# +# Proplot defines **continuous colormaps** as color palettes that sample some +# *continuous function* between two end colors. They are generally used +# to encode data values on a pseudo-third dimension. They are implemented +# in proplot with the `~proplot.colors.ContinuousColormap` and +# `~proplot.colors.PerceptualColormap` classes, which are +# :ref:`subclassed from ` +# `matplotlib.colors.LinearSegmentedColormap`. +# +# Proplot :ref:`adds several features ` to help you use +# colormaps effectively in your figures. This section documents the new registered +# colormaps, explains how to make and modify colormaps, and shows how to apply them +# to your plots. + + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_cmaps_included: +# +# Included colormaps +# ------------------ +# +# On import, proplot registers a few sample +# :ref:`perceptually uniform colormaps `, plus several +# colormaps from other online data viz projects. Use `~proplot.demos.show_cmaps` +# to generate a table of registered colormaps. The figure is broken down into +# the following sections: +# +# * "User" colormaps created with `~proplot.constructor.Colormap` +# or loaded from `~proplot.config.Configurator.user_folder`. +# * `Matplotlib `_ and `seaborn `_ original colormaps. +# * Proplot original :ref:`perceptually uniform colormaps `. +# * The `cmOcean `_ colormaps, designed for +# oceanographic data but useful for everyone. +# * Fabio Crameri's `"scientific colour maps" `_. +# * Cynthia Brewer's `ColorBrewer `_ colormaps, +# included with matplotlib by default. +# * Colormaps from the `SciVisColor `_ project. There are so many +# of these because they are intended to be merged into more complex colormaps. +# +# Matplotlib colormaps with erratic color transitions like ``'jet'`` are still +# registered, but they are hidden from this table by default, and their usage is +# discouraged. If you need a list of colors associated with a registered or +# on-the-fly colormap, simply use `~proplot.utils.get_colors`. +# +# .. note:: +# +# Colormap and :ref:`color cycle ` identification is more flexible in +# proplot. The names are are case-insensitive (e.g., ``'Viridis'``, ``'viridis'``, +# and ``'ViRiDiS'`` are equivalent), diverging colormap names can be specified in +# their "reversed" form (e.g., ``'BuRd'`` is equivalent to ``'RdBu_r'``), and +# appending ``'_r'`` or ``'_s'`` to *any* colormap name will return a +# `~proplot.colors.ContinuousColormap.reversed` or +# `~proplot.colors.ContinuousColormap.shifted` version of the colormap +# or color cycle. See `~proplot.colors.ColormapDatabase` for more info. + +# %% +import proplot as pplt +fig, axs = pplt.show_cmaps(rasterized=True) + + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_perceptual: +# +# Perceptually uniform colormaps +# ------------------------------ +# +# Proplot's custom colormaps are instances of the +# `~proplot.colors.PerceptualColormap` class. These colormaps +# generate colors by interpolating between coordinates in any +# of the following three hue-saturation-luminance colorspaces: +# +# * **HCL** (a.k.a. `CIE LChuv `__): +# A purely perceptually uniform colorspace, where colors are broken down +# into “hue” (color, range 0-360), “chroma” (saturation, range 0-100), and +# “luminance” (brightness, range 0-100). This colorspace is difficult to work +# with due to *impossible colors* -- colors that, when translated back from +# HCL to RGB, result in RGB channels greater than ``1``. +# * **HPL** (a.k.a. `HPLuv `__): Hue and +# luminance are identical to HCL, but 100 saturation is set to the minimum +# maximum saturation *across all hues* for a given luminance. HPL restricts +# you to soft pastel colors, but is closer to HCL in terms of uniformity. +# * **HSL** (a.k.a. `HSLuv `__): Hue and +# luminance are identical to HCL, but 100 saturation is set to the maximum +# saturation *for a given hue and luminance*. HSL gives you access to the +# entire RGB colorspace, but often results in sharp jumps in chroma. +# +# The colorspace used by a `~proplot.colors.PerceptualColormap` +# is set with the `space` keyword arg. To plot arbitrary cross-sections of +# these colorspaces, use `~proplot.demos.show_colorspaces` (the black +# regions represent impossible colors). To see how colormaps vary with +# respect to each channel, use `~proplot.demos.show_channels`. Some examples +# are shown below. +# +# In theory, "uniform" colormaps should have *straight* lines in hue, chroma, +# and luminance (second figure, top row). In practice, this is +# difficult to accomplish due to impossible colors. Matplotlib's and seaborn's +# ``'magma'`` and ``'Rocket'`` colormaps are fairly linear with respect to +# hue and luminance, but not chroma. Proplot's ``'Fire'`` is linear in hue, +# luminance, and *HSL saturation* (bottom left), while ``'Dusk'`` is linear +# in hue, luminance, and *HPL saturation* (bottom right). + +# %% +# Colorspace demo +import proplot as pplt +fig, axs = pplt.show_colorspaces(refwidth=1.6, luminance=50) +fig, axs = pplt.show_colorspaces(refwidth=1.6, saturation=60) +fig, axs = pplt.show_colorspaces(refwidth=1.6, hue=0) + +# %% +# Compare colormaps +import proplot as pplt +for cmaps in (('magma', 'rocket'), ('fire', 'dusk')): + fig, axs = pplt.show_channels( + *cmaps, refwidth=1.5, minhue=-180, maxsat=400, rgb=False + ) + + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_cmaps_new: +# +# Making colormaps +# ---------------- +# +# Proplot includes tools for merging colormaps, modifying existing colormaps, +# making new :ref:`perceptually uniform colormaps `, and +# saving colormaps for future use. Most of these features can be accessed via the +# `~proplot.constructor.Colormap` :ref:`constructor function `. +# Note that every `~proplot.axes.PlotAxes` command that accepts a `cmap` keyword passes +# it through this function (see the :ref:`2D plotting section `). +# +# To make `~proplot.colors.PerceptualColormap`\ s from +# scratch, you have the following three options: +# +# * Pass a color name, HEX string, or RGB tuple to `~proplot.constructor.Colormap`. +# This builds a monochromatic (single hue) colormap by calling +# `~proplot.colors.PerceptualColormap.from_color`. The colormap colors will +# progress from the specified color to a color with the same hue but changed +# saturation or luminance. These can be set with the `saturation` and `luminance` +# keywords (or their shorthands `s` and `l`). By default, the colormap will +# progress to pure white. +# * Pass a list of color names, HEX strings, or RGB +# tuples to `~proplot.constructor.Colormap`. This calls +# `~proplot.colors.PerceptualColormap.from_list`, which linearly interpolates +# between the hues, saturations, and luminances of the input colors. To facillitate +# the construction of diverging colormaps, the hue channel values for nuetral +# colors (i.e., white, black, and gray) are adjusted to the hues of the preceding +# and subsequent colors in the list, with sharp hue cutoffs at the neutral colors. +# This permits generating diverging colormaps with e.g. ``['blue', 'white', 'red']``. +# * Pass the keywords `hue`, `saturation`, or `luminance` (or their shorthands `h`, +# `s`, and `l`) to `~proplot.constructor.Colormap` without any positional arguments +# (or pass a dictionary containing these keys as a positional argument). +# This calls `~proplot.colors.PerceptualColormap.from_hsl`, which +# linearly interpolates between the specified channel values. Channel values can be +# specified with numbers between ``0`` and ``100``, color strings, or lists thereof. +# For color strings, the value is *inferred* from the specified color. You can +# end any color string with ``'+N'`` or ``'-N'`` to *offset* the channel +# value by the number ``N`` (e.g., ``hue='red+50'``). +# +# To change the :ref:`colorspace ` used to construct the colormap, +# use the `space` keyword. The default colorspace is ``'hsl'``. In the below example, +# we use all of these methods to make `~proplot.colors.PerceptualColormap`\ s +# in the ``'hsl'`` and ``'hpl'`` colorspaces. + +# %% +# Sample data +import proplot as pplt +import numpy as np +state = np.random.RandomState(51423) +data = state.rand(30, 30).cumsum(axis=1) + +# %% +# Colormap from a color +# The trailing '_r' makes the colormap go dark-to-light instead of light-to-dark +fig = pplt.figure(refwidth=2, span=False) +ax = fig.subplot(121, title='From single named color') +cmap1 = pplt.Colormap('prussian blue_r', l=100, name='Pacific', space='hpl') +m = ax.contourf(data, cmap=cmap1) +ax.colorbar(m, loc='b', ticks='none', label=cmap1.name) + +# Colormap from lists +ax = fig.subplot(122, title='From list of colors') +cmap2 = pplt.Colormap(('maroon', 'light tan'), name='Heatwave') +m = ax.contourf(data, cmap=cmap2) +ax.colorbar(m, loc='b', ticks='none', label=cmap2.name) +fig.format( + xticklabels='none', + yticklabels='none', + suptitle='Making PerceptualColormaps' +) + +# Display the channels +fig, axs = pplt.show_channels(cmap1, cmap2, refwidth=1.5, rgb=False) + +# %% +# Sequential colormap from channel values +cmap3 = pplt.Colormap( + h=('red', 'red-720'), s=(80, 20), l=(20, 100), space='hpl', name='CubeHelix' +) +fig = pplt.figure(refwidth=2, span=False) +ax = fig.subplot(121, title='Sequential from channel values') +m = ax.contourf(data, cmap=cmap3) +ax.colorbar(m, loc='b', ticks='none', label=cmap3.name) + +# Cyclic colormap from channel values +ax = fig.subplot(122, title='Cyclic from channel values') +cmap4 = pplt.Colormap( + h=(0, 360), c=50, l=70, space='hcl', cyclic=True, name='Spectrum' +) +m = ax.contourf(data, cmap=cmap4) +ax.colorbar(m, loc='b', ticks='none', label=cmap4.name) +fig.format( + xticklabels='none', + yticklabels='none', + suptitle='Making PerceptualColormaps' +) + +# Display the channels +fig, axs = pplt.show_channels(cmap3, cmap4, refwidth=1.5, rgb=False) + + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_cmaps_merge: +# +# Merging colormaps +# ----------------- +# +# To *merge* colormaps, you can pass multiple positional arguments to the +# `~proplot.constructor.Colormap` constructor function. This calls the +# `~proplot.colors.ContinuousColormap.append` method. Each positional +# argument can be a colormap name, a colormap instance, or a +# :ref:`special argument ` that generates a new colormap +# on-the-fly. This lets you create new diverging colormaps and segmented +# `SciVisColor `__ style colormaps +# right inside proplot. Segmented colormaps are often desirable for complex +# datasets with complex statistical distributions. +# +# In the below example, we create a new divering colormap and +# reconstruct the colormap from `this SciVisColor example +# `__. +# We also save the results for future use by passing ``save=True`` to +# `~proplot.constructor.Colormap`. + +# %% +import proplot as pplt +import numpy as np +state = np.random.RandomState(51423) +data = state.rand(30, 30).cumsum(axis=1) + +# Generate figure +fig, axs = pplt.subplots([[0, 1, 1, 0], [2, 2, 3, 3]], refwidth=2.4, span=False) +axs.format( + xlabel='xlabel', ylabel='ylabel', + suptitle='Merging colormaps' +) + +# Diverging colormap example +title1 = 'Diverging from sequential maps' +cmap1 = pplt.Colormap('Blues4_r', 'Reds3', name='Diverging', save=True) + +# SciVisColor examples +title2 = 'SciVisColor example' +cmap2 = pplt.Colormap( + 'Greens1_r', 'Oranges1', 'Blues1_r', 'Blues6', + ratios=(1, 3, 5, 10), name='SciVisColorUneven', save=True +) +title3 = 'SciVisColor with equal ratios' +cmap3 = pplt.Colormap( + 'Greens1_r', 'Oranges1', 'Blues1_r', 'Blues6', + name='SciVisColorEven', save=True +) + +# Plot examples +for ax, cmap, title in zip(axs, (cmap1, cmap2, cmap3), (title1, title2, title3)): + m = ax.contourf(data, cmap=cmap, levels=500) + ax.colorbar(m, loc='b', locator='null', label=cmap.name) + ax.format(title=title) + + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_cmaps_mod: +# +# Modifying colormaps +# ------------------- +# +# Proplot lets you create modified versions of *existing* colormaps +# using the `~proplot.constructor.Colormap` constructor function and the +# new `~proplot.colors.ContinuousColormap` and +# `~proplot.colors.DiscreteColormap` classes, which replace the native +# matplotlib colormap classes. They can be modified in the following ways: +# +# * To remove colors from the left or right ends of a colormap, pass `left` +# or `right` to `~proplot.constructor.Colormap`. This calls the +# `~proplot.colors.ContinuousColormap.truncate` method, and can be +# useful when you want to use colormaps as :ref:`color cycles ` +# and need to remove the light part so that your lines stand out +# against the background. +# * To modify the central colors of a diverging colormap, pass `cut` to +# `~proplot.constructor.Colormap`. This calls the +# `~proplot.colors.ContinuousColormap.cut` method, and can be used +# to create a sharper cutoff between negative and positive values or (when +# `cut` is negative) to expand the "neutral" region of the colormap. +# * To rotate a cyclic colormap, pass `shift` to +# `~proplot.constructor.Colormap`. This calls the +# `~proplot.colors.ContinuousColormap.shifted` method. Proplot ensures +# the colors at the ends of "shifted" colormaps are *distinct* so that +# levels never blur together. +# * To change the opacity of a colormap or add an opacity *gradation*, pass +# `alpha` to `~proplot.constructor.Colormap`. This calls the +# `~proplot.colors.ContinuousColormap.set_alpha` method, and can be +# useful when *layering* filled contour or mesh elements. +# * To change the "gamma" of a `~proplot.colors.PerceptualColormap`, +# pass `gamma` to `~proplot.constructor.Colormap`. This calls the +# `~proplot.colors.PerceptualColormap.set_gamma` method, and +# controls how the luminance and saturation channels vary between colormap +# segments. ``gamma > 1`` emphasizes high luminance, low saturation colors, +# while ``gamma < 1`` emphasizes low luminance, high saturation colors. This +# is similar to the effect of the `HCL wizard +# `__ "power" sliders. + +# %% +import proplot as pplt +import numpy as np +state = np.random.RandomState(51423) +data = state.rand(40, 40).cumsum(axis=0) + +# Generate figure +fig, axs = pplt.subplots([[0, 1, 1, 0], [2, 2, 3, 3]], refwidth=1.9, span=False) +axs.format(xlabel='y axis', ylabel='x axis', suptitle='Truncating sequential colormaps') + +# Cutting left and right +cmap = 'Ice' +for ax, coord in zip(axs, (None, 0.3, 0.7)): + if coord is None: + title, cmap_kw = 'Original', {} + elif coord < 0.5: + title, cmap_kw = f'left={coord}', {'left': coord} + else: + title, cmap_kw = f'right={coord}', {'right': coord} + ax.format(title=title) + ax.contourf( + data, cmap=cmap, cmap_kw=cmap_kw, colorbar='b', colorbar_kw={'locator': 'null'} + ) + +# %% +import proplot as pplt +import numpy as np +state = np.random.RandomState(51423) +data = (state.rand(40, 40) - 0.5).cumsum(axis=0).cumsum(axis=1) + +# Create figure +fig, axs = pplt.subplots(ncols=2, nrows=2, refwidth=1.7, span=False) +axs.format( + xlabel='x axis', ylabel='y axis', xticklabels='none', + suptitle='Modifying diverging colormaps', +) + +# Cutting out central colors +titles = ( + 'Negative-positive cutoff', 'Neutral-valued center', + 'Sharper cutoff', 'Expanded center' +) +for i, (ax, title, cut) in enumerate(zip(axs, titles, (None, None, 0.2, -0.1))): + if i % 2 == 0: + kw = {'levels': pplt.arange(-10, 10, 2)} # negative-positive cutoff + else: + kw = {'values': pplt.arange(-10, 10, 2)} # dedicated center + if cut is not None: + fmt = pplt.SimpleFormatter() # a proper minus sign + title = f'{title}\ncut = {fmt(cut)}' + ax.format(title=title) + m = ax.contourf( + data, cmap='Div', cmap_kw={'cut': cut}, extend='both', + colorbar='b', colorbar_kw={'locator': 'null'}, + **kw # level edges or centers + ) + +# %% +import proplot as pplt +import numpy as np +state = np.random.RandomState(51423) +data = (state.rand(50, 50) - 0.48).cumsum(axis=0).cumsum(axis=1) % 30 + +# Rotating cyclic colormaps +fig, axs = pplt.subplots(ncols=3, refwidth=1.7, span=False) +for ax, shift in zip(axs, (0, 90, 180)): + m = ax.pcolormesh(data, cmap='romaO', cmap_kw={'shift': shift}, levels=12) + ax.format( + xlabel='x axis', ylabel='y axis', title=f'shift = {shift}', + suptitle='Rotating cyclic colormaps' + ) + ax.colorbar(m, loc='b', locator='null') + +# %% +import proplot as pplt +import numpy as np +state = np.random.RandomState(51423) +data = state.rand(20, 20).cumsum(axis=1) + +# Changing the colormap opacity +fig, axs = pplt.subplots(ncols=3, refwidth=1.7, span=False) +for ax, alpha in zip(axs, (1.0, 0.5, 0.0)): + alpha = (alpha, 1.0) + cmap = pplt.Colormap('batlow_r', alpha=alpha) + m = ax.imshow(data, cmap=cmap, levels=10, extend='both') + ax.colorbar(m, loc='b', locator='none') + ax.format( + title=f'alpha = {alpha}', xlabel='x axis', ylabel='y axis', + suptitle='Adding opacity gradations' + ) + +# %% +import proplot as pplt +import numpy as np +state = np.random.RandomState(51423) +data = state.rand(20, 20).cumsum(axis=1) + +# Changing the colormap gamma +fig, axs = pplt.subplots(ncols=3, refwidth=1.7, span=False) +for ax, gamma in zip(axs, (0.7, 1.0, 1.4)): + cmap = pplt.Colormap('boreal', gamma=gamma) + m = ax.pcolormesh(data, cmap=cmap, levels=10, extend='both') + ax.colorbar(m, loc='b', locator='none') + ax.format( + title=f'gamma = {gamma}', xlabel='x axis', ylabel='y axis', + suptitle='Changing the PerceptualColormap gamma' + ) + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_cmaps_dl: +# +# Downloading colormaps +# --------------------- +# +# There are several interactive online tools for generating perceptually +# uniform colormaps, including +# `Chroma.js `__, +# `HCLWizard `__, +# `HCL picker `__, +# `SciVisColor `__, +# and `CCC-tool `__. +# +# To add colormaps downloaded from any of these sources, save the color data file +# to the ``cmaps`` subfolder inside `~proplot.config.Configurator.user_folder`, +# or to a folder named ``proplot_cmaps`` in the same directory as your python session +# or an arbitrary parent directory (see `~proplot.config.Configurator.local_folders`). +# After adding the file, call `~proplot.config.register_cmaps` or restart your python +# session. You can also use `~proplot.colors.ContinuousColormap.from_file` or manually +# pass `~proplot.colors.ContinuousColormap` instances or file paths to +# `~proplot.config.register_cmaps`. See `~proplot.config.register_cmaps` +# for a table of recognized file extensions. diff --git a/docs/colors.py b/docs/colors.py new file mode 100644 index 000000000..46bf7c7b1 --- /dev/null +++ b/docs/colors.py @@ -0,0 +1,189 @@ +# -*- coding: utf-8 -*- +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.11.4 +# kernelspec: +# display_name: Python 3 +# language: python +# name: python3 +# --- + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_colors: +# +# Color names +# =========== +# +# Proplot registers several new color names and includes tools for defining +# your own color names. These features are described below. + + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_colors_included: +# +# Included colors +# --------------- +# +# Proplot adds new color names from the `XKCD color survey +# `__ and +# the `Open Color `__ UI design color +# palettes. You can use `~proplot.demos.show_colors` to generate a table of these +# colors. Note that matplotlib's native `X11/CSS4 named colors +# `__ are still +# registered, but some of these color names may be overwritten by the XKCD names, +# and we encourage choosing colors from the below tables instead. XKCD colors +# are `available in matplotlib +# `__ under the +# ``xkcd:`` prefix, but proplot doesn't require this prefix because the XKCD +# selection is larger and the names are generally more likely to match your +# intuition for what a color "should" look like. +# +# For all colors, proplot ensures that ``'grey'`` is a synonym of ``'gray'`` +# (for example, ``'grey5'`` and ``'gray5'`` are both valid). Proplot also +# retricts the available XKCD colors with a filtering algorithm so they are +# "distinct" in :ref:`perceptually uniform space `. This +# makes it a bit easier to pick out colors from the table generated with +# `~proplot.demos.show_colors`. The filtering algorithm also cleans up similar +# names -- for example, ``'reddish'`` and ``'reddy'`` are changed to ``'red'``. +# You can adjust the filtering algorithm by calling `~proplot.config.register_colors` +# with the `space` or `margin` keywords. + +# %% +import proplot as pplt +fig, axs = pplt.show_colors() + + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_colors_change: +# +# Modifying colors +# ---------------- +# +# Proplot provides the top-level `~proplot.utils.set_alpha`, +# `~proplot.utils.set_hue`, `~proplot.utils.set_saturation`, +# `~proplot.utils.set_luminance`, `~proplot.utils.shift_hue`, +# `~proplot.utils.scale_saturation`, and `~proplot.utils.scale_luminance` +# functions for quickly modifying existing colors. The ``set`` functions change +# individual hue, saturation, or luminance values in the :ref:`perceptually uniform +# colorspace ` specified by the `space` keyword (default is ``'hcl'``). +# The ``shift`` and ``scale`` functions shift or scale the +# hue, saturation, or luminance by the input value -- for example, +# ``pplt.scale_luminance('color', 1.2)`` makes ``'color'`` 20% brighter. These +# are useful for creating color gradations outside of `~proplot.colors.Cycle` +# or if you simply spot a color you like and want to make it a bit +# brighter, less vibrant, etc. + + +# %% +import proplot as pplt +import numpy as np + +# Figure +state = np.random.RandomState(51423) +fig, axs = pplt.subplots(ncols=3, axwidth=2) +axs.format( + suptitle='Modifying colors', + toplabels=('Shifted hue', 'Scaled luminance', 'Scaled saturation'), + toplabelweight='normal', + xformatter='none', yformatter='none', +) + +# Shifted hue +N = 50 +fmt = pplt.SimpleFormatter() +marker = 'o' +for shift in (0, -60, 60): + x, y = state.rand(2, N) + color = pplt.shift_hue('grass', shift) + axs[0].scatter(x, y, marker=marker, c=color, legend='b', label=fmt(shift)) + +# Scaled luminance +for scale in (0.2, 1, 2): + x, y = state.rand(2, N) + color = pplt.scale_luminance('bright red', scale) + axs[1].scatter(x, y, marker=marker, c=color, legend='b', label=fmt(scale)) + +# Scaled saturation +for scale in (0, 1, 3): + x, y = state.rand(2, N) + color = pplt.scale_saturation('ocean blue', scale) + axs[2].scatter(x, y, marker=marker, c=color, legend='b', label=fmt(scale)) + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_colors_cmaps: +# +# Colors from colormaps +# --------------------- +# +# If you want to draw an individual color from a colormap or a color cycle, +# use ``key=(cmap, coord)`` or ``key=(cycle, index)`` with any keyword `key` +# that accepts color specifications (e.g., `color`, `edgecolor`, or `facecolor`). +# The ``coord`` should be a float between ``0`` and ``1``, denoting the coordinate +# within a smooth colormap, while the ``index`` should be the integer index +# on the discrete colormap color list. This feature is powered by the +# `~proplot.colors.ColorDatabase` class. This is useful if you spot a +# nice color in one of the available colormaps or color cycles and want +# to use it for some arbitrary plot element. Use the `~proplot.utils.to_rgb` or +# `~proplot.utils.to_rgba` functions to retrieve the RGB or RGBA channel values. + +# %% +import proplot as pplt +import numpy as np + +# Initial figure and random state +state = np.random.RandomState(51423) +fig = pplt.figure(refwidth=2.2, share=False) + +# Drawing from colormaps +name = 'Deep' +idxs = pplt.arange(0, 1, 0.2) +state.shuffle(idxs) +ax = fig.subplot(121, grid=True, title=f'Drawing from colormap {name!r}') +for idx in idxs: + data = (state.rand(20) - 0.4).cumsum() + h = ax.plot( + data, lw=5, color=(name, idx), + label=f'idx {idx:.1f}', legend='l', legend_kw={'ncols': 1} + ) +ax.colorbar(pplt.Colormap(name), loc='l', locator='none') + +# Drawing from color cycles +name = 'Qual1' +idxs = np.arange(6) +state.shuffle(idxs) +ax = fig.subplot(122, title=f'Drawing from color cycle {name!r}') +for idx in idxs: + data = (state.rand(20) - 0.4).cumsum() + h = ax.plot( + data, lw=5, color=(name, idx), + label=f'idx {idx:.0f}', legend='r', legend_kw={'ncols': 1} + ) +ax.colorbar(pplt.Colormap(name), loc='r', locator='none') +fig.format( + abc='A.', titleloc='l', + suptitle='On-the-fly color selections', + xformatter='null', yformatter='null', +) + + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_colors_user: +# +# Using your own colors +# --------------------- +# +# You can register your own colors by adding ``.txt`` files to the +# ``colors`` subfolder inside `~proplot.config.Configurator.user_folder`, +# or to a folder named ``proplot_colors`` in the same directory as your python session +# or an arbitrary parent directory (see `~proplot.config.Configurator.local_folders`). +# After adding the file, call `~proplot.config.register_colors` or restart your python +# session. You can also manually pass file paths, dictionaries, ``name=color`` +# keyword arguments to `~proplot.config.register_colors`. Each color +# file should contain lines that look like ``color: #xxyyzz`` +# where ``color`` is the registered color name and ``#xxyyzz`` is +# the HEX value. Lines beginning with ``#`` are ignored as comments. diff --git a/docs/colors_fonts.ipynb b/docs/colors_fonts.ipynb deleted file mode 100644 index f9f008fe5..000000000 --- a/docs/colors_fonts.ipynb +++ /dev/null @@ -1,244 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Colors and fonts" - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "ProPlot registers several new color names and font families, and includes tools for defining your own color names and adding your own font families. These features are described below." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Included colors" - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "ProPlot defines new color names from the `XKCD color survey `__, official `Crayola crayon colors `__, and from the `\"Open color\" `__ Github project. This was inspired by `seaborn `__. Use `~proplot.styletools.show_colors` to generate tables of these colors. Note that the native matplotlib `CSS4 named colors `__ are still registered, but we encourage using colors from the tables instead.\n", - "\n", - "To reduce the number of registered color names to a more manageable size, ProPlot filters the available XKCD and Crayola colors so that they have *sufficiently distinct coordinates* in the perceptually uniform hue-chroma-luminance colorspace. This makes it a bit easier to pick out colors from the table generated with `~proplot.styletools.show_colors`. Similar names were also cleaned up -- for example, ``'reddish'`` and ``'reddy'`` are changed to ``'red'``." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", - "figs = plot.show_colors()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Colors from colormaps" - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "If you want to draw an individual color from a colormap or a color cycle, use ``color=(cmap, coord)`` or ``color=(cycle, index)`` with any command that accepts the `color` keyword. The ``coord`` should be between ``0`` and ``1``, while the ``index`` is the index on the list of cycle colors. This feature is powered by the `~proplot.styletools.ColorDict` class. This is useful if you spot a nice color in one of the available colormaps and want to use it for some arbitrary plot element." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", - "import numpy as np\n", - "state = np.random.RandomState(51423)\n", - "f, axs = plot.subplots(nrows=2, aspect=2, axwidth=3, share=0)\n", - "\n", - "# Drawing from colormap\n", - "ax = axs[0]\n", - "cmap = 'deep'\n", - "m = ax.pcolormesh([[0], [1]], cmap=cmap, N=1000)\n", - "idxs = plot.arange(0, 1, 0.2)\n", - "state.shuffle(idxs)\n", - "for idx in idxs:\n", - " h = ax.plot(\n", - " (np.random.rand(20) - 0.4).cumsum(), lw=5, color=(cmap, idx),\n", - " label=f'idx {idx:.1f}', legend='r', legend_kw={'ncols': 1}\n", - " )\n", - "ax.colorbar(m, loc='ul', locator=0.2, label='colormap')\n", - "ax.format(title='Drawing from the Solar colormap', grid=True)\n", - "\n", - "# Drawing from color cycle\n", - "ax = axs[1]\n", - "idxs = np.arange(6)\n", - "state.shuffle(idxs)\n", - "for idx in idxs:\n", - " h = ax.plot(\n", - " (np.random.rand(20)-0.4).cumsum(), lw=5, color=('qual1', idx),\n", - " label=f'idx {idx:.0f}', legend='r', legend_kw={'ncols': 1}\n", - " )\n", - "ax.format(title='Drawing from the ggplot color cycle')\n", - "axs.format(\n", - " xlocator='null', abc=True, abcloc='ur', abcstyle='A.',\n", - " suptitle='Getting individual colors from colormaps and cycles'\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Using your own colors" - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "You can register your own colors by adding ``.txt`` files to the ``~/.proplot/colors`` directory and calling `~proplot.styletools.register_colors`. This command is also called on import. Each file should contain lines that look like ``color: #xxyyzz`` where ``color`` is the registered color name and ``#xxyyzz`` is the HEX color value." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Included fonts" - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "ProPlot adds several open source fonts, including the `TeX Gyre `__ font series, and introduces a `~proplot.styletools.show_fonts` command to compare fonts. By default, this command displays the *sans-serif* fonts packaged with ProPlot and available on your system (see `~matplotlib.font_manager`). Generally speaking, sans-serif fonts are more appropriate for figures than serif fonts.\n", - "\n", - "ProPlot also changes the default font to the Helvetica-lookalike `TeX Gyre Heros `__. Matplotlib uses `DejaVu Sans `__ in part because it includes glyphs for a wider range of mathematical symbols (where you see the “¤” dummy symbol in the below table, that character is unavailable), but IMHO TeX Gyre Heros is much more aesthetically pleasing. If your plot has lots of symbols, you may want to switch to DejaVu Sans or `Fira Math `__ (which is also packaged with ProPlot)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", - "f = plot.show_fonts()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Using your own fonts" - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "You can register your own fonts by adding files to the ``~/.proplot/fonts`` directory and calling `~proplot.styletools.register_fonts`. This command is also called on import. To change the default font, use the `~proplot.rctools.rc` object or modify your ``~/.proplotrc``. See :ref:`Configuring proplot` for details.\n", - "\n", - "Sometimes the font you would like to use *is* installed, but the font file is not stored under the matplotlib-compatible ``.ttf``, ``.otf``, or ``.afm`` formats. For example, several macOS fonts are unavailable because they are stored as ``.dfont`` collections. Also, while matplotlib nominally supports ``.ttc`` collections, ProPlot manually removes them because figures with ``.ttc`` fonts `cannot be saved as PDFs `__. You can get matplotlib to use these fonts by expanding the \"collections\" into individual ``.ttf`` files with the `DFontSplitter application `__, then saving the files in-place or in the ``~/.proplot/fonts`` folder.\n", - "\n", - "To find font files, check the paths listed in ``OSXFontDirectories``, ``X11FontDirectories``, ``MSUserFontDirectories``, and ``MSFontDirectories`` under the `~matplotlib.font_manager` module. Note that if the font in question has a \"thin\" style, implied by file names with the word ``Thin``, `a matplotlib bug `__ may cause these styles to override the \"normal\" style!" - ] - } - ], - "metadata": { - "celltoolbar": "Raw Cell Format", - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.3" - }, - "toc": { - "colors": { - "hover_highlight": "#ece260", - "navigate_num": "#000000", - "navigate_text": "#000000", - "running_highlight": "#FF0000", - "selected_highlight": "#fff968", - "sidebar_border": "#ffffff", - "wrapper_background": "#ffffff" - }, - "moveMenuLeft": false, - "nav_menu": { - "height": "84px", - "width": "252px" - }, - "navigate_menu": true, - "number_sections": true, - "sideBar": true, - "threshold": 4, - "toc_cell": false, - "toc_section_display": "block", - "toc_window_display": true, - "widenNotebook": false - }, - "varInspector": { - "cols": { - "lenName": 16, - "lenType": 16, - "lenVar": 40 - }, - "kernels_config": { - "python": { - "delete_cmd_postfix": "", - "delete_cmd_prefix": "del ", - "library": "var_list.py", - "varRefreshCmd": "print(var_dic_list())" - }, - "r": { - "delete_cmd_postfix": ") ", - "delete_cmd_prefix": "rm(", - "library": "var_list.r", - "varRefreshCmd": "cat(var_dic_list()) " - } - }, - "types_to_exclude": [ - "module", - "function", - "builtin_function_or_method", - "instance", - "_Feature" - ], - "window_display": false - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/docs/conf.py b/docs/conf.py index 8be537223..a9b48620e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -9,35 +9,28 @@ # full list see the documentation: # http://www.sphinx-doc.org/en/master/config -# -- Path setup -------------------------------------------------------------- +# -- Imports and paths -------------------------------------------------------------- +# Import statements import os import sys +import datetime +import subprocess -# Add proplot to path for sphinx-automodapi +# Update path for sphinx-automodapi and sphinxext extension +sys.path.append(os.path.abspath('.')) sys.path.insert(0, os.path.abspath('..')) -# Add docs folder to PATH for local 'sphinxext' extensions -sys.path.append(os.path.abspath('.')) +# Print available system fonts +from matplotlib.font_manager import fontManager +print('Font files:', end=' ') +print(', '.join(os.path.basename(font.fname) for font in fontManager.ttflist)) -# Hack to get basemap to work -# See: https://github.com/readthedocs/readthedocs.org/issues/5339 -on_rtd = os.environ.get('READTHEDOCS', None) == 'True' -if on_rtd: - os.environ['PROJ_LIB'] = ( - '{}/{}/share/proj'.format( - os.environ['CONDA_ENVS_PATH'], os.environ['CONDA_DEFAULT_ENV'] - ) - ) -else: - os.environ['PROJ_LIB'] = '{}/share/proj'.format( - os.environ['CONDA_PREFIX'] - ) - -# -- Project information ----------------------------------------------------- +# -- Project information ------------------------------------------------------- +# The basic info project = 'ProPlot' -copyright = '2019, Luke L. B. Davis' +copyright = f'{datetime.datetime.today().year}, Luke L. B. Davis' author = 'Luke L. B. Davis' # The short X.Y version @@ -46,6 +39,40 @@ # The full version, including alpha/beta/rc tags release = '' + +# -- Create files -------------------------------------------------------------- + +# Create RST table and sample proplotrc file +from proplot.config import rc +folder = os.path.join(os.path.dirname(os.path.abspath(__file__)), '_static') +if not os.path.isdir(folder): + os.mkdir(folder) +rc._save_rst(os.path.join(folder, 'rctable.rst')) +rc._save_yaml(os.path.join(folder, 'proplotrc')) + +# -- Setup basemap -------------------------------------------------------------- + +# Hack to get basemap to work +# See: https://github.com/readthedocs/readthedocs.org/issues/5339 +if os.environ.get('READTHEDOCS', None) == 'True': + conda = os.path.join(os.environ['CONDA_ENVS_PATH'], os.environ['CONDA_DEFAULT_ENV']) +else: + conda = os.environ['CONDA_PREFIX'] +os.environ['GEOS_DIR'] = conda +os.environ['PROJ_LIB'] = os.path.join(conda, 'share', 'proj') + +# Install basemap if does not exist +# Extremely ugly but impossible to install in environment.yml. Must set +# GEOS_DIR before installing so cannot install with pip and basemap conflicts +# with conda > 0.19 so cannot install with conda in environment.yml. +try: + import mpl_toolkits.basemap # noqa: F401 +except ImportError: + subprocess.check_call( + ['pip', 'install', 'git+https://github.com/matplotlib/basemap@v1.2.2rel'] + ) + + # -- General configuration --------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. @@ -53,11 +80,8 @@ # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -# For plot_directiev: extensions = [ - # 'matplotlib.sphinxext.plot_directive', # see: https://matplotlib.org/sampledoc/extensions.html # noqa - 'IPython.sphinxext.ipython_console_highlighting', - 'IPython.sphinxext.ipython_directive', # for ipython highlighting + # 'matplotlib.sphinxext.plot_directive', # see: https://matplotlib.org/sampledoc/extensions.html # noqa: E501 'sphinx.ext.autodoc', # include documentation from docstrings 'sphinx.ext.doctest', # >>> examples 'sphinx.ext.extlinks', # for :pr:, :issue:, :commit: @@ -65,63 +89,79 @@ 'sphinx.ext.todo', # Todo headers and todo:: directives 'sphinx.ext.mathjax', # LaTeX style math 'sphinx.ext.viewcode', # view code links - 'sphinx.ext.autosummary', # autosummary directive 'sphinx.ext.napoleon', # for NumPy style docstrings 'sphinx.ext.intersphinx', # external links + 'sphinx.ext.autosummary', # autosummary directive 'sphinxext.custom_roles', # local extension - 'sphinx_automodapi.automodapi', # see: https://github.com/lukelbd/sphinx-automodapi/tree/proplot-mods # noqa - 'nbsphinx', - ] + 'sphinx_automodapi.automodapi', # fork of automodapi + 'sphinx_rtd_light_dark', # use custom theme + 'sphinx_copybutton', # add copy button to code + 'nbsphinx', # parse rst books +] -extlinks = { - 'issue': ('https://github.com/lukelbd/proplot/issues/%s', 'GH#'), - 'commit': ('https://github.com/lukelbd/proplot/commit/%s', '@'), - 'pr': ('https://github.com/lukelbd/proplot/pull/%s', 'GH#'), -} +# The master toctree document. +master_doc = 'index' -# Give *lots* of time for cell execution! -# Note nbsphinx compiles *all* notebooks in docs unless excluded -nbsphinx_timeout = 300 +# The suffix(es) of source filenames, either a string or list. +source_suffix = ['.rst', '.html'] -# Set InlineBackend params, maybe nbsphinx skips ones in rctools.py -# Not necessary because rctools.py configures the backend -# nbsphinx_execute_arguments = [ -# "--InlineBackend.figure_formats={'svg'}", -# "--InlineBackend.rc={'figure.dpi': 100}", -# ] +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] -# Do not run doctest tests, these are just to show syntax and expected -# output may be graphical -doctest_test_doctest_blocks = '' +# List of file patterns relative to source dir that should be ignored +exclude_patterns = [ + 'conf.py', 'sphinxext', '_build', '_templates', '_themes', + '*.ipynb', '**.ipynb_checkpoints' '.DS_Store', 'trash', 'tmp', +] + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +language = None + +# Role. Default family is py but can also set default role so don't need +# :func:`name`, :module:`name`, etc. +default_role = 'py:obj' + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +add_module_names = False # proplot imports everything in top-level namespace + +# Autodoc configuration. Here we concatenate class and __init__ docstrings +# See: http://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html +autoclass_content = 'both' # options are 'class', 'both', 'init' # Generate stub pages whenever ::autosummary directive encountered # This way don't have to call sphinx-autogen manually autosummary_generate = True -# Use automodapi tool, created by astropy people. See: -# https://sphinx-automodapi.readthedocs.io/en/latest/automodapi.html#overview -# Normally have to *enumerate* function names manually. This will document -# them automatically. Just be careful, if you use from x import *, to exclude -# them in the automodapi:: directive -automodapi_toctreedirnm = 'api' # create much better URL for the page +# Automodapi tool: https://sphinx-automodapi.readthedocs.io/en/latest/automodapi.html +# Normally have to *enumerate* function names manually. This will document them +# automatically. Just be careful to exclude public names from automodapi:: directive. +automodapi_toctreedirnm = 'api' automodsumm_inherited_members = False -# Logo -html_logo = '_static/logo_square.png' +# Doctest configuration. For now do not run tests, they are just to show syntax +# and expected output may be graphical +doctest_test_doctest_blocks = '' -# Turn off code and image links for embedded mpl plots -# plot_html_show_source_link = False -# plot_html_show_formats = False +# Cupybutton configuration +# See: https://sphinx-copybutton.readthedocs.io/en/latest/ +copybutton_prompt_text = r'>>> |\.\.\. |\$ |In \[\d*\]: | {2,5}\.\.\.: | {5,8}: ' +copybutton_prompt_is_regexp = True +copybutton_only_copy_prompt_lines = True +copybutton_remove_prompts = True -# One of 'class', 'both', or 'init' -# The 'both' concatenates class and __init__ docstring -# See: http://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html -autoclass_content = 'both' +# Links for What's New github commits, issues, and pull requests +extlinks = { + 'issue': ('https://github.com/proplot-dev/proplot/issues/%s', 'GH#'), + 'commit': ('https://github.com/proplot-dev/proplot/commit/%s', '@'), + 'pr': ('https://github.com/proplot-dev/proplot/pull/%s', 'GH#'), +} # Set up mapping for other projects' docs intersphinx_mapping = { 'cycler': ('https://matplotlib.org/cycler/', None), - 'matplotlib': ('https://matplotlib.org', None), + 'matplotlib': ('https://matplotlib.org/stable', None), 'sphinx': ('http://www.sphinx-doc.org/en/stable', None), 'python': ('https://docs.python.org/3', None), 'numpy': ('https://docs.scipy.org/doc/numpy', None), @@ -130,14 +170,23 @@ 'cartopy': ('https://scitools.org.uk/cartopy/docs/latest', None), 'basemap': ('https://matplotlib.org/basemap', None), 'pandas': ('https://pandas.pydata.org/pandas-docs/stable', None), + 'pint': ('https://pint.readthedocs.io/en/stable/', None), } -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -add_module_names = False # proplot imports everything in top-level namespace +# Fix duplicate class member documentation from autosummary + numpydoc +# See: https://github.com/phn/pytpm/issues/3#issuecomment-12133978 +numpydoc_show_class_members = False # Napoleon options # See: http://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html +# * use_param is set to False so that we can put multiple "parameters" +# on one line -- for example 'xlocator, ylocator : locator-spec, optional' +# * docs claim napoleon_preprocess_types and napoleon_type_aliases only work +# when napoleon_use_param is True but xarray sets to False and it still works +# * use_keyword is set to False because we do not want separate 'Keyword Arguments' +# section and have same issue for multiple keywords. +# * use_ivar and use_rtype are set to False for (presumably) style consistency +# with the above options set to False. napoleon_use_ivar = False napoleon_use_param = False napoleon_use_keyword = False @@ -145,73 +194,50 @@ napoleon_numpy_docstring = True napoleon_google_docstring = False napoleon_include_init_with_doc = False # move init doc to 'class' doc +napoleon_preprocess_types = True +napoleon_type_aliases = { + # Python or inherited terms + # NOTE: built-in types are automatically included + 'callable': ':py:func:`callable`', + 'sequence': ':term:`sequence`', + 'dict-like': ':term:`dict-like `', + 'path-like': ':term:`path-like `', + 'array-like': ':term:`array-like `', + # Proplot defined terms + 'unit-spec': ':py:func:`unit-spec `', + 'locator-spec': ':py:func:`locator-spec `', + 'formatter-spec': ':py:func:`formatter-spec `', + 'scale-spec': ':py:func:`scale-spec `', + 'colormap-spec': ':py:func:`colormap-spec `', + 'cycle-spec': ':py:func:`cycle-spec `', + 'norm-spec': ':py:func:`norm-spec `', + 'color-spec': ':py:func:`color-spec `', + 'artist': ':py:func:`artist `', +} -# Fix duplicate class member documentation from autosummary + numpydoc -# See: https://github.com/phn/pytpm/issues/3#issuecomment-12133978 -numpydoc_show_class_members = False - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix(es) of source filenames. -# You can specify multiple suffix as a list of string. -source_suffix = '.rst' - -# The master toctree document. -master_doc = 'index' +# Fail on error. Note nbsphinx compiles all notebooks in docs unless excluded +nbsphinx_allow_errors = False -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# -# This is also used if you do content translation via gettext catalogs. -# Usually you set "language" from the command line for these cases. -language = None +# Give *lots* of time for cell execution +nbsphinx_timeout = 300 -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -exclude_patterns = [ - '_templates', '_themes', 'sphinxext', - '.DS_Store', '**.ipynb_checkpoints', - # '[0-9a-eg-su-z]*.ipynb', # only run [figures|tight].ipynb for debugging -] +# Add jupytext support to nbsphinx +nbsphinx_custom_formats = {'.py': ['jupytext.reads', {'fmt': 'py:percent'}]} # The name of the Pygments (syntax highlighting) style to use. # The light-dark theme toggler overloads this, but set default anyway pygments_style = 'none' -# Create local pygments copies -# Previously used: https://github.com/richleland/pygments-css -# But do not want to depend on some random repository -from pygments.formatters import HtmlFormatter # noqa: E402 -from pygments.styles import get_all_styles # noqa: E402 -path = os.path.join('_static', 'pygments') -if not os.path.isdir(path): - os.mkdir(path) -for style in get_all_styles(): - path = os.path.join('_static', 'pygments', style + '.css') - if os.path.isfile(path): - continue - with open(path, 'w') as f: - f.write(HtmlFormatter(style=style).get_style_defs('.highlight')) - -# Create sample .proplotrc file -from proplot.rctools import _write_defaults # noqa: E402 -_write_defaults(os.path.join('_static', 'proplotrc'), comment=False) - -# Role -# default family is py, but can also set default role so don't need -# :func:`name`, :module:`name`, etc. -default_role = 'py:obj' # -- Options for HTML output ------------------------------------------------- +# Logo +html_logo = os.path.join('_static', 'logo_square.png') + # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. - -# Rtd theme still the best -# in _templates but can just use below optoin. -# We set "style_nav_header_background" in custom.css -html_theme = 'sphinx_rtd_theme' +# Use modified RTD theme with overrides in custom.css and custom.js +html_theme = 'sphinx_rtd_light_dark' html_theme_options = { 'logo_only': True, 'display_version': False, @@ -235,9 +261,8 @@ # 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. Static folder is for CSS and image files. -# For icons see: https://icons8.com/icon -# To convert: convert logo_blank.png logo_blank.ico +# pixels large. Static folder is for CSS and image files. Use ImageMagick to +# convert png to ico on command line with 'convert image.png image.ico' html_favicon = os.path.join('_static', 'logo_blank.ico') # -- Options for HTMLHelp output --------------------------------------------- @@ -266,7 +291,7 @@ # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'proplot.tex', 'proplot Documentation', + (master_doc, 'proplot.tex', 'ProPlot Documentation', 'Luke L. B. Davis', 'manual'), ] @@ -276,8 +301,13 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - (master_doc, 'proplot', 'proplot Documentation', - [author], 1) + ( + master_doc, + 'proplot', + 'ProPlot Documentation', + [author], + 1 + ) ] @@ -287,14 +317,19 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'proplot', 'proplot Documentation', - author, 'proplot', 'One line description of project.', - 'Miscellaneous'), + ( + master_doc, + 'proplot', + 'ProPlot Documentation', + author, + 'proplot', + 'A succinct matplotlib wrapper for making beautiful, ' + 'publication-quality graphics.', + 'Miscellaneous' + ) ] -# -- Extension configuration ------------------------------------------------- - # -- Options for todo extension ---------------------------------------------- # If true, `todo` and `todoList` produce output, else they produce nothing. diff --git a/docs/configuration.rst b/docs/configuration.rst index 0e3d0bd5a..a501d9e2a 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -1,200 +1,188 @@ +.. _ug_rcmpl: https://matplotlib.org/stable/tutorials/introductory/customizing.html + +.. _ug_mplrc: https://matplotlib.org/stable/tutorials/introductory/customizing.html#customizing-with-matplotlibrc-files + +.. _ug_config: + Configuring proplot =================== Overview -------- -A special object named `~proplot.rctools.rc`, belonging to the -`~proplot.rctools.rc_configurator` class, is created on import. -This is your one-stop shop for changing global settings belonging to any of -the following three categories: +A dictionary-like object named `~proplot.config.rc`, belonging to the +`~proplot.config.Configurator` class, is created when you import proplot. +This is your one-stop shop for working with +`matplotlib settings `_ +stored in `~proplot.config.rc_matplotlib` +(our name for the `~matplotlib.rcParams` dictionary) +and :ref:`proplot settings ` +stored in `~proplot.config.rc_proplot`. -1. Builtin matplotlib `rcParams `__ - settings. These have the format ``x.y`` or ``x.y.z``. -2. ProPlot :ref:`rcParamsLong` settings. These also have the format ``x.y`` - (see below). -3. ProPlot :ref:`rcParamsShort` settings. These have no dots (see below). +To change global settings on-the-fly, simply update `~proplot.config.rc` +using either dot notation or as you would any other dictionary: -You can change settings with the `~proplot.rctools.rc` object as follows: +.. code-block:: python -* ``plot.rc.name = value`` -* ``plot.rc['name'] = value`` -* ``plot.rc.update(name1=value1, name2=value2)`` -* ``plot.rc.update({'name1':value1, 'name2':value2})`` + import proplot as pplt + pplt.rc.name = value + pplt.rc['name'] = value + pplt.rc.update(name1=value1, name2=value2) + pplt.rc.update({'name1': value1, 'name2': value2}) -To temporarily change settings on a particular axes, use either of the -following: +To apply settings to a particular axes or figure, pass the setting +to `proplot.axes.Axes.format` or `proplot.figure.Figure.format`: -* ``ax.format(name=value)`` -* ``ax.format(rc_kw={'name':value})`` +.. code-block:: python -In all of these examples, if the setting name ``name`` contains -any dots, you can simply **omit the dots**. For example, to change the -:rcraw:`title.loc` property, use ``plot.rc.titleloc = value``, -``plot.rc.update(titleloc=value)``, or ``ax.format(titleloc=value)``. + import proplot as pplt + fig, ax = pplt.subplots() + ax.format(name1=value1, name2=value2) + ax.format(rc_kw={'name1': value1, 'name2': value2}) + +To temporarily modify settings for particular figure(s), pass the setting +to the `~proplot.config.Configurator.context` command: + +.. code-block:: python + + import proplot as pplt + with pplt.rc.context(name1=value1, name2=value2): + fig, ax = pplt.subplots() + with pplt.rc.context({'name1': value1, 'name2': value2}): + fig, ax = pplt.subplots() -rcParamsShort -------------- -These are **simple, short** names used to change multiple matplotlib and -ProPlot settings at once, as shorthands for settings with longer names, or -for special options. For example, :rcraw:`ticklen` changes the tick length for -the *x* and *y* axes in one go. - -================ ============================================================================================================================================================================================================================================== -Key Description -================ ============================================================================================================================================================================================================================================== -``abc`` Boolean, whether to draw a-b-c labels by default. -``align`` Whether to align axis labels during draw. See `aligning labels `__. -``alpha`` The opacity of the background axes patch. -``borders`` Boolean, toggles country border lines on and off. -``cmap`` The default colormap. -``coast`` Boolean, toggles coastline lines on and off. -``color`` The color of axis spines, tick marks, tick labels, and labels. -``cycle`` The default color cycle name, used e.g. for lines. -``facecolor`` The color of the background axes patch. -``fontname`` Name of font used for all text in the figure. The default is Helvetica Neue. See `~proplot.fonttools` for details. -``geogrid`` Boolean, toggles meridian and parallel gridlines on and off. -``grid`` Boolean, toggles major grid lines on and off. -``gridminor`` Boolean, toggles minor grid lines on and off. -``gridratio`` Ratio of minor gridline width to major gridline width. -``inlinefmt`` The inline backend figure format or list thereof. Valid formats include ``'svg'``, ``'pdf'``, ``'retina'``, ``'png'``, and ``jpeg``. -``innerborders`` Boolean, toggles internal border lines on and off, e.g. for states and provinces. -``lakes`` Boolean, toggles lake patches on and off. -``land`` Boolean, toggles land patches on and off. -``large`` Font size for titles, "super" titles, and a-b-c subplot labels. -``linewidth`` Thickness of axes spines and major tick lines. -``lut`` The number of colors to put in the colormap lookup table. -``margin`` The margin of space between axes edges and objects plotted inside the axes, if ``xlim`` and ``ylim`` are unset. -``ocean`` Boolean, toggles ocean patches on and off. -``reso`` Resolution of geographic features, one of ``'lo'``, ``'med'``, or ``'hi'`` -``rgbcycle`` If ``True``, and ``colorblind`` is the current cycle, this registers the ``colorblind`` colors as ``'r'``, ``'b'``, ``'g'``, etc., like in `seaborn `__. -``rivers`` Boolean, toggles river lines on and off. -``share`` The axis sharing level, one of ``0``, ``1``, ``2``, or ``3``. See `~proplot.subplots.subplots` for details. -``small`` Font size for legend text, tick labels, axis labels, and text generated with `~matplotlib.axes.Axes.text`. -``span`` Boolean, toggles spanning axis labels. See `~proplot.subplots.subplots` for details. -``tickdir`` Major and minor tick direction. Must be one of ``out``, ``in``, or ``inout``. -``ticklen`` Length of major ticks in points. -``ticklenratio`` Ratio of minor tickline length to major tickline length. -``tickpad`` Padding between ticks and tick labels in points. -``titlepad`` Padding between the axes and the title, alias for :rcraw:`axes.titlepad`. -``tickratio`` Ratio of minor tickline width to major tickline width. -``tight`` Boolean, indicates whether to auto-adjust figure bounds and subplot spacings. -================ ============================================================================================================================================================================================================================================== - -rcParamsLong ------------- -These are **longer, specific** setting names -used to customize things not covered by -`~matplotlib.rcParams`. - -The ``subplots`` category controls the default layout for figures -and axes. The ``abc``, ``title``, and ``tick`` categories control -a-b-c label, title, and axis tick label settings. The -``suptitle``, ``leftlabel``, ``toplabel``, ``rightlabel``, and ``bottomlabel`` -categories control figure title and edge label settings. - -There are two new additions to the ``image`` category, and the new -``colorbar`` category controls *inset* and *outer* -`~proplot.axes.Axes.colorbar` properties. -The new ``gridminor`` category controls minor gridline settings, -and the new ``geogrid`` category controls meridian and parallel line settings -for `~proplot.axes.ProjAxes`. Note that when a ``grid`` property is changed, -it also changed the corresponding ``gridminor`` property. - -Finally, the ``geoaxes``, ``land``, ``ocean``, ``rivers``, ``lakes``, -``borders``, and ``innerborders`` categories control various -`~proplot.axes.ProjAxes` settings. These are used when the boolean -toggles for the corresponding :ref:`rcParamsShort` settings are turned on. - -=============================== ========================================================================================================================================================================================================================================================= -Key(s) Description -=============================== ========================================================================================================================================================================================================================================================= -``abc.style`` a-b-c label style. For options, see `~proplot.axes.Axes.format`. -``abc.loc`` a-b-c label position. For options, see `~proplot.axes.Axes.format`. -``abc.border`` Boolean, indicates whether to draw a white border around a-b-c labels inside an axes. -``abc.borderwidth`` Width of the white border around a-b-c labels. -``abc.color`` a-b-c label color. -``abc.size`` a-b-c label font size. -``abc.weight`` a-b-c label font weight. -``axes.formatter.zerotrim`` Boolean, indicates whether trailing decimal zeros are trimmed on tick labels. -``axes.formatter.timerotation`` Float, indicates the default *x* axis tick label rotation for datetime tick labels. -``borders.color`` Line color for country borders. -``borders.linewidth`` Line width for country borders. -``bottomlabel.color`` Font color for column labels on the bottom of the figure. -``bottomlabel.size`` Font size for column labels on the bottom of the figure. -``bottomlabel.weight`` Font weight for column labels on the bottom of the figure. -``colorbar.loc`` Inset colorbar location, options are listed in `~proplot.axes.Axes.colorbar`. -``colorbar.grid`` Boolean, indicates whether to draw borders between each level of the colorbar. -``colorbar.frameon`` Boolean, indicates whether to draw a frame behind inset colorbars. -``colorbar.framealpha`` Opacity for inset colorbar frames. -``colorbar.length`` Length of outer colorbars. -``colorbar.insetlength`` Length of inset colorbars. Units are interpreted by `~proplot.utils.units`. -``colorbar.width`` Width of outer colorbars. Units are interpreted by `~proplot.utils.units`. -``colorbar.insetwidth`` Width of inset colorbars. Units are interpreted by `~proplot.utils.units`. -``colorbar.axespad`` Padding between axes edge and inset colorbars. Units are interpreted by `~proplot.utils.units`. -``colorbar.extend`` Length of rectangular or triangular "extensions" for panel colorbars. Units are interpreted by `~proplot.utils.units`. -``colorbar.insetextend`` Length of rectangular or triangular "extensions" for inset colorbars. Units are interpreted by `~proplot.utils.units`. -``geoaxes.facecolor`` Face color for the map outline patch. -``geoaxes.edgecolor`` Edge color for the map outline patch. -``geoaxes.linewidth`` Edge width for the map outline patch. -``geogrid.labels`` Boolean, indicates whether to label the parallels and meridians. -``geogrid.labelsize`` Font size for latitude and longitude labels. Inherits from ``small``. -``geogrid.latmax`` Absolute latitude in degrees, poleward of which meridian gridlines are cut off. -``geogrid.lonstep`` Default interval for meridian gridlines in degrees. -``geogrid.latstep`` Default interval for parallel gridlines in degrees. -``gridminor.linewidth`` Minor gridline width. -``gridminor.linestyle`` Minor gridline style. -``gridminor.alpha`` Minor gridline transparency. -``gridminor.color`` Minor gridline color. -``image.levels`` Default number of levels for ``pcolormesh`` and ``contourf`` plots. -``image.edgefix`` Whether to fix the `white-lines-between-filled-contours `__ and `white-lines-between-pcolor-rectangles `__ issues. This slows down figure rendering a bit. -``innerborders.color`` Line color for internal border lines. -``innerborders.linewidth`` Line width for internal border lines. -``land.color`` Face color for land patches. -``lakes.color`` Face color for lake patches. -``leftlabel.color`` Font color for row labels on the left-hand side. -``leftlabel.size`` Font size for row labels on the left-hand side. -``leftlabel.weight`` Font weight for row labels on the left-hand side. -``ocean.color`` Face color for ocean patches. -``rightlabel.color`` Font color for row labels on the right-hand side. -``rightlabel.size`` Font size for row labels on the right-hand side. -``rightlabel.weight`` Font weight for row labels on the right-hand side. -``rivers.color`` Line color for river lines. -``rivers.linewidth`` Line width for river lines. -``subplots.axwidth`` Default width of each axes. Units are interpreted by `~proplot.utils.units`. -``subplots.panelwidth`` Width of side panels. Units are interpreted by `~proplot.utils.units`. -``subplots.pad`` Padding around figure edge. Units are interpreted by `~proplot.utils.units`. -``subplots.axpad`` Padding between adjacent subplots. Units are interpreted by `~proplot.utils.units`. -``subplots.panelpad`` Padding between subplots and panels, and between stacked panels. Units are interpreted by `~proplot.utils.units`. -``suptitle.color`` Figure title color. -``suptitle.size`` Figure title font size. -``suptitle.weight`` Figure title font weight. -``tick.color`` Axis tick label color. Mirrors the *axis* label :rcraw:`axes.labelcolor` setting. -``tick.size`` Axis tick label font size. Mirrors the *axis* label :rcraw:`axes.labelsize` setting. -``tick.weight`` Axis tick label font weight. Mirrors the *axis* label :rcraw:`axes.labelweight` setting. -``title.loc`` Title position. For options, see `~proplot.axes.Axes.format`. -``title.border`` Boolean, indicates whether to draw a white border around titles inside an axes. -``title.borderwidth`` Width of the white border around titles. -``title.pad`` The title offset in arbitrary units. Alias for :rcraw:`axes.titlepad`. -``title.color`` Axes title color. -``title.size`` Axes title font size. -``title.weight`` Axes title font weight. -``toplabel.color`` Font color for column labels on the top of the figure. -``toplabel.size`` Font size for column labels on the top of the figure. -``toplabel.weight`` Font weight for column labels on the top of the figure. -=============================== ========================================================================================================================================================================================================================================================= - -The .proplotrc file +In all of these examples, if the setting name contains dots, +you can simply omit the dots. For example, to change the +:rcraw:`title.loc` property, the following approaches are valid: + +.. code-block:: python + + import proplot as pplt + # Apply globally + pplt.rc.titleloc = value + pplt.rc.update(titleloc=value) + # Apply locally + fig, ax = pplt.subplots() + ax.format(titleloc=value) + +.. _ug_rcmatplotlib: + +Matplotlib settings ------------------- -To modify the global settings, edit your -``~/.proplotrc`` file. To modify settings for a particular project, -create a ``.proplotrc`` file in the same directory as your ipython -notebook, or in an arbitrary parent directory. -As an example, a ``.proplotrc`` file containing the default settings -is shown below. The syntax is mostly the same as the syntax used for -`matplotlibrc files `__. +Matplotlib settings are natively stored in the `~matplotlib.rcParams` +dictionary. Proplot makes this dictionary available in the top-level namespace as +`~proplot.config.rc_matplotlib`. All matplotlib settings can also be changed with +`~proplot.config.rc`. Details on the matplotlib settings can be found on +`this page `_. + +.. _ug_rcproplot: + +Proplot settings +---------------- + +Proplot settings are natively stored in the `~proplot.config.rc_proplot` dictionary. +They should almost always be changed with `~proplot.config.rc` rather than +`~proplot.config.rc_proplot` to ensure that :ref:`meta-settings ` are +synced. These settings are not found in `~matplotlib.rcParams` -- they either +control proplot-managed features (e.g., a-b-c labels and geographic gridlines) +or they represent existing matplotlib settings with more clear or succinct names. +Here's a broad overview of the new settings: + +* The ``subplots`` category includes settings that control the default + subplot layout and padding. +* The ``geo`` category contains settings related to geographic plotting, including the + geographic backend, gridline label settings, and map bound settings. +* The ``abc``, ``title``, and ``label`` categories control a-b-c labels, axes + titles, and axis labels. The latter two replace ``axes.title`` and ``axes.label``. +* The ``suptitle``, ``leftlabel``, ``toplabel``, ``rightlabel``, and ``bottomlabel`` + categories control the figure titles and subplot row and column labels. +* The ``formatter`` category supersedes matplotlib's ``axes.formatter`` + and includes settings that control the `~proplot.ticker.AutoFormatter` behavior. +* The ``cmap`` category supersedes matplotlib's ``image`` and includes + settings relevant to colormaps and the `~proplot.colors.DiscreteNorm` normalizer. +* The ``tick`` category supersedes matplotlib's ``xtick`` and ``ytick`` + to simultaneously control *x* and *y* axis tick and tick label settings. +* The matplotlib ``grid`` category includes new settings that control the meridian + and parallel gridlines and gridline labels managed by `~proplot.axes.GeoAxes`. +* The ``gridminor`` category optionally controls minor gridlines separately + from major gridlines. +* The ``land``, ``ocean``, ``rivers``, ``lakes``, ``borders``, and ``innerborders`` + categories control geographic content managed by `~proplot.axes.GeoAxes`. + +.. _ug_rcmeta: + +Meta-settings +------------- + +Some proplot settings may be more accurately described as "meta-settings", +as they change several matplotlib and proplot settings at once (note that settings +are only synced when they are changed on the `~proplot.config.rc` object rather than +the `~proplot.config.rc_proplot` and `~proplot.config.rc_matplotlib` dictionaries). +Here's a broad overview of the "meta-settings": + +* Setting :rcraw:`font.small` (or, equivalently, :rcraw:`fontsmall`) changes + the :rcraw:`tick.labelsize`, :rcraw:`grid.labelsize`, + :rcraw:`legend.fontsize`, and :rcraw:`axes.labelsize`. +* Setting :rcraw:`font.large` (or, equivalently, :rcraw:`fontlarge`) changes + the :rcraw:`abc.size`, :rcraw:`title.size`, :rcraw:`suptitle.size`, + :rcraw:`leftlabel.size`, :rcraw:`toplabel.size`, :rcraw:`rightlabel.size` + :rcraw:`bottomlabel.size`. +* Setting :rcraw:`meta.color` changes the :rcraw:`axes.edgecolor`, + :rcraw:`axes.labelcolor` :rcraw:`tick.labelcolor`, :rcraw:`hatch.color`, + :rcraw:`xtick.color`, and :rcraw:`ytick.color` . +* Setting :rcraw:`meta.width` changes the :rcraw:`axes.linewidth` and the major + and minor tickline widths :rcraw:`xtick.major.width`, :rcraw:`ytick.major.width`, + :rcraw:`xtick.minor.width`, and :rcraw:`ytick.minor.width`. The minor tickline widths + are scaled by :rcraw:`tick.widthratio` (or, equivalently, :rcraw:`tickwidthratio`). +* Setting :rcraw:`tick.len` (or, equivalently, :rcraw:`ticklen`) changes the major and + minor tickline lengths :rcraw:`xtick.major.size`, :rcraw:`ytick.major.size`, + :rcraw:`xtick.minor.size`, and :rcraw:`ytick.minor.size`. The minor tickline lengths + are scaled by :rcraw:`tick.lenratio` (or, equivalently, :rcraw:`ticklenratio`). +* Setting :rcraw:`grid.color`, :rcraw:`grid.linewidth`, :rcraw:`grid.linestyle`, + or :rcraw:`grid.alpha` also changes the corresponding ``gridminor`` settings. Any + distinct ``gridminor`` settings must be applied after ``grid`` settings. +* Setting :rcraw:`grid.linewidth` changes the major and minor gridline widths. + The minor gridline widths are scaled by :rcraw:`grid.widthratio` + (or, equivalently, :rcraw:`gridwidthratio`). +* Setting :rcraw:`title.border` or :rcraw:`abc.border` to ``True`` automatically + sets :rcraw:`title.bbox` or :rcraw:`abc.bbox` to ``False``, and vice versa. + +.. _ug_rctable: + +Table of settings +----------------- + +A comprehensive table of the new proplot settings is shown below. + +.. include:: _static/rctable.rst + +.. _ug_proplotrc: + +The proplotrc file +------------------ + +When you import proplot for the first time, a ``proplotrc`` file is generated with +all lines commented out. This file is just like `matplotlibrc `_, +except it controls both matplotlib *and* proplot settings. The syntax is essentially +the same as matplotlibrc, and the file path is very similar to matplotlibrc. On most +platforms it is found in ``~/.proplot/proplotrc``, but a loose hidden file in the +home directory named ``~/.proplotrc`` is also allowed (use +`~proplot.config.Configurator.user_file` to print the path). To update this file +after a version change, simply remove it and restart your python session. + +To change the global `~proplot.config.rc` settings, edit and uncomment the lines +in the ``proplotrc`` file. To change the settings for a specific project, place a file +named either ``.proplotrc`` or ``proplotrc`` in the same directory as your python +session, or in an arbitrary parent directory. To generate a ``proplotrc`` file +containing the settings you have changed during a python session, use +`~proplot.config.Configurator.save` (use `~proplot.config.Configurator.changed` +to preview a dictionary of the changed settings). To explicitly load a ``proplotrc`` +file, use `~proplot.config.Configurator.load`. + +As an example, a ``proplotrc`` file containing the default settings +is shown below. .. include:: _static/proplotrc :literal: diff --git a/docs/contributing.rst b/docs/contributing.rst new file mode 100644 index 000000000..3bdd7dc21 --- /dev/null +++ b/docs/contributing.rst @@ -0,0 +1 @@ +.. include:: ../CONTRIBUTING.rst \ No newline at end of file diff --git a/docs/contributions.rst b/docs/contributions.rst deleted file mode 100644 index 25b5d4702..000000000 --- a/docs/contributions.rst +++ /dev/null @@ -1 +0,0 @@ -.. include:: ../HOWTOCONTRIBUTE.rst diff --git a/docs/cycles.ipynb b/docs/cycles.ipynb deleted file mode 100644 index a02618cc7..000000000 --- a/docs/cycles.ipynb +++ /dev/null @@ -1,277 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Color cycles" - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "ProPlot defines **color cycles** as color palettes comprising sets of *distinct colors*. Unlike :ref:`colormaps `, interpolation between these colors may not make sense. Color cycles are generally used with bar plots, line plots, and other distinct plot elements. ProPlot uses the `~proplot.styletools.ListedColormap` class to *name* color cycles, then applies them to plots by updating `the property cycler `__. Color cycles can also be made by sampling colormaps (see :ref:`Making new color cycles`).\n", - "\n", - "ProPlot adds several features to help you use color cycles effectively in your figures. This section documents the new registered color cycles, explains how to make and modify colormaps, and shows how to apply them to your plots." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Included color cycles" - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "Use `~proplot.styletools.show_cycles` to generate a table of the color cycles registered by default and loaded from your ``~/.proplot/cycles`` folder. You can make your own color cycles using the `~proplot.styletools.Cycle` constructor function." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", - "f = plot.show_cycles()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Changing the color cycle" - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "You can make and apply new property cyclers with the `~proplot.styletools.Cycle` constructor function. Various plotting commands like `~matplotlib.axes.Axes.plot` and `~matplotlib.axes.Axes.scatter` now accept a `cycle` keyword arg, which is passed to `~proplot.styletools.Cycle` (see `~proplot.wrappers.cycle_changer`). To save your color cycle data and use it every time ProPlot is imported, simply pass ``save=True`` to `~proplot.styletools.Cycle`. If you want to change the global property cycler, pass a *name* to the :rcraw:`cycle` setting or pass the result of `~proplot.styletools.Cycle` to the :rcraw:`axes.prop_cycle` setting (see :ref:`Configuring proplot`)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", - "import numpy as np\n", - "lw = 5\n", - "state = np.random.RandomState(51423)\n", - "data = (state.rand(12, 6) - 0.45).cumsum(axis=0)\n", - "kwargs = {'legend': 'b', 'labels': list('abcdef')}\n", - "\n", - "# Modify the default color cycle\n", - "plot.rc.cycle = '538'\n", - "f, axs = plot.subplots(ncols=3, axwidth=1.9)\n", - "axs.format(suptitle='Changing the color cycle globally and locally')\n", - "ax = axs[0]\n", - "ax.plot(data, lw=lw, **kwargs)\n", - "\n", - "# Pass the cycle to a plotting command\n", - "ax = axs[1]\n", - "ax.plot(data, cycle='qual1', lw=lw, **kwargs)\n", - "\n", - "# As above but draw each line individually\n", - "# Note that the color cycle is not reset with each plot call\n", - "ax = axs[2]\n", - "labels = kwargs['labels']\n", - "for i in range(data.shape[1]):\n", - " ax.plot(data[:, i], cycle='qual1', legend='b', label=labels[i], lw=lw)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Making new color cycles" - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "You can make new color cycles with the `~proplot.styletools.Cycle` constructor function. One great way to make cycles is by sampling a colormap! Just pass the colormap name to `~proplot.styletools.Cycle`, and optionally specify the number of samples you want to draw as the last positional argument (e.g. ``plot.Cycle('Blues', 5)``).\n", - "\n", - "Positional arguments passed to `~proplot.styletools.Cycle` are interpreted by the `~proplot.styletools.Colormap` constructor, and the resulting colormap is sampled at discrete values. To exclude near-white colors on the end of a colormap, pass e.g. ``left=x`` to `~proplot.styletools.Cycle`, or supply a plotting command with e.g. ``cycle_kw={'left':x}``. See :ref:`Colormaps` for details.\n", - "\n", - "In the below example, several cycles are constructed from scratch, and the lines are referenced with colorbars and legends. For making colorbars from a list of lines, see :ref:`Colorbars and legends`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", - "import numpy as np\n", - "f, axs = plot.subplots(ncols=2, share=0, axwidth=2, aspect=1.2)\n", - "state = np.random.RandomState(51423)\n", - "data = (20*state.rand(10, 21) - 10).cumsum(axis=0)\n", - "\n", - "# Cycle from on-the-fly monochromatic colormap\n", - "ax = axs[0]\n", - "lines = ax.plot(data[:, :5], cycle='plum', cycle_kw={'left': 0.3}, lw=5)\n", - "f.colorbar(lines, loc='b', col=1, values=np.arange(0, len(lines)))\n", - "f.legend(lines, loc='b', col=1, labels=np.arange(0, len(lines)))\n", - "ax.format(title='Cycle from color')\n", - "\n", - "# Cycle from registered colormaps\n", - "ax = axs[1]\n", - "cycle = plot.Cycle('blues', 'reds', 'oranges', 15, left=0.1)\n", - "lines = ax.plot(data[:, :15], cycle=cycle, lw=5)\n", - "f.colorbar(lines, loc='b', col=2, values=np.arange(0, len(lines)), locator=2)\n", - "f.legend(lines, loc='b', col=2, labels=np.arange(0, len(lines)), ncols=4)\n", - "ax.format(\n", - " title='Cycle from merged colormaps',\n", - " suptitle='Color cycles from colormaps'\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Generic property cycles" - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "`~proplot.styletools.Cycle` can also generate cyclers that change properties other than color. Below, a single-color dash style cycler is constructed and applied to the axes locally. To apply it globally, simply use ``plot.rc['axes.prop_cycle'] = cycle``." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", - "import numpy as np\n", - "import pandas as pd\n", - "f, ax = plot.subplots(axwidth=3, aspect=1.5)\n", - "state = np.random.RandomState(51423)\n", - "data = (state.rand(20, 4) - 0.5).cumsum(axis=0)\n", - "data = pd.DataFrame(data, columns=pd.Index(['a', 'b', 'c', 'd'], name='label'))\n", - "ax.format(suptitle='Plot without color cycle')\n", - "cycle = plot.Cycle(dashes=[(1, 0.5), (1, 1.5), (3, 0.5), (3, 1.5)])\n", - "obj = ax.plot(\n", - " data, lw=3, cycle=cycle, legend='ul',\n", - " legend_kw={'ncols': 2, 'handlelength': 3}\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Downloading color cycles" - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "There are plenty of online interactive tools for generating and testing color cycles, including `i want hue `__, `coolers `__, and `viz palette `__.\n", - "\n", - "To add color cycles downloaded from any of these sources, save the cycle data to a file in your ``~/.proplot/cycles`` folder and call `~proplot.styletools.register_cycles` (or restart your python session), or use `~proplot.styletools.ListedColormap.from_file`. The file name is used as the registered cycle name. See `~proplot.styletools.ListedColormap.from_file` for a table of valid file extensions." - ] - } - ], - "metadata": { - "celltoolbar": "Raw Cell Format", - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.3" - }, - "toc": { - "colors": { - "hover_highlight": "#ece260", - "navigate_num": "#000000", - "navigate_text": "#000000", - "running_highlight": "#FF0000", - "selected_highlight": "#fff968", - "sidebar_border": "#ffffff", - "wrapper_background": "#ffffff" - }, - "moveMenuLeft": false, - "nav_menu": { - "height": "84px", - "width": "252px" - }, - "navigate_menu": true, - "number_sections": true, - "sideBar": true, - "threshold": 4, - "toc_cell": false, - "toc_section_display": "block", - "toc_window_display": true, - "widenNotebook": false - }, - "varInspector": { - "cols": { - "lenName": 16, - "lenType": 16, - "lenVar": 40 - }, - "kernels_config": { - "python": { - "delete_cmd_postfix": "", - "delete_cmd_prefix": "del ", - "library": "var_list.py", - "varRefreshCmd": "print(var_dic_list())" - }, - "r": { - "delete_cmd_postfix": ") ", - "delete_cmd_prefix": "rm(", - "library": "var_list.r", - "varRefreshCmd": "cat(var_dic_list()) " - } - }, - "types_to_exclude": [ - "module", - "function", - "builtin_function_or_method", - "instance", - "_Feature" - ], - "window_display": false - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/docs/cycles.py b/docs/cycles.py new file mode 100644 index 000000000..3cf06d172 --- /dev/null +++ b/docs/cycles.py @@ -0,0 +1,219 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.11.4 +# kernelspec: +# display_name: Python 3 +# language: python +# name: python3 +# --- + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_cycles: +# +# Color cycles +# ============ +# +# Proplot defines **color cycles** or **discrete colormaps** as color palettes +# comprising sets of *distinct colors*. Unlike :ref:`continuous colormaps `, +# interpolation between these colors may not make sense. Generally, color cycles are +# used with distinct plot elements like lines and bars. Occasionally, +# they are used with categorical data as "qualitative" colormaps. Proplot's +# color cycles are registered as `~proplot.colors.DiscreteColormap`\ s, +# and can be easily converted into `property cyclers +# `__ +# for use with distinct plot elements using the `~proplot.constructor.Cycle` +# constructor function. `~proplot.constructor.Cycle` can also +# :ref:`extract colors ` from `~proplot.colors.ContinuousColormap`\ s. +# +# Proplot :ref:`adds several features ` to help you use color +# cycles effectively in your figures. This section documents the new registered +# color cycles, explains how to make and modify color cycles, and shows how to +# apply them to your plots. + + +# %% [raw] raw_mimetype="text/restructuredtext" tags=[] +# .. _ug_cycles_included: +# +# Included color cycles +# --------------------- +# +# Use `~proplot.demos.show_cycles` to generate a table of registered color +# cycles. The table includes the default color cycles registered by proplot and +# "user" color cycles created with the `~proplot.constructor.Cycle` constructor +# function or loaded from `~proplot.config.Configurator.user_folder`. If you need +# the list of colors associated with a registered or on-the-fly color cycle, +# simply use `~proplot.utils.get_colors`. + +# %% +import proplot as pplt +fig, axs = pplt.show_cycles(rasterized=True) + + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_cycles_changing: +# +# Changing the color cycle +# ------------------------ +# +# Most 1D `~proplot.axes.PlotAxes` commands like `~proplot.axes.PlotAxes.line` +# and `~proplot.axes.PlotAxes.scatter` accept a `cycle` keyword (see the +# :ref:`1D plotting section `). This can be used to change the +# color cycle on-the-fly, whether plotting with successive calls to +# `~proplot.axes.PlotAxes` commands or a single call using 2D array(s) (see +# the :ref:`1D plotting section `). To change the global property +# cycler, pass a `~proplot.colors.DiscreteColormap` or cycle name +# to :rcraw:`cycle` or pass the result of `~proplot.constructor.Cycle` +# to :rcraw:`axes.prop_cycle` (see the :ref:`configuration guide `). + +# %% +import proplot as pplt +import numpy as np + +# Sample data +state = np.random.RandomState(51423) +data = (state.rand(12, 6) - 0.45).cumsum(axis=0) +kwargs = {'legend': 'b', 'labels': list('abcdef')} + +# Figure +lw = 5 +pplt.rc.cycle = '538' +fig = pplt.figure(refwidth=1.9, suptitle='Changing the color cycle') + +# Modify the default color cycle +ax = fig.subplot(131, title='Global color cycle') +ax.plot(data, lw=lw, **kwargs) + +# Pass the cycle to a plotting command +ax = fig.subplot(132, title='Local color cycle') +ax.plot(data, cycle='qual1', lw=lw, **kwargs) + +# As above but draw each line individually +# Note that passing cycle=name to successive plot calls does +# not reset the cycle position if the cycle is unchanged +ax = fig.subplot(133, title='Multiple plot calls') +labels = kwargs['labels'] +for i in range(data.shape[1]): + ax.plot(data[:, i], cycle='qual1', legend='b', label=labels[i], lw=lw) + + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_cycles_new: +# +# Making color cycles +# ------------------- +# +# Proplot includes tools for merging color cycles, modifying existing color +# cycles, making new color cycles, and saving color cycles for future use. +# Most of these features can be accessed via the `~proplot.constructor.Cycle` +# :ref:`constructor function `. This command returns +# `~cycler.Cycler` instances whose `color` properties are determined by the +# positional arguments (see :ref:`below ` for changing other +# properties). Note that every `~proplot.axes.PlotAxes` command that accepts a +# `cycle` keyword passes it through this function (see the :ref:`1D plotting +# section `). + +# Positional arguments passed to `~proplot.constructor.Cycle` are interpreted +# by the `~proplot.constructor.Colormap` constructor function. If the result +# is a `~proplot.colors.DiscreteColormap`, those colors are used for the resulting +# `~cycler.Cycler`. If the result is a `~proplot.colors.ContinuousColormap`, the +# colormap is sampled at `N` discrete values -- for example, ``pplt.Cycle('Blues', 5)`` +# selects 5 evenly-spaced values. When building color cycles on-the-fly, for example +# with ``ax.plot(data, cycle='Blues')``, proplot automatically selects as many colors +# as there are columns in the 2D array (i.e., if we are drawing 10 lines using an array +# with 10 columns, proplot will select 10 evenly-spaced values from the colormap). +# To exclude near-white colors on the end of a colormap, pass e.g. ``left=x`` +# to `~proplot.constructor.Cycle`, or supply a plotting command with e.g. +# ``cycle_kw={'left': x}``. See the :ref:`colormaps section ` for details. +# +# In the below example, several color cycles are constructed from scratch, and +# the lines are referenced with colorbars and legends. Note that proplot permits +# generating colorbars from :ref:`lists of artists `. + +# %% +import proplot as pplt +import numpy as np +fig = pplt.figure(refwidth=2, share=False) +state = np.random.RandomState(51423) +data = (20 * state.rand(10, 21) - 10).cumsum(axis=0) + +# Cycle from on-the-fly monochromatic colormap +ax = fig.subplot(121) +lines = ax.plot(data[:, :5], cycle='plum', lw=5) +fig.colorbar(lines, loc='b', col=1, values=np.arange(0, len(lines))) +fig.legend(lines, loc='b', col=1, labels=np.arange(0, len(lines))) +ax.format(title='Cycle from a single color') + +# Cycle from registered colormaps +ax = fig.subplot(122) +cycle = pplt.Cycle('blues', 'reds', 'oranges', 15, left=0.1) +lines = ax.plot(data[:, :15], cycle=cycle, lw=5) +fig.colorbar(lines, loc='b', col=2, values=np.arange(0, len(lines)), locator=2) +fig.legend(lines, loc='b', col=2, labels=np.arange(0, len(lines)), ncols=4) +ax.format(title='Cycle from merged colormaps', suptitle='Color cycles from colormaps') + + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_cycles_other: +# +# Cycles of other properties +# -------------------------- +# +# `~proplot.constructor.Cycle` can generate `~cycler.Cycler` instances that +# change `~proplot.axes.PlotAxes.line` and `~proplot.axes.PlotAxes.scatter` +# properties other than `color`. In the below example, a single-color line +# property cycler is constructed and applied to the axes locally using the +# line properties `lw` and `dashes` (the aliases `linewidth` or `linewidths` +# would also work). The resulting property cycle can be applied globally +# using ``pplt.rc['axes.prop_cycle'] = cycle``. + +# %% +import proplot as pplt +import numpy as np +import pandas as pd + +# Cycle that loops through 'dashes' Line2D property +cycle = pplt.Cycle(lw=3, dashes=[(1, 0.5), (1, 1.5), (3, 0.5), (3, 1.5)]) + +# Sample data +state = np.random.RandomState(51423) +data = (state.rand(20, 4) - 0.5).cumsum(axis=0) +data = pd.DataFrame(data, columns=pd.Index(['a', 'b', 'c', 'd'], name='label')) + +# Plot data +fig, ax = pplt.subplots(refwidth=2.5, suptitle='Plot without color cycle') +obj = ax.plot( + data, cycle=cycle, legend='ll', + legend_kw={'ncols': 2, 'handlelength': 2.5} +) + + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_cycles_dl: +# +# Downloading color cycles +# ------------------------ +# +# There are several interactive online tools for generating perceptually +# distinct color cycles, including +# `i want hue `__, +# `Color Cycle Picker `__, +# `Colorgorical `__, +# `Adobe Color `__, +# `Color Hunt `__, +# `Coolers `__, +# and `Color Drop `__. + +# To add color cycles downloaded from any of these sources, save the color data file +# to the ``cycles`` subfolder inside `~proplot.config.Configurator.user_folder`, +# or to a folder named ``proplot_cycles`` in the same directory as your python session +# or an arbitrary parent directory (see `~proplot.config.Configurator.local_folders`). +# After adding the file, call `~proplot.config.register_cycles` or restart your python +# session. You can also use `~proplot.colors.DiscreteColormap.from_file` or manually +# pass `~proplot.colors.DiscreteColormap` instances or file paths to +# `~proplot.config.register_cycles`. See `~proplot.config.register_cycles` +# for a table of recognized data file extensions. diff --git a/docs/environment.yml b/docs/environment.yml index 742d62d18..0565f0de5 100644 --- a/docs/environment.yml +++ b/docs/environment.yml @@ -1,24 +1,39 @@ # Hard requirements for notebook examples and documentation build -# Proplot itself just needs matplotlib -# NOTE: PyQt5 is needed by pyplot, RTD server *happens* to already have it -# but creating local environment will fail. +# WARNING: Keep this up-to-date with ci/environment.yml +# * basemap is broken as of matplotlib >= 3.3 so for documentation +# use 3.2.1. Probably lots of basemap holdouts for next ~5 years. +# * basemap geography is weird with the geos >= 3.9.0 required by cartopy >= +# 0.19, but important to use 0.19 becuase it fixes padding, so live with it. +# * pyqt5 is needed by pyplot, RTD server *happens* to already have it +# but creating local environment will fail. +# * jinja >= 3.0 and nbsphinx >= 0.8.2 seem to break default ReST roles when +# jupytext notebooks are converted to HTML. Interpreted as italics instead. +# * markupsafe >= 2.1.0 seems to be broken with jinja < 3.0 so also have to +# manually specify that: https://github.com/pallets/markupsafe/issues/284 +# * docutils >= 0.17 breaks bullet points. See the following thread +# for more info: https://github.com/readthedocs/sphinx_rtd_theme/issues/1115 name: proplot-dev channels: - conda-forge dependencies: - - python>=3.6 - - numpy - - xarray + - python==3.8 + - numpy==1.19.5 + - matplotlib==3.2.2 + - cartopy==0.20.2 - pandas - - matplotlib - - cartopy - - basemap - - pandoc + - xarray - ipykernel + - pandoc - pip - pip: - .. - pyqt5 - - nbsphinx - - sphinx_rtd_theme - - git+https://github.com/lukelbd/sphinx-automodapi@v0.6.proplot-mods + - docutils==0.16 + - sphinx>=3.0 + - sphinx-copybutton + - sphinx-rtd-light-dark + - jinja2==2.11.3 + - markupsafe==2.0.1 + - nbsphinx==0.8.1 + - jupytext + - git+https://github.com/proplot-dev/sphinx-automodapi@proplot-mods diff --git a/docs/external-links.rst b/docs/external-links.rst index 9fee09fb3..3fd2d9941 100644 --- a/docs/external-links.rst +++ b/docs/external-links.rst @@ -1,48 +1,125 @@ +.. _external_links: + ============== External links ============== -This page contain links to related projects and projects that inspired ProPlot -or are directly used by ProPlot. - +This page contains links to related external projects. Python packages =============== -* `matplotlib `__ - The venerable plotting package that we all know and love. -* `xarray `__ - Package for working with annotated ND numpy arrays. If you haven't heard of it and you work with NetCDF files, it will change your life. -* `pandas `__ - Package that turns spreadsheets and tables into annotated 2D numpy arrays. Invaluable for certain types of data. -* `seaborn `__ - A statistical data visualization package. It has some awesome features, but few people in my field use it because it is geared more toward statistical data than geophysical data. + +The following packages inspired proplot, are required or optional +dependencies of proplot, or are distributed with proplot: + +* `matplotlib `__ - The powerful data visualization + package we all know and love. +* `xarray `__ - A package for working with + annotated ND numpy arrays. If you haven't heard of it and you work with NetCDF files, + it will change your life. +* `pandas `__ - A package that turns spreadsheets and + tables into annotated 2D numpy arrays. Invaluable for many types of datasets. +* `pint `__ - A package for tracking and + converting between physical units during mathematical operations and when + plotting in matplotlib axes. +* `cartopy `__ - A package for + plotting geographic and geophysical data in matplotlib. Includes a suite of + different map projections. +* `basemap `__ - The original cartographic + plotting package. Basemap is less closely integrated with matplotlib than + cartopy but still quite popular. As of 2020 it is no longer actively maintained. +* `seaborn `__ - A statistical data visualization package. + Seaborn is based on matplotlib but its interface is mostly separate from matplotlib. + It is not generally suitable for geophysical data. +* `hsluv-python `__ - + A python implementation of `HSLuv `__ used for + the hue, saturation, luminance math required by `~proplot.colors.PerceptualColormap`. +* `TeX Gyre `__ - + An open source re-implementation of popular fonts like + `Helvetica `__ + and `Century `__. + These are distributed with proplot and used for its default font families. +* `Fira Math `__ - + An open source sans-serif font with a zillion glyphs for mathematical symbols. + This is distributed with proplot as a viable alternative to + `DejaVu Sans `__. Downloadable colormaps ====================== + The following colormap repositories are -imported and registered by ProPlot. +imported and registered by proplot. + +* `Color Brewer `__ - The + O.G. perceptually uniform colormap distribution. These are included with + matplotlib by default. +* `cmOcean `__ - Perceptually uniform colormaps + designed for oceanography, but suitable for plenty of other applications. +* `SciVisColor `__ - Science-focused colormaps created by the + viz team at UT Austin. Provides tools for concatenating colormaps, suitable for + complex datasets with funky distributions. +* `Fabio Crameri `__ - Perceptually + uniform colormaps for geoscientists. These maps have unusual and interesting + color transitions. -#. `Color Brewer `__ - The O.G. perceptually uniform colormap distribution. These are included with the matplotlib distribution. -#. `cmocean `__ - Perceptually uniform colormaps designed for oceanography, but suitable for plenty of other applications. -#. `SciVisColor `__ - Science-focused colormaps created by the viz team at UT Austin. Provides tools for *concatenating* colormaps, suitable for complex datasets with weird distributions. -#. `Fabio Crameri `__ - Perceptually uniform colormaps for geosciences. These maps have very unusual and interesting color transitions. +.. + * `Cube Helix `__ - A + series of colormaps generated by rotating through RGB channel values. The colormaps + were added from `Palletable `__. -Online tools for making new colormaps -===================================== +Tools for making new colormaps +============================== -Use these resources to make colormaps from scratch. +Use these resources to make colormaps from scratch. Then import +them into proplot by adding files to the ``.proplot/cmaps`` folder +(see :ref:`this section ` for details). -#. `Proplot API `__ -#. `HCL Picker `__ -#. `Chroma.js `__ -#. `HCL Wizard `__ -#. `SciVisColor `__ +* `The proplot API `__ - + Namely, the `~proplot.colors.ContinuousColormap` class and + `~proplot.constructor.Colormap` constructor function. +* `HCL Wizard `__ - + An advanced interface for designing perceptually uniform colormaps, + with example plots, channel plots, and lots of sliders. +* `SciVisColor `__ - + An advanced interface for concatenating segments from a suite of colormap + presets. Useful for datasets with complex statistical distributions. +* `CCC-tool `__ - + An advanced interface for designing, analyzing, and concatenating colormaps, + leaning on the `SciViscolor `__ presets. +* `HCL Picker `__ - + A simple interface for taking cross-sections of the HCL colorspace. + Resembles the examples :ref:`shown here `. +* `Chroma.js `__ - + A simple interface for Bezier interpolating between lists of colors, + with adjustable hue, chroma, and luminance channels. -Online tools for making new color cycles -======================================== +Tools for making new color cycles +================================= -Use these resources to make color cycles from scratch. +Use these resources to make color cycles from scratch. Then import +them into proplot by adding files to the ``.proplot/cycles`` folder +(see :ref:`this section ` for details). -#. `Proplot API `__ -#. `Color Cycle Picker `__ -#. `i want hue `__ -#. `Coolors `__ -#. `Color Hunt `__ -#. `Color Drop `__ -#. `Adobe Color `__ +* `The proplot API `__ - + Namely, the `~proplot.colors.DiscreteColormap` class and + `~proplot.constructor.Cycle` constructor function. +* `i want hue `__ - + An advanced interface for generating perceptually distinct color sets + with options for restricting the hue, chroma, and luminance ranges. +* `Color Cycle Picker `__ - + An advanced interface for generating perceptually distinct color sets + based on seed colors, with colorblind-friendliness measures included. +* `Colorgorical `__ - + An advanced interface for making perceptually distinct colors sets + with both seed color and channel restriction options. +* `Adobe Color `__ - A simple interface + for selecting color sets derived from sample images, including an option + to upload images and a searchable image database. +* `Color Hunt `__ - A simple interface for selecting + preset color sets voted on by users and grouped into stylistic categories + like "summer" and "winter". +* `Coolors `__ - A simple interface for building + randomly-generated aesthetically-pleasing color sets that are not + necessarily uniformly perceptually distinct. +* `Color Drop `__ - A simple interface + for selecting preset color sets voted on by users. diff --git a/docs/faq.rst b/docs/faq.rst index c9eb894cb..49a5fede9 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -2,54 +2,109 @@ Frequently asked questions ========================== -What makes this matplotlib wrapper different? -============================================= - -There is already a great matplotlib wrapper called `seaborn `__. Also, `pandas `__ and `xarray `__ both offer convenient matplotlib plotting commands. How does ProPlot compare against these tools? - -* ProPlot, seaborn, pandas, and xarray all offer tools for generating rigid, simple, nice-looking plots from data stored in `~pandas.DataFrame`\ s and `~xarray.DataArray`\ s (ProPlot tries to apply labels from these objects, just like pandas and xarray). -* Unlike seaborn, pandas, and xarray, ProPlot *also* works for arbitrarily complex subplot grids, and ProPlot provides tools for heavily customizing plots. -* ProPlot is integrated with *cartopy* and *basemap*. You will find plotting geophysical data in ProPlot to be much more concise than working with cartopy and basemap directly. -* ProPlot *expands upon* the seaborn tools for working with color and global settings. For example, see `~proplot.styletools.Colormap`, `~proplot.styletools.PerceptuallyUniformColormap`, and `~proplot.rctools.rc_configurator`. -* ProPlot *expands upon* matplotlib by fixing various quirks, developing a more sophisticated automatic layout algorithm, simplifying the process of drawing outer colorbars and legends, and much more. -* ProPlot is *built right into the matplotlib API*, thanks to special subclasses of the `~matplotlib.figure.Figure` and `~matplotlib.axes.Axes` classes, while seaborn, pandas, and xarray are meant to be used separately from the matplotlib API. - -In a nutshell, ProPlot is intended to *unify the convenience of seaborn, pandas, and xarray plotting with the power and customizability of the underlying matplotlib API*. +What makes this project different? +================================== + +There is already a great matplotlib wrapper called +`seaborn `__. Also, `pandas +`__ +and `xarray `__ +both offer convenient matplotlib plotting commands. +How does proplot compare against these tools? + +* Proplot, seaborn, pandas, and xarray all offer tools for generating rigid, simple, + nice-looking plots from data stored in `~pandas.DataFrame`\ s and + `~xarray.DataArray`\ s (proplot tries to apply labels from these objects, just like + pandas and xarray). +* Proplot is integrated with *cartopy* and *basemap*. You will find plotting geophysical + data in proplot to be much more concise than working with cartopy and basemap + directly. +* Proplot *expands upon* the seaborn tools for working with color and global settings. + For example, see `~proplot.constructor.Colormap`, + `~proplot.colors.PerceptualColormap`, and `~proplot.config.Configurator`. +* Proplot *expands upon* matplotlib by fixing various quirks, developing a more + advanced automatic layout algorithm, simplifying the process of drawing outer + colorbars and legends, and much more. +* Proplot is *built right into the matplotlib API*, thanks to special subclasses of the + `~matplotlib.figure.Figure` and `~matplotlib.axes.Axes` classes, while seaborn, + pandas, and xarray are meant to be used separately from the matplotlib API. + +In a nutshell, proplot is intended to *unify the convenience of seaborn, pandas, and +xarray plotting with the power and customizability of the underlying matplotlib API*. .. - So while ProPlot includes similar tools, the scope and goals are largely different. - Indeed, parts of ProPlot were inspired by these projects -- in particular, ``rctools.py`` and ``colortools.py`` are modeled after seaborn. However the goals and scope of ProPlot are largely different: + So while proplot includes similar tools, the scope and goals are largely different. + Indeed, parts of proplot were inspired by these projects -- in particular, + ``setup.py`` and ``colortools.py`` are modeled after seaborn. However the goals and + scope of proplot are largely different: Why didn't you add to matplotlib directly? ========================================== -Since ProPlot is built right into the matplotlib API, you might be wondering why we didn't contribute to the matplotlib project directly. - -* Certain features directly conflict with matplotlib. For example, ProPlot's tight layout algorithm conflicts with matplotlib's `tight layout `__ by permitting *fluid figure dimensions*, and the new `~proplot.subplots.GridSpec` class permits *variable spacing* between rows and columns and uses *physical units* rather than figure-relative and axes-relative units. -* Certain features are arguably too redundant. For example, `~proplot.axes.Axes.format` is convenient, but the same tasks can be accomplished with existing axes and axis "setter" methods. Also, some of the functionality of `~proplot.subplots.subplots` can be replicated with `axes_grid1 `__. Following `TOOWTDI `__ philosophy, these features should probably not be integrated. +Since proplot is built right into the matplotlib API, you might be wondering why we +didn't contribute to the matplotlib project directly. + +* Certain features directly conflict with matplotlib. For example, proplot's tight + layout algorithm conflicts with matplotlib's `tight layout + `__ by + permitting *fluid figure dimensions*, and the new `~proplot.gridspec.GridSpec` class + permits *variable spacing* between rows and columns and uses *physical units* rather + than figure-relative and axes-relative units. +* Certain features are arguably too redundant. For example, `~proplot.axes.Axes.format` + is convenient, but the same tasks can be accomplished with existing axes and axis + "setter" methods. Also, some of the functionality of `~proplot.ui.subplots` can be + replicated with `axes_grid1 + `__. Following `TOOWTDI + `__ philosophy, these features should probably + not be integrated. .. - * ProPlot design choices are made with the academic scientist working with ipython notebooks in mind, while matplotlib has a much more diverse base of hundreds of thousands of users. Matplotlib developers have to focus on support and API consistency, while ProPlot can make more dramatic improvements. + * Proplot design choices are made with the academic scientist working with ipython + notebooks in mind, while matplotlib has a much more diverse base of hundreds of + thousands of users. Matplotlib developers have to focus on support and API + consistency, while proplot can make more dramatic improvements. -Nevertheless, if any core matplotlib developers think that some of ProPlot's features should be added to matplotlib, please contact `Luke Davis `__ and let him know! +.. + Nevertheless, if any core matplotlib developers think that some + of proplot's features should be added to matplotlib, please contact + `Luke Davis `__ and let him know! Why do my inline figures look different? ======================================== -These days, most publications prefer plots saved as `vector graphics `__ [1]_ rather than `raster graphics `__ [2]_. When you save vector graphics, the content sizes should be appropriate for embedding the plot in a document (for example, if an academic journal recommends 8-point font for plots, you should use 8-point font in your plotting code). +These days, most publications prefer plots saved as +`vector graphics `__ [1]_ +rather than `raster graphics `__ [2]_. +When you save vector graphics, the content sizes should be appropriate for embedding the +plot in a document (for example, if an academic journal recommends 8-point font for +plots, you should use 8-point font in your plotting code). + +Most of the default matplotlib backends make low-quality, artifact-plagued jpegs. To +keep them legible, matplotlib uses a fairly large default figure width of 6.5 inches +(usually only suitable for multi-panel plots) and a slightly large default font size of +10 points (where most journals recommend 5-9 points). This means your figures have to be +downscaled so the sizes used in your plotting code are *not* the sizes that appear in +the document. + +Proplot helps you get your figure sizes *correct* for embedding them as vector graphics +inside publications. It uses a slightly smaller default font size, calculates the +default figure size from the number of subplot rows and columns, and adds the `journal` +keyword argument to `~proplot.figure.Figure` which can be used to employ figure +dimensions from a particular journal standard. To keep the inline figures legible, +proplot also employs a *higher quality* default inline backend. + +.. [1] `Vector graphics `__ use physical + units (e.g. inches, `points `__), + are infinitely scalable, and often have much smaller file sizes than bitmap graphics. + You should consider using them even when your plots are not destined for publication. + PDF, SVG, and EPS are the most common formats. + +.. [2] `Raster graphics `__ use pixels + and are *not* infinitely scalable. They tend to be faster to display and easier + to view, but they are discouraged by most academic publishers. PNG and JPG are the + most common formats. -Most of the default matplotlib backends make low-quality, artifact-plagued jpegs. To keep them legible, matplotlib uses a fairly large default figure width of 6.5 inches (usually only suitable for multi-panel plots) and a slightly large default font size of 10 points (where most journals recommend 5-9 points). This means your figures have to be downscaled so the sizes used in your plotting code are *not* the sizes that appear in the document. - -ProPlot helps you get your figure sizes *correct* for embedding -them as vector graphics inside publications. -It uses a slightly smaller default font size, calculates the default figure -size from the number of subplot rows and columns, and -adds the `journal` keyword argument to `~proplot.subplots.Figure` which can -be used to employ figure dimensions from a particular journal standard. -To keep the inline figures legible, ProPlot also employs a *higher quality* default -inline backend. - -.. [1] `Vector graphics `__ use physical units (e.g. inches, `points `__), are infinitely scalable, and often have much smaller file sizes than bitmap graphics. You should consider using them even when your plots are not destined for publication. PDF, SVG, and EPS are the most common formats. -.. [2] `Raster graphics `__ use pixels and are *not* infinitely scalable. They tend to be faster to display and easier to view, but they are discouraged by most academic publishers. PNG and JPG are the most common formats. - -.. users to enlarge their figure dimensions and font sizes so that content inside of the inline figure is visible -- but when saving the figures for publication, it generally has to be shrunk back down! +.. + users to enlarge their figure dimensions and font sizes so that content inside of the + inline figure is visible -- but when saving the figures for publication, it generally + has to be shrunk back down! diff --git a/docs/fonts.py b/docs/fonts.py new file mode 100644 index 000000000..6cc1264c3 --- /dev/null +++ b/docs/fonts.py @@ -0,0 +1,151 @@ +# -*- coding: utf-8 -*- +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.11.4 +# kernelspec: +# display_name: Python 3 +# language: python +# name: python3 +# --- + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_fonts: +# +# +# Font selection +# ============== +# +# Proplot registers several new fonts and includes tools +# for adding your own fonts. These features are described below. +# +# +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_fonts_included: +# +# Included fonts +# -------------- +# +# Matplotlib provides a `~matplotlib.font_manager` module for working with +# system fonts and classifies fonts into `five font families +# `__: +# :rcraw:`font.serif` :rcraw:`font.sans-serif`, :rcraw:`font.monospace`, +# :rcraw:`font.cursive`, and :rcraw:`font.fantasy`. The default font family +# is sans-serif, because sans-serif fonts are generally more suitable for +# figures than serif fonts, and the default font name belonging to this family +# is `DejaVu Sans `__, which comes packaged with +# matplotlib. +# +# Matplotlib uses DejaVu Sans in part because it includes glyphs for a very wide +# range of symbols, especially mathematical symbols. However in our opinion, +# DejaVu Sans is not very aesthetically pleasing. To improve the font selection while +# keeping things consistent across different workstations, proplot is packaged +# the open source `TeX Gyre fonts `__ and a few +# additional open source sans-serif fonts. Proplot also uses the TeX Gyre fonts as the +# first (i.e., default) entries for each of matplotlib's `font family lists +# `__: +# +# * The `Helvetica `__ lookalike +# :rcraw:`font.sans-serif` = ``'TeX Gyre Heros'``. +# * The `Century `__ lookalike +# :rcraw:`font.serif` = ``'TeX Gyre Schola'``. +# * The `Chancery `__ lookalike +# :rcraw:`font.cursive` = ``'TeX Gyre Chorus'``. +# * The `Avant Garde `__ lookalike +# :rcraw:`font.fantasy` = ``'TeX Gyre Adventor'``. +# * The `Courier `__ lookalike +# :rcraw:`font.monospace` = ``'TeX Gyre Cursor'``. +# +# After importing proplot, the default matplotlib font will be +# `TeX Gyre Heros `__, which +# emulates the more conventional and (in our opinion) aesthetically pleasing +# font `Helvetica `__. The default font +# family lists are shown in the :ref:`default proplotrc file `. +# To compare different fonts, use the `~proplot.demos.show_fonts` command with the +# `family` keyword (default behavior is ``family='sans-serif'``). Tables of the TeX +# Gyre and sans-serif fonts packaged with proplot are shown below. + +# %% +import proplot as pplt +fig, axs = pplt.show_fonts(family='sans-serif') + +# %% +import proplot as pplt +fig, axs = pplt.show_fonts(family='tex-gyre') + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_fonts_math: +# +# Math text fonts +# --------------- +# +# In matplotlib, math text rendered by TeX can be produced by surrounding +# an expression with ``$dollar signs$``. To help math text jive better with +# the new default :ref:`non-math text font `, proplot changes +# :rcraw:`mathtext.fontset` to ``'custom'``. This means that math is drawn with +# the italicized version of the non-math font (see the matplotlib `math text +# guide `__ +# for details). This generally improves the appearance of figures with simple +# math expressions. However, if you need unusual math symbols or complex math +# operators, you may want to change :rcraw:`font.name` to something more suitable +# for math (e.g., the proplot-packaged font ``'Fira Math'`` or the matplotlib-packaged +# font ``'DejaVu Sans'``; see `this page `__ for +# more on Fira Math). Alternatively, you can change the math text font alone by setting +# :rcraw:`mathtext.fontset` back to one of matplotlib's math-specialized font sets +# (e.g., ``'stixsans'`` or ``'dejavusans'``). +# +# A table of math text containing the sans-serif fonts packaged with proplot is shown +# below. The dummy glyph "¤" is shown where a given math character is unavailable +# for a particular font (in practice, the fallback font :rc:`mathtext.fallback` is used +# whenever a math character is unavailable, but `~proplot.demos.show_fonts` disables +# this fallback font in order to highlight the missing characters). +# +# .. note:: +# +# Proplot modifies matplotlib's math text internals so that the ``'custom'`` +# font set can be applied with modifications to the currently active non-math +# font rather than only a global font family. This works by changing the default +# values of :rcraw:`mathtext.bf`, :rcraw:`mathtext.it`, :rcraw:`mathtext.rm`, +# :rcraw:`mathtext.sf` from the global default font family ``'sans'`` to the local +# font family ``'regular'``, where ``'regular'`` is a dummy name permitted by +# proplot (see the :ref:`proplotrc file ` for details). This means +# that if :rcraw:`mathtext.fontset` is ``'custom'`` and the font family is changed +# for an arbitrary `~matplotlib.text.Text` instance, then any LaTeX-generated math +# in the text string will also use this font family. + +# %% +import proplot as pplt +fig, axs = pplt.show_fonts(family='sans-serif', math=True) + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_fonts_user: +# +# Using your own fonts +# -------------------- +# +# You can register your own fonts by adding files to the ``fonts`` subfolder +# inside `~proplot.config.Configurator.user_folder` and calling +# `~proplot.config.register_fonts`. This command is called on import. You can +# also manually pass file paths to `~proplot.config.register_fonts`. +# To change the default font, use the `~proplot.config.rc` +# object or modify your ``proplotrc``. See the +# :ref:`configuration section ` for details. +# +# Sometimes the font you would like to use *is* installed, but the font file +# is not stored under the matplotlib-compatible ``.ttf``, ``.otf``, or ``.afm`` +# formats. For example, several macOS fonts are unavailable because they are +# stored as ``.dfont`` collections. Also, while matplotlib nominally supports +# ``.ttc`` collections, proplot ignores them because figures with ``.ttc`` fonts +# `cannot be saved as PDFs `__. +# You can get matplotlib to use ``.dfont`` and ``.ttc`` collections by +# expanding them into individual ``.ttf`` files with the +# `DFontSplitter application `__, +# then saving the files in-place or in the ``~/.proplot/fonts`` folder. +# +# To find font collections, check the paths listed in ``OSXFontDirectories``, +# ``X11FontDirectories``, ``MSUserFontDirectories``, and ``MSFontDirectories`` +# under the `matplotlib.font_manager` module. diff --git a/docs/index.rst b/docs/index.rst index b358ff5eb..14712793f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,16 +1,17 @@ .. ProPlot documentation master file, created by sphinx-quickstart on Wed Feb 20 01:31:20 2019. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. + You can adapt this file completely to your liking, but + it should at least contain the root `toctree` directive. ======= ProPlot ======= -A comprehensive `matplotlib `__ wrapper for making beautiful, publication-quality graphics. This project is published `on GitHub `__. - -Please note that due to my day job as a graduate student, `certain feature additions `__ may be delayed to the summer of 2020. In the meantime, if you are interested in contributing to ProPlot, please see the :ref:`contribution guide `. Any amount of help is welcome! +A succinct `matplotlib `__ wrapper +for making beautiful, publication-quality graphics. This project +is `published on GitHub `__ and can +be cited using its `Zenodo DOI `__. .. toctree:: :maxdepth: 1 @@ -26,15 +27,17 @@ Please note that due to my day job as a graduate student, `certain feature addit basics subplots + cartesian + projections colorbars_legends - colormaps - cycles - colors_fonts - projection - axis insets_panels 1dplots 2dplots + stats + colormaps + cycles + colors + fonts configuration .. toctree:: @@ -42,14 +45,14 @@ Please note that due to my day job as a graduate student, `certain feature addit :caption: Reference api - changelog - contributions external-links + whatsnew + contributing authors - Indices and tables ================== * :ref:`genindex` * :ref:`modindex` +* :ref:`glossary` diff --git a/docs/insets_panels.ipynb b/docs/insets_panels.ipynb deleted file mode 100644 index 306aab4b9..000000000 --- a/docs/insets_panels.ipynb +++ /dev/null @@ -1,226 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Insets and panels" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Panel axes\n", - "\n" - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "Often, it can be useful to have narrow \"panels\" along the edge of a larger subplot for plotting secondary 1-dimensional datasets or summary statistics. In matplotlib, this requires a lot of work. In ProPlot, you can create \"panels\" by passing a location (e.g. ``'r'`` or ``'right'``) to the `~proplot.axes.Axes.panel` or `~proplot.axes.Axes.panel_axes` methods. The resulting axes are instances of `~proplot.axes.XYAxes`.\n", - "\n", - "To generate \"stacked\" panels, simply call `~proplot.axes.Axes.panel` or `~proplot.axes.Axes.panel_axes` more than once. To include panels when centering spanning axis labels and super titles, pass ``includepanels=True`` to `~proplot.subplots.Figure`. Panels do not interfere the tight layout algorithm and do not affect the aspect ratios of subplots (see :ref:`Subplots features` for details). In the first example, the panel separation from the main subplot is manually set to ``space=0``. In the second example, it is adjusted automatically by the tight layout algorithm." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", - "import numpy as np\n", - "state = np.random.RandomState(51423)\n", - "data = (state.rand(20, 20) - 0.48).cumsum(axis=1).cumsum(axis=0)\n", - "data = 10 * (data - data.min()) / (data.max() - data.min())\n", - "\n", - "# Stacked panels with outer colorbars\n", - "for loc_cbar, loc_panel in ('rb', 'br'):\n", - " f, axs = plot.subplots(\n", - " axwidth=1.6, nrows=1, ncols=2,\n", - " share=0, panelpad=0.1, includepanels=True\n", - " )\n", - " axs.contourf(\n", - " data, cmap='glacial', extend='both',\n", - " colorbar=loc_cbar, colorbar_kw={'label': 'colorbar'},\n", - " )\n", - " \n", - " # Summary statistics and settings\n", - " x1 = x2 = np.arange(20)\n", - " y1 = data.mean(axis=int(loc_panel == 'r'))\n", - " y2 = data.std(axis=int(loc_panel == 'r'))\n", - " titleloc = 'upper center'\n", - " if loc_panel == 'r':\n", - " titleloc = 'center'\n", - " x1, x2, y1, y2 = y1, y2, x1, x2\n", - " space = 0\n", - " width = '30pt'\n", - " kwargs = {'xreverse': False, 'yreverse': False, 'titleloc': titleloc}\n", - " \n", - " # Panels for plotting the mean\n", - " paxs1 = axs.panel(loc_panel, space=space, width=width)\n", - " paxs1.plot(x1, y1, color='gray7')\n", - " paxs1.format(title='Mean', **kwargs)\n", - " \n", - " # Panels for plotting the standard deviation\n", - " paxs2 = axs.panel(loc_panel, space=space, width=width)\n", - " paxs2.plot(x2, y2, color='gray7', ls='--')\n", - " paxs2.format(title='Stdev', **kwargs)\n", - " \n", - " # Apply formatting *after*\n", - " axs.format(\n", - " xlabel='xlabel', ylabel='ylabel', title='Title',\n", - " suptitle='Using panels for summary statistics',\n", - " )" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", - "f, axs = plot.subplots(axwidth=1.5, nrows=2, ncols=2, share=0)\n", - "\n", - "# Panels do not interfere with subplot layout\n", - "for ax, side in zip(axs, 'tlbr'):\n", - " ax.panel_axes(side, width='3em')\n", - "axs.format(\n", - " title='Title', suptitle='Complex arrangement of panels', collabels=['Column 1', 'Column 2'],\n", - " abcloc='ul', titleloc='uc', xlabel='xlabel', ylabel='ylabel', abc=True, abovetop=False\n", - ")\n", - "axs.format(\n", - " xlim=(0, 1), ylim=(0, 1),\n", - " ylocator=plot.arange(0.2, 0.8, 0.2),\n", - " xlocator=plot.arange(0.2, 0.8, 0.2)\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Inset axes" - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "`Inset axes `__ can be generated with the `~proplot.axes.Axes.inset` or `~proplot.axes.Axes.inset_axes` command. The resulting axes are instances of `~proplot.axes.XYAxes`, and therefore can be modified with the `~proplot.axes.XYAxes.format` command.\n", - "\n", - "Passing ``zoom=True`` to `~proplot.axes.Axes.inset` draws \"zoom indication\" lines with `~matplotlib.axes.Axes.indicate_inset_zoom`, and ProPlot *automatically updates* the lines when the axis limits of the parent axes change. To modify the line properties, simply use the `zoom_kw` argument." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", - "import numpy as np\n", - "N = 20\n", - "# Inset axes representing a \"zoom\"\n", - "state = np.random.RandomState(51423)\n", - "f, ax = plot.subplots(axwidth=3)\n", - "x, y = np.arange(10), np.arange(10)\n", - "data = state.rand(10, 10)\n", - "m = ax.pcolormesh(data, cmap='Grays', levels=N)\n", - "ax.colorbar(m, loc='b', label='label')\n", - "ax.format(xlabel='xlabel', ylabel='ylabel')\n", - "axi = ax.inset(\n", - " [5, 5, 4, 4], transform='data', zoom=True,\n", - " zoom_kw={'color': 'red3', 'lw': 2}\n", - ")\n", - "axi.format(\n", - " xlim=(2, 4), ylim=(2, 4), color='red7',\n", - " linewidth=1.5, ticklabelweight='bold'\n", - ")\n", - "axi.pcolormesh(data, cmap='Grays', levels=N)\n", - "ax.format(suptitle='\"Zooming in\" with an inset axes')" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.3" - }, - "toc": { - "colors": { - "hover_highlight": "#ece260", - "navigate_num": "#000000", - "navigate_text": "#000000", - "running_highlight": "#FF0000", - "selected_highlight": "#fff968", - "sidebar_border": "#ffffff", - "wrapper_background": "#ffffff" - }, - "moveMenuLeft": false, - "nav_menu": { - "height": "66px", - "width": "252px" - }, - "navigate_menu": true, - "number_sections": true, - "sideBar": true, - "threshold": 4, - "toc_cell": false, - "toc_section_display": "block", - "toc_window_display": true, - "widenNotebook": false - }, - "varInspector": { - "cols": { - "lenName": 16, - "lenType": 16, - "lenVar": 40 - }, - "kernels_config": { - "python": { - "delete_cmd_postfix": "", - "delete_cmd_prefix": "del ", - "library": "var_list.py", - "varRefreshCmd": "print(var_dic_list())" - }, - "r": { - "delete_cmd_postfix": ") ", - "delete_cmd_prefix": "rm(", - "library": "var_list.r", - "varRefreshCmd": "cat(var_dic_list()) " - } - }, - "types_to_exclude": [ - "module", - "function", - "builtin_function_or_method", - "instance", - "_Feature" - ], - "window_display": false - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/docs/insets_panels.py b/docs/insets_panels.py new file mode 100644 index 000000000..c9909c839 --- /dev/null +++ b/docs/insets_panels.py @@ -0,0 +1,186 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.11.4 +# kernelspec: +# display_name: Python 3 +# language: python +# name: python3 +# --- + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_insets_panels: +# +# Insets and panels +# ================= + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_panels: +# +# Panel axes +# ---------- +# +# It is often useful to have narrow "panels" along the edge of a larger +# subplot for plotting secondary 1-dimensional datasets or summary statistics. +# In proplot, you can generate panels using the `~proplot.axes.Axes.panel_axes` +# command (or its shorthand, `~proplot.axes.Axes.panel`). The panel location +# is specified with a string, e.g. ``ax.panel('r')`` or ``ax.panel('right')`` +# for a right-hand side panel, and the resulting panels are instances of +# `~proplot.axes.CartesianAxes`. By default, the panel shares its axis limits, +# axis labels, tick positions, and tick labels with the main subplot, but +# this can be disabled by passing ``share=False``. To generate "stacked" panels, +# call `~proplot.axes.Axes.panel_axes` more than once. To generate several +# panels at once, call `~proplot.gridspec.SubplotGrid.panel_axes` on +# the `~proplot.gridspec.SubplotGrid` returned by `~proplot.figure.Figure.subplots`. +# +# In the first example below, the distances are automatically adjusted by the +# :ref:`tight layout algorithm ` according to the `pad` keyword +# (the default is :rcraw:`subplots.panelpad` -- this can be changed for an entire +# figure by passing `panelpad` to `~proplot.figure.Figure`). In the second example, +# the tight layout algorithm is overriden by manually setting the `space` to ``0``. +# Panel widths are specified in physical units, with the default controlled +# by :rcraw:`subplots.panelwidth`. This helps preserve the look of the +# figure if the figure size changes. Note that by default, panels are excluded +# when centering :ref:`spanning axis labels ` and super titles -- +# to include the panels, pass ``includepanels=True`` to `~proplot.figure.Figure`. +# +# .. important:: +# +# Proplot adds panel axes by allocating new rows and columns in the +# `~proplot.gridspec.GridSpec` rather than "stealing" space from the parent +# subplot (note that subsequently indexing the `~proplot.gridspec.GridSpec` will +# ignore the slots allocated for panels). This approach means that panels +# :ref:`do not affect subplot aspect ratios ` and +# :ref:`do not affect subplot spacing `, which lets +# proplot avoid relying on complicated `"constrained layout" algorithms +# `__ +# and tends to improve the appearance of figures with even the +# most complex arrangements of subplots and panels. + +# %% +import proplot as pplt + +# Demonstrate that complex arrangements preserve +# spacing, aspect ratios, and axis sharing +gs = pplt.GridSpec(nrows=2, ncols=2) +fig = pplt.figure(refwidth=1.5, share=False) +for ss, side in zip(gs, 'tlbr'): + ax = fig.add_subplot(ss) + px = ax.panel_axes(side, width='3em') +fig.format( + xlim=(0, 1), ylim=(0, 1), + xlabel='xlabel', ylabel='ylabel', + xticks=0.2, yticks=0.2, + title='Title', suptitle='Complex arrangement of panels', + toplabels=('Column 1', 'Column 2'), + abc=True, abcloc='ul', titleloc='uc', titleabove=False, +) + +# %% +import proplot as pplt +import numpy as np +state = np.random.RandomState(51423) +data = (state.rand(20, 20) - 0.48).cumsum(axis=1).cumsum(axis=0) +data = 10 * (data - data.min()) / (data.max() - data.min()) + +# Stacked panels with outer colorbars +for cbarloc, ploc in ('rb', 'br'): + # Create figure + fig, axs = pplt.subplots( + nrows=1, ncols=2, refwidth=1.8, panelpad=0.8, + share=False, includepanels=True + ) + axs.format( + xlabel='xlabel', ylabel='ylabel', title='Title', + suptitle='Using panels for summary statistics', + ) + + # Plot 2D dataset + for ax in axs: + ax.contourf( + data, cmap='glacial', extend='both', + colorbar=cbarloc, colorbar_kw={'label': 'colorbar'}, + ) + + # Get summary statistics and settings + axis = int(ploc == 'r') # dimension along which stats are taken + x1 = x2 = np.arange(20) + y1 = data.mean(axis=axis) + y2 = data.std(axis=axis) + titleloc = 'upper center' + if ploc == 'r': + titleloc = 'center' + x1, x2, y1, y2 = y1, y2, x1, x2 + + # Panels for plotting the mean. Note SubplotGrid.panel() returns a SubplotGrid + # of panel axes. We use this to call format() for all the panels at once. + space = 0 + width = '4em' + kwargs = {'titleloc': titleloc, 'xreverse': False, 'yreverse': False} + pxs = axs.panel(ploc, space=space, width=width) + pxs.format(title='Mean', **kwargs) + for px in pxs: + px.plot(x1, y1, color='gray7') + + # Panels for plotting the standard deviation + pxs = axs.panel(ploc, space=space, width=width) + pxs.format(title='Stdev', **kwargs) + for px in pxs: + px.plot(x2, y2, color='gray7', ls='--') + + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_insets: +# +# Inset axes +# ---------- +# +# `Inset axes +# `__ +# can be generated with the `~proplot.axes.Axes.inset_axes` command (or its +# shorthand, `~proplot.axes.Axes.inset`). To generate several insets at once, call +# `~proplot.gridspec.SubplotGrid.inset_axes` on the `~proplot.gridspec.SubplotGrid` +# returned by `~proplot.figure.Figure.subplots`. By default, inset axes have the +# same projection as the parent axes, but you can also request a :ref:`different +# projection ` (e.g., ``ax.inset_axes(bounds, proj='polar')``). When +# the axes are both `~proplot.axes.CartesianAxes`, you can pass ``zoom=True`` +# to `~proplot.axes.Axes.inset_axes` to quickly add a "zoom indication" box and +# lines (this uses `~matplotlib.axes.Axes.indicate_inset_zoom` internally). The box +# and line positions automatically follow the axis limits of the inset axes and parent +# axes. To modify the zoom line properties, you can pass a dictionary to `zoom_kw`. + +# %% +import proplot as pplt +import numpy as np + +# Sample data +N = 20 +state = np.random.RandomState(51423) +x, y = np.arange(10), np.arange(10) +data = state.rand(10, 10).cumsum(axis=0) +data = np.flip(data, (0, 1)) + +# Plot data in the main axes +fig, ax = pplt.subplots(refwidth=3) +m = ax.pcolormesh(data, cmap='Grays', levels=N) +ax.colorbar(m, loc='b', label='label') +ax.format( + xlabel='xlabel', ylabel='ylabel', + suptitle='"Zooming in" with an inset axes' +) + +# Create an inset axes representing a "zoom-in" +# See the 1D plotting section for more on the "inbounds" keyword +ix = ax.inset( + [5, 5, 4, 4], transform='data', zoom=True, + zoom_kw={'ec': 'blush', 'ls': '--', 'lw': 2} +) +ix.format( + xlim=(2, 4), ylim=(2, 4), color='red8', + linewidth=1.5, ticklabelweight='bold' +) +ix.pcolormesh(data, cmap='Grays', levels=N, inbounds=False) diff --git a/docs/install.rst b/docs/install.rst index a23a75e3e..b914117c9 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -1 +1 @@ -.. include:: ../INSTALL.rst +.. include:: ../INSTALL.rst \ No newline at end of file diff --git a/docs/logo_long.png b/docs/logo_long.png new file mode 100644 index 000000000..480c19a76 Binary files /dev/null and b/docs/logo_long.png differ diff --git a/docs/make.bat b/docs/make.bat deleted file mode 100644 index 2119f5109..000000000 --- a/docs/make.bat +++ /dev/null @@ -1,35 +0,0 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set SOURCEDIR=. -set BUILDDIR=_build - -if "%1" == "" goto help - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% - -:end -popd diff --git a/docs/projection.ipynb b/docs/projection.ipynb deleted file mode 100644 index cd1b08037..000000000 --- a/docs/projection.ipynb +++ /dev/null @@ -1,473 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Geographic and polar plots" - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "ProPlot includes features for working with `polar axes `__ and the `cartopy `__ and `basemap `__ map projection packages. These features are optional; installation of cartopy and basemap are not required.\n", - "\n", - "To change the axes projection, pass ``proj='name'`` to `~proplot.subplots.subplots`. To use different projections for different subplots, pass a dictionary of projection names with the subplot number as the key -- for example, ``proj={1:'name'}``. The default \"projection\" is always `~proplot.axes.XYAxes`. \n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Polar projections" - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "To draw polar axes, pass ``proj='polar'`` or e.g. ``proj={1:'polar'}`` to `~proplot.subplots.subplots`. This generates a `~proplot.axes.PolarAxes` instance. Its `~proplot.axes.PolarAxes.format` command permits polar-specific modifications like changing the central radius, the zero azimuth location, the radial and azimuthal limits, and the positive azimuthal direction. A demonstration is below." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", - "import numpy as np\n", - "state = np.random.RandomState(51423)\n", - "N = 200\n", - "x = np.linspace(0, 2*np.pi, N)\n", - "y = 100*(state.rand(N, 5)-0.3).cumsum(axis=0)/N\n", - "f, axs = plot.subplots([[1, 1, 2, 2], [0, 3, 3, 0]], proj='polar')\n", - "axs.format(\n", - " suptitle='Polar axes demo', linewidth=1,\n", - " ticklabelsize=9, rlines=0.5, rlim=(0, 19),\n", - " titlepad='1.5em' # matplotlib default title offset is incorrect \n", - ")\n", - "for i in range(5):\n", - " axs.plot(x + i*2*np.pi/5, y[:, i], cycle='FlatUI', zorder=0, lw=3)\n", - "\n", - "# Standard polar plot\n", - "axs[0].format(\n", - " title='Normal plot', thetaformatter='pi', rlines=5, gridalpha=1, gridlinestyle=':',\n", - " rlabelpos=180, color='gray8', ticklabelweight='bold'\n", - ")\n", - "\n", - "# Sector plot\n", - "axs[1].format(\n", - " title='Sector plot', thetadir=-1, thetalines=90, thetalim=(0, 270), theta0='N',\n", - " rlim=(0, 22), rlines=5\n", - ")\n", - "\n", - "# Annular plot\n", - "axs[2].format(\n", - " title='Annular plot', thetadir=-1, thetalines=10,\n", - " r0=0, rlim=(10, 22), rformatter='null', rlocator=2\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Geographic projections" - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "To specify a geographic projection, pass ``proj='name'`` or e.g. ``proj={1:'name'}`` to `~proplot.subplots.subplots` where ``'name'`` is any valid `PROJ `__ projection name listed in the `~proplot.projs.Proj` table. This generates a `~proplot.axes.GeoAxes` or `~proplot.axes.BasemapAxes`, depending on whether you passed ``basemap=True`` to `~proplot.subplots.subplots`.\n", - "\n", - "* `~proplot.axes.GeoAxes` joins the cartopy `~cartopy.mpl.geoaxes.GeoAxes` class with the ProPlot `~matplotlib.axes.Axes` class and adds a `~proplot.axes.ProjAxes.format` command. This class includes all the normal `~cartopy.mpl.geoaxes.GeoAxes` methods, and its `~proplot.axes.ProjAxes.format` method can be used to set the map bounds with `~cartopy.mpl.geoaxes.GeoAxes.set_extent` and add geographic features with `~cartopy.mpl.geoaxes.GeoAxes.add_feature`.\n", - "* `~proplot.axes.BasemapAxes` redirects the plot, scatter, contour, contourf, pcolor, pcolormesh, quiver, streamplot, and barb methods to identically named methods on the `~mpl_toolkits.basemap.Basemap` instance, and provides access to `~mpl_toolkits.basemap.Basemap` geographic plotting commands like `~mpl_toolkits.basemap.Basemap.fillcontinents` via the `~proplot.axes.ProjAxes.format` command.\n", - "\n", - "So with ProPlot, you no longer have to invoke verbose cartopy `~cartopy.crs.Projection` classes like `~cartopy.crs.LambertAzimuthalEqualArea`, and you never have to directly reference the `~mpl_toolkits.basemap.Basemap` instance -- ProPlot works with the `~mpl_toolkits.basemap.Basemap` instance under the hood.\n", - "\n", - "To pass keyword args to `~mpl_toolkits.basemap.Basemap` and `~cartopy.crs.Projection`, use the `proj_kw` dictionary. To make things a bit more consistent, ProPlot lets you supply native `PROJ `__ keyword names to the cartopy `~cartopy.crs.Projection` classes, e.g. `lon_0` instead of `central_longitude`. ProPlot also lets you instantiate `~mpl_toolkits.basemap.Basemap` projections with *sensible defaults* from the `~proplot.projs.basemap_kwargs` dictionary, rather than raising an error when certain projection arguments are omitted." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", - "plot.rc.coastlinewidth = plot.rc.linewidth = 0.8\n", - "\n", - "# Simple figure with just one projection\n", - "f, axs = plot.subplots(\n", - " ncols=2, axwidth=2.5,\n", - " proj='robin', proj_kw={'lon_0': 180}\n", - ")\n", - "axs.format(\n", - " suptitle='Figure with single projection',\n", - " coast=True, latlines=30, lonlines=60\n", - ")\n", - "\n", - "# Complex figure with different projections\n", - "f, axs = plot.subplots(\n", - " hratios=(1.5, 1, 1, 1, 1),\n", - " basemap={\n", - " (1, 3, 5, 7, 9): False,\n", - " (2, 4, 6, 8, 10): True\n", - " },\n", - " proj={\n", - " (1, 2): 'mill',\n", - " (3, 4): 'cyl',\n", - " (5, 6): 'moll',\n", - " (7, 8): 'sinu',\n", - " (9, 10): 'npstere'\n", - " },\n", - " ncols=2, nrows=5\n", - ")\n", - "axs.format(suptitle='Figure with several projections')\n", - "axs.format(coast=True, latlines=30, lonlines=60)\n", - "axs[:, 1].format(labels=True, lonlines=plot.arange(-180, 179, 60))\n", - "axs[-1, -1].format(labels=True, lonlines=30)\n", - "axs.format(collabels=['Cartopy projections', 'Basemap projections'])\n", - "plot.rc.reset()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Included projections" - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "The available `cartopy `__ and `basemap `__ projections are plotted below. See `~proplot.projs.Proj` for a table of projection names with links to the relevant `PROJ `__ documentation.\n", - "\n", - "ProPlot uses the cartopy API to add the Aitoff, Hammer, Winkel Tripel, and Kavrisky VII projections (i.e. ``'aitoff'``, ``'hammer'``, ``'wintri'``, and ``'kav7'``), as well as North and South polar versions of the Azimuthal Equidistant, Lambert Azimuthal Equal-Area, and Gnomic projections (i.e. ``'npaeqd'``, ``'spaeqd'``, ``'nplaea'``, ``'splaea'``, ``'npgnom'``, and ``'spgnom'``), modeled after the existing `~cartopy.crs.NorthPolarStereo` and `~cartopy.crs.SouthPolarStereo` projections.\n", - "\n", - "Note that while cartopy projection bounds can be any shape, basemap projection bounds are usually rectangles. Basemap used to have many more projections than cartopy, but the ProPlot additions have evened things out." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", - "import numpy as np\n", - "\n", - "# Table of cartopy projections\n", - "projs = [\n", - " 'cyl', 'merc', 'mill', 'lcyl', 'tmerc',\n", - " 'robin', 'hammer', 'moll', 'kav7', 'aitoff', 'wintri', 'sinu',\n", - " 'geos', 'ortho', 'nsper', 'aea', 'eqdc', 'lcc', 'gnom',\n", - " 'npstere', 'nplaea', 'npaeqd', 'npgnom', 'igh',\n", - " 'eck1', 'eck2', 'eck3', 'eck4', 'eck5', 'eck6'\n", - "]\n", - "f, axs = plot.subplots(ncols=3, nrows=10, proj=projs)\n", - "axs.format(\n", - " land=True, reso='lo', labels=False,\n", - " suptitle='Table of cartopy projections'\n", - ")\n", - "for proj, ax in zip(projs, axs):\n", - " ax.format(title=proj, titleweight='bold', labels=False)\n", - " \n", - "# Table of basemap projections\n", - "projs = [\n", - " 'cyl', 'merc', 'mill', 'cea', 'gall', 'sinu',\n", - " 'eck4', 'robin', 'moll', 'kav7', 'hammer', 'mbtfpq',\n", - " 'geos', 'ortho', 'nsper',\n", - " 'vandg', 'aea', 'eqdc', 'gnom', 'cass', 'lcc',\n", - " 'npstere', 'npaeqd', 'nplaea'\n", - "]\n", - "f, axs = plot.subplots(ncols=3, nrows=8, basemap=True, proj=projs)\n", - "axs.format(\n", - " land=True, labels=False,\n", - " suptitle='Table of basemap projections'\n", - ")\n", - "for proj, ax in zip(projs, axs):\n", - " ax.format(title=proj, titleweight='bold', labels=False)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Zooming into projections" - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "To zoom into cartopy projections, you can use `~cartopy.mpl.geoaxes.GeoAxes.set_extent`, or alternatively pass `lonlim`, `latlim`, or `boundinglat` to `~proplot.axes.ProjAxes.format`. The `boundinglat` controls the *circular boundary extent* for North Polar and South Polar Stereographic, Azimuthal Equidistant, Lambert Azimuthal Equal-Area, and Gnomonic projections (ProPlot makes sure these projections *always* have circular bounds).\n", - "\n", - "To zoom into basemap projections, you must pass the limits to the `~mpl_toolkits.basemap.Basemap` class directly by passing `proj_kw` to `~proplot.subplots.subplots` with any of the `boundinglat`, `llcrnrlon`, `llcrnrlat`, `urcrnrlon`, `urcrnrlat`, `llcrnrx`, `llcrnry`, `urcrnrx`, `urcrnry`, `width`, or `height` keyword args." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", - "f, axs = plot.subplots(\n", - " nrows=2, axwidth=4.5,\n", - " proj='pcarree', basemap={1: False, 2: True},\n", - " proj_kw={2: {'llcrnrlon': -20, 'llcrnrlat': -10, 'urcrnrlon': 180, 'urcrnrlat': 50}}\n", - ")\n", - "\n", - "# Ordinary projection\n", - "axs.format(\n", - " land=True, labels=True, lonlines=20,\n", - " latlines=20, suptitle='Zooming into projections'\n", - ")\n", - "axs[0].format(\n", - " lonlim=(-140, 60), latlim=(-10, 50),\n", - " labels=True, title='Cartopy example'\n", - ")\n", - "axs[1].format(title='Basemap example')\n", - "\n", - "# Polar projection\n", - "f, axs = plot.subplots(\n", - " ncols=2, axwidth=2.2,\n", - " proj={1: 'splaea', 2: 'npaeqd'},\n", - " basemap={1: False, 2: True},\n", - " proj_kw={2: {'boundinglat': 60}}\n", - ")\n", - "axs.format(\n", - " land=True, latlines=10, latmax=80,\n", - " suptitle='Zooming into polar projections'\n", - ")\n", - "axs[0].format(boundinglat=-60, title='Cartopy example')\n", - "axs[1].format(title='Basemap example')\n", - "\n", - "# Example from basemap website\n", - "f, axs = plot.subplots(\n", - " ncols=2, axwidth=2, proj='lcc',\n", - " basemap={1: False, 2: True},\n", - " proj_kw={\n", - " 1: {'lon_0': 0},\n", - " 2: {'lon_0': -100, 'lat_0': 45, 'width': 8e6, 'height': 8e6}\n", - " }\n", - ")\n", - "axs.format(suptitle='Zooming into specific regions', land=True)\n", - "axs[0].format(\n", - " title='Cartopy example', land=True,\n", - " lonlim=(-20, 50), latlim=(30, 70)\n", - ")\n", - "axs[1].format(title='Basemap example', land=True)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Plotting geophysical data" - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "The below example demonstrates how to plot geophysical data with ProPlot. It is mostly the same as cartopy, but with some new features powered by the `~proplot.wrappers.standardize_2d`, `~proplot.wrappers.default_transform`, and `~proplot.wrappers.default_latlon` wrappers.\n", - "\n", - "* For both basemap and cartopy projections, you can pass ``globe=True`` to 2D plotting commands to ensure global data coverage.\n", - "* For `~proplot.axes.GeoAxes` plotting methods, ``transform=crs.PlateCarree()`` is now the default behavior. That is, ProPlot assumes your data is in longitude-latitude coordinates rather than map projection coordinates.\n", - "* For `~proplot.axes.BasemapAxes` plotting methods, ``latlon=True`` is now the default behavior. Again, plotting methods are now called on the *axes* instead of the `~mpl_toolkits.basemap.Basemap` instance." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", - "import numpy as np\n", - "offset = -40\n", - "x = plot.arange(offset, 360 + offset-1, 60)\n", - "y = plot.arange(-60, 60+1, 30)\n", - "state = np.random.RandomState(51423)\n", - "data = state.rand(len(y), len(x))\n", - "\n", - "# Same figure with and without \"global coverage\"\n", - "titles = ('Geophysical data demo', 'Global coverage demo')\n", - "for globe in (False, True,):\n", - " f, axs = plot.subplots(\n", - " ncols=2, nrows=2, axwidth=2.5,\n", - " proj='kav7', basemap={(1, 3): False, (2, 4): True})\n", - " for i, ax in enumerate(axs):\n", - " cmap = ('sunset', 'sunrise')[i % 2]\n", - " if i < 2:\n", - " m = ax.contourf(x, y, data, cmap=cmap, globe=globe, extend='both')\n", - " f.colorbar(\n", - " m, loc='b', span=i+1, label='values',\n", - " tickminor=False, extendsize='1.7em'\n", - " )\n", - " else:\n", - " ax.pcolor(x, y, data, cmap=cmap, globe=globe, extend='both')\n", - " if globe:\n", - " continue\n", - " ix = offset + np.linspace(0, 360, 20)\n", - " for cmd in (np.sin, np.cos):\n", - " iy = cmd(ix*np.pi/180)*60\n", - " ax.plot(ix, iy, color='k', lw=0, marker='o')\n", - " axs.format(\n", - " suptitle=titles[globe],\n", - " collabels=['Cartopy example', 'Basemap example'],\n", - " rowlabels=['Contourf', 'Pcolor'],\n", - " latlabels='r', lonlabels='b', lonlines=90,\n", - " abc=True, abcstyle='a)', abcloc='ul', abcborder=False\n", - " )" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Customizing projections" - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "`~proplot.axes.GeoAxes` and `~proplot.axes.BasemapAxes` both derive from `~proplot.axes.ProjAxes`, which provides a `~proplot.axes.ProjAxes.format` method. `~proplot.axes.ProjAxes.format` can be used to draw gridlines, add gridline labels, set gridline label locations, modify the projection bounding box, and add and stylize geographic features, like land masses, coastlines, and international borders. This method also calls `format` on `~proplot.axes.Axes`, and so can be used for subplot titles, a-b-c labels, and figure titles as before." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", - "f, axs = plot.subplots(\n", - " [[1, 1, 2], [3, 3, 3]],\n", - " axwidth=4, proj={1: 'robin', 2: 'ortho', 3: 'wintri'}\n", - ")\n", - "axs.format(\n", - " suptitle='Projection axes formatting demo',\n", - " collabels=['Column 1', 'Column 2'],\n", - " abc=True, abcstyle='A.', abcloc='ul', abcborder=False, linewidth=1.5\n", - ")\n", - "\n", - "# Styling projections in different ways\n", - "ax = axs[0]\n", - "ax.format(\n", - " title='Robinson map', land=True, landcolor='navy blue', facecolor='pale blue',\n", - " coastcolor='gray5', borderscolor='gray5', innerborderscolor='gray5',\n", - " geogridlinewidth=1, geogridcolor='gray5', geogridalpha=1,\n", - " coast=True, innerborders=True, borders=True\n", - ")\n", - "ax = axs[1]\n", - "ax.format(\n", - " title='Ortho map', reso='med', land=True, coast=True, latlines=10, lonlines=15,\n", - " landcolor='mushroom', suptitle='Projection axes formatting demo',\n", - " facecolor='petrol', coastcolor='charcoal', coastlinewidth=0.8, geogridlinewidth=1\n", - ")\n", - "ax = axs[2]\n", - "ax.format(\n", - " land=True, facecolor='ocean blue', landcolor='almond', title='Winkel tripel map',\n", - " lonlines=60, latlines=15\n", - ")" - ] - } - ], - "metadata": { - "celltoolbar": "Raw Cell Format", - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.3" - }, - "toc": { - "colors": { - "hover_highlight": "#ece260", - "navigate_num": "#000000", - "navigate_text": "#000000", - "running_highlight": "#FF0000", - "selected_highlight": "#fff968", - "sidebar_border": "#ffffff", - "wrapper_background": "#ffffff" - }, - "moveMenuLeft": false, - "nav_menu": { - "height": "12px", - "width": "250px" - }, - "navigate_menu": true, - "number_sections": true, - "sideBar": true, - "threshold": 4, - "toc_cell": false, - "toc_section_display": "block", - "toc_window_display": true, - "widenNotebook": false - }, - "varInspector": { - "cols": { - "lenName": 16, - "lenType": 16, - "lenVar": 40 - }, - "kernels_config": { - "python": { - "delete_cmd_postfix": "", - "delete_cmd_prefix": "del ", - "library": "var_list.py", - "varRefreshCmd": "print(var_dic_list())" - }, - "r": { - "delete_cmd_postfix": ") ", - "delete_cmd_prefix": "rm(", - "library": "var_list.r", - "varRefreshCmd": "cat(var_dic_list()) " - } - }, - "types_to_exclude": [ - "module", - "function", - "builtin_function_or_method", - "instance", - "_Feature" - ], - "window_display": false - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/docs/projections.py b/docs/projections.py new file mode 100644 index 000000000..9be3ffc69 --- /dev/null +++ b/docs/projections.py @@ -0,0 +1,495 @@ +# -*- coding: utf-8 -*- +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.11.4 +# kernelspec: +# display_name: Python 3 +# language: python +# name: python3 +# --- + +# %% [raw] raw_mimetype="text/restructuredtext" +# +# .. _polar: https://matplotlib.org/3.1.0/gallery/pie_and_polar_charts/polar_demo.html +# +# .. _cartopy: https://scitools.org.uk/cartopy/docs/latest/ +# +# .. _basemap: https://matplotlib.org/basemap/index.html +# +# .. _ug_proj: +# +# Geographic and polar axes +# ========================= +# +# This section documents several useful features for working with `polar`_ plots +# and :ref:`geographic projections `. The geographic features are powered by +# `cartopy`_ (or, optionally, `basemap`_). Note that these features are *optional* -- +# installation of cartopy or basemap are not required to use proplot. +# +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_polar: +# +# Polar axes +# ---------- +# +# To create `polar axes `_, pass ``proj='polar'`` to an axes-creation +# command like `proplot.figure.Figure.add_subplot`. Polar axes are represented with the +# `~proplot.axes.PolarAxes` subclass, which has its own `~proplot.axes.PolarAxes.format` +# command. `proplot.axes.PolarAxes.format` facilitates polar-specific modifications +# like changing the central radius `r0`, the zero azimuth location `theta0`, +# and the positive azimuthal direction `thetadir`. It also supports toggling and +# configuring the "major" and "minor" gridline locations with `grid`, `rlocator`, +# `thetalocator`, `gridminor`, `rminorlocator`, and `thetaminorlocator` and formatting +# the gridline labels with `rformatter` and `thetaformatter` (analogous to `xlocator`, +# `xformatter`, and `xminorlocator` used by `proplot.axes.CartesianAxes.format`), +# and creating "annular" or "sector" plots by changing the radial or azimuthal +# bounds `rlim` and `thetalim`. Finally, since `proplot.axes.PolarAxes.format` +# calls `proplot.axes.Axes.format`, it can be used to add axes titles, a-b-c +# labels, and figure titles. +# +# For details, see `proplot.axes.PolarAxes.format`. + +# %% +import proplot as pplt +import numpy as np +N = 200 +state = np.random.RandomState(51423) +x = np.linspace(0, 2 * np.pi, N)[:, None] + np.arange(5) * 2 * np.pi / 5 +y = 100 * (state.rand(N, 5) - 0.3).cumsum(axis=0) / N +fig, axs = pplt.subplots([[1, 1, 2, 2], [0, 3, 3, 0]], proj='polar') +axs.format( + suptitle='Polar axes demo', linewidth=1, titlepad='1em', + ticklabelsize=9, rlines=0.5, rlim=(0, 19), +) +for ax in axs: + ax.plot(x, y, cycle='default', zorder=0, lw=3) + +# Standard polar plot +axs[0].format( + title='Normal plot', thetaformatter='tau', + rlabelpos=225, rlines=pplt.arange(5, 30, 5), + edgecolor='red8', tickpad='1em', +) + +# Sector plot +axs[1].format( + title='Sector plot', thetadir=-1, thetalines=90, thetalim=(0, 270), theta0='N', + rlim=(0, 22), rlines=pplt.arange(5, 30, 5), +) + +# Annular plot +axs[2].format( + title='Annular plot', thetadir=-1, thetalines=20, gridcolor='red', + r0=-20, rlim=(0, 22), rformatter='null', rlocator=2 +) + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_geo: +# +# Geographic axes +# --------------- +# +# To create geographic axes, pass ``proj='name'`` to an axes-creation command like +# `proplot.figure.Figure.add_subplot`, where ``name`` is any valid :ref:`PROJ projection +# name `. Alternatively, you can pass a `cartopy.crs.Projection` or +# `~mpl_toolkits.basemap.Basemap` instance returned by the `~proplot.constructor.Proj` +# :ref:`constructor function ` to `proj` (see below for details). If +# you want to create your subplots :ref:`all-at-once ` with e.g. +# `~proplot.ui.subplots` but need different projections for each subplot, you can pass +# a list or dictionary to the `proj` keyword (e.g., ``proj=('cartesian', 'pcarree')`` +# or ``proj={2: 'pcarree'}`` -- see `~proplot.figure.Figure.subplots` for details). +# Geographic axes are represented with the `~proplot.axes.GeoAxes` subclass, which +# has its own `~proplot.axes.GeoAxes.format` command. `proplot.axes.GeoAxes.format` +# facilitates :ref:`geographic-specific modifications ` like meridional +# and parallel gridlines and land mass outlines. The syntax is very similar to +# `proplot.axes.CartesianAxes.format`. Note that the `proj` keyword and several of +# the `~proplot.axes.GeoAxes.format` keywords are inspired by the basemap API. +# In the below example, we create and format a very simple geographic plot. + +# %% +# Use an on-the-fly projection +import proplot as pplt +fig = pplt.figure(refwidth=3) +axs = fig.subplots(nrows=2, proj='robin', proj_kw={'lon0': 150}) +# proj = pplt.Proj('robin', lon0=180) +# axs = pplt.subplots(nrows=2, proj=proj) # equivalent to above +axs.format( + suptitle='Figure with single projection', + land=True, latlines=30, lonlines=60, +) + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_backends: +# +# Geographic backends +# ------------------- +# +# The `proplot.axes.GeoAxes` class uses either `cartopy`_ or `basemap`_ as "backends" +# to :ref:`format the axes ` and :ref:`plot stuff ` in +# the axes. A few details: +# +# * Cartopy is the default backend. When you request projection names with cartopy +# as the backend (or pass a `cartopy.crs.Projection` to the `proj` keyword), the +# returned axes is a subclass of `cartopy.mpl.geoaxes.GeoAxes`. Under the hood, +# invoking `~proplot.axes.GeoAxes.format` with cartopy as the backend changes map +# bounds using `~cartopy.mpl.geoaxes.GeoAxes.set_extent`, adds major and minor +# gridlines using `~cartopy.mpl.geoaxes.GeoAxes.gridlines`, and adds geographic +# features using `~cartopy.mpl.geoaxes.GeoAxes.add_feature`. If you prefer, you can +# use the standard `cartopy.mpl.geoaxes.GeoAxes` methods just like you would in +# cartopy. If you need to use the underlying `~cartopy.crs.Projection` instance, it +# is available via the `~proplot.axes.GeoAxes.projection` attribute. If you want +# to work with the projection classes directly, they are available in the +# top-level namespace (e.g., ``proj=pplt.PlateCarre()`` is allowed). +# +# * Basemap is an alternative backend. To use basemap, set :rcraw:`geo.backend` to +# ``'basemap'`` or pass ``backend='basemap'`` to the axes-creation command. When +# you request a projection name with basemap as the backend (or pass a +# `~mpl_toolkits.basemap.Basemap` to the `proj` keyword), the returned axes +# redirects the plotting methods plot, scatter, contour, contourf, pcolor, +# pcolormesh, quiver, streamplot, and barb to the identically named methods on +# the `~mpl_toolkits.basemap.Basemap` instance. This means you can work +# with the standard axes plotting methods rather than the basemap methods -- +# just like cartopy. Under the hood, invoking `~proplot.axes.GeoAxes.format` +# with basemap as the backend adds major and minor gridlines using +# `~mpl_toolkits.basemap.Basemap.drawmeridians` and +# `~mpl_toolkits.basemap.Basemap.drawparallels` and adds geographic features +# using methods like `~mpl_toolkits.basemap.Basemap.fillcontinents` +# and `~mpl_toolkits.basemap.Basemap.drawcoastlines`. If you need to +# use the underlying `~mpl_toolkits.basemap.Basemap` instance, it is +# available as the `~proplot.axes.GeoAxes.projection` attribute. +# +# Together, these features let you work with geophysical data without invoking +# verbose cartopy classes like `~cartopy.crs.LambertAzimuthalEqualArea` or +# keeping track of separate `~mpl_toolkits.basemap.Basemap` instances. This +# considerably reduces the amount of code needed to make complex geographic +# plots. In the below examples, we create a variety of plots using both +# cartopy and basemap as backends. +# +# .. important:: +# +# * By default, proplot bounds polar cartopy projections like +# `~cartopy.crs.NorthPolarStereo` at the equator and gives non-polar cartopy +# projections global extent by calling `~cartopy.mpl.geoaxes.GeoAxes.set_global`. +# This is a deviation from cartopy, which determines map boundaries automatically +# based on the coordinates of the plotted content. To revert to cartopy's +# default behavior, set :rcraw:`geo.extent` to ``'auto`` or pass ``extent='auto'`` +# to `~proplot.axes.GeoAxes.format`. +# * By default, proplot gives circular boundaries to polar cartopy and basemap +# projections like `~cartopy.crs.NorthPolarStereo` (see `this example +# `__ +# from the cartopy website). To disable this feature, set :rcraw:`geo.round` to +# ``False`` or pass ``round=False` to `~proplot.axes.GeoAxes.format`. Please note +# that older versions of cartopy cannot add gridlines to maps bounded by circles. +# * To make things more consistent, the `~proplot.constructor.Proj` constructor +# function lets you supply native `PROJ `__ keyword names +# for the cartopy `~cartopy.crs.Projection` classes (e.g., `lon0` instead +# of `central_longitude`) and instantiates `~mpl_toolkits.basemap.Basemap` +# projections with sensible default PROJ parameters rather than raising an error +# when they are omitted (e.g., ``lon0=0`` as the default for most projections). +# +# .. warning:: +# +# The `basemap`_ package is `no longer actively maintained \ +# `__ +# and will not work with matplotlib versions more recent than 3.2.2. We originally +# included basemap support because its gridline labeling was more powerful +# than cartopy gridline labeling. However, as cartopy gridline labeling has +# significantly improved since version 0.18, proplot may deprecate basemap support +# in a future release and fully remove basemap support by version 1.0.0. + +# %% +import proplot as pplt +fig = pplt.figure() + +# Add projections +gs = pplt.GridSpec(ncols=2, nrows=3, hratios=(1, 1, 1.4)) +for i, proj in enumerate(('cyl', 'hammer', 'npstere')): + ax1 = fig.subplot(gs[i, 0], proj=proj) # default cartopy backend + ax2 = fig.subplot(gs[i, 1], proj=proj, backend='basemap') # basemap backend + +# Format projections +axs = fig.subplotgrid +axs.format( + land=True, + suptitle='Figure with several projections', + toplabels=('Cartopy examples', 'Basemap examples'), + toplabelweight='normal', + latlines=30, lonlines=60, +) +axs[:2].format(lonlabels='b', latlabels='r') # or lonlabels=True, lonlabels='bottom', +axs[2:4].format(lonlabels=False, latlabels='both') +axs[4:].format(lonlabels='all', lonlines=30) +pplt.rc.reset() + + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_geoplot: +# +# Plotting in projections +# ----------------------- +# +# In proplot, plotting with `~proplot.axes.GeoAxes` is just like plotting +# with `~proplot.axes.CartesianAxes`. Proplot makes longitude-latitude +# (i.e., Plate Carrée) coordinates the *default* coordinate system for all plotting +# commands by internally passing ``transform=ccrs.PlateCarree()`` to cartopy commands +# and ``latlon=True`` to basemap commands. And again, when `basemap`_ is the backend, +# plotting is done "cartopy-style" by calling methods from the `proplot.axes.GeoAxes` +# instance rather than the `~mpl_toolkits.basemap.Basemap` instance. +# +# To ensure that a 2D `~proplot.axes.PlotAxes` command like +# `~proplot.axes.PlotAxes.contour` or `~proplot.axes.PlotAxes.pcolor` +# fills the entire globe, simply pass ``globe=True`` to the command. +# This interpolates the data to the North and South poles and across the longitude +# seam before plotting. This is a convenient and succinct alternative to cartopy's +# `~cartopy.util.add_cyclic_point` and basemap's `~mpl_toolkits.basemap.addcyclic`. +# +# To draw content above or underneath a given geographic feature, simply change +# the `zorder `__ +# property for that feature. For example, to draw land patches on top of all plotted +# content as a "land mask" you can use ``ax.format(land=True, landzorder=4)`` or set +# ``pplt.rc['land.zorder'] = 4`` (see the :ref:`next section ` +# for details). + +# %% +import proplot as pplt +import numpy as np + +# Fake data with unusual longitude seam location and without coverage over poles +offset = -40 +lon = pplt.arange(offset, 360 + offset - 1, 60) +lat = pplt.arange(-60, 60 + 1, 30) +state = np.random.RandomState(51423) +data = state.rand(len(lat), len(lon)) + +# Plot data both without and with globe=True +for globe in (False, True): + string = 'with' if globe else 'without' + gs = pplt.GridSpec(nrows=2, ncols=2) + fig = pplt.figure(refwidth=2.5) + for i, ss in enumerate(gs): + cmap = ('sunset', 'sunrise')[i % 2] + backend = ('cartopy', 'basemap')[i % 2] + ax = fig.subplot(ss, proj='kav7', backend=backend) + if i > 1: + ax.pcolor(lon, lat, data, cmap=cmap, globe=globe, extend='both') + else: + m = ax.contourf(lon, lat, data, cmap=cmap, globe=globe, extend='both') + fig.colorbar(m, loc='b', span=i + 1, label='values', extendsize='1.7em') + fig.format( + suptitle=f'Geophysical data {string} global coverage', + toplabels=('Cartopy example', 'Basemap example'), + leftlabels=('Filled contours', 'Grid boxes'), + toplabelweight='normal', leftlabelweight='normal', + coast=True, lonlines=90, + abc='A.', abcloc='ul', abcborder=False, + ) + + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_geoformat: +# +# Formatting projections +# ---------------------- +# +# The `proplot.axes.GeoAxes.format` command facilitates geographic-specific axes +# modifications. It can toggle and configure the "major" and "minor" longitude and +# latitude gridline locations using the `grid`, `lonlocator`, `latlocator`, `gridminor`, +# `lonminorlocator`, and `latminorlocator` keys, and configure gridline label formatting +# with `lonformatter` and `latformatter` (analogous to `xlocator`, `xminorlocator`, +# and `xformatter` used by `proplot.axes.CartesianAxes.format`). By default, inline +# cartopy labels and cartopy label rotation are turned off, but inline labels can +# be turned on using ``loninline=True``, ``latinline=True``, or ``inlinelabels=True`` +# or by setting :rcraw:`grid.inlinelabels` to ``True``, and label rotation can be +# turned on using ``rotatelabels=True`` or by setting :rcraw:`grid.rotatelabels` +# to ``True``. The padding between the map edge and the labels can be changed +# using `labelpad` or by changing :rcraw:`grid.labelpad`. +# +# `proplot.axes.GeoAxes.format` can also set the cartopy projection bounding longitudes +# and latitudes with `lonlim` and `latlim` (analogous to `xlim` and `ylim`), set the +# latitude bound for circular polar projections using `boundinglat`, and toggle and +# configure geographic features like land masses, coastlines, and administrative +# borders using :ref:`settings ` like `land`, `landcolor`, `coast`, +# `coastcolor`, and `coastlinewidth`. Finally, since `proplot.axes.GeoAxes.format` +# calls `proplot.axes.Axes.format`, it can be used to add axes titles, a-b-c labels, +# and figure titles, just like `proplot.axes.CartesianAxes.format`. +# +# For details, see the `proplot.axes.GeoAxes.format` documentation. + +# %% +import proplot as pplt +gs = pplt.GridSpec(ncols=3, nrows=2, wratios=(1, 1, 1.2), hratios=(1, 1.2)) +fig = pplt.figure(refwidth=4) + +# Styling projections in different ways +ax = fig.subplot(gs[0, :2], proj='eqearth') +ax.format( + title='Equal earth', land=True, landcolor='navy', facecolor='pale blue', + coastcolor='gray5', borderscolor='gray5', innerborderscolor='gray5', + gridlinewidth=1.5, gridcolor='gray5', gridalpha=0.5, + gridminor=True, gridminorlinewidth=0.5, + coast=True, borders=True, borderslinewidth=0.8, +) +ax = fig.subplot(gs[0, 2], proj='ortho') +ax.format( + title='Orthographic', reso='med', land=True, coast=True, latlines=10, lonlines=15, + landcolor='mushroom', suptitle='Projection axes formatting demo', + facecolor='petrol', coastcolor='charcoal', coastlinewidth=0.8, gridlinewidth=1 +) +ax = fig.subplot(gs[1, :], proj='wintri') +ax.format( + land=True, facecolor='ocean blue', landcolor='bisque', title='Winkel tripel', + lonlines=60, latlines=15, + gridlinewidth=0.8, gridminor=True, gridminorlinestyle=':', + lonlabels=True, latlabels='r', loninline=True, + gridlabelcolor='gray8', gridlabelsize='med-large', +) +fig.format( + suptitle='Projection axes formatting demo', + toplabels=('Column 1', 'Column 2'), + abc='A.', abcloc='ul', abcborder=False, linewidth=1.5 +) + + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_zoom: +# +# Zooming into projections +# ------------------------ +# +# To zoom into cartopy projections, use +# `~cartopy.mpl.geoaxes.GeoAxes.set_extent` or pass `lonlim`, +# `latlim`, or `boundinglat` to `~proplot.axes.GeoAxes.format`. The `boundinglat` +# keyword controls the circular latitude boundary for North Polar and +# South Polar Stereographic, Azimuthal Equidistant, Lambert Azimuthal +# Equal-Area, and Gnomonic projections. By default, proplot tries to use the +# degree-minute-second cartopy locators and formatters made available in cartopy +# 0.18. You can switch from minute-second subintervals to traditional decimal +# subintervals by passing ``dms=False`` to `~proplot.axes.GeoAxes.format` +# or by setting :rcraw:`grid.dmslabels` to ``False``. +# +# To zoom into basemap projections, pass any of the `boundinglat`, +# `llcrnrlon`, `llcrnrlat`, `urcrnrlon`, `urcrnrlat`, `llcrnrx`, `llcrnry`, +# `urcrnrx`, `urcrnry`, `width`, or `height` keyword arguments to +# the `~proplot.constructor.Proj` constructor function either directly or via +# the `proj_kw` `~proplot.ui.subplots` keyword argument. You can also pass +# `lonlim` and `latlim` to `~proplot.constructor.Proj` and these arguments +# will be used for `llcrnrlon`, `llcrnrlat`, etc. You cannot zoom into basemap +# projections with `format` after they have already been created. + +# %% +import proplot as pplt + +# Plate Carrée map projection +pplt.rc.reso = 'med' # use higher res for zoomed in geographic features +basemap = pplt.Proj('cyl', lonlim=(-20, 180), latlim=(-10, 50), backend='basemap') +fig, axs = pplt.subplots(nrows=2, refwidth=5, proj=('cyl', basemap)) +axs.format( + land=True, labels=True, lonlines=20, latlines=20, + gridminor=True, suptitle='Zooming into projections' +) +axs[0].format(lonlim=(-140, 60), latlim=(-10, 50), labels=True) +axs[0].format(title='Cartopy example') +axs[1].format(title='Basemap example') + +# %% +import proplot as pplt + +# Pole-centered map projections +basemap = pplt.Proj('npaeqd', boundinglat=60, backend='basemap') +fig, axs = pplt.subplots(ncols=2, refwidth=2.7, proj=('splaea', basemap)) +fig.format(suptitle='Zooming into polar projections') +axs.format(land=True, latmax=80) # no gridlines poleward of 80 degrees +axs[0].format(boundinglat=-60, title='Cartopy example') +axs[1].format(title='Basemap example') + +# %% +import proplot as pplt + +# Zooming in on continents +fig = pplt.figure(refwidth=3) +ax = fig.subplot(121, proj='lcc', proj_kw={'lon0': 0}) +ax.format(lonlim=(-20, 50), latlim=(30, 70), title='Cartopy example') +proj = pplt.Proj('lcc', lon0=-100, lat0=45, width=8e6, height=8e6, backend='basemap') +ax = fig.subplot(122, proj=proj) +ax.format(lonlines=20, title='Basemap example') +fig.format(suptitle='Zooming into specific regions', land=True) + + +# %% +import proplot as pplt + +# Zooming in with cartopy degree-minute-second labels +pplt.rc.reso = 'hi' +fig = pplt.figure(refwidth=2.5) +ax = fig.subplot(121, proj='cyl') +ax.format(lonlim=(-7.5, 2), latlim=(49.5, 59)) +ax = fig.subplot(122, proj='cyl') +ax.format(lonlim=(-6, -2), latlim=(54.5, 58.5)) +fig.format( + land=True, labels=True, + borders=True, borderscolor='white', + suptitle='Cartopy degree-minute-second labels', +) +pplt.rc.reset() + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _proj_included: +# +# Included projections +# -------------------- +# +# The available `cartopy `__ +# and `basemap `__ projections are +# plotted below. The full table of projection names with links to the relevant +# `PROJ `__ documentation is found :ref:`here `. +# +# Proplot uses the cartopy API to add the Aitoff, Hammer, Winkel Tripel, and +# Kavrayskiy VII projections (i.e., ``'aitoff'``, ``'hammer'``, ``'wintri'``, +# and ``'kav7'``), as well as North and South polar versions of the Azimuthal +# Equidistant, Lambert Azimuthal Equal-Area, and Gnomonic projections (i.e., +# ``'npaeqd'``, ``'spaeqd'``, ``'nplaea'``, ``'splaea'``, ``'npgnom'``, and +# ``'spgnom'``), modeled after cartopy's existing `~cartopy.crs.NorthPolarStereo` +# and `~cartopy.crs.SouthPolarStereo` projections. + +# %% +import proplot as pplt + +# Table of cartopy projections +projs = [ + 'cyl', 'merc', 'mill', 'lcyl', 'tmerc', + 'robin', 'hammer', 'moll', 'kav7', 'aitoff', 'wintri', 'sinu', + 'geos', 'ortho', 'nsper', 'aea', 'eqdc', 'lcc', 'gnom', + 'npstere', 'nplaea', 'npaeqd', 'npgnom', 'igh', + 'eck1', 'eck2', 'eck3', 'eck4', 'eck5', 'eck6' +] +fig, axs = pplt.subplots(ncols=3, nrows=10, figwidth=7, proj=projs) +axs.format( + land=True, reso='lo', labels=False, + suptitle='Table of cartopy projections' +) +for proj, ax in zip(projs, axs): + ax.format(title=proj, titleweight='bold', labels=False) + +# %% +import proplot as pplt + +# Table of basemap projections +projs = [ + 'cyl', 'merc', 'mill', 'cea', 'gall', 'sinu', + 'eck4', 'robin', 'moll', 'kav7', 'hammer', 'mbtfpq', + 'geos', 'ortho', 'nsper', + 'vandg', 'aea', 'eqdc', 'gnom', 'cass', 'lcc', + 'npstere', 'npaeqd', 'nplaea' +] +fig, axs = pplt.subplots(ncols=3, nrows=8, figwidth=7, proj=projs, backend='basemap') +axs.format( + land=True, labels=False, + suptitle='Table of basemap projections' +) +for proj, ax in zip(projs, axs): + ax.format(title=proj, titleweight='bold', labels=False) diff --git a/docs/proplotrc b/docs/proplotrc new file mode 100644 index 000000000..695703999 --- /dev/null +++ b/docs/proplotrc @@ -0,0 +1,4 @@ +# Use SVG because quality of examples is highest priority +# Tested SVG vs. PNG and speeds are comparable! +inlineformat: svg +docstring.hardcopy: True diff --git a/docs/sphinxext/custom_roles.py b/docs/sphinxext/custom_roles.py index af3a8bc59..36a4e4570 100644 --- a/docs/sphinxext/custom_roles.py +++ b/docs/sphinxext/custom_roles.py @@ -1,38 +1,61 @@ +#!/usr/bin/env python3 +""" +Custom :rc: and :rcraw: roles for rc settings. +""" +import os + from docutils import nodes -from os.path import sep -from proplot import rc +from matplotlib import rcParams + +from proplot.internals import rcsetup -def get_nodes(rawtext, text, inliner): - rctext = (f"rc['{text}']" if '.' in text else f'rc.{text}') - rendered = nodes.Text(rctext) - source = inliner.document.attributes['source'].replace(sep, '/') +def _node_list(rawtext, text, inliner): + """ + Return a singleton node list or an empty list if source is unknown. + """ + source = inliner.document.attributes['source'].replace(os.path.sep, '/') relsource = source.split('/docs/', 1) if len(relsource) == 1: return [] - levels = relsource[1].count('/') # distance to 'docs' folder - refuri = ( - '../' * levels - + f'en/latest/configuration.html?highlight={text}#' - + ('rcparamslong' if '.' in text else 'rcparamsshort') - ) - ref = nodes.reference(rawtext, rendered, refuri=refuri) + if text in rcParams: + refuri = 'https://matplotlib.org/stable/tutorials/introductory/customizing.html' + refuri = f'{refuri}?highlight={text}#the-matplotlibrc-file' + else: + path = '../' * relsource[1].count('/') + 'en/stable' + refuri = f'{path}/configuration.html?highlight={text}#table-of-settings' + node = nodes.Text(f'rc[{text!r}]' if '.' in text else f'rc.{text}') + ref = nodes.reference(rawtext, node, refuri=refuri) return [nodes.literal('', '', ref)] -def rc_role(name, rawtext, text, lineno, inliner, options={}, content=[]): - node_list = get_nodes(rawtext, text, inliner) - if text in rc: - node_list.append(nodes.Text(' = ')) - node_list.append(nodes.literal('', '', nodes.Text(repr(rc[text])))) +def rc_raw_role(name, rawtext, text, lineno, inliner, options={}, content=[]): # noqa: U100, E501 + """ + The :rcraw: role. Includes a link to the setting. + """ + node_list = _node_list(rawtext, text, inliner) return node_list, [] -def rc_role_raw(name, rawtext, text, lineno, inliner, options={}, content=[]): - return get_nodes(rawtext, text, inliner), [] +def rc_role(name, rawtext, text, lineno, inliner, options={}, content=[]): # noqa: U100 + """ + The :rc: role. Includes a link to the setting and its default value. + """ + node_list = _node_list(rawtext, text, inliner) + try: + default = rcsetup._get_default_param(text) + except KeyError: + pass + else: + node_list.append(nodes.Text(' = ')) + node_list.append(nodes.literal('', '', nodes.Text(repr(default)))) + return node_list, [] def setup(app): + """ + Set up the roles. + """ app.add_role('rc', rc_role) - app.add_role('rcraw', rc_role_raw) + app.add_role('rcraw', rc_raw_role) return {'parallel_read_safe': True, 'parallel_write_safe': True} diff --git a/docs/stats.py b/docs/stats.py new file mode 100644 index 000000000..b4bdb453e --- /dev/null +++ b/docs/stats.py @@ -0,0 +1,259 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.11.4 +# kernelspec: +# display_name: Python 3 +# language: python +# name: python3 +# --- + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _pandas: https://pandas.pydata.org +# +# .. _xarray: http://xarray.pydata.org/en/stable/ +# +# .. _seaborn: https://seaborn.pydata.org +# +# .. _ug_stats: +# +# Statistical plotting +# ==================== +# +# This section documents a few basic additions to matplotlib's plotting commands +# that can be useful for statistical analysis. These features are implemented +# using the intermediate `~proplot.axes.PlotAxes` subclass (see the :ref:`1D plotting +# ` section for details). Some of these tools will be expanded in the +# future, but for a more comprehensive suite of statistical plotting utilities, you +# may be interested in `seaborn`_ (we try to ensure that seaborn plotting commands +# are compatible with proplot figures and axes). + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_errorbars: +# +# Error bars and shading +# ---------------------- +# +# Error bars and error shading can be quickly added on-the-fly to +# `~proplot.axes.PlotAxes.line`, `~proplot.axes.PlotAxes.linex` +# (equivalently, `~proplot.axes.PlotAxes.plot`, +# `~proplot.axes.PlotAxes.plotx`), `~proplot.axes.PlotAxes.scatter`, +# `~proplot.axes.PlotAxes.scatterx`, `~proplot.axes.PlotAxes.bar`, and +# `~proplot.axes.PlotAxes.barh` plots using any of several keyword arguments. +# +# If you pass 2D arrays to these commands with ``mean=True``, ``means=True``, +# ``median=True``, or ``medians=True``, the means or medians of each column are +# drawn as lines, points, or bars, while *error bars* or *error shading* +# indicates the spread of the distribution in each column. Invalid data is +# ignored. You can also specify the error bounds *manually* with the `bardata`, +# `boxdata`, `shadedata`, and `fadedata` keywords. These commands can draw and +# style thin error bars (the ``bar`` keywords), thick "boxes" overlaid on top of +# these bars (the ``box`` keywords; think of them as miniature boxplots), a +# transparent primary shading region (the ``shade`` keywords), and a more +# transparent secondary shading region (the ``fade`` keywords). See the +# documentation on the `~proplot.axes.PlotAxes` commands for details. + + +# %% +import numpy as np +import pandas as pd + +# Sample data +# Each column represents a distribution +state = np.random.RandomState(51423) +data = state.rand(20, 8).cumsum(axis=0).cumsum(axis=1)[:, ::-1] +data = data + 20 * state.normal(size=(20, 8)) + 30 +data = pd.DataFrame(data, columns=np.arange(0, 16, 2)) +data.columns.name = 'column number' +data.name = 'variable' + +# Calculate error data +# Passed to 'errdata' in the 3rd subplot example +means = data.mean(axis=0) +means.name = data.name # copy name for formatting +fadedata = np.percentile(data, (5, 95), axis=0) # light shading +shadedata = np.percentile(data, (25, 75), axis=0) # dark shading + +# %% +import proplot as pplt +import numpy as np + +# Loop through "vertical" and "horizontal" versions +varray = [[1], [2], [3]] +harray = [[1, 1], [2, 3], [2, 3]] +for orientation, array in zip(('vertical', 'horizontal'), (varray, harray)): + # Figure + fig = pplt.figure(refwidth=4, refaspect=1.5, share=False) + axs = fig.subplots(array, hratios=(2, 1, 1)) + axs.format(abc='A.', suptitle=f'Indicating {orientation} error bounds') + + # Medians and percentile ranges + ax = axs[0] + kw = dict( + color='light red', edgecolor='k', legend=True, + median=True, barpctile=90, boxpctile=True, + # median=True, barpctile=(5, 95), boxpctile=(25, 75) # equivalent + ) + if orientation == 'horizontal': + ax.barh(data, **kw) + else: + ax.bar(data, **kw) + ax.format(title='Bar plot') + + # Means and standard deviation range + ax = axs[1] + kw = dict( + color='denim', marker='x', markersize=8**2, linewidth=0.8, + label='mean', shadelabel=True, + mean=True, shadestd=1, + # mean=True, shadestd=(-1, 1) # equivalent + ) + if orientation == 'horizontal': + ax.scatterx(data, legend='b', legend_kw={'ncol': 1}, **kw) + else: + ax.scatter(data, legend='ll', **kw) + ax.format(title='Marker plot') + + # User-defined error bars + ax = axs[2] + kw = dict( + shadedata=shadedata, fadedata=fadedata, + label='mean', shadelabel='50% CI', fadelabel='90% CI', + color='ocean blue', barzorder=0, boxmarker=False, + ) + if orientation == 'horizontal': + ax.linex(means, legend='b', legend_kw={'ncol': 1}, **kw) + else: + ax.line(means, legend='ll', **kw) + ax.format(title='Line plot') + + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_boxplots: +# +# Box plots and violin plots +# -------------------------- +# +# Vertical and horizontal box and violin plots can be drawn using +# `~proplot.axes.PlotAxes.boxplot`, `~proplot.axes.PlotAxes.violinplot`, +# `~proplot.axes.PlotAxes.boxploth`, and `~proplot.axes.PlotAxes.violinploth` (or +# their new shorthands, `~proplot.axes.PlotAxes.box`, `~proplot.axes.PlotAxes.violin`, +# `~proplot.axes.PlotAxes.boxh`, and `~proplot.axes.PlotAxes.violinh`). The +# proplot versions employ aesthetically pleasing defaults and permit flexible +# configuration using keywords like `color`, `barcolor`, and `fillcolor`. +# They also automatically apply axis labels based on the `~pandas.DataFrame` +# or `~xarray.DataArray` column labels. Violin plot error bars are controlled +# with the same keywords used for :ref:`on-the-fly error bars `. + +# %% +import proplot as pplt +import numpy as np +import pandas as pd + +# Sample data +N = 500 +state = np.random.RandomState(51423) +data1 = state.normal(size=(N, 5)) + 2 * (state.rand(N, 5) - 0.5) * np.arange(5) +data1 = pd.DataFrame(data1, columns=pd.Index(list('abcde'), name='label')) +data2 = state.rand(100, 7) +data2 = pd.DataFrame(data2, columns=pd.Index(list('abcdefg'), name='label')) + +# Figure +fig, axs = pplt.subplots([[1, 1, 2, 2], [0, 3, 3, 0]], span=False) +axs.format( + abc='A.', titleloc='l', grid=False, + suptitle='Boxes and violins demo' +) + +# Box plots +ax = axs[0] +obj1 = ax.box(data1, means=True, marker='x', meancolor='r', fillcolor='gray4') +ax.format(title='Box plots') + +# Violin plots +ax = axs[1] +obj2 = ax.violin(data1, fillcolor='gray6', means=True, points=100) +ax.format(title='Violin plots') + +# Boxes with different colors +ax = axs[2] +ax.boxh(data2, cycle='pastel2') +ax.format(title='Multiple colors', ymargin=0.15) + + +# %% [raw] raw_mimetype="text/restructuredtext" tags=[] +# .. _ug_hist: +# +# Histograms and kernel density +# ----------------------------- +# +# Vertical and horizontal histograms can be drawn with +# `~proplot.axes.PlotAxes.hist` and `~proplot.axes.PlotAxes.histh`. +# As with the other 1D `~proplot.axes.PlotAxes` commands, multiple histograms +# can be drawn by passing 2D arrays instead of 1D arrays, and the color +# cycle used to color histograms can be changed on-the-fly using +# the `cycle` and `cycle_kw` keywords. Likewise, 2D histograms can +# be drawn with the `~proplot.axes.PlotAxes.hist2d` +# `~proplot.axes.PlotAxes.hexbin` commands, and their colormaps can +# be changed on-the-fly with the `cmap` and `cmap_kw` keywords (see +# the :ref:`2D plotting section `). Marginal distributions +# for the 2D histograms can be added using :ref:`panel axes `. +# +# In the future, proplot will include options for adding "smooth" kernel density +# estimations to histograms plots using a `kde` keyword. It will also include +# separate `proplot.axes.PlotAxes.kde` and `proplot.axes.PlotAxes.kde2d` commands. +# The `~proplot.axes.PlotAxes.violin` and `~proplot.axes.PlotAxes.violinh` commands +# will use the same algorithm for kernel density estimation as the `kde` commands. + +# %% +import proplot as pplt +import numpy as np + +# Sample data +M, N = 300, 3 +state = np.random.RandomState(51423) +x = state.normal(size=(M, N)) + state.rand(M)[:, None] * np.arange(N) + 2 * np.arange(N) + +# Sample overlayed histograms +fig, ax = pplt.subplots(refwidth=4, refaspect=(3, 2)) +ax.format(suptitle='Overlaid histograms', xlabel='distribution', ylabel='count') +res = ax.hist( + x, pplt.arange(-3, 8, 0.2), filled=True, alpha=0.7, edgecolor='k', + cycle=('indigo9', 'gray3', 'red9'), labels=list('abc'), legend='ul', +) + +# %% +import proplot as pplt +import numpy as np + +# Sample data +N = 500 +state = np.random.RandomState(51423) +x = state.normal(size=(N,)) +y = state.normal(size=(N,)) +bins = pplt.arange(-3, 3, 0.25) + +# Histogram with marginal distributions +fig, axs = pplt.subplots(ncols=2, refwidth=2.3) +axs.format( + abc='A.', abcloc='l', titleabove=True, + ylabel='y axis', suptitle='Histograms with marginal distributions' +) +colors = ('indigo9', 'red9') +titles = ('Group 1', 'Group 2') +for ax, which, color, title in zip(axs, 'lr', colors, titles): + ax.hist2d( + x, y, bins, vmin=0, vmax=10, levels=50, + cmap=color, colorbar='b', colorbar_kw={'label': 'count'} + ) + color = pplt.scale_luminance(color, 1.5) # histogram colors + px = ax.panel(which, space=0) + px.histh(y, bins, color=color, fill=True, ec='k') + px.format(grid=False, xlocator=[], xreverse=(which == 'l')) + px = ax.panel('t', space=0) + px.hist(x, bins, color=color, fill=True, ec='k') + px.format(grid=False, ylocator=[], title=title, titleloc='l') diff --git a/docs/subplots.ipynb b/docs/subplots.ipynb deleted file mode 100644 index 7ffd9bc96..000000000 --- a/docs/subplots.ipynb +++ /dev/null @@ -1,424 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Subplots features" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Automatic figure sizing" - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "By default, ProPlot automatically determines the suitable figure size given the geometry of your subplot grid. The figure size is constrained by the physical size of a *reference subplot*. This algorithm is controlled by a variety of `~proplot.subplots.subplots` keyword arguments:\n", - "\n", - "* The `ref` parameter sets the reference subplot number (default is ``1``, i.e. the subplot in the upper left corner).\n", - "* The `aspect` parameter sets the reference subplot aspect ratio (default is ``1``). You can also use the built-in matplotlib `~matplotlib.axes.Axes.set_aspect` method.\n", - "* The `axwidth` and `axheight` parameters set the physical dimensions of the *reference subplot* (default is ``axwidth=2``). If one is specified, the other is calculated to satisfy `aspect`. If both are specified, `aspect` is ignored. The physical dimensions of the *figure* are determined automatically.\n", - "* The `width` and `height` parameters set the physical dimensions of the *figure*. If one is specified, the other is calculated to satisfy `aspect` and the subplot spacing. If both are specified (or if the matplotlib `figsize` parameter is specified), `aspect` is ignored.\n", - "\n", - "This algorithm also includes the following notable features:\n", - "\n", - "* For very simple subplot grids (e.g. ``plot.subplots(ncols=2, nrows=3)``), `aspect`, `axwidth`, and `axheight` apply to every subplot in the figure -- not just the reference subplot.\n", - "* When the reference subplot `aspect ratio `__ has been manually overridden (e.g. with ``ax.set_aspect(1)``) or is set to ``'equal'`` (as with :ref:`map projections ` and `~matplotlib.axes.Axes.imshow` images), the `aspect` parameter is ignored.\n", - "* When `~proplot.axes.Axes.colorbar`\\ s and `~proplot.axes.Axes.panel`\\ s are present in the figure, their physical widths are *preserved* during figure resizing. ProPlot specifies their widths in physical units to help avoid colorbars that look \"too skinny\" or \"too fat\".\n", - "\n", - "The below examples demonstrate the default behavior of the automatic figure sizing algorithm, and how it can be controlled with `~proplot.subplots.subplots` keyword arguments." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", - "import numpy as np\n", - "# Cartopy projections\n", - "f, axs = plot.subplots(ncols=2, nrows=3, proj='robin')\n", - "axs.format(\n", - " land=True, landcolor='k',\n", - " suptitle='Auto figure sizing with grid of cartopy projections'\n", - ")\n", - "\n", - "# Images\n", - "state = np.random.RandomState(51423)\n", - "f, axs = plot.subplots(ncols=2, nrows=3)\n", - "colors = state.rand(10, 20, 3).cumsum(axis=2)\n", - "colors /= colors.max()\n", - "axs.imshow(colors)\n", - "axs.format(\n", - " suptitle='Auto figure sizing with grid of images'\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", - "# Loop through different axes widths\n", - "suptitle = 'Effect of subplot properties on figure size'\n", - "for axwidth in ('4cm', '6cm'):\n", - " f, axs = plot.subplots(ncols=2, axwidth=axwidth,)\n", - " axs[0].format(\n", - " suptitle=suptitle,\n", - " title=f'axwidth = {axwidth}', titleweight='bold',\n", - " titleloc='uc', titlecolor='red9',\n", - " )\n", - " \n", - "# Loop through different aspect ratios\n", - "for aspect in (1, (3,2)):\n", - " f, axs = plot.subplots(ncols=2, nrows=2, axwidth=1.6, aspect=aspect)\n", - " axs[0].format(\n", - " suptitle=suptitle,\n", - " title=f'aspect = {aspect}', titleweight='bold',\n", - " titleloc='uc', titlecolor='red9',\n", - " )" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", - "# Changing the reference subplot in the presence of unequal width/height ratios\n", - "suptitle = 'Effect of reference subplot on figure size'\n", - "for ref in (1, 2):\n", - " f, axs = plot.subplots(\n", - " ref=ref, nrows=3, ncols=3, wratios=(3, 2, 2),\n", - " axwidth=1.1,\n", - " )\n", - " axs[ref-1].format(\n", - " suptitle=suptitle,\n", - " title='reference axes', titleweight='bold',\n", - " titleloc='uc', titlecolor='red9'\n", - " )\n", - "\n", - "# Changing the reference subplot in a complex grid\n", - "for ref in (3, 2):\n", - " f, axs = plot.subplots(\n", - " [[1, 1, 2], [3, 4, 4]],\n", - " hratios=(1, 1.5), wratios=(3, 2, 2),\n", - " ref=ref, axwidth=1.1, span=False\n", - " )\n", - " axs[ref-1].format(\n", - " suptitle=suptitle,\n", - " title='reference axes', titleweight='bold',\n", - " titleloc='uc', titlecolor='red9'\n", - " )" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Automatic subplot spacing" - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "By default, ProPlot applies a *tight layout* algorithm to every figure. This algorithm automatically adjusts the space between subplot rows and columns and the figure edge to accomadate labels. It can be disabled by passing ``tight=False`` to `~proplot.subplots.subplots`.\n", - "\n", - "While matplotlib has `its own tight layout algorithm `__, ProPlot's algorithm permits variable spacing between subsequent subplot rows and columns (see the new `~proplot.subplots.GridSpec` class) and may change the figure size (depending on the keyword arguments passed to `~proplot.subplots.subplots`).\n", - "\n", - "ProPlot's tight layout algorithm can also be easily overridden. When you pass a spacing argument like `left`, `right`, `top`, `bottom`, `wspace`, or `hspace` to `~proplot.subplots.subplots`, that value is always respected:\n", - "\n", - "* With ``left='2em'``, the left margin is fixed but the right, bottom, and top margins are calculated automatically.\n", - "* With ``wspace=('3em', None)`` (and ``ncols=3``), the space between the first two columns is fixed, while the space between the second two columns is calculated automatically.\n", - "\n", - "The below examples demonstrate how the tight layout algorithm permits variable spacing between subplot rows and columns." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", - "f, axs = plot.subplots(\n", - " ref=ref, nrows=3, ncols=3, axwidth=1.1, share=0\n", - ")\n", - "axs[4].format(\n", - " title='title\\ntitle\\ntitle',\n", - " suptitle='Tight layout with variable row-column spacing'\n", - ")\n", - "axs[1].format(ylabel='ylabel\\nylabel\\nylabel')\n", - "axs[:4:2].format(xlabel='xlabel\\nxlabel\\nxlabel')\n", - "axs.format(\n", - " rowlabels=['Row 1', 'Row 2', 'Row 3'],\n", - " collabels=['Column 1', 'Column 2', 'Column 3']\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", - "f, axs = plot.subplots(\n", - " ncols=4, nrows=3, wspace=(0, 0, None), hspace=(0, None),\n", - " bottom='5em', right='5em', span=False,\n", - " axwidth=1.1,\n", - ")\n", - "axs.format(\n", - " suptitle='Tight layout with user overrides',\n", - " rowlabels=['Row 1', 'Row 2', 'Row 3'],\n", - " collabels=['Column 1', 'Column 2', 'Column 3', 'Column 4']\n", - ")\n", - "axs[0, :].format(xtickloc='top')\n", - "axs[2, :].format(xtickloc='both')\n", - "axs[:, 1].format(ytickloc='neither')\n", - "axs[:, 2].format(ytickloc='right')\n", - "axs[:, 3].format(ytickloc='both')\n", - "axs[-1, :].format(title='Title\\nTitle\\nTitle', xlabel='xlabel')\n", - "axs[:, 0].format(ylabel='ylabel\\nylabel')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Arbitrary physical units" - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "ProPlot supports arbitrary *physical units* for controlling the figure `width` and `height`; the reference subplot `axwidth` and `axheight`; the gridspec spacing values `left`, `right`, `bottom`, `top`, `wspace`, and `hspace`; and in a few other places, e.g. `~proplot.axes.Axes.panel` and `~proplot.axes.Axes.colorbar` widths. This feature is powered by the `~proplot.utils.units` function.\n", - "\n", - "If a sizing argument is numeric, the units are inches or points; if it is string, the units are converted to inches or points by `~proplot.utils.units`. A table of acceptable units is found in the `~proplot.utils.units` documentation. They include centimeters, millimeters, pixels, `em-heights `__, and `points `__." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", - "import numpy as np\n", - "with plot.rc.context(small='12px', large='15px'):\n", - " f, axs = plot.subplots(\n", - " ncols=3, width='15cm', height='2.5in',\n", - " wspace=('10pt', '20pt'), right='10mm'\n", - " )\n", - " panel = axs[2].panel_axes('r', width='2em')\n", - "axs.format(\n", - " suptitle='Arguments with arbitrary units',\n", - " xlabel='x axis', ylabel='y axis'\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Subplot numbers and a-b-c labels" - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "ProPlot can be used to add \"a-b-c\" labels to subplots. This is possible because `~proplot.subplots.subplots` assigns unique `~proplot.axes.Axes.number`\\ s to each subplot. If you passed an `array` to `~proplot.subplots.subplots`, the subplot numbers correspond to the numbers in the array; otherwise, if you used the `ncols` and `nrows` keyword arguments, the number order is row-major by default but can be switched to column-major by passing ``order='C'`` to `~proplot.subplots.subplots`. The number order also determines the subplot order in the `~proplot.subplots.subplot_grid` returned by `~proplot.subplots.subplots`.\n", - "\n", - "To turn on \"a-b-c\" labels, set :rcraw:`abc` to ``True`` or pass ``abc=True`` to `~proplot.axes.Axes.format` (see :ref:`the format command ` for details). To change the label style, modify :rcraw:`abc.style` or pass e.g. ``abcstyle='A.'`` to `~proplot.axes.Axes.format`. You can also modify the \"a-b-c\" label location, weight, and size with the :rcraw:`abc.loc`, :rcraw:`abc.weight`, and :rcraw:`abc.size` settings." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", - "f, axs = plot.subplots(nrows=8, ncols=8, axwidth=0.7, space=0)\n", - "axs.format(\n", - " abc=True, abcloc='ur', xlabel='x axis', ylabel='y axis',\n", - " xticks=[], yticks=[], suptitle='Subplot labels demo'\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Shared and spanning labels" - ] - }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "Matplotlib has an \"axis sharing\" feature that holds axis limits the same for axes within a grid of subplots. But this has no effect on the axis labels and tick labels, which can lead to lots of redundancies.\n", - "\n", - "To help you eliminate these redundancies, ProPlot introduces *four axis-sharing options* and a new *spanning label option*, controlled by the `share`, `sharex`, `sharey`, `span`, `spanx`, and `spany` `~proplot.subplots.subplots` keyword args. \"Sharing level\" ``1`` hides inner *x* and *y* axis labels. \"Sharing level\" ``2`` is the same as ``1``, but the *x* and *y* axis limits are locked. \"Sharing level\" ``3`` is the same as ``2``, but the *x* and *y* tick labels are hidden. \"Spanning labels\" are centered *x* and *y* axis labels used for subplots whose spines are on the same row or column. See the below example.\n", - "\n", - "Note that the the \"shared\" and \"spanning\" axes are determined automatically based on the extent of each subplot in the `~proplot.subplots.GridSpec`. Since ProPlot uses just one `~proplot.subplots.GridSpec` per figure, this can be done with zero ambiguity." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", - "import numpy as np\n", - "N = 50\n", - "M = 40\n", - "state = np.random.RandomState(51423)\n", - "colors = plot.Colors('grays_r', M, left=0.1, right=0.8)\n", - "datas = []\n", - "for scale in (1, 3, 7, 0.2):\n", - " data = scale * (state.rand(N, M) - 0.5).cumsum(axis=0)[N//2:, :]\n", - " datas.append(data)\n", - " \n", - "# Same plot with different sharing and spanning settings\n", - "for share in (0, 1, 2, 3):\n", - " f, axs = plot.subplots(\n", - " ncols=4, aspect=1, axwidth=1.2,\n", - " sharey=share, spanx=share//2\n", - " )\n", - " for ax, data in zip(axs, datas):\n", - " ax.plot(data, cycle=colors)\n", - " ax.format(\n", - " suptitle=f'Axis-sharing level: {share}, spanning labels {[\"off\",\"on\"][share//2]}',\n", - " grid=False, xlabel='spanning', ylabel='shared'\n", - " )" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", - "import numpy as np\n", - "plot.rc.reset()\n", - "plot.rc.cycle = 'Set3'\n", - "state = np.random.RandomState(51423)\n", - "titles = ['With redundant labels', 'Without redundant labels']\n", - "\n", - "# Same plot with and without default sharing settings\n", - "for mode in (0, 1):\n", - " f, axs = plot.subplots(\n", - " nrows=4, ncols=4, share=3*mode,\n", - " span=1*mode, axwidth=1\n", - " )\n", - " for ax in axs:\n", - " ax.plot((state.rand(100, 20) - 0.4).cumsum(axis=0))\n", - " axs.format(\n", - " xlabel='xlabel', ylabel='ylabel', suptitle=titles[mode],\n", - " abc=True, abcloc='ul',\n", - " grid=False, xticks=25, yticks=5\n", - " )" - ] - } - ], - "metadata": { - "celltoolbar": "Raw Cell Format", - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.3" - }, - "toc": { - "colors": { - "hover_highlight": "#ece260", - "navigate_num": "#000000", - "navigate_text": "#000000", - "running_highlight": "#FF0000", - "selected_highlight": "#fff968", - "sidebar_border": "#ffffff", - "wrapper_background": "#ffffff" - }, - "moveMenuLeft": false, - "nav_menu": { - "height": "12px", - "width": "250px" - }, - "navigate_menu": true, - "number_sections": true, - "sideBar": true, - "threshold": 4, - "toc_cell": false, - "toc_section_display": "block", - "toc_window_display": true, - "widenNotebook": false - }, - "varInspector": { - "cols": { - "lenName": 16, - "lenType": 16, - "lenVar": 40 - }, - "kernels_config": { - "python": { - "delete_cmd_postfix": "", - "delete_cmd_prefix": "del ", - "library": "var_list.py", - "varRefreshCmd": "print(var_dic_list())" - }, - "r": { - "delete_cmd_postfix": ") ", - "delete_cmd_prefix": "rm(", - "library": "var_list.r", - "varRefreshCmd": "cat(var_dic_list()) " - } - }, - "types_to_exclude": [ - "module", - "function", - "builtin_function_or_method", - "instance", - "_Feature" - ], - "window_display": false - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/docs/subplots.py b/docs/subplots.py new file mode 100644 index 000000000..0af5e74b4 --- /dev/null +++ b/docs/subplots.py @@ -0,0 +1,465 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.11.4 +# kernelspec: +# display_name: Python 3 +# language: python +# name: python3 +# --- + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_layout: +# +# Subplots +# ======== +# +# This section documents a variety of features related to proplot subplots, +# including a-b-c subplot labels, axis sharing between subplots, automatic +# "tight layout" spacing between subplots, and a unique feature where the figure +# width and/or height are automatically adjusted based on the subplot geometry. +# +# .. note:: +# +# Proplot only supports one `~proplot.gridspec.GridSpec` per figure +# (see the section on :ref:`adding subplots `), and proplot +# does not officially support the "nested" matplotlib structures +# `~matplotlib.gridspec.GridSpecFromSubplotSpec` and `~matplotlib.figure.SubFigure`. +# These restrictions have the advantage of 1) considerably simplifying the +# :ref:`tight layout ` and :ref:`figure size ` +# algorithms and 2) reducing the ambiguity of :ref:`a-b-c label assignment ` +# and :ref:`automatic axis sharing ` between subplots. If you need the +# features associated with "nested" matplotlib structures, some are reproducible +# with proplot -- including :ref:`different spaces ` between distinct +# subplot rows and columns and :ref:`different formatting ` for +# distinct groups of subplots. +# +# +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_abc: +# +# A-b-c labels +# ------------ +# +# Proplot can quickly add "a-b-c" labels using the +# `~proplot.axes.Axes.number` assigned to each subplot. +# If you add subplots one-by-one with `~proplot.figure.Figure.add_subplot`, you can +# manually specify the number with the `number` keyword. By default, the subplot number +# is incremented by ``1`` each time you call `~proplot.figure.Figure.add_subplot`. +# If you draw all of your subplots at once with `~proplot.figure.Figure.add_subplots`, +# the numbers depend on the input arguments. If you +# :ref:`passed an array `, the subplot numbers correspond to the numbers +# in the array. But if you used the `ncols` and `nrows` keyword arguments, the +# number order is row-major by default and can be switched to column-major by +# passing ``order='F'`` (note the number order also determines the list order in the +# `~proplot.gridspec.SubplotGrid` returned by `~proplot.figure.Figure.add_subplots`). +# +# To turn on "a-b-c" labels, set :rcraw:`abc` to ``True`` or pass ``abc=True`` +# to `~proplot.axes.Axes.format` (see :ref:`the format command ` +# for details). To change the label style, set :rcraw:`abc` to e.g. ``'A.'`` or +# pass e.g. ``abc='A.'`` to `~proplot.axes.Axes.format`. You can also modify +# the "a-b-c" label location, weight, and size with the :rcraw:`abc.loc`, +# :rcraw:`abc.weight`, and :rcraw:`abc.size` settings. Also note that if the +# an "a-b-c" label and title are in the same position, they are automatically +# offset away from each other. +# +# .. note:: +# +# "Inner" a-b-c labels and titles are surrounded with a white border when +# :rcraw:`abc.border` and :rcraw:`title.border` are ``True`` (the default). +# White boxes can be used instead by setting :rcraw:`abc.bbox` and +# :rcraw:`title.bbox` to ``True``. These options help labels stand out +# against plotted content. Any text can be given "borders" or "boxes" by +# passing ``border=True`` or ``bbox=True`` to `proplot.axes.Axes.text`. + +# %% +import proplot as pplt +fig = pplt.figure(space=0, refwidth='10em') +axs = fig.subplots(nrows=3, ncols=3) +axs.format( + abc='A.', abcloc='ul', + xticks='null', yticks='null', facecolor='gray5', + xlabel='x axis', ylabel='y axis', + suptitle='A-b-c label offsetting, borders, and boxes', +) +axs[:3].format(abcloc='l', titleloc='l', title='Title') +axs[-3:].format(abcbbox=True) # also disables abcborder +# axs[:-3].format(abcborder=True) # this is already the default + +# %% +import proplot as pplt +fig = pplt.figure(space=0, refwidth=0.7) +axs = fig.subplots(nrows=8, ncols=8) +axs.format( + abc=True, abcloc='ur', + xlabel='x axis', ylabel='y axis', xticks=[], yticks=[], + suptitle='A-b-c label stress test' +) + + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_autosize: +# +# Figure width and height +# ----------------------- +# +# Proplot automatically adjusts the figure width and height by default to +# respect the physical size of a "reference" subplot and the geometry of the +# `~proplot.figure.Figure.gridspec`. The "reference" subplot is the subplot whose +# `~proplot.axes.Axes.number` matches the `refnum` that was passed to +# `~proplot.figure.Figure` (the default `refnum` of ``1`` usually matches the subplot +# in the upper-left corner -- see :ref:`this section ` for more on subplot +# numbers). Alternatively, you can request a fixed figure width (height), and the +# algorithm will automatically adjusts the figure height (width) to respect +# the `~proplot.figure.Figure.gridspec` geometry. + +# This algorithm is extremely powerful and generally produces more aesthetically +# pleasing subplot grids out-of-the-box, especially when they contain images or map +# projections (see below). It is constrained by the following `~proplot.figure.Figure` +# keyword arguments: +# +# * `refwidth` and `refheight` set the physical width and height of the reference +# subplot (default is :rc:`subplots.refwidth`). If just the width (height) is +# specified, then the height (width) is automatically adjusted to satisfy the +# subplot spacing and the reference subplot aspect ratio `refaspect` (default +# is ``1`` unless the data aspect ratio is fixed -- see below). If both the +# width and height are specified, then `refaspect` is ignored. +# * `figwidth` and `figheight` set the physical width and height of the figure. +# As in matplotlib, you can use `figsize` to set both at once. If just the width +# (height) is specified, then the height (width) is automatically adjusted, just +# like with `refwidth` and `refheight`. If both the width and height are specified +# (e.g., using `figsize`), then `refaspect` is ignored and the figure size is fixed. +# Note that `figwidth` and `figheight` always override `refwidth` and `refheight`. +# * `journal` sets the physical dimensions of the figure to meet requirements +# for submission to an academic journal. For example, ``journal='nat1'`` sets +# `figwidth` according to the `*Nature* standard for single-column figures +# `__ and +# ``journal='aaas2'`` sets `figwidth` according to the +# `*Science* standard for dual-column figures +# `__. +# See :ref:`this table ` for the currently available journal +# specifications (feel free to add to this list with a :ref:`pull request +# `). +# +# The below examples demonstrate how different keyword arguments and +# subplot arrangements influence the figure size algorithm. +# +# .. important:: +# +# * If the `data aspect ratio +# `__ +# of the reference subplot is fixed (either due to calling +# `~matplotlib.axes.Axes.set_aspect` or filling the subplot with a +# :ref:`geographic projection `, `~proplot.axes.PlotAxes.imshow` +# plot, or `~proplot.axes.PlotAxes.heatmap` plot), then this is used as +# the default value for the reference aspect ratio `refaspect`. This helps +# minimize excess space between grids of subplots with fixed aspect ratios. +# * For the simplest subplot grids (e.g., those created by passing integers to +# `~proplot.figure.Figure.add_subplot` or passing `ncols` or `nrows` to +# `~proplot.figure.Figure.add_subplots`) the keyword arguments `refaspect`, +# `refwidth`, and `refheight` effectively apply to every subplot in the +# figure -- not just the reference subplot. +# * The physical widths of proplot `~proplot.axes.Axes.colorbar`\ s and +# `~proplot.axes.Axes.panel`\ s are always independent of the figure size. +# `~proplot.gridspec.GridSpec` specifies their widths in physical units to help +# users avoid drawing colorbars and panels that look "too skinny" or "too fat" +# depending on the number of subplots in the figure. + +# %% +import proplot as pplt +import numpy as np + +# Grid of images (note the square pixels) +state = np.random.RandomState(51423) +colors = np.tile(state.rand(8, 12, 1), (1, 1, 3)) +fig, axs = pplt.subplots(ncols=3, nrows=2, refwidth=1.7) +fig.format(suptitle='Auto figure dimensions for grid of images') +for ax in axs: + ax.imshow(colors) + +# Grid of cartopy projections +fig, axs = pplt.subplots(ncols=2, nrows=3, proj='robin') +axs.format(land=True, landcolor='k') +fig.format(suptitle='Auto figure dimensions for grid of cartopy projections') + + +# %% +import proplot as pplt +pplt.rc.update(grid=False, titleloc='uc', titleweight='bold', titlecolor='red9') + +# Change the reference subplot width +suptitle = 'Effect of subplot width on figure size' +for refwidth in ('3cm', '5cm'): + fig, axs = pplt.subplots(ncols=2, refwidth=refwidth,) + axs[0].format(title=f'refwidth = {refwidth}', suptitle=suptitle) + +# Change the reference subplot aspect ratio +suptitle = 'Effect of subplot aspect ratio on figure size' +for refaspect in (1, 2): + fig, axs = pplt.subplots(ncols=2, refwidth=1.6, refaspect=refaspect) + axs[0].format(title=f'refaspect = {refaspect}', suptitle=suptitle) + +# Change the reference subplot +suptitle = 'Effect of reference subplot on figure size' +for ref in (1, 2): # with different width ratios + fig, axs = pplt.subplots(ncols=3, wratios=(3, 2, 2), ref=ref, refwidth=1.1) + axs[ref - 1].format(title='reference', suptitle=suptitle) +for ref in (1, 2): # with complex subplot grid + fig, axs = pplt.subplots([[1, 2], [1, 3]], refnum=ref, refwidth=1.8) + axs[ref - 1].format(title='reference', suptitle=suptitle) + +pplt.rc.reset() + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_tight: +# +# Spacing and tight layout +# ------------------------ +# +# Proplot automatically adjusts the spacing between subplots +# by default to accomadate labels using its own `"tight layout" algorithm +# `__. +# In contrast to matplotlib's algorithm, proplot's algorithm can :ref:`change the +# figure size ` and permits variable spacing between each subplot +# row and column (see `proplot.gridspec.GridSpec` for details). +# This algorithm can be disabled entirely by passing ``tight=False`` to +# `~proplot.figure.Figure` or by setting :rcraw:`subplots.tight` to ``False``, or +# it can be partly overridden by passing any of the spacing arguments `left`, `right`, +# `top`, `bottom`, `wspace`, or `hspace` to `~proplot.figure.Figure` or +# `~proplot.gridspec.GridSpec`. For example: +# +# * ``left=2`` fixes the left margin at 2 em-widths, while the right, +# bottom, and top margin widths are determined by the tight layout algorithm. +# * ``wspace=1`` fixes the space between subplot columns at 1 em-width, while the +# space between subplot rows is determined by the tight layout algorithm. +# * ``wspace=(3, None)`` fixes the space between the first two columns of +# a three-column plot at 3 em-widths, while the space between the second two +# columns is determined by the tight layout algorithm. +# +# The padding between the tight layout extents (rather than the absolute spaces +# between subplot edges) can also be changed by passing `outerpad`, `innerpad`, +# or `panelpad` to `~proplot.figure.Figure` or `~proplot.gridspec.GridSpec`. +# This padding can be set locally by passing an array of values to `wpad` +# and `hpad` (analogous to `wspace` and `hspace`), or by passing the `pad` +# keyword when creating :ref:`panel axes ` or :ref:`outer +# colorbars or legends ` (analogous to `space`). +# +# All the subplot spacing arguments can be specified with a +# :ref:`unit string ` interpreted by `~proplot.utils.units`. +# The default unit assumed for numeric arguments is an "em-width" (i.e., a +# :rcraw:`font.size` width -- see the :ref:`units table ` for details). +# +# .. note:: + +# The core behavior of the tight layout algorithm can be modified with a few +# keyword arguments and settings. Using ``wequal=True``, ``hequal=True``, or +# ``equal=True`` (or setting :rcraw:`subplots.equalspace` to ``True``) constrains +# the tight layout algorithm to produce equal spacing between main subplot columns +# or rows (note that equal spacing is the default behavior when tight layout is +# disabled). Similarly, using ``wgroup=False``, ``hgroup=False``, or ``group=False`` +# (or setting :rcraw:`subplots.groupspace` to ``False``) disables the default +# behavior of only comparing subplot extent between adjacent subplot "groups" +# and instead compares subplot extents across entire columns and rows +# (note the spacing between the first and second row in the below example). + +# %% +import proplot as pplt + +# Stress test of the tight layout algorithm +# This time override the algorithm between selected subplot rows/columns +fig, axs = pplt.subplots( + ncols=4, nrows=3, refwidth=1.1, span=False, + bottom='5em', right='5em', # margin spacing overrides + wspace=(0, 0, None), hspace=(0, None), # column and row spacing overrides +) +axs.format( + grid=False, + xlocator=1, ylocator=1, tickdir='inout', + xlim=(-1.5, 1.5), ylim=(-1.5, 1.5), + suptitle='Tight layout with user overrides', + toplabels=('Column 1', 'Column 2', 'Column 3', 'Column 4'), + leftlabels=('Row 1', 'Row 2', 'Row 3'), +) +axs[0, :].format(xtickloc='top') +axs[2, :].format(xtickloc='both') +axs[:, 1].format(ytickloc='neither') +axs[:, 2].format(ytickloc='right') +axs[:, 3].format(ytickloc='both') +axs[-1, :].format(xlabel='xlabel', title='Title\nTitle\nTitle') +axs[:, 0].format(ylabel='ylabel') + + +# %% +import proplot as pplt + +# Stress test of the tight layout algorithm +# Add large labels along the edge of one subplot +equals = [('unequal', False), ('unequal', False), ('equal', True)] +groups = [('grouped', True), ('ungrouped', False), ('grouped', True)] +for (name1, equal), (name2, group) in zip(equals, groups): + suffix = ' (default)' if group and not equal else '' + suptitle = f'Tight layout with "{name1}" and "{name2}" row-column spacing{suffix}' + fig, axs = pplt.subplots( + nrows=3, ncols=3, refwidth=1.1, share=False, equal=equal, group=group, + ) + axs[1].format( + xlabel='xlabel\nxlabel', + ylabel='ylabel\nylabel\nylabel\nylabel' + ) + axs[3:6:2].format( + title='Title\nTitle', + titlesize='med', + ) + axs.format( + grid=False, + toplabels=('Column 1', 'Column 2', 'Column 3'), + leftlabels=('Row 1', 'Row 2', 'Row 3'), + suptitle=suptitle, + ) + +# %% [raw] raw_mimetype="text/restructuredtext" tags=[] +# .. _ug_share: +# +# Axis label sharing +# ------------------ +# +# Figures with lots of subplots often have :ref:`redundant labels `. +# To help address this, the matplotlib command `matplotlib.pyplot.subplots` includes +# `sharex` and `sharey` keywords that permit sharing axis limits and ticks between +# like rows and columns of subplots. Proplot builds on this feature by: +# +# #. Automatically sharing axes between subplots and :ref:`panels ` +# occupying the same rows or columns of the `~proplot.gridspec.GridSpec`. This +# works for :ref:`aribtrarily complex subplot grids `. It also works +# for subplots generated one-by-one with `~proplot.figure.Figure.add_subplot` +# rather than `~proplot.figure.Figure.subplots`. It is controlled by the `sharex` +# and `sharey` `~proplot.figure.Figure` keywords (default is :rc:`subplots.share`). +# Use the `share` keyword as a shorthand to set both `sharex` and `sharey`. +# #. Automatically sharing labels across subplots and :ref:`panels ` +# with edges along the same row or column of the `~proplot.gridspec.GridSpec`. +# This also works for complex subplot grids and subplots generated one-by-one. +# It is controlled by the `spanx` and `spany` `~proplot.figure.Figure` +# keywords (default is :rc:`subplots.span`). Use the `span` keyword +# as a shorthand to set both `spanx` and `spany`. Note that unlike +# `~matplotlib.figure.Figure.supxlabel` and `~matplotlib.figure.Figure.supylabel`, +# these labels are aligned between gridspec edges rather than figure edges. +# #. Supporting five sharing "levels". These values can be passed to `sharex`, +# `sharey`, or `share`, or assigned to :rcraw:`subplots.share`. The levels +# are defined as follows: +# +# * ``False`` or ``0``: Axis sharing is disabled. +# * ``'labels'``, ``'labs'``, or ``1``: Axis labels are shared, but +# nothing else. Labels will appear on the leftmost and bottommost subplots. +# * ``'limits'``, ``'lims'``, or ``2``: Same as ``1``, but axis limits, axis +# scales, and major and minor tick locations and formatting are also shared. +# * ``True`` or ``3`` (default): Same as ``2``, but axis tick labels are also +# shared. Tick labels will appear on the leftmost and bottommost subplots. +# * ``'all'`` or ``4``: Same as ``3``, but axis limits, axis scales, and +# axis ticks are shared even between subplots not in the same row or column. +# +# The below examples demonstrate the effect of various axis and label sharing +# settings on the appearance of several subplot grids. + +# %% +import proplot as pplt +import numpy as np +N = 50 +M = 40 +state = np.random.RandomState(51423) +cycle = pplt.Cycle('grays_r', M, left=0.1, right=0.8) +datas = [] +for scale in (1, 3, 7, 0.2): + data = scale * (state.rand(N, M) - 0.5).cumsum(axis=0)[N // 2:, :] + datas.append(data) + +# Plots with different sharing and spanning settings +# Note that span=True and share=True are the defaults +spans = (False, False, True, True) +shares = (False, 'labels', 'limits', True) +for i, (span, share) in enumerate(zip(spans, shares)): + fig = pplt.figure(refaspect=1, refwidth=1.06, spanx=span, sharey=share) + axs = fig.subplots(ncols=4) + for ax, data in zip(axs, datas): + on = ('off', 'on')[int(span)] + ax.plot(data, cycle=cycle) + ax.format( + grid=False, xlabel='spanning axis', ylabel='shared axis', + suptitle=f'Sharing mode {share!r} (level {i}) with spanning labels {on}' + ) + +# %% +import proplot as pplt +import numpy as np +state = np.random.RandomState(51423) + +# Plots with minimum and maximum sharing settings +# Note that all x and y axis limits and ticks are identical +spans = (False, True) +shares = (False, 'all') +titles = ('Minimum sharing', 'Maximum sharing') +for span, share, title in zip(spans, shares, titles): + fig = pplt.figure(refwidth=1, span=span, share=share) + axs = fig.subplots(nrows=4, ncols=4) + for ax in axs: + data = (state.rand(100, 20) - 0.4).cumsum(axis=0) + ax.plot(data, cycle='Set3') + axs.format( + abc=True, abcloc='ul', suptitle=title, + xlabel='xlabel', ylabel='ylabel', + grid=False, xticks=25, yticks=5 + ) + + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_units: +# +# Physical units +# -------------- +# +# Proplot supports arbitrary physical units for controlling the figure +# `figwidth` and `figheight`; the reference subplot `refwidth` and `refheight`; +# the gridspec spacing and tight layout padding keywords `left`, `right`, `bottom`, +# `top`, `wspace`, `hspace`, `outerpad`, `innerpad`, `panelpad`, `wpad`, and `hpad`; +# the `~proplot.axes.Axes.colorbar` and `~proplot.axes.Axes.panel` widths; +# various `~proplot.axes.Axes.legend` spacing and padding arguments; various +# `~proplot.axes.Axes.format` font size and padding arguments; the line width and +# marker size arguments passed to `~proplot.axes.PlotAxes` commands; and all +# applicable `~proplot.config.rc` settings, e.g. :rcraw:`subplots.refwidth`, +# :rcraw:`legend.columnspacing`, and :rcraw:`axes.labelpad`. This feature is +# powered by the physical units engine `~proplot.utils.units`. +# +# When one of these keyword arguments is numeric, a default physical unit is +# used. For subplot and figure sizes, the defult unit is inches. For gridspec and +# legend spaces, the default unit is `em-widths +# `__. +# For font sizes, text padding, and +# line widths, the default unit is +# `points `__. +# See the relevant documentation in the :ref:`API reference ` for details. +# A table of acceptable physical units is found :ref:`here ` +# -- they include centimeters, millimeters, pixels, +# `em-widths `__, +# `en-heights `__, +# and `points `__. + +# %% +import proplot as pplt +import numpy as np +with pplt.rc.context(fontsize='12px'): # depends on rc['figure.dpi'] + fig, axs = pplt.subplots( + ncols=3, figwidth='15cm', figheight='3in', + wspace=('10pt', '20pt'), right='10mm', + ) + cb = fig.colorbar( + 'Mono', loc='b', extend='both', label='colorbar', + width='2em', extendsize='3em', shrink=0.8, + ) + pax = axs[2].panel_axes('r', width='5en') +axs.format( + suptitle='Arguments with arbitrary units', + xlabel='x axis', ylabel='y axis', +) diff --git a/docs/usage.rst b/docs/usage.rst index bd4e1274a..0fa69ec00 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -1,96 +1,203 @@ +.. _cartopy: https://scitools.org.uk/cartopy/docs/latest/ + +.. _basemap: https://matplotlib.org/basemap/index.html + +.. _seaborn: https://seaborn.pydata.org + +.. _pandas: https://pandas.pydata.org + +.. _xarray: http://xarray.pydata.org/en/stable/ + +.. _usage: + ============= -Using ProPlot +Using proplot ============= -.. - This page gives a condensed overview of these features, along with features - outside of these classes. -.. - This page is meant as the starting point for new users. It is - populated with links to the :ref:`API reference` and User Guide. - For more in-depth descriptions, see :ref:`Why ProPlot?`. +This page offers a condensed overview of proplot's features. It is populated +with links to the :ref:`API reference` and :ref:`User Guide `. +For a more in-depth discussion, see :ref:`Why proplot?`. + +.. _usage_background: Background ========== -ProPlot is an object-oriented matplotlib wrapper. The "wrapper" part means that -ProPlot's features are largely a *superset* of matplotlib. -You can use your favorite plotting commands like -`~matplotlib.axes.Axes.plot`, `~matplotlib.axes.Axes.scatter`, `~matplotlib.axes.Axes.contour`, and `~matplotlib.axes.Axes.pcolor` like you always have. -The "object-oriented" part means that ProPlot's features are implemented with *subclasses* of the `~matplotlib.figure.Figure` and `~matplotlib.axes.Axes` classes. +Proplot is an object-oriented matplotlib wrapper. The "wrapper" part means +that proplot's features are largely a *superset* of matplotlib. You can use +plotting commands like `~matplotlib.axes.Axes.plot`, `~matplotlib.axes.Axes.scatter`, +`~matplotlib.axes.Axes.contour`, and `~matplotlib.axes.Axes.pcolor` like you always +have. The "object-oriented" part means that proplot's features are implemented with +*subclasses* of the `~matplotlib.figure.Figure` and `~matplotlib.axes.Axes` classes. -If you tend to use `~matplotlib.pyplot` and are not familiar with figure and axes *classes*, check out `this guide from the matplotlib documentation `__. Working with objects directly tends to be more clear and concise than `~matplotlib.pyplot`, makes things easier when working with multiple figures and axes, and is certainly more "`pythonic `__". Therefore, although some ProPlot features may still work, we do not officially support the `~matplotlib.pyplot` API. +If you tend to use `~matplotlib.pyplot` and are not familiar with the figure and axes +classes, check out `this guide `__. +Directly working with matplotlib classes tends to be more clear and concise than +`~matplotlib.pyplot`, makes things easier when working with multiple figures and axes, +and is certainly more "`pythonic `__". +Therefore, although many proplot features may still work, we do not officially +support the `~matplotlib.pyplot` interface. +.. _usage_import: Importing proplot ================= -We recommend importing ProPlot as follows: +Importing proplot immediately adds several +new :ref:`colormaps `, :ref:`property cycles `, +:ref:`color names `, and :ref:`fonts ` to matplotlib. +If you are only interested in these features, you may want to +import proplot at the top of your script and do nothing else! +We recommend importing proplot as follows: .. code-block:: python - import proplot as plot + import proplot as pplt -This differentiates ProPlot from the usual ``plt`` abbreviation used for the `~matplotlib.pyplot` module. -Importing ProPlot immediately adds several new colormaps, property cyclers, color names, and fonts to matplotlib. See :ref:`Colormaps`, :ref:`Color cycles`, and :ref:`Colors and fonts` for details. +This differentiates proplot from the usual ``plt`` abbreviation reserved for +the `~matplotlib.pyplot` module. + +.. _usage_classes: Figure and axes classes ======================= -Making figures in ProPlot always begins with a call to the -`~proplot.subplots.subplots` command: - -.. code-block:: python - f, axs = plot.subplots(...) +Creating figures with proplot is very similar to +matplotlib. You can either create the figure and +all of its subplots at once: -`~proplot.subplots.subplots` is modeled after -matplotlib's native `matplotlib.pyplot.subplots` command. -It creates an instance of ProPlot's -`~proplot.subplots.Figure` class -populated with instances of ProPlot's -`~proplot.axes.Axes` classes. -See :ref:`The basics` -and :ref:`Subplots features` for details. +.. code-block:: python -Each `~proplot.axes.Axes` returned by `~proplot.subplots.subplots` -belongs to one of the following three child classes: + fig, axs = pplt.subplots(...) -* `~proplot.axes.XYAxes`: For plotting simple data with *x* and *y* coordinates. -* `~proplot.axes.ProjAxes`: For geographic plots with *longitude* and *latitude* coordinates. -* `~proplot.axes.PolarAxes`: For polar plots with *radius* and *azimuth* coordinates. +or create an empty figure +then fill it with subplots: -See :ref:`X and Y axis settings` for details on working with `~proplot.axes.XYAxes` and -:ref:`Geographic and polar plots` for details on working with -`~proplot.axes.ProjAxes` and `~proplot.axes.PolarAxes`. +.. code-block:: python -Figure and axes methods -======================= -The `~proplot.subplots.Figure` and `~proplot.axes.Axes` subclasses -include several *brand new* methods and add to the functionality of several *existing* methods. - -* The new `~proplot.axes.Axes.format` method is used to fine-tune various axes settings. Its behavior depends on whether the axes is an `~proplot.axes.XYAxes`, `~proplot.axes.PolarAxes`, or `~proplot.axes.ProjAxes`. Think of this as a dedicated `~matplotlib.artist.Artist.update` method for axes artists. See :ref:`Formatting subplots` and :ref:`Changing rc settings` for details. -* The `~proplot.subplots.Figure` `~proplot.subplots.Figure.colorbar` and `~proplot.subplots.Figure.legend` and `~proplot.axes.Axes` `~proplot.axes.Axes.colorbar` and `~proplot.axes.Axes.legend` commands are used to add colorbars and legends *inside* of subplots, along the *outside edge* of subplots, and along the *edge of the figure*. They considerably simplify the process of drawing colorbars and legends. See :ref:`Colorbars and legends` for details. -* ProPlot adds a huge variety of features for working with `~matplotlib.axes.Axes.contour` plots, `~matplotlib.axes.Axes.pcolor` plots, `~matplotlib.axes.Axes.plot` lines, `~proplot.axes.Axes.heatmap` plots, `~matplotlib.axes.Axes.errorbar` bars, `~matplotlib.axes.Axes.bar` plots, `~proplot.axes.Axes.area` plots, and `~proplot.axes.Axes.parametric` plots. See :ref:`1d plotting` and :ref:`2d plotting` for details. - -Integration with other packages -=============================== -ProPlot's features are integrated with the data containers -introduced by `xarray` and `pandas` and the -`cartopy` and `~mpl_toolkits.basemap` geographic -plotting toolkits. - -* Axis labels, tick labels, titles, colorbar labels, and legend labels are automatically applied when you pass an `xarray.DataArray`, `pandas.DataFrame`, or `pandas.Series` object to any plotting command. This works just like the native `xarray.DataArray.plot` and `pandas.DataFrame.plot` methods. See :ref:`1d plotting` and :ref:`2d plotting` for details. -* The `~proplot.projs.Proj` function lets you make arbitrary grids of basemap `~mpl_toolkits.basemap.Basemap` and cartopy `~cartopy.crs.Projection` projections. It is used to interpret the `proj` keyword arg passed to `~proplot.subplots.subplots`. The resulting axes are instances of `~proplot.axes.ProjAxes` with `~proplot.axes.ProjAxes.format` methods that can be used to add geographic features and custom meridian and parallel gridlines. See :ref:`Geographic and polar plots` for details. - -New functions and classes -========================= -ProPlot includes several useful *constructor functions* -and *subclasses* outside -of the `~proplot.subplots.Figure` and `~proplot.axes.Axes` subclasses. - -* The `~proplot.styletools.Colormap` and `~proplot.styletools.Cycle` constructor functions can slice, merge, and modify colormaps and color cycles. See :ref:`Colormaps`, :ref:`Color cycles`, and :ref:`Colors and fonts` for details. -* The `~proplot.styletools.LinearSegmentedColormap` and `~proplot.styletools.ListedColormap` subclasses replace the default matplotlib colormap classes and add several methods. The new `~proplot.styletools.PerceptuallyUniformColormap` class is used to make colormaps with perceptually uniform transitions. See :ref:`Colormaps` for details. -* The `~proplot.styletools.show_cmaps`, `~proplot.styletools.show_cycles`, `~proplot.styletools.show_colors`, `~proplot.styletools.show_fonts`, `~proplot.styletools.show_channels`, and `~~proplot.styletools.show_colorspaces` functions are used to visualize your color scheme and font options and inspect individual colormaps. -* The `~proplot.styletools.Norm` constructor function generates colormap normalizers from shorthand names. The new `~proplot.styletools.LinearSegmentedNorm` normalizer scales colors evenly w.r.t. index for arbitrarily spaced monotonic levels, and the new `~proplot.styletools.BinNorm` meta-normalizer is used to discretized colormap colors. See :ref:`2d plotting` for details. -* The `~proplot.axistools.Locator`, `~proplot.axistools.Formatter`, and `~proplot.axistools.Scale` constructor functions, used to generate class instances from variable input types. These are used to interpret keyword arguments passed to `~proplot.axes.Axes.format` and `~proplot.subplots.Figure.colorbar`. See :ref:`X and Y axis settings` for details. -* The `~proplot.rctools.rc` object, an instance of `~proplot.rctools.rc_configurator`, is used for modifying *individual* global settings, changing settings in *bulk*, and temporarily changing settings in *context blocks*. It also sets up the inline plotting backend, so that your inline figures look the same as your saved figures. See :ref:`Configuring proplot` for details. + fig = pplt.figure(...) + axs = fig.add_subplots(...) # add several subplots + ax = fig.add_subplot(...) # add a single subplot + # axs = fig.subplots(...) # shorthand + # ax = fig.subplot(...) # shorthand + +These commands are modeled after `matplotlib.pyplot.subplots` and +`matplotlib.pyplot.figure` and are :ref:`packed with new features `. +One highlight is the `~proplot.figure.Figure.auto_layout` algorithm that +:ref:`automatically adjusts the space between subplots ` (similar to +matplotlib's `tight layout +`__) +and :ref:`automatically adjusts the figure size ` to preserve subplot +sizes and aspect ratios (particularly useful for grids of map projections +and images). All sizing arguments take :ref:`arbitrary units `, +including metric units like ``cm`` and ``mm``. + +Instead of the native `matplotlib.figure.Figure` and `matplotlib.axes.Axes` +classes, proplot uses the `proplot.figure.Figure`, `proplot.axes.Axes`, and +`proplot.axes.PlotAxes` subclasses. Proplot figures are saved with +`~proplot.figure.Figure.save` or `~matplotlib.figure.Figure.savefig`, +and proplot axes belong to one of the following three child classes: + +* `proplot.axes.CartesianAxes`: + For ordinary plots with *x* and *y* coordinates. +* `proplot.axes.GeoAxes`: + For geographic plots with *longitude* and *latitude* coordinates. +* `proplot.axes.PolarAxes`: + For polar plots with *azimuth* and *radius* coordinates. + +Most of proplot's features are implemented using these subclasses. +They include several new figure and axes methods and added +functionality to existing figure and axes methods. + +* The `proplot.axes.Axes.format` and `proplot.figure.Figure.format` commands fine-tunes + various axes and figure settings. Think of this as a dedicated + `~matplotlib.artist.Artist.update` method for axes and figures. See + :ref:`formatting subplots ` for a broad overview, along with the + individual sections on formatting :ref:`Cartesian plots `, + :ref:`geographic plots `, and :ref:`polar plots `. +* The `proplot.axes.Axes.colorbar` and `proplot.axes.Axes.legend` commands + draw colorbars and legends inside of subplots or along the outside edges of + subplots. The `proplot.figure.Figure.colorbar` and `proplot.figure.Figure.legend` + commands draw colorbars or legends along the edges of figures (aligned by subplot + boundaries). These commands considerably :ref:`simplify ` the + process of drawing colorbars and legends. +* The `proplot.axes.PlotAxes` subclass (used for all proplot axes) + adds many, many useful features to virtually every plotting command + (including `~proplot.axes.PlotAxes.plot`, `~proplot.axes.PlotAxes.scatter`, + `~proplot.axes.PlotAxes.bar`, `~proplot.axes.PlotAxes.area`, + `~proplot.axes.PlotAxes.box`, `~proplot.axes.PlotAxes.violin`, + `~proplot.axes.PlotAxes.contour`, `~proplot.axes.PlotAxes.pcolor`, + and `~proplot.axes.PlotAxes.imshow`). See the :ref:`1D plotting ` + and :ref:`2D plotting ` sections for details. + +.. _usage_integration: + +Integration features +==================== + +Proplot includes *optional* integration features with four external +packages: the `pandas`_ and `xarray`_ packages, used for working with annotated +tables and arrays, and the `cartopy`_ and `basemap`_ geographic +plotting packages. + +* The `~proplot.axes.GeoAxes` class uses the `cartopy`_ or + `basemap`_ packages to :ref:`plot geophysical data `, + :ref:`add geographic features `, and + :ref:`format projections `. `~proplot.axes.GeoAxes` provides + provides a simpler, cleaner interface than the original `cartopy`_ and `basemap`_ + interfaces. Figures can be filled with `~proplot.axes.GeoAxes` by passing the + `proj` keyword to `~proplot.ui.subplots`. +* If you pass a `pandas.Series`, `pandas.DataFrame`, or `xarray.DataArray` + to any plotting command, the axis labels, tick labels, titles, colorbar + labels, and legend labels are automatically applied from the metadata. If + you did not supply the *x* and *y* coordinates, they are also inferred from + the metadata. This works just like the native `xarray.DataArray.plot` and + `pandas.DataFrame.plot` commands. See the sections on :ref:`1D plotting + ` and :ref:`2D plotting ` for a demonstration. + +Since these features are optional, +proplot can be used without installing any of these packages. + +.. _usage_features: + +Additional features +=================== + +Outside of the features provided by the `proplot.figure.Figure` and +`proplot.axes.Axes` subclasses, proplot includes several useful +classes and :ref:`constructor functions `. + +* The `~proplot.constructor.Colormap` and `~proplot.constructor.Cycle` + constructor functions can be used to :ref:`slice `, + and :ref:`merge ` existing colormaps and color + cycles. It can also :ref:`make new colormaps ` + and :ref:`color cycles ` from scratch. +* The `~proplot.colors.ContinuousColormap` and + `~proplot.colors.DiscreteColormap` subclasses replace the default matplotlib + colormap classes and add several methods. The new + `~proplot.colors.PerceptualColormap` class is used to make + colormaps with :ref:`perceptually uniform transitions `. +* The `~proplot.demos.show_cmaps`, `~proplot.demos.show_cycles`, + `~proplot.demos.show_colors`, `~proplot.demos.show_fonts`, + `~proplot.demos.show_channels`, and `~proplot.demos.show_colorspaces` + functions are used to visualize your :ref:`color scheme ` + and :ref:`font options ` and + :ref:`inspect individual colormaps `. +* The `~proplot.constructor.Norm` constructor function generates colormap + normalizers from shorthand names. The new + `~proplot.colors.SegmentedNorm` normalizer scales colors evenly + w.r.t. index for arbitrarily spaced monotonic levels, and the new + `~proplot.colors.DiscreteNorm` meta-normalizer is used to + :ref:`break up colormap colors into discrete levels `. +* The `~proplot.constructor.Locator`, `~proplot.constructor.Formatter`, and + `~proplot.constructor.Scale` constructor functions return corresponding class + instances from flexible input types. These are used to interpret keyword + arguments passed to `~proplot.axes.Axes.format`, and can be used to quickly + and easily modify :ref:`x and y axis settings `. +* The `~proplot.config.rc` object, an instance of + `~proplot.config.Configurator`, is used for + :ref:`modifying individual settings, changing settings in bulk, and + temporarily changing settings in context blocks `. + It also introduces several :ref:`new setings ` + and sets up the inline plotting backend with `~proplot.config.inline_backend_fmt` + so that your inline figures look the same as your saved figures. diff --git a/docs/whatsnew.rst b/docs/whatsnew.rst new file mode 100644 index 000000000..1552d9d78 --- /dev/null +++ b/docs/whatsnew.rst @@ -0,0 +1 @@ +.. include:: ../WHATSNEW.rst diff --git a/docs/why.rst b/docs/why.rst index 8f3e34ed4..3c5c9db9a 100644 --- a/docs/why.rst +++ b/docs/why.rst @@ -1,536 +1,924 @@ -============ -Why ProPlot? -============ - -Matplotlib is an extremely powerful plotting package used -by academics, engineers, and data scientists far and wide. However, certain -plotting tasks can be cumbersome or repetitive for users who... - -* ...make very rich, complex figures with multiple subplots. -* ...want to finely tune their figure annotations and aesthetics. -* ...create new figures nearly every day. +.. _cartopy: https://scitools.org.uk/cartopy/docs/latest/ -ProPlot's core mission is to provide a smoother plotting experience -for heavy matplotlib users. -We do this by *expanding upon* the object-oriented matplotlib API -- -ProPlot makes changes that would be hard to justify or difficult -to incorporate into matplotlib itself, owing to design choices and backwards -compatibility considerations. -This page enumerates these changes and explains how they -address limitations of the matplotlib API. - -.. - This page is not comprehensive -- - see the User Guide for a comprehensive overview - with worked examples. +.. _basemap: https://matplotlib.org/basemap/index.html -.. - To start using these new features, see - see :ref:`Usage overview` and the User Guide. +.. _seaborn: https://seaborn.pydata.org -Less typing, more plotting -========================== +.. _pandas: https://pandas.pydata.org -.. rubric:: Problem +.. _xarray: http://xarray.pydata.org/en/stable/ -Matplotlib users often need to change lots of plot settings all at once. With the default API, this requires calling a series of one-liner setter methods. +.. _rainbow: https://doi.org/10.1175/BAMS-D-13-00155.1 -This workflow is quite verbose -- it tends to require "boilerplate code" that gets copied and pasted a hundred times. It can also be confusing -- it is often unclear whether properties are applied from an `~matplotlib.axes.Axes` setter (e.g. `~matplotlib.axes.Axes.set_title`, `~matplotlib.axes.Axes.set_xlabel` and `~matplotlib.axes.Axes.set_xticks`), an `~matplotlib.axis.XAxis` or `~matplotlib.axis.YAxis` setter (e.g. `~matplotlib.axis.Axis.set_major_locator` and `~matplotlib.axis.Axis.set_major_formatter`), a `~matplotlib.spines.Spine` setter (e.g. `~matplotlib.spines.Spine.set_bounds`), a miscellaneous "bulk" setter (e.g. `~matplotlib.axes.Axes.tick_params`), or whether they require tinkering with several different objects. Also, one often needs to *loop through* lists of subplots to apply identical settings to each subplot. +.. _xkcd: https://blog.xkcd.com/2010/05/03/color-survey-results/ -.. - This is perhaps one reason why many users prefer the `~matplotlib.pyplot` API to the object-oriented API (see :ref:`Using ProPlot`). +.. _opencolor: https://yeun.github.io/open-color/ -.. rubric:: Solution +.. _cmocean: https://matplotlib.org/cmocean/ -ProPlot introduces the `~proplot.axes.Axes.format` method for changing arbitrary settings *in bulk*. Think of this as an expanded and thoroughly documented version of the -`~matplotlib.artist.Artist` `~matplotlib.artist.Artist.update` method. -`~proplot.axes.Axes.format` can -also be used to update :ref:`Bulk global settings` and various :ref:`other rc settings ` for a particular subplot, and to concisely work with verbose classes using the :ref:`Class constructor functions`. Further, :ref:`The subplot container class` can be used to invoke `~proplot.axes.Axes.format` on several subplots at once. - -Together, these features significantly reduce -the amount of code needed to create highly customized figures. -As an example, it is trivial to see that - -.. code-block:: python - - import proplot as plot - f, axs = plot.subplots(ncols=2) - axs.format(linewidth=1, color='gray') - axs.format(xticks=20, xtickminor=True, xlabel='x axis', ylabel='y axis') - -...is much more succinct than - -.. code-block:: python +.. _fabio: http://www.fabiocrameri.ch/colourmaps.php - import matplotlib.pyplot as plt - import matplotlib.ticker as mticker - from matplotlib import rcParams - rcParams['axes.linewidth'] = 1 - rcParams['axes.color'] = 'gray' - fig, axs = plt.subplots(ncols=2) - for ax in axs: - ax.xaxis.set_major_locator(mticker.MultipleLocator(10)) - ax.tick_params(width=1, color='gray', labelcolor='gray') - ax.tick_params(axis='x', which='minor', bottom=True) - ax.set_xlabel('x axis', color='gray') - ax.set_ylabel('y axis', color='gray') - plt.style.use('default') # restore +.. _brewer: http://colorbrewer2.org/ +.. _sciviscolor: https://sciviscolor.org/home/colormoves/ -Class constructor functions -=========================== -.. rubric:: Problem +.. _matplotlib: https://matplotlib.org/stable/tutorials/colors/colormaps.html -Matplotlib and cartopy introduce a bunch of classes with verbose names like `~matplotlib.ticker.MultipleLocator`, `~matplotlib.ticker.FormatStrFormatter`, and -`~cartopy.crs.LambertAzimuthalEqualArea`. Since plotting code has a half life of about 30 seconds, typing out all of these extra class names and import statements can be a *major* drag. +.. _seacolor: https://seaborn.pydata.org/tutorial/color_palettes.html -Parts of the matplotlib API were actually designed with this in mind. -`Backend classes `__, -`native axes projections `__, -`axis scales `__, -`box styles `__, `arrow styles `__, and -`arc styles `__ -are referenced with "registered" string names, -as are `basemap projection types `__. -So, why not "register" everything else? +.. _texgyre: https://frommindtotype.wordpress.com/2018/04/23/the-tex-gyre-font-family/ -.. rubric:: Solution +.. _why: -In ProPlot, tick locators, tick formatters, axis scales, cartopy projections, colormaps, and property cyclers are all "registered". This is done by creating several **constructor functions** and passing various keyword argument *through* the constructor functions. -This may seem "unpythonic" but it is absolutely invaluable when writing -plotting code. +============ +Why proplot? +============ -Each constructor function accepts various *other* input types for your convenience. For -example, scalar numbers passed to `~proplot.axistools.Locator` returns -a `~matplotlib.ticker.MultipleLocator` instance, lists of strings passed -to `~proplot.axistools.Formatter` returns a `~matplotlib.ticker.FixedFormatter` instance, and `~proplot.styletools.Colormap` and `~proplot.styletools.Cycle` accept colormap names, individual colors, and lists of colors. When a *class instance* is passed to the relevant constructor function, it is simply returned. See :ref:`X and Y axis settings`, :ref:`Colormaps`, and :ref:`Color cycles` for details. +Matplotlib is an extremely versatile plotting package used by +scientists and engineers far and wide. However, +matplotlib can be cumbersome or repetitive for users who... -The below table lists the constructor functions and the keyword arguments that -use them. +* Make highly complex figures with many subplots. +* Want to finely tune their annotations and aesthetics. +* Need to make new figures nearly every day. -============================== ============================================================ ============================================================= ================================================================================================================================================================================================= -Function Returns Used by Keyword argument(s) -============================== ============================================================ ============================================================= ================================================================================================================================================================================================= -`~proplot.axistools.Locator` Axis `~matplotlib.ticker.Locator` `~proplot.axes.Axes.format` and `~proplot.axes.Axes.colorbar` ``locator=``, ``xlocator=``, ``ylocator=``, ``minorlocator=``, ``xminorlocator=``, ``yminorlocator=``, ``ticks=``, ``xticks=``, ``yticks=``, ``minorticks=``, ``xminorticks=``, ``yminorticks=`` -`~proplot.axistools.Formatter` Axis `~matplotlib.ticker.Formatter` `~proplot.axes.Axes.format` and `~proplot.axes.Axes.colorbar` ``formatter=``, ``xformatter=``, ``yformatter=``, ``ticklabels=``, ``xticklabels=``, ``yticklabels=`` -`~proplot.axistools.Scale` Axis `~matplotlib.scale.ScaleBase` `~proplot.axes.Axes.format` ``xscale=``, ``yscale=`` -`~proplot.styletools.Cycle` Property `~cycler.Cycler` 1d plotting methods ``cycle=`` -`~proplot.styletools.Colormap` `~matplotlib.colors.Colormap` instance 2d plotting methods ``cmap=`` -`~proplot.styletools.Norm` `~matplotlib.colors.Normalize` instance 2d plotting methods ``norm=`` -`~proplot.projs.Proj` `~cartopy.crs.Projection` or `~mpl_toolkits.basemap.Basemap` `~proplot.subplots.subplots` ``proj=`` -============================== ============================================================ ============================================================= ================================================================================================================================================================================================= +Proplot's core mission is to provide a smoother plotting experience for +matplotlib's most demanding users. We accomplish this by *expanding upon* +matplotlib's :ref:`object-oriented interface `. Proplot +makes changes that would be hard to justify or difficult to incorporate +into matplotlib itself, owing to differing design choices and backwards +compatibility considerations. -Note that `~matplotlib.axes.Axes.set_xscale` and `~matplotlib.axes.Axes.set_yscale` -now accept instances of `~matplotlib.scale.ScaleBase` thanks to a monkey patch -applied by ProPlot. +This page enumerates these changes and explains how they address the +limitations of matplotlib's default interface. To start using these +features, see the :ref:`usage introduction ` +and the :ref:`user guide `. -Automatic dimensions and spacing -================================ +.. _why_less_typing: -.. rubric:: Problem +Less typing, more plotting +========================== -Matplotlib plots tend to require lots of "tweaking" when you have more than one subplot in the figure. This is partly because you must specify the physical dimensions of the figure, while the dimensions of the *individual subplots* are more important: +Limitation +---------- + +Matplotlib users often need to change lots of plot settings all at once. With +the default interface, this requires calling a series of one-liner setter methods. + +This workflow is quite verbose -- it tends to require "boilerplate code" that +gets copied and pasted a hundred times. It can also be confusing -- it is +often unclear whether properties are applied from an `~matplotlib.axes.Axes` +setter (e.g. `~matplotlib.axes.Axes.set_xlabel` and +`~matplotlib.axes.Axes.set_xticks`), an `~matplotlib.axis.XAxis` or +`~matplotlib.axis.YAxis` setter (e.g. +`~matplotlib.axis.Axis.set_major_locator` and +`~matplotlib.axis.Axis.set_major_formatter`), a `~matplotlib.spines.Spine` +setter (e.g. `~matplotlib.spines.Spine.set_bounds`), or a "bulk" property +setter (e.g. `~matplotlib.axes.Axes.tick_params`), or whether one must dig +into the figure architecture and apply settings to several different objects. +It seems like there should be a more unified, straightforward way to change +settings without sacrificing the advantages of object-oriented design. + +Changes +------- + +Proplot includes the `proplot.axes.Axes.format` command to resolve this. +Think of this as an expanded and thoroughly documented version of the +`matplotlib.artist.Artist.update` command. `~proplot.axes.Axes.format` can modify things +like axis labels and titles and apply new :ref:`"rc" settings ` to existing +axes. It also integrates with various :ref:`constructor functions ` +to help keep things succinct. Further, the `proplot.figure.Figure.format` +and `proplot.gridspec.SubplotGrid.format` commands can be used to +`~proplot.axes.Axes.format` several subplots at once. + +Together, these features significantly reduce the amount of code needed to create +highly customized figures. As an example, it is trivial to see that... -#. The subplot aspect ratio is usually more relevant than the figure aspect ratio, e.g. for map projections. -#. The subplot width and height control the evident thickness of text and other content plotted inside the axes. +.. code-block:: python -Matplotlib has a `tight layout `__ algorithm to keep you from having to "tweak" the spacing, but the algorithm cannot apply different amounts of spacing between different subplot row and column boundaries. This limitation often results in unnecessary whitespace, and can be a major problem when you want to put e.g. a legend on the outside of a subplot. + import proplot as pplt + fig, axs = pplt.subplots(ncols=2) + axs.format(color='gray', linewidth=1) + axs.format(xlim=(0, 100), xticks=10, xtickminor=True, xlabel='foo', ylabel='bar') -.. rubric:: Solution +is much more succinct than... -In ProPlot, you can specify the physical dimensions of a *reference subplot* instead of the figure by passing `axwidth`, `axheight`, and/or `aspect` to `~proplot.subplots.Figure`. The default behavior is ``aspect=1`` and ``axwidth=2`` (inches). If the `aspect ratio mode `__ for the reference subplot is set to ``'equal'``, as with :ref:`Geographic and polar plots` and `~matplotlib.axes.Axes.imshow` plots, the *imposed* aspect ratio will be used instead. -Figure dimensions are constrained as follows: +.. code-block:: python -* When `axwidth` or `axheight` are specified, the figure width and height are calculated automatically. -* When `width` is specified, the figure height is calculated automatically. -* When `height` is specified, the figure width is calculated automatically. -* When `width` *and* `height` or `figsize` is specified, the figure dimensions are fixed. + import matplotlib.pyplot as plt + import matplotlib.ticker as mticker + import matplotlib as mpl + with mpl.rc_context(rc={'axes.linewidth': 1, 'axes.edgecolor': 'gray'}): + fig, axs = plt.subplots(ncols=2, sharey=True) + axs[0].set_ylabel('bar', color='gray') + for ax in axs: + ax.set_xlim(0, 100) + ax.xaxis.set_major_locator(mticker.MultipleLocator(10)) + ax.tick_params(width=1, color='gray', labelcolor='gray') + ax.tick_params(axis='x', which='minor', bottom=True) + ax.set_xlabel('foo', color='gray') + +Links +----- + +* For an introduction, see :ref:`this page `. +* For `~proplot.axes.CartesianAxes` formatting, + see :ref:`this page `. +* For `~proplot.axes.PolarAxes` formatting, + see :ref:`this page `. +* For `~proplot.axes.GeoAxes` formatting, + see :ref:`this page `. + +.. _why_constructor: -.. - Several matplotlib backends require figure dimensions to be fixed. When `~proplot.subplots.Figure.draw` changes the figure dimensions, this can "surprise" the backend and cause unexpected behavior. ProPlot fixes this issue for the static inline backend and the Qt popup backend. However, this issue is unfixable the "notebook" inline backend, the "macosx" popup backend, and possibly other untested backends. +Class constructor functions +=========================== -ProPlot also uses a custom tight layout algorithm that automatically determines the `left`, `right`, `bottom`, `top`, `wspace`, and `hspace` `~matplotlib.gridspec.GridSpec` parameters. This algorithm is simpler because: +Limitation +---------- -* The new `~proplot.subplots.GridSpec` class permits variable spacing between rows and columns. It turns out this is critical for putting :ref:`Colorbars and legends` on the outside of subplots. -* Figures are restricted to have only *one* `~proplot.subplots.GridSpec` per figure. This is done by requiring users to draw all of their subplots at once with `~proplot.subplots.subplots` (see :pr:`50`). +Matplotlib and `cartopy`_ define several classes with verbose names like +`~matplotlib.ticker.MultipleLocator`, `~matplotlib.ticker.FormatStrFormatter`, +and `~cartopy.crs.LambertAzimuthalEqualArea`. They also keep them out of the +top-level package namespace. Since plotting code has a half life of about 30 seconds, +typing out these extra class names and import statements can be frustrating. -See :ref:`Automatic figure sizing` and :ref:`Automatic subplot spacing` for details. +Parts of matplotlib's interface were designed with this in mind. +`Backend classes `__, +`native axes projections `__, +`axis scales `__, +`colormaps `__, +`box styles `__, +`arrow styles `__, +and `arc styles `__ +are referenced with "registered" string names, +as are `basemap projections `__. +So, why not "register" everything else? -.. - #. The `~proplot.subplots.GridSpec` spacing parameters are specified in physical units instead of figure-relative units. +Changes +------- + +In proplot, tick locators, tick formatters, axis scales, property cycles, colormaps, +normalizers, and `cartopy`_ projections are all "registered". This is accomplished +by defining "constructor functions" and passing various keyword arguments through +these functions. + +The constructor functions also accept intuitive inputs alongside "registered" +names. For example, a scalar passed to `~proplot.constructor.Locator` +returns a `~matplotlib.ticker.MultipleLocator`, a +lists of strings passed to `~proplot.constructor.Formatter` returns a +`~matplotlib.ticker.FixedFormatter`, and `~proplot.constructor.Cycle` +and `~proplot.constructor.Colormap` accept colormap names, individual colors, and +lists of colors. Passing the relevant class instance to a constructor function +simply returns it, and all the registered classes are available in the top-level +namespace -- so class instances can be directly created with e.g. +``pplt.MultipleLocator(...)`` or ``pplt.LogNorm(...)`` rather than +relying on constructor functions. + +The below table lists the constructor functions and the keyword arguments that use them. + +================================ ============================================================ ============================================================================== ================================================================================================================================================================================================ +Function Return type Used by Keyword argument(s) +================================ ============================================================ ============================================================================== ================================================================================================================================================================================================ +`~proplot.constructor.Proj` `~cartopy.crs.Projection` or `~mpl_toolkits.basemap.Basemap` `~proplot.figure.Figure.add_subplot` and `~proplot.figure.Figure.add_subplots` ``proj=`` +`~proplot.constructor.Locator` `~matplotlib.ticker.Locator` `~proplot.axes.Axes.format` and `~proplot.axes.Axes.colorbar` ``locator=``, ``xlocator=``, ``ylocator=``, ``minorlocator=``, ``xminorlocator=``, ``yminorlocator=``, ``ticks=``, ``xticks=``, ``yticks=``, ``minorticks=``, ``xminorticks=``, ``yminorticks=`` +`~proplot.constructor.Formatter` `~matplotlib.ticker.Formatter` `~proplot.axes.Axes.format` and `~proplot.axes.Axes.colorbar` ``formatter=``, ``xformatter=``, ``yformatter=``, ``ticklabels=``, ``xticklabels=``, ``yticklabels=`` +`~proplot.constructor.Scale` `~matplotlib.scale.ScaleBase` `~proplot.axes.Axes.format` ``xscale=``, ``yscale=`` +`~proplot.constructor.Colormap` `~matplotlib.colors.Colormap` 2D `~proplot.axes.PlotAxes` commands ``cmap=`` +`~proplot.constructor.Norm` `~matplotlib.colors.Normalize` 2D `~proplot.axes.PlotAxes` commands ``norm=`` +`~proplot.constructor.Cycle` `~cycler.Cycler` 1D `~proplot.axes.PlotAxes` commands ``cycle=`` +================================ ============================================================ ============================================================================== ================================================================================================================================================================================================ + +Links +----- + +* For more on axes projections, + see :ref:`this page `. +* For more on axis locators, + see :ref:`this page `. +* For more on axis formatters, + see :ref:`this page `. +* For more on axis scales, + see :ref:`this page `. +* For more on datetime locators and formatters, + see :ref:`this page `. +* For more on colormaps, + see :ref:`this page `. +* For more on normalizers, + see :ref:`this page `. +* For more on color cycles, see + :ref:`this page `. + +.. _why_spacing: -.. - The `~matplotlib.gridspec.GridSpec` class is useful for creating figures with complex subplot geometry. -.. - Users want to control axes positions with gridspecs. -.. - * Matplotlib permits arbitrarily many `~matplotlib.gridspec.GridSpec`\ s per figure. This greatly complicates the tight layout algorithm for little evident gain. -.. - ProPlot introduces a marginal limitation (see discussion in :pr:`50`) but *considerably* simplifies the tight layout algorithm. +Automatic dimensions and spacing +================================ -Eliminating redundancies -======================== +Limitation +---------- + +Matplotlib plots tend to require "tweaking" when you have more than one +subplot in the figure. This is partly because you must specify the physical +dimensions of the figure, despite the fact that... + +#. The subplot aspect ratio is generally more relevant than the figure + aspect ratio. A default aspect ratio of ``1`` is desirable for most plots, and + the aspect ratio must be held fixed for :ref:`geographic and polar ` + projections and most `~matplotlib.axes.Axes.imshow` plots. +#. The subplot width and height control the "apparent" size of lines, markers, + text, and other plotted content. If the figure size is fixed, adding more + subplots will decrease the average subplot size and increase the "apparent" + sizes. If the subplot size is fixed instead, this can be avoided. + +Matplotlib also includes `"tight layout" +`__ +and `"constrained layout" +`__ +algorithms that can help users avoid having to tweak +`~matplotlib.gridspec.GridSpec` spacing parameters like `left`, `bottom`, and `wspace`. +However, these algorithms are disabled by default and somewhat `cumbersome to configure +`__. +They also cannot apply different amounts of spacing between different subplot row and +column boundaries. + +Changes +------- + +By default, proplot fixes the physical dimensions of a *reference subplot* rather +than the figure. The reference subplot dimensions are controlled with the `refwidth`, +`refheight`, and `refaspect` `~proplot.figure.Figure` keywords, with a default +behavior of ``refaspect=1`` and ``refwidth=2.5`` (inches). If the `data aspect ratio +`__ +of the reference subplot is fixed (as with :ref:`geographic `, +:ref:`polar `, `~matplotlib.axes.Axes.imshow`, and +`~proplot.axes.Axes.heatmap` plots) then this is used instead of `refaspect`. + +Alternatively, you can independently specify the width or height of the *figure* +with the `figwidth` and `figheight` parameters. If only one is specified, the +other is adjusted to preserve subplot aspect ratios. This is very often useful +when preparing figures for submission to a publication. To request figure +dimensions suitable for submission to a :ref:`specific publication `, +use the `journal` keyword. + +By default, proplot also uses :ref:`its own tight layout algorithm ` -- +preventing text labels from overlapping with subplots. This algorithm works with the +`proplot.gridspec.GridSpec` subclass rather than `matplotlib.gridspec.GridSpec`, which +provides the following advantages: + +* The `proplot.gridspec.GridSpec` subclass interprets spacing parameters + with font size-relative units rather than figure size-relative units. + This is more consistent with the tight layout `pad` arguments + (which, like matplotlib, are specified in font size-relative units) + and obviates the need to adjust spaces when the figure size or font size changes. +* The `proplot.gridspec.GridSpec` subclass permits variable spacing + between rows and columns, and the tight layout algorithm takes + this into account. Variable spacing is critical for making + outer :ref:`colorbars and legends ` and + :ref:`axes panels ` without "stealing space" + from the parent subplot -- these objects usually need to be + spaced closer to their parents than other subplots. +* You can :ref:`override ` particular spacing parameters + and leave the tight layout algorithm to adjust the + unspecified spacing parameters. For example, passing ``right=1`` to + `~proplot.figure.Figure.add_subplots` fixes the right margin + at 1 font size-width while the others are adjusted automatically. +* Only one `proplot.gridspec.GridSpec` is permitted per figure, + considerably simplifying the tight layout algorithm calculations. + This restriction is enforced by requiring successive + `~proplot.figure.Figure.add_subplot` calls to imply the same geometry and + include only subplot specs generated from the same `~proplot.gridspec.GridSpec`. + +Links +----- + +* For more on figure sizing, see :ref:`this page `. +* For more on subplot spacing, see :ref:`this page `. + +.. _why_redundant: + +Working with multiple subplots +============================== -.. rubric:: Problem +Limitation +---------- -For many of us, figures with just one subplot are a rarity. We tend to need multiple -subplots for comparing different datasets and illustrating complex concepts. -Unfortunately, it is easy to end up with *redundant* figure elements -when drawing multiple subplots; namely: +When working with multiple subplots in matplotlib, the path of least resistance +often leads to *redundant* figure elements. Namely... * Repeated axis tick labels. * Repeated axis labels. * Repeated colorbars. * Repeated legends. -These sorts of redundancies are extremely common even in publications, where -they waste valuable page space. They arise because this is the path of least -resistance for the default API -- removing redundancies -tends to require a fair amount of extra work. - -.. rubric:: Solution - -ProPlot seeks to eliminate redundant elements -to help you make clear, concise figures. -We tackle this issue using -:ref:`Shared and spanning labels` and :ref:`Figure colorbars and legends`. - -* By default, axis tick labels and axis labels are *shared* between subplots in the same row or column. This is controlled by the `sharex`, `sharey`, `spanx`, and `spany` `~proplot.subplots.subplots` keyword args. -* The new `~proplot.subplots.Figure` `~proplot.subplots.Figure.colorbar` and `~proplot.subplots.Figure.legend` methods make it easy to draw colorbars and legends intended to reference more than one subplot. For details, see the next section. - -Outer colorbars and legends -=========================== - -.. rubric:: Problem - -In matplotlib, it is difficult to draw `~matplotlib.figure.Figure.colorbar`\ s and -`~matplotlib.axes.Axes.legend`\ s intended to reference more than one subplot or -along the outside of subplots: - -* To draw legends outside of subplots, you usually need to position the legend manually and adjust various `~matplotlib.gridspec.GridSpec` spacing properties to make *room* for the legend. -* To make colorbars that span multiple subplots, you have to supply `~matplotlib.figure.Figure.colorbar` with a `cax` you drew yourself. This requires so much tinkering that most users just add identical colorbars to every single subplot! - -Furthermore, drawing colorbars with ``fig.colorbar(..., ax=ax)`` tends to mess up subplot aspect ratios since the space allocated for the colorbar is "stolen" from the parent axes. - -.. - And since colorbar widths are specified in *axes relative* coordinates, they often look "too skinny" or "too fat" after the first draw. - - -.. - The matplotlib example for `~matplotlib.figure.Figure` legends is `not pretty `__. - -.. - Drawing colorbars and legends is pretty clumsy in matplotlib -- especially when trying to draw them outside of the figure. They can be too narrow, too wide, and mess up your subplot aspect ratios. - -.. rubric:: Solution - -ProPlot introduces a brand new framework for drawing :ref:`Axes colorbars and legends` -(colorbars and legends inside or along the outside edge of a subplot) -and :ref:`Figure colorbars and legends` -(colorbars and legends sapnning contiguous subplots along the edge of the figure): - -* Passing an "outer" location to `~proplot.axes.Axes` `~proplot.axes.Axes.colorbar` or `~proplot.axes.Axes` `~proplot.axes.Axes.legend` (e.g. ``loc='l'`` or ``loc='left'``) draws the colorbar or legend along the outside of the axes. Passing an "inner" location (e.g. ``loc='ur'`` or ``loc='upper right'``) draws an *inset* colorbar or legend. And yes, that's right, you can now draw inset colorbars! -* To draw a colorbar or legend along the edge of the figure, use `~proplot.subplots.Figure` `~proplot.subplots.Figure.colorbar` and `~proplot.subplots.Figure.legend`. The `col`, `row`, and `span` keyword args control which `~matplotlib.gridspec.GridSpec` rows and columns are spanned by the colorbar or legend. -* Since `~proplot.subplots.GridSpec` permits variable spacing between subplot rows and columns, "outer" colorbars and legends do not mess up subplot spacing or add extra whitespace. This is critical e.g. if you have a colorbar between columns 1 and 2 but nothing between columns 2 and 3. -* `~proplot.subplots.Figure` and `~proplot.axes.Axes` colorbar widths are specified in *physical* units rather than relative units. This makes colorbar thickness independent of subplot size and easier to get just right. - -There are also several :ref:`New colorbar features` and :ref:`New legend features`. - -The subplot container class -=========================== - -.. - The `~matplotlib.pyplot.subplots` command is useful for generating a scaffolding of * axes all at once. This is generally faster than successive `~matplotlib.subplots.Figure.add_subplot` commands. - -.. rubric:: Problem - -In matplotlib, `~matplotlib.pyplot.subplots` returns a 2d `~numpy.ndarray` for figures with more than one column and row, a 1d `~numpy.ndarray` for single-row or single-column figures, or just an `~matplotlib.axes.Axes` instance for single-subplot figures. - -.. rubric:: Solution - -In ProPlot, `~proplot.subplots.subplots` returns a `~proplot.subplots.subplot_grid` -filled with `~proplot.axes.Axes` instances. -This container lets you call arbitrary methods on arbitrary subplots all at once, which can be useful when you want to style your subplots identically (e.g. ``axs.format(tickminor=False)``). -The `~proplot.subplots.subplot_grid` class also -unifies the behavior of the three possible `matplotlib.pyplot.subplots` return values: - -* `~proplot.subplots.subplot_grid` permits 2d indexing, e.g. ``axs[1,0]``. Since `~proplot.subplots.subplots` can generate figures with arbitrarily complex subplot geometry, this 2d indexing is useful only when the arrangement happens to be a clean 2d matrix. -* `~proplot.subplots.subplot_grid` also permits 1d indexing, e.g. ``axs[0]``, since it is a `list` subclass. The default order can be switched from row-major to column-major by passing ``order='F'`` to `~proplot.subplots.subplots`. -* When it is singleton, `~proplot.subplots.subplot_grid` behaves like a scalar. So when you make a single axes with ``f, axs = plot.subplots()``, ``axs[0].method(...)`` is equivalent to ``axs.method(...)``. - -See :ref:`Subplot grids` for details. - -.. - This goes with ProPlot's theme of preserving the object-oriented spirit, but making things easier for users. - -New and improved plotting methods -================================= - -.. rubric:: Problem - -Certain plotting tasks are quite difficult to accomplish -with the default matplotlib API. The `seaborn`, `xarray`, and `pandas` -packages offer improvements, but it would be nice -to have this functionality build right into matplotlib. -There is also room for improvement of the native matplotlib plotting methods -that none of these packages address. - -.. - Matplotlib also has some finicky plotting issues - that normally requires -.. - For example, when you pass coordinate *centers* to `~matplotlib.axes.Axes.pcolor` and `~matplotlib.axes.Axes.pcolormesh`, they are interpreted as *edges* and the last column and row of your data matrix is ignored. Also, to add labels to `~matplotlib.axes.Axes.contour` and `~matplotlib.axes.Axes.contourf`, you need to call a dedicated `~matplotlib.axes.Axes.clabel` method instead of just using a keyword argument. - - -.. rubric:: Solution - - -ProPlot adds various -`seaborn`, `xarray`, and `pandas` features -to the `~proplot.axes.Axes` plotting methods -along with several *brand new* features designed to -make your life easier. - -* The new `~proplot.axes.Axes.area` and `~proplot.axes.Axes.areax` methods call `~matplotlib.axes.Axes.fill_between` and `~matplotlib.axes.Axes.fill_betweenx`. These methods now accept 2D arrays and *stack* or *overlay* successive columns, and a `negpos` keyword argument that can be used to assign separate colors for negative and positive data. -* The new `~proplot.axes.Axes.parametric` method draws *parametric* line plots, where the parametric coordinate is denoted with a colorbar rather than text annotations. This is much cleaner and more aesthetically pleasing than the conventional approach. -* The new `~proplot.axes.Axes.heatmap` method invokes `~matplotlib.axes.Axes.pcolormesh` and draws ticks at the center of each box. This is more convenient for things like covariance matrices. -* The `~matplotlib.axes.Axes.bar` and `~matplotlib.axes.Axes.barh` methods accept 2D arrays and *stack* or *group* successive columns. Just like `~matplotlib.axes.Axes.fill_between` and `~matplotlib.axes.Axes.fill_betweenx`, you will be able to use different colors for positive/negative bars. -* All :ref:`1d plotting` can be used to draw :ref:`On-the-fly error bars` using the `means`, `medians`, `boxdata`, and `bardata` keyword arguments. You no longer have to work with `~matplotlib.axes.Axes.add_errobar` method directly. -* All :ref:`1d plotting` methods accept a `cycle` keyword argument interpreted by `~proplot.styletools.Cycle` and optional `legend` and `colorbar` keyword arguments for populating legends and colorbars at the specified location with the result of the plotting command. See :ref:`Color cycles` and :ref:`Colorbars and legends`. -* All :ref:`2d plotting` methods accept a `cmap` keyword argument interpreted by `~proplot.styletools.Colormap`, a `norm` keyword argument interpreted by `~proplot.styletools.Norm`, and an optional `colorbar` keyword argument for drawing on-the-fly colorbars with the resulting mappable. See :ref:`Colormaps` and :ref:`Colorbars and legends`. -* All :ref:`2d plotting` methods accept a `labels` keyword argument. This is used to draw contour labels or grid box labels on heatmap plots. Labels are colored black or white according to the luminance of the underlying filled contour or grid box color. See :ref:`2d plotting` for details. -* ProPlot fixes the irritating `white-lines-between-filled-contours `__, `white-lines-between-pcolor-patches `__, and `white-lines-between-colorbar-patches `__ vector graphic issues. -* Matplotlib requires coordinate *centers* for contour plots and *edges* for pcolor plots. If you pass *centers* to pcolor, matplotlib treats them as *edges* and silently trims one row/column of your data. Most people don't realize this! ProPlot changes this behavior: If edges are passed to `~matplotlib.axes.Axes.contour` or `~matplotlib.axes.Axes.contourf`, centers are *calculated* from the edges; if centers are passed to `~matplotlib.axes.Axes.pcolor` or `~matplotlib.axes.Axes.pcolormesh`, edges are *estimated* from the centers. - -.. - ProPlot also provides - *constistent behavior* when - switching between different commands, for - example `~matplotlib.axes.Axes.plot` and `~matplotlib.axes.Axes.scatter` - or `~matplotlib.axes.Axes.contourf` and `~matplotlib.axes.Axes.pcolormesh`. - -.. - ProPlot also uses wrappers to *unify* the behavior of various - plotting methods. - -.. - All positional arguments for "1d" plotting methods are standardized by `~proplot.wrappers.standardize_1d`. All positional arguments for "2d" plotting methods are standardized by `~proplot.wrappers.standardize_2d`. See :ref:`1d plotting` and :ref:`2d plotting` for details. - -Xarray and pandas integration +These sorts of redundancies are very common even in publications, where they waste +valuable page space. It is also generally necessary to add "a-b-c" labels to +figures with multiple subplots before submitting them to publications, but +matplotlib has no built-in way of doing this. + +Changes +------- + +Proplot makes it easier to work with multiple subplots and create clear, +concise figures. + +* Axis tick labels and axis labels are automatically + :ref:`shared and aligned ` between subplot in the same + `~proplot.gridspec.GridSpec` row or column. This is controlled by the `sharex`, + `sharey`, `spanx`, `spany`, `alignx`, and `aligny` figure keywords. +* The figure `proplot.figure.Figure.colorbar` and `proplot.figure.Figure.legend` + commands can easily draw colorbars and legends intended to reference more than + one subplot in arbitrary contiguous rows and columns. See the + :ref:`next section ` for details. +* A-b-c labels can be added to subplots simply using the :rcraw:`abc` + setting -- for example, ``pplt.rc['abc'] = 'A.'`` or ``axs.format(abc='A.')``. + This is possible because `~proplot.figure.Figure.add_subplot` assigns a unique + `~proplot.axes.Axes.number` to every new subplot. +* The `proplot.gridspec.SubplotGrid.format` command can easily format multiple subplots + at once or add colorbars, legends, panels, twin axes, or inset axes to multiple + subplots at once. A `~proplot.gridspec.SubplotGrid` is returned by + `proplot.figure.Figure.subplots`, and can be indexed like a list or a 2D array. +* The `~proplot.axes.Axes.panel_axes` (shorthand `~proplot.axes.Axes.panel`) commands + draw :ref:`thin panels ` along the edges of subplots. This can be useful + for plotting 1D summary statistics alongside 2D plots. You can also add twin axes and + panel axes to several subplots at once using `~proplot.gridspec.SubplotGrid` commands. + +Links +----- + +* For more on axis sharing, see :ref:`this page `. +* For more on panels, see :ref:`this page `. +* For more on colorbars and legends, see :ref:`this page `. +* For more on a-b-c labels, see :ref:`this page `. +* For more on subplot grids, see :ref:`this page `. + +.. _why_colorbars_legends: + +Simpler colorbars and legends ============================= -.. rubric:: Problem - -When you pass the array-like `xarray.DataArray`, `pandas.DataFrame`, and `pandas.Series` containers to matplotlib plotting commands, the metadata is ignored. To create plots that are automatically labeled with this metadata, you must use -the dedicated `xarray.DataArray.plot`, `pandas.DataFrame.plot`, and `pandas.Series.plot` -tools instead. - -This approach is fine for quick plots, but not ideal for complex ones. -It requires learning a different syntax from matplotlib, and tends to encourage using the `~matplotlib.pyplot` API rather than the object-oriented API. -These tools also introduce features that would be useful additions to matplotlib -in their *own* right, without requiring special data containers and -an entirely separate API. - -.. rubric:: Solution - -ProPlot *reproduces* most of the `xarray.DataArray.plot`, `pandas.DataFrame.plot`, and `pandas.Series.plot` features on the `~proplot.axes.Axes` plotting methods themselves. -Passing an `~xarray.DataArray`, `~pandas.DataFrame`, or `~pandas.Series` through -any plotting method automatically updates the -axis tick labels, axis labels, subplot titles, and colorbar and legend labels -from the metadata. This can be disabled by passing -``autoformat=False`` to the plotting method or to `~proplot.subplots.subplots`. +Limitation +---------- + +In matplotlib, it can be difficult to draw `~matplotlib.figure.Figure.legend`\ s +along the outside of subplots. Generally, you need to position the legend +manually and tweak the spacing to make room for the legend. + +Also, `~matplotlib.figure.Figure.colorbar`\ s drawn along the outside of subplots +with e.g. ``fig.colorbar(..., ax=ax)`` need to "steal" space from the parent subplot. +This can cause asymmetry in figures with more than one subplot. It is also generally +difficult to draw "inset" colorbars in matplotlib and to generate outer colorbars +with consistent widths (i.e., not too "skinny" or "fat"). + +Changes +------- + +Proplot includes a simple framework for drawing colorbars and legends +that reference :ref:`individual subplots ` and +:ref:`multiple contiguous subplots `. + +* To draw a colorbar or legend on the outside of a specific subplot, pass an + "outer" location (e.g. ``loc='l'`` or ``loc='left'``) + to `proplot.axes.Axes.colorbar` or `proplot.axes.Axes.legend`. +* To draw a colorbar or legend on the inside of a specific subplot, pass an + "inner" location (e.g. ``loc='ur'`` or ``loc='upper right'``) + to `proplot.axes.Axes.colorbar` or `proplot.axes.Axes.legend`. +* To draw a colorbar or legend along the edge of the figure, use + `proplot.figure.Figure.colorbar` and `proplot.figure.Figure.legend`. + The `col`, `row`, and `span` keywords control which + `~proplot.gridspec.GridSpec` rows and columns are spanned + by the colorbar or legend. + +Since `~proplot.gridspec.GridSpec` permits variable spacing between subplot +rows and columns, "outer" colorbars and legends do not alter subplot +spacing or add whitespace. This is critical e.g. if you have a +colorbar between columns 1 and 2 but nothing between columns 2 and 3. +Also, `~proplot.figure.Figure` and `~proplot.axes.Axes` colorbar widths are +now specified in *physical* units rather than relative units, which makes +colorbar thickness independent of subplot size and easier to get just right. + +Links +----- + +* For more on single-subplot colorbars and legends, + see :ref:`this page `. +* For more on multi-subplot colorbars and legends, + see :ref:`this page `. +* For new colorbar features, + see :ref:`this page `. +* For new legend features, + see :ref:`this page `. + +.. _why_plotting: + +Improved plotting commands +========================== -Also, as described in :ref:`New and improved plotting methods`, ProPlot implements certain -features like grouped bar plots, layered area plots, heatmap plots, -and on-the-fly colorbars and legends from the -`xarray` and `pandas` APIs directly on the `~proplot.axes.Axes` class. +Limitation +---------- + +A few common plotting tasks take a lot of work using matplotlib alone. The `seaborn`_, +`xarray`_, and `pandas`_ packages offer improvements, but it would be nice to +have this functionality built right into matplotlib's interface. + +Changes +------- + +Proplot uses the `~proplot.axes.PlotAxes` subclass to add various `seaborn`_, +`xarray`_, and `pandas`_ features to existing matplotlib plotting commands +along with several additional features designed to make things easier. + +The following features are relevant for "1D" `~proplot.axes.PlotAxes` commands +like `~proplot.axes.PlotAxes.line` (equivalent to `~proplot.axes.PlotAxes.plot`) +and `~proplot.axes.PlotAxes.scatter`: + +* The treatment of data arguments passed to the 1D `~proplot.axes.PlotAxes` + commands is :ref:`standardized `. This makes them more flexible + and arguably more intuitive to use than their matplotlib counterparts. +* The `cycle` keyword is interpreted by the `~proplot.constructor.Cycle` + :ref:`constructor function ` and applies + :ref:`property cyclers ` on-the-fly. This permits succinct + and flexible property cycler declaration. +* The `legend` and `colorbar` keywords draw :ref:`on-the-fly legends and colorbars + ` using the result of the `~proplot.axes.PlotAxes` command. + Note that colorbars can be drawn from :ref:`lists of artists `. +* The default `ylim` (`xlim`) in the presence of a fixed `xlim` (`ylim`) is now + adjusted to exclude out-of-bounds data. This can be useful when "zooming in" on + a dependent variable axis but can be disabled by setting :rcraw:`axes.inbounds` + to ``False`` or passing ``inbounds=False`` to `~proplot.axes.PlotAxes` commands. +* The `~proplot.axes.PlotAxes.bar` and `~proplot.axes.PlotAxes.barh` commands accept 2D + arrays and can :ref:`stack or group ` successive columns. Likewise, the + `~proplot.axes.PlotAxes.area` and `~proplot.axes.PlotAxes.areax` commands (shorthands + for `~proplot.axes.PlotAxes.fill_between` and `~proplot.axes.PlotAxes.fill_betweenx`) + accept 2D arrays and can :ref:`stack or overlay ` successive columns. +* The `~proplot.axes.PlotAxes.bar`, `~proplot.axes.PlotAxes.barh`, + `~proplot.axes.PlotAxes.vlines`, `~proplot.axes.PlotAxes.hlines`, + `~proplot.axes.PlotAxes.area`, and `~proplot.axes.PlotAxes.areax` + commands accept a `negpos` keyword argument that :ref:`assigns different + colors ` to "negative" and "positive" regions. +* The `~proplot.axes.PlotAxes.linex` and `~proplot.axes.PlotAxes.scatterx` commands + are just like `~proplot.axes.PlotAxes.line` and `~proplot.axes.PlotAxes.scatter`, + but positional arguments are interpreted as *x* coordinates or (*y*, *x*) pairs. + There are also the related commands `~proplot.axes.PlotAxes.stemx`, + `~proplot.axes.PlotAxes.stepx`, `~proplot.axes.PlotAxes.boxh` (shorthand for + `~proplot.axes.PlotAxes.boxploth`), and `~proplot.axes.PlotAxes.violinh` (shorthand + for `~proplot.axes.PlotAxes.violinploth`). +* The `~proplot.axes.PlotAxes.line`, `~proplot.axes.PlotAxes.linex`, + `~proplot.axes.PlotAxes.scatter`, `~proplot.axes.PlotAxes.scatterx`, + `~proplot.axes.PlotAxes.bar`, and `~proplot.axes.PlotAxes.barh` commands can + draw vertical or horizontal :ref:`error bars or "shading" ` using a + variety of keyword arguments. This is often more convenient than working directly + with `~matplotlib.axes.Axes.errorbar` or `~matplotlib.axes.Axes.fill_between`. +* The `~proplot.axes.PlotAxes.parametric` command draws clean-looking + :ref:`parametric lines ` by encoding the parametric + coordinate using colormap colors rather than text annotations. + +The following features are relevant for "2D" `~proplot.axes.PlotAxes` commands +like `~proplot.axes.PlotAxes.pcolor` and `~proplot.axes.PlotAxes.contour`: + +* The treatment of data arguments passed to the 2D `~proplot.axes.PlotAxes` + commands is :ref:`standardized `. This makes them more flexible + and arguably more intuitive to use than their matplotlib counterparts. +* The `cmap` and `norm` :ref:`keyword arguments ` are interpreted + by the `~proplot.constructor.Colormap` and `~proplot.constructor.Norm` + :ref:`constructor functions `. This permits succinct + and flexible colormap and normalizer application. +* The `colorbar` keyword draws :ref:`on-the-fly colorbars ` using the + result of the plotting command. Note that :ref:`"inset" colorbars ` can + also be drawn, analogous to "inset" legends. +* The `~proplot.axes.PlotAxes.contour`, `~proplot.axes.PlotAxes.contourf`, + `~proplot.axes.PlotAxes.pcolormesh`, and `~proplot.axes.PlotAxes.pcolor` commands + all accept a `labels` keyword. This draws :ref:`contour and grid box labels + ` on-the-fly. Labels are automatically colored black or white + according to the luminance of the underlying grid box or filled contour. +* The default `vmin` and `vmax` used to normalize colormaps now excludes data + outside the *x* and *y* axis bounds `xlim` and `ylim` if they were explicitly + fixed. This can be disabled by setting :rcraw:`cmap.inbounds` to ``False`` + or by passing ``inbounds=False`` to `~proplot.axes.PlotAxes` commands. +* The `~proplot.colors.DiscreteNorm` normalizer is paired with most colormaps by + default. It can easily divide colormaps into distinct levels, similar to contour + plots. This can be disabled by setting :rcraw:`cmap.discrete` to ``False`` or + by passing ``discrete=False`` to `~proplot.axes.PlotAxes` commands. +* The `~proplot.colors.DivergingNorm` normalizer is perfect for data with a + :ref:`natural midpoint ` and offers both "fair" and "unfair" scaling. + The `~proplot.colors.SegmentedNorm` normalizer can generate + uneven color gradations useful for :ref:`unusual data distributions `. +* The `~proplot.axes.PlotAxes.heatmap` command invokes + `~proplot.axes.PlotAxes.pcolormesh` then applies an `equal axes apect ratio + `__, + adds ticks to the center of each gridbox, and disables minor ticks and gridlines. + This can be convenient for things like covariance matrices. +* Coordinate centers passed to commands like `~proplot.axes.PlotAxes.pcolor` are + automatically translated to "edges", and coordinate edges passed to commands like + `~proplot.axes.PlotAxes.contour` are automatically translated to "centers". In + matplotlib, ``pcolor`` simply truncates and offsets the data when it receives centers. +* Commands like `~proplot.axes.PlotAxes.pcolor`, `~proplot.axes.PlotAxes.contourf` + and `~proplot.axes.Axes.colorbar` automatically fix an irritating issue where + saved vector graphics appear to have thin white lines between `filled contours + `__, `grid boxes + `__, and `colorbar segments + `__. This can be disabled by + passing ``edgefix=False`` to `~proplot.axes.PlotAxes` commands. + +Links +----- + +* For the 1D plotting features, + see :ref:`this page `. +* For the 2D plotting features, + see :ref:`this page `. +* For treatment of 1D data arguments, + see :ref:`this page `. +* For treatment of 2D data arguments, + see :ref:`this page `. + +.. _why_cartopy_basemap: Cartopy and basemap integration =============================== -.. rubric:: Problem - -There are two widely-used engines -for plotting geophysical data with matplotlib: `cartopy` and `~mpl_toolkits.basemap`. -Using cartopy tends to be verbose and involve boilerplate code, -while using basemap requires you to use plotting commands on a -separate `~mpl_toolkits.basemap.Basemap` object rather than an axes object. - -Also, `cartopy` and `~mpl_toolkits.basemap` plotting commands assume -*map projection coordinates* unless specified otherwise. For most of us, this -choice is very frustrating, since geophysical data are usually stored in -longitude-latitude or "Plate Carrée" coordinates. - -.. rubric:: Solution - -ProPlot integrates various `cartopy` and `~mpl_toolkits.basemap` features -into the `~proplot.axes.ProjAxes` `~proplot.axes.ProjAxes.format` method. -This lets you apply all kinds of geographic plot settings, like coastlines, continents, political boundaries, and meridian and parallel gridlines. -`~proplot.axes.ProjAxes` also -overrides various plotting methods: - -* The new default for all `~proplot.axes.GeoAxes` plotting methods is ``transform=ccrs.PlateCarree()``. -* The new default for all `~proplot.axes.BasemapAxes` plotting methods is ``latlon=True``. -* *Global* coverage over the poles and across the matrix longitude boundaries can be enforced by passing ``globe=True`` to any 2d plotting command, e.g. `~matplotlib.axes.Axes.pcolormesh` and `~matplotlib.axes.Axes.contourf`. - -See :ref:`Geographic and polar plots` for details. -Note that active development on basemap will `halt after 2020 `__. -For now, cartopy is -`missing several features `__ -offered by basemap -- namely, flexible meridian and parallel gridline labels, -drawing physical map scales, and convenience features for adding background images like -the "blue marble". But once these are added to cartopy, ProPlot may remove the `~mpl_toolkits.basemap` integration features. - -.. - This is the right decision: Cartopy is integrated more closely with the matplotlib API - and is more amenable to further development. - -Colormaps and property cycles +Limitation +---------- + +There are two widely-used engines for working with geographic data in +matplotlib: `cartopy`_ and `basemap`_. Using cartopy tends to be +verbose and involve boilerplate code, while using basemap requires plotting +with a separate `~mpl_toolkits.basemap.Basemap` object rather than the +`~matplotlib.axes.Axes`. They both require separate import statements and extra +lines of code to configure the projection. + +Furthermore, when you use `cartopy`_ and `basemap`_ plotting +commands, "map projection" coordinates are the default coordinate system +rather than longitude-latitude coordinates. This choice is confusing for +many users, since the vast majority of geophysical data are stored with +longitude-latitude (i.e., "Plate Carrée") coordinates. + +Changes +------- + +Proplot can succinctly create detailed geographic plots using either cartopy or +basemap as "backends". By default, cartopy is used, but basemap can be used by passing +``backend='basemap'`` to axes-creation commands or by setting :rcraw:`geo.backend` to +``'basemap'``. To create a geographic plot, simply pass the `PROJ `__ +name to an axes-creation command, e.g. ``fig, ax = pplt.subplots(proj='pcarree')`` +or ``fig.add_subplot(proj='pcarree')``. Alternatively, use the +`~proplot.constructor.Proj` constructor function to quickly generate +a `cartopy.crs.Projection` or `~mpl_toolkits.basemap.Basemap` instance. + +Requesting geographic projections creates a `proplot.axes.GeoAxes` +with unified support for `cartopy`_ and `basemap`_ features via the +`proplot.axes.GeoAxes.format` command. This lets you quickly modify geographic +plot features like latitude and longitude gridlines, gridline labels, continents, +coastlines, and political boundaries. The syntax is conveniently analogous to the +syntax used for `proplot.axes.CartesianAxes.format` and `proplot.axes.PolarAxes.format`. + +The `~proplot.axes.GeoAxes` subclass also makes longitude-latitude coordinates +the "default" coordinate system by passing ``transform=ccrs.PlateCarree()`` +or ``latlon=True`` to `~proplot.axes.PlotAxes` commands (depending on whether cartopy +or basemap is the backend). And to enforce global coverage over the poles and across +longitude seams, you can pass ``globe=True`` to 2D `~proplot.axes.PlotAxes` commands +like `~proplot.axes.PlotAxes.contour` and `~proplot.axes.PlotAxes.pcolormesh`. + +Links +----- + +* For an introduction, + see :ref:`this page `. +* For more on cartopy and basemap as backends, + see :ref:`this page `. +* For plotting in `~proplot.axes.GeoAxes`, + see :ref:`this page `. +* For formatting `~proplot.axes.GeoAxes`, + see :ref:`this page `. +* For changing the `~proplot.axes.GeoAxes` bounds, + see :ref:`this page `. + +.. _why_xarray_pandas: + +Pandas and xarray integration ============================= -.. rubric:: Problem - -In matplotlib, colormaps are implemented with the `~matplotlib.colors.ListedColormap` and `~matplotlib.colors.LinearSegmentedColormap` classes. -They are hard to edit and hard to create from scratch. - -.. - Colormap identification is also suboptimal, since the names are case-sensitive, and reversed versions of each colormap are not guaranteed to exist. - -.. rubric:: Solution - -In ProPlot, it is easy to manipulate colormaps and property cycles: - -* The `~proplot.styletools.Colormap` constructor function can be used to slice and merge existing colormaps and/or generate brand new ones. -* The `~proplot.styletools.Cycle` constructor function can be used to make *color cycles* from *colormaps*! Color cycles can be applied to plots in a variety of ways; see :ref:`Color cycles` for details. -* The new `~proplot.styletools.ListedColormap` and `~proplot.styletools.LinearSegmentedColormap` classes include several convenient methods and have a much nicer REPL string representation. -* The `~proplot.styletools.PerceptuallyUniformColormap` class is used to make :ref:`Perceptually uniform colormaps`. These have smooth, aesthetically pleasing color transitions represent your data *accurately*. - -Importing ProPlot also makes all colormap names *case-insensitive*, and colormaps can be *reversed* or *cyclically shifted* by 180 degrees simply by appending ``'_r'`` or ``'_shifted'`` to the colormap name. This is powered by the `~proplot.styletools.CmapDict` dictionary, which replaces matplotlib's native colormap database. - -Smarter colormap normalization -============================== -.. rubric:: Problem - -In matplotlib, when ``extend='min'``, ``extend='max'``, or ``extend='neither'`` is passed to `~matplotlib.figure.Figure.colorbar` , the colormap colors reserved for "out-of-bounds" values are truncated. This can be irritating for plots with very few colormap levels, which are often more desirable (see :ref:`Discrete colormap levels`). - -The problem is that matplotlib "discretizes" colormaps by generating low-resolution lookup tables (see `~matplotlib.colors.LinearSegmentedColormap`). -While straightforward, -this approach has limitations and results in unnecessary -plot-specific copies of the colormap. -Ideally, the task of discretizing colormap colors should be left to the *normalizer*; matplotlib provides `~matplotlib.colors.BoundaryNorm` for this purpose, but it is seldom used and its features are limited. - -.. rubric:: Solution - -In ProPlot, all colormaps retain a high-resolution lookup table and the `~proplot.styletools.BinNorm` class is applied to every plot. `~proplot.styletools.BinNorm` restricts your plot to a *subset* of lookup table colors matching the number of requested levels. It chooses indices such that the colorbar levels *always* traverse the full range of colors, no matter the `extend` setting, and makes sure the end colors on *cyclic* colormaps are distinct. - -Also, before discretization, `~proplot.styletools.BinNorm` passes values through the *continuous* normalizer requested by the user with the `norm` keyword argument (e.g. `~matplotlib.colors.LogNorm` or `~proplot.styletools.MidpointNorm`). You can thus think of `~proplot.styletools.BinNorm` as a "meta-normalizer": `~proplot.styletools.BinNorm` simply discretizes the result of any arbitrary continuous transformation. - -Bulk global settings -==================== -.. rubric:: Problem - -In matplotlib, there are several `~matplotlib.rcParams` that you often -want to set *all at once*, like the tick lengths and spine colors. -It is also often desirable to change these settings for *individual subplots* -or *individual blocks of code* rather than globally. - -.. rubric:: Solution - -In ProPlot, you can use the `~proplot.rctools.rc` object to -change lots of settings at once with convenient shorthands. -This is meant to replace matplotlib's `~matplotlib.rcParams`. -dictionary. Settings can be changed with ``plot.rc.key = value``, ``plot.rc[key] = value``, -``plot.rc.update(...)``, with the `~proplot.axes.Axes.format` method, or with the -`~proplot.rctools.rc_configurator.context` method. - -For details, see :ref:`Configuring proplot`. -The most notable bulk settings are described below. +Limitation +---------- + +Scientific data is commonly stored in array-like containers +that include metadata -- namely, `xarray.DataArray`\ s, `pandas.DataFrame`\ s, +and `pandas.Series`. When matplotlib receives these objects, it ignores +the associated metadata. To create plots that are labeled with the metadata, +you must use the `xarray.DataArray.plot`, `pandas.DataFrame.plot`, +and `pandas.Series.plot` commands instead. + +This approach is fine for quick plots, but not ideal for complex ones. It requires +learning a different syntax from matplotlib, and tends to encourage using the +`~matplotlib.pyplot` interface rather than the object-oriented interface. The +``plot`` commands also include features that would be useful additions to matplotlib +in their own right, without requiring special containers and a separate interface. + +Changes +------- + +Proplot reproduces many of the `xarray.DataArray.plot`, +`pandas.DataFrame.plot`, and `pandas.Series.plot` +features directly on the `~proplot.axes.PlotAxes` commands. +This includes :ref:`grouped or stacked ` bar plots +and :ref:`layered or stacked ` area plots from two-dimensional +input data, auto-detection of :ref:`diverging datasets ` for +application of diverging colormaps and normalizers, and +:ref:`on-the-fly colorbars and legends ` using `colorbar` +and `legend` keywords. + +Proplot also handles metadata associated with `xarray.DataArray`, `pandas.DataFrame`, +`pandas.Series`, and `pint.Quantity` objects. When a plotting command receives these +objects, it updates the axis tick labels, axis labels, subplot title, and +colorbar and legend labels from the metadata. For `~pint.Quantity` arrays (including +`~pint.Quantity` those stored inside `~xarray.DataArray` containers), a unit string +is generated from the `pint.Unit` according to the :rcraw:`unitformat` setting +(note proplot also automatically calls `pint.UnitRegistry.setup_matplotlib` +whenever a `~pint.Quantity` is used for *x* and *y* coordinates and removes the +units from *z* coordinates to avoid the stripped-units warning message). +These features can be disabled by setting :rcraw:`autoformat` to ``False`` +or passing ``autoformat=False`` to any plotting command. + +Links +----- + +* For integration with 1D `~proplot.axes.PlotAxes` commands, + see :ref:`this page `. +* For integration with 2D `~proplot.axes.PlotAxes` commands, + see :ref:`this page `. +* For bar and area plots, + see :ref:`this page `. +* For diverging datasets, + see :ref:`this page `. +* For on-the-fly colorbars and legends, + see :ref:`this page `. + +.. _why_aesthetics: + +Aesthetic colors and fonts +========================== -============= ============================================= =========================================================================================================================================================================== -Key Description Children -============= ============================================= =========================================================================================================================================================================== -``color`` The color for axes bounds, ticks, and labels. ``axes.edgecolor``, ``geoaxes.edgecolor``, ``axes.labelcolor``, ``tick.labelcolor``, ``hatch.color``, ``xtick.color``, ``ytick.color`` -``linewidth`` The width of axes bounds and ticks. ``axes.linewidth``, ``geoaxes.linewidth``, ``hatch.linewidth``, ``xtick.major.width``, ``ytick.major.width`` -``small`` Font size for "small" labels. ``font.size``, ``tick.labelsize``, ``xtick.labelsize``, ``ytick.labelsize``, ``axes.labelsize``, ``legend.fontsize``, ``geogrid.labelsize`` -``large`` Font size for "large" labels. ``abc.size``, ``figure.titlesize``, ``axes.titlesize``, ``suptitle.size``, ``title.size``, ``leftlabel.size``, ``toplabel.size``, ``rightlabel.size``, ``bottomlabel.size`` -``tickpad`` Padding between ticks and labels. ``xtick.major.pad``, ``xtick.minor.pad``, ``ytick.major.pad``, ``ytick.minor.pad`` -``tickdir`` Tick direction. ``xtick.direction``, ``ytick.direction`` -``ticklen`` Tick length. ``xtick.major.size``, ``ytick.major.size``, ``ytick.minor.size * tickratio``, ``xtick.minor.size * tickratio`` -``tickratio`` Ratio between major and minor tick lengths. ``xtick.major.size``, ``ytick.major.size``, ``ytick.minor.size * tickratio``, ``xtick.minor.size * tickratio`` -``margin`` Margin width when limits not explicitly set. ``axes.xmargin``, ``axes.ymargin`` -============= ============================================= =========================================================================================================================================================================== +Limitation +---------- + +A common problem with scientific visualizations is the use of "misleading" +colormaps like ``'jet'``. These colormaps have jarring jumps in +`hue, saturation, and luminance `_ that can trick the human eye into seeing +non-existing patterns. It is important to use "perceptually uniform" colormaps +instead. Matplotlib comes packaged with `a few of its own `_, plus +the `ColorBrewer `_ colormap series, but external projects offer +a larger variety of aesthetically pleasing "perceptually uniform" colormaps +that would be nice to have in one place. + +Matplotlib also "registers" the X11/CSS4 color names, but these are relatively +limited. The more numerous and arguably more intuitive `XKCD color survey `_ +names can only be accessed with the ``'xkcd:'`` prefix. As with colormaps, there +are also external projects with useful color names like `open color `_. + +Finally, matplotlib comes packaged with ``DejaVu Sans`` as the default font. +This font is open source and include glyphs for a huge variety of characters. +However in our opinion, it is not very aesthetically pleasing. It is also +difficult to switch to other fonts on limited systems or systems with fonts +stored in incompatible file formats (see :ref:`below `). + +Changes +------- + +Proplot adds new colormaps, colors, and fonts to help you make more +aesthetically pleasing figures. + +* Proplot adds colormaps from the `seaborn `_, `cmocean `_, + `SciVisColor `_, and `Scientific Colour Maps `_ projects. + It also defines a few default :ref:`perceptually uniform colormaps ` + and includes a `~proplot.colors.PerceptualColormap` class for generating + new ones. A :ref:`table of colormap ` and + :ref:`color cycles ` can be shown using + `~proplot.demos.show_cmaps` and `~proplot.demos.show_cycles`. + Colormaps like ``'jet'`` can still be accessed, but this is discouraged. +* Proplot adds colors from the `open color `_ project and adds + `XKCD color survey `_ names without the ``'xkcd:'`` prefix after + *filtering* them to exclude perceptually-similar colors and *normalizing* the + naming pattern to make them more self-consistent. Old X11/CSS4 colors can still be + accessed, but this is discouraged. A :ref:`table of color names ` + can be shown using `~proplot.demos.show_colors`. +* Proplot comes packaged with several additional :ref:`sans-serif fonts + ` and the entire `TeX Gyre `_ font series. TeX Gyre + consists of open-source fonts designed to resemble more popular, commonly-used fonts + like Helvetica and Century. They are used as the new default serif, sans-serif, + monospace, cursive, and "fantasy" fonts, and they are available on all workstations. + A :ref:`table of font names ` can be shown + using `~proplot.demos.show_fonts`. + +Links +----- + +* For more on colormaps, + see :ref:`this page `. +* For more on color cycles, + see :ref:`this page `. +* For more on fonts, + see :ref:`this page `. +* For importing custom colormaps, colors, and fonts, + see :ref:`this page `. + +.. _why_colormaps_cycles: + +Manipulating colormaps +====================== + +Limitation +---------- + +In matplotlib, colormaps are implemented with the +`~matplotlib.colors.LinearSegmentedColormap` class (representing "smooth" +color gradations) and the `~matplotlib.colors.ListedColormap` class (representing +"categorical" color sets). They are somewhat cumbersome to modify or create from +scratch. Meanwhile, property cycles used for individual plot elements are implemented +with the `~cycler.Cycler` class. They are easier to modify but they cannot be +"registered" by name like colormaps. + +The `seaborn`_ package includes "color palettes" to make working with colormaps +and property cycles easier, but it would be nice to have similar features +integrated more closely with matplotlib's colormap and property cycle constructs. + +Changes +------- + +Proplot tries to make it easy to manipulate colormaps and property cycles. + +* All colormaps in proplot are replaced with the `~proplot.colors.ContinuousColormap` + and `~proplot.colors.DiscreteColormap` subclasses of + `~matplotlib.colors.LinearSegmentedColormap` and `~matplotlib.colors.ListedColormap`. + These classes include several useful features leveraged by the + :ref:`constructor functions ` + `~proplot.constructor.Colormap` and `~proplot.constructor.Cycle`. +* The `~proplot.constructor.Colormap` function can merge, truncate, and + modify existing colormaps or generate brand new colormaps. It can also + create new `~proplot.colors.PerceptualColormap`\ s -- a type of + `proplot.colors.ContinuousColormap` with linear transitions in the + :ref:`perceptually uniform-like ` hue, saturation, + and luminance channels rather then the red, blue, and green channels. +* The `~proplot.constructor.Cycle` function can make property cycles from + scratch or retrieve "registered" color cycles from their associated + `~proplot.colors.DiscreteColormap` instances. It can also make property + cycles by splitting up the colors from registered or on-the-fly + `~proplot.colors.ContinuousColormap`\ s and `~proplot.colors.PerceptualColormap`\ s. + +Proplot also makes all colormap and color cycle names case-insensitive, and +colormaps are automatically reversed or cyclically shifted 180 degrees if you +append ``'_r'`` or ``'_s'`` to any colormap name. These features are powered by +`~proplot.colors.ColormapDatabase`, which replaces matplotlib's native +colormap database. + +Links +----- + +* For making new colormaps, + see :ref:`this page `. +* For making new color cycles, + see :ref:`this page `. +* For merging colormaps and cycles, + see :ref:`this page `. +* For modifying colormaps and cycles, + see :ref:`this page `. + +.. _why_norm: Physical units engine ===================== -.. rubric:: Problem - -Matplotlib requires users to use -inches for the figure size `figsize`. This may be confusing for users outside -of the U.S. - -Matplotlib also uses figure-relative units for the margins -`left`, `right`, `bottom`, and `top`, and axes-relative units -for the column and row spacing `wspace` and `hspace`. -Relative units tend to require "tinkering" with numbers until you find the -right one. And since they are *relative*, if you decide to change your -figure size or add a subplot, they will have to be readjusted. - -.. rubric:: Solution - -ProPlot introduces the physical units engine `~proplot.utils.units` -for interpreting `figsize`, `width`, `height`, `axwidth`, `axheight`, -`left`, `right`, `top`, `bottom`, `wspace`, `hspace`, and arguments -in a few other places. Acceptable units include inches, centimeters, -millimeters, pixels, `points `__, -`picas `__, `em-heights `__, and `light years `__ (because why not?). -Em-heights are particularly useful, as labels already -present can be useful *rulers* for figuring out the amount -of space needed. - -`~proplot.utils.units` is also used to convert settings -passed to `~proplot.rctools.rc` from arbitrary physical units -to *points* -- for example, :rcraw:`linewidth`, :rcraw:`ticklen`, -:rcraw:`axes.titlesize`, and :rcraw:`axes.titlepad`. -See :ref:`Configuring proplot` for details. - - -The .proplot folder -=================== -.. rubric:: Problem - -In matplotlib, it can be difficult to design your -own colormaps and color cycles, and there is no builtin -way to *save* them for future use. It is also -difficult to get matplotlib to use custom ``.ttc``, ``.ttf``, -and ``.otf`` font files, which may be desirable when you are -working on Linux servers with limited font selections. - - -.. rubric:: Solution - -ProPlot automatically adds colormaps, color cycles, and font files -saved in the ``.proplot/cmaps``, ``.proplot/cycles``, and ``.proplot/fonts`` -folders in your home directory. -You can save colormaps and color -cycles to these folders simply by passing ``save=True`` to -`~proplot.styletools.Colormap` and `~proplot.styletools.Cycle`. -To *manually* load from these folders, e.g. if you have added -files to these folders but you do not want to restart your -ipython session, simply call -`~proplot.styletools.register_cmaps`, -`~proplot.styletools.register_cycles`, and -`~proplot.styletools.register_fonts`. - -.. - As mentioned above, - ProPlot introduces the `~proplot.styletools.Colormap` and `~proplot.styletools.Cycle`. - functions for designing your own colormaps and color cycles. - -.. - ...and much more! - ================= - This page is not comprehensive -- it just - illustrates how ProPlot addresses - some of the stickiest matplotlib limitations - that bug your average power user. - See the User Guide for a more comprehensive overview. + +Limitation +---------- + +Matplotlib uses figure-relative units for the margins `left`, `right`, +`bottom`, and `top`, and axes-relative units for the column and row spacing +`wspace` and `hspace`. Relative units tend to require "tinkering" with +numbers until you find the right one. And since they are *relative*, if you +decide to change your figure size or add a subplot, they will have to be +readjusted. + +Matplotlib also requires users to set the figure size `figsize` in inches. +This may be confusing for users outside of the United States. + +Changes +------- + +Proplot uses physical units for the `~proplot.gridspec.GridSpec` keywords +`left`, `right`, `top`, `bottom`, `wspace`, `hspace`, `pad`, `outerpad`, and +`innerpad`. The default unit (assumed when a numeric argument is passed) is +`em-widths `__. Em-widths are +particularly appropriate for this context, as plot text can be a useful "ruler" +when figuring out the amount of space you need. Proplot also permits arbitrary +string units for these keywords, for the `~proplot.figure.Figure` keywords +`figsize`, `figwidth`, `figheight`, `refwidth`, and `refheight`, and in a +few other places. This is powered by the physical units engine `~proplot.utils.units`. +Acceptable units include inches, centimeters, millimeters, +pixels, `points `__, and `picas +`__ (a table of acceptable +units is found :ref:`here `). Note the `~proplot.utils.units` engine +also translates rc settings assigned to `~proplot.config.rc_matplotlib` and +`~proplot.config.rc_proplot`, e.g. :rcraw:`subplots.refwidth`, +:rcraw:`legend.columnspacing`, and :rcraw:`axes.labelpad`. + +Links +----- + +* For more on physical units, + see :ref:`this page `. +* For more on `~proplot.gridspec.GridSpec` spacing units, + see :ref:`this page ` +* For more on colorbar width units, + see :ref:`this page `, +* For more on panel width units, + see :ref:`this page `, + +.. _why_rc: + +Flexible global settings +======================== + +Limitation +---------- + +In matplotlib, there are several `~matplotlib.rcParams` that would be +useful to set all at once, like spine and label colors. It might also +be useful to change these settings for individual subplots rather +than globally. + +Changes +------- + +In proplot, you can use the `~proplot.config.rc` object to change both native +matplotlib settings (found in `~proplot.config.rc_matplotlib`) and added proplot +settings (found in `~proplot.config.rc_proplot`). Assigned settings are always +validated, and "meta" settings like ``meta.edgecolor``, ``meta.linewidth``, and +``font.smallsize`` can be used to update many settings all at once. Settings can +be changed with ``pplt.rc.key = value``, ``pplt.rc[key] = value``, +``pplt.rc.update(key=value)``, using `proplot.axes.Axes.format`, or using +`proplot.config.Configurator.context`. Settings that have changed during the +python session can be saved to a file with `proplot.config.Configurator.save` +(see `~proplot.config.Configurator.changed`), and settings can be loaded from +files with `proplot.config.Configurator.load`. + +Links +----- + +* For an introduction, + see :ref:`this page `. +* For more on changing settings, + see :ref:`this page `. +* For more on proplot settings, + see :ref:`this page `. +* For more on meta settings, + see :ref:`this page `. +* For a table of the new settings, + see :ref:`this page `. + +.. _why_dotproplot: + +Loading stuff +============= + +Limitation +---------- + +Matplotlib `~matplotlib.rcParams` can be changed persistently by placing +`matplotlibrc `_ files in the same directory as your python script. +But it can be difficult to design and store your own colormaps and color cycles for +future use. It is also difficult to get matplotlib to use custom ``.ttf`` and +``.otf`` font files, which may be desirable when you are working on +Linux servers with limited font selections. + +Changes +------- + +Proplot settings can be changed persistently by editing the default ``proplotrc`` +file in the location given by `~proplot.config.Configurator.user_file` (this is +usually ``$HOME/.proplot/proplotrc``) or by adding loose ``proplotrc`` files to +either the current directory or an arbitrary parent directory. Adding files to +parent directories can be useful when working in projects with lots of subfolders. + +Proplot also automatically registers colormaps, color cycles, colors, and font +files stored in subfolders named ``cmaps``, ``cycles``, ``colors``, and ``fonts`` +in the location given by `~proplot.config.Configurator.user_folder` (this is usually +``$HOME/.proplot``), as well as loose subfolders named ``proplot_cmaps``, +``proplot_cycles``, ``proplot_colors``, and ``proplot_fonts`` in the current +directory or an arbitrary parent directory. You can save colormaps and color cycles to +`~proplot.config.Configurator.user_folder` simply by passing ``save=True`` to +`~proplot.constructor.Colormap` and `~proplot.constructor.Cycle`. To re-register +these files during an active python session, or to register arbitrary input arguments, +you can use `~proplot.config.register_cmaps`, `~proplot.config.register_cycles`, +`~proplot.config.register_colors`, or `~proplot.config.register_fonts`. + +Links +----- + +* For the ``proplotrc`` file, + see :ref:`this page `. +* For registering colormaps, + see :ref:`this page `. +* For registering color cycles, + see :ref:`this page `. +* For registering colors, + see :ref:`this page `. +* For registering fonts, + see :ref:`this page `. diff --git a/proplot/__init__.py b/proplot/__init__.py index de2b87135..a4809e6d5 100644 --- a/proplot/__init__.py +++ b/proplot/__init__.py @@ -1,42 +1,99 @@ #!/usr/bin/env python3 -# Import everything into the top-level module namespace -# Make sure to load styletools early so we can try to update TTFPATH before -# the fontManager is loaded by other modules (requiring a rebuild) -import os as _os -import pkg_resources as _pkg -from .utils import _benchmark -from .utils import * # noqa: F401 F403 -with _benchmark('total time'): - with _benchmark('styletools'): - from .styletools import * # noqa: F401 F403 - with _benchmark('pyplot'): - import matplotlib.pyplot as _ # noqa; sets up the backend and ipython display hooks - with _benchmark('rctools'): - from .rctools import * # noqa: F401 F403 - with _benchmark('axistools'): - from .axistools import * # noqa: F401 F403 - with _benchmark('wrappers'): - from .wrappers import * # noqa: F401 F403 - with _benchmark('projs'): - from .projs import * # noqa: F401 F403 - with _benchmark('axes'): - from .axes import * # noqa: F401 F403 - with _benchmark('subplots'): - from .subplots import * # noqa: F401 F403 - - -# Initialize customization folders -_rc_folder = _os.path.join(_os.path.expanduser('~'), '.proplot') -if not _os.path.isdir(_rc_folder): - _os.mkdir(_rc_folder) -for _rc_sub in ('cmaps', 'cycles', 'colors', 'fonts'): - _rc_sub = _os.path.join(_rc_folder, _rc_sub) - if not _os.path.isdir(_rc_sub): - _os.mkdir(_rc_sub) - +""" +A succinct matplotlib wrapper for making beautiful, publication-quality graphics. +""" # SCM versioning +import pkg_resources as pkg name = 'proplot' try: - version = __version__ = _pkg.get_distribution(__name__).version -except _pkg.DistributionNotFound: + version = __version__ = pkg.get_distribution(__name__).version +except pkg.DistributionNotFound: version = __version__ = 'unknown' + +# Import dependencies early to isolate import times +from . import internals, externals, tests # noqa: F401 +from .internals.benchmarks import _benchmark +with _benchmark('pyplot'): + from matplotlib import pyplot # noqa: F401 +with _benchmark('cartopy'): + try: + import cartopy # noqa: F401 + except ImportError: + pass +with _benchmark('basemap'): + try: + from mpl_toolkits import basemap # noqa: F401 + except ImportError: + pass + +# Import everything to top level +with _benchmark('config'): + from .config import * # noqa: F401 F403 +with _benchmark('proj'): + from .proj import * # noqa: F401 F403 +with _benchmark('utils'): + from .utils import * # noqa: F401 F403 +with _benchmark('colors'): + from .colors import * # noqa: F401 F403 +with _benchmark('ticker'): + from .ticker import * # noqa: F401 F403 +with _benchmark('scale'): + from .scale import * # noqa: F401 F403 +with _benchmark('axes'): + from .axes import * # noqa: F401 F403 +with _benchmark('gridspec'): + from .gridspec import * # noqa: F401 F403 +with _benchmark('figure'): + from .figure import * # noqa: F401 F403 +with _benchmark('constructor'): + from .constructor import * # noqa: F401 F403 +with _benchmark('ui'): + from .ui import * # noqa: F401 F403 +with _benchmark('demos'): + from .demos import * # noqa: F401 F403 + +# Dynamically add registered classes to top-level namespace +from . import proj as crs # backwards compatibility # noqa: F401 +from .constructor import NORMS, LOCATORS, FORMATTERS, SCALES, PROJS +_globals = globals() +for _src in (NORMS, LOCATORS, FORMATTERS, SCALES, PROJS): + for _key, _cls in _src.items(): + if isinstance(_cls, type): # i.e. not a scale preset + _globals[_cls.__name__] = _cls # may overwrite proplot names + +# Register objects +from .config import register_cmaps, register_cycles, register_colors, register_fonts +with _benchmark('cmaps'): + register_cmaps(default=True) +with _benchmark('cycles'): + register_cycles(default=True) +with _benchmark('colors'): + register_colors(default=True) +with _benchmark('fonts'): + register_fonts(default=True) + +# Validate colormap names and propagate 'cycle' to 'axes.prop_cycle' +# NOTE: cmap.sequential also updates siblings 'cmap' and 'image.cmap' +from .config import rc +from .internals import rcsetup, warnings +rcsetup.VALIDATE_REGISTERED_CMAPS = True +for _key in ('cycle', 'cmap.sequential', 'cmap.diverging', 'cmap.cyclic', 'cmap.qualitative'): # noqa: E501 + try: + rc[_key] = rc[_key] + except ValueError as err: + warnings._warn_proplot(f'Invalid user rc file setting: {err}') + rc[_key] = 'Greys' # fill value + +# Validate color names now that colors are registered +# NOTE: This updates all settings with 'color' in name (harmless if it's not a color) +from .config import rc_proplot, rc_matplotlib +rcsetup.VALIDATE_REGISTERED_COLORS = True +for _src in (rc_proplot, rc_matplotlib): + for _key in _src: # loop through unsynced properties + if 'color' not in _key: + continue + try: + _src[_key] = _src[_key] + except ValueError as err: + warnings._warn_proplot(f'Invalid user rc file setting: {err}') + _src[_key] = 'black' # fill value diff --git a/proplot/axes.py b/proplot/axes.py deleted file mode 100644 index 887a9a80c..000000000 --- a/proplot/axes.py +++ /dev/null @@ -1,4100 +0,0 @@ -#!/usr/bin/env python3 -""" -The axes classes used for all ProPlot figures. -""" -import numpy as np -import functools -from numbers import Integral, Number -import matplotlib.projections as mproj -import matplotlib.axes as maxes -import matplotlib.dates as mdates -import matplotlib.text as mtext -import matplotlib.path as mpath -import matplotlib.ticker as mticker -import matplotlib.patches as mpatches -import matplotlib.gridspec as mgridspec -import matplotlib.transforms as mtransforms -import matplotlib.collections as mcollections -from . import projs, axistools -from .utils import _warn_proplot, _notNone, units, arange, edges -from .rctools import rc, _rc_nodots -from .wrappers import ( - _get_transform, _norecurse, _redirect, - _add_errorbars, _bar_wrapper, _barh_wrapper, _boxplot_wrapper, - _default_crs, _default_latlon, _default_transform, _cmap_changer, - _cycle_changer, _fill_between_wrapper, _fill_betweenx_wrapper, - _hist_wrapper, _plot_wrapper, _scatter_wrapper, - _standardize_1d, _standardize_2d, - _text_wrapper, _violinplot_wrapper, - colorbar_wrapper, legend_wrapper, -) -try: - from cartopy.mpl.geoaxes import GeoAxes -except ModuleNotFoundError: - GeoAxes = object -try: # use this for debugging instead of print()! - from icecream import ic -except ImportError: # graceful fallback if IceCream isn't installed - ic = lambda *a: None if not a else (a[0] if len(a) == 1 else a) # noqa - -__all__ = [ - 'Axes', - 'BasemapAxes', - 'GeoAxes', - 'PolarAxes', 'ProjAxes', - 'XYAxes', -] - -# Translator for inset colorbars and legends -ABC_STRING = 'abcdefghijklmnopqrstuvwxyz' -LOC_TRANSLATE = { - 'inset': 'best', - 'i': 'best', - 0: 'best', - 1: 'upper right', - 2: 'upper left', - 3: 'lower left', - 4: 'lower right', - 5: 'center left', - 6: 'center right', - 7: 'lower center', - 8: 'upper center', - 9: 'center', - 'l': 'left', - 'r': 'right', - 'b': 'bottom', - 't': 'top', - 'c': 'center', - 'ur': 'upper right', - 'ul': 'upper left', - 'll': 'lower left', - 'lr': 'lower right', - 'cr': 'center right', - 'cl': 'center left', - 'uc': 'upper center', - 'lc': 'lower center', -} - - -def _abc(i): - """ - Return a...z...aa...zz...aaa...zzz. - """ - if i < 26: - return ABC_STRING[i] - else: - return _abc(i - 26) + ABC_STRING[i % 26] # sexy sexy recursion - - -def _disable_decorator(msg): - """ - Return a decorator that disables methods with message `msg`. The - docstring is set to ``None`` so the ProPlot fork of automodapi doesn't add - these methods to the website documentation. Users can still call - help(ax.method) because python looks for superclass method docstrings if a - docstring is empty. - """ - def decorator(func): - @functools.wraps(func) - def _wrapper(self, *args, **kwargs): - raise RuntimeError(msg.format(func.__name__)) - _wrapper.__doc__ = None - return _wrapper - return decorator - - -def _parse_format(mode=2, rc_kw=None, **kwargs): - """ - Separate `~proplot.rctools.rc` setting name value pairs from - `~Axes.format` keyword arguments. - """ - kw = {} - rc_kw = rc_kw or {} - for key, value in kwargs.items(): - key_fixed = _rc_nodots.get(key, None) - if key_fixed is None: - kw[key] = value - else: - rc_kw[key_fixed] = value - return rc_kw, mode, kw - - -class Axes(maxes.Axes): - """ - Lowest-level axes subclass. Handles titles and axis - sharing. Adds several new methods and overrides existing ones. - """ - def __init__(self, *args, number=None, main=False, **kwargs): - """ - Parameters - ---------- - number : int - The subplot number, used for a-b-c labeling. See `~Axes.format` - for details. Note the first axes is ``1``, not ``0``. - main : bool, optional - Used internally, indicates whether this is a "main axes" rather - than a twin, panel, or inset axes. - *args, **kwargs - Passed to `~matplotlib.axes.Axes`. - - See also - -------- - :py:obj:`matplotlib.axes.Axes`, - :py:obj:`XYAxes`, - :py:obj:`PolarAxes`, - :py:obj:`ProjAxes` - """ - super().__init__(*args, **kwargs) - - # Ensure isDefault_minloc enabled at start, needed for dual axes - self.xaxis.isDefault_minloc = self.yaxis.isDefault_minloc = True - - # Properties - self._abc_loc = None - self._abc_text = None - self._titles_dict = {} # dictionary of titles and locs - self._title_loc = None # location of main title - self._title_pad = rc['axes.titlepad'] # format() can overwrite - self._title_pad_active = None - self._above_top_panels = True # TODO: add rc prop? - self._bottom_panels = [] - self._top_panels = [] - self._left_panels = [] - self._right_panels = [] - self._tightbbox = None # bounding boxes are saved - self._panel_parent = None - self._panel_side = None - self._panel_share = False - self._panel_filled = False # True when "filled" with cbar/legend - self._sharex_override = False - self._sharey_override = False - self._inset_parent = None - self._inset_zoom = False - self._inset_zoom_data = None - self._alty_child = None - self._altx_child = None - self._alty_parent = None - self._altx_parent = None - self.number = number # for abc numbering - if main: - self.figure._axes_main.append(self) - - # On-the-fly legends and colorbars - self._auto_colorbar = {} - self._auto_legend = {} - - # Figure row and column labels - # NOTE: Most of these sit empty for most subplots - # TODO: Implement this with EdgeStack - coltransform = mtransforms.blended_transform_factory( - self.transAxes, self.figure.transFigure - ) - rowtransform = mtransforms.blended_transform_factory( - self.figure.transFigure, self.transAxes - ) - self._left_label = self.text( - 0, 0.5, '', va='center', ha='right', transform=rowtransform - ) - self._right_label = self.text( - 0, 0.5, '', va='center', ha='left', transform=rowtransform - ) - self._bottom_label = self.text( - 0.5, 0, '', va='top', ha='center', transform=coltransform - ) - self._top_label = self.text( - 0.5, 0, '', va='bottom', ha='center', transform=coltransform - ) - - # Axes inset title labels - transform = self.transAxes - self._upper_left_title = self.text( - 0, 0, '', va='top', ha='left', transform=transform, - ) - self._upper_center_title = self.text( - 0, 0, '', va='top', ha='center', transform=transform, - ) - self._upper_right_title = self.text( - 0, 0, '', va='top', ha='right', transform=transform, - ) - self._lower_left_title = self.text( - 0, 0, '', va='bottom', ha='left', transform=transform, - ) - self._lower_center_title = self.text( - 0, 0, '', va='bottom', ha='center', transform=transform, - ) - self._lower_right_title = self.text( - 0, 0, '', va='bottom', ha='right', transform=transform, - ) - - # Abc label - self._abc_label = self.text(0, 0, '', transform=transform) - - # Automatic axis sharing and formatting - self._share_setup() - self.format(mode=1) # mode == 1 applies the rcShortParams - - def _draw_auto_legends_colorbars(self): - """ - Generate automatic legends and colorbars. Wrapper funcs - let user add handles to location lists with successive calls to - make successive calls to plotting commands. - """ - for loc, (handles, kwargs) in self._auto_colorbar.items(): - self.colorbar(handles, **kwargs) - for loc, (handles, kwargs) in self._auto_legend.items(): - self.legend(handles, **kwargs) - self._auto_legend = {} - self._auto_colorbar = {} - - def _get_extent_axes(self, x, panels=False): - """ - Return the axes whose horizontal or vertical extent in the main - gridspec matches the horizontal or vertical extend of this axes. - The lefmost or bottommost axes are at the start of the list. - """ - if not hasattr(self, 'get_subplotspec'): - return [self] - y = 'y' if x == 'x' else 'x' - idx = 0 if x == 'x' else 1 - argfunc = np.argmax if x == 'x' else np.argmin - irange = self._range_gridspec(x) - axs = [ - ax for ax in self.figure._axes_main - if ax._range_gridspec(x) == irange - ] - if not axs: - return [self] - else: - pax = axs.pop(argfunc([ax._range_gridspec(y)[idx] for ax in axs])) - return [pax, *axs] - - def _get_side_axes(self, side, panels=False): - """ - Return the axes whose left, right, top, or bottom sides abutt - against the same row or column as this axes. - """ - if side not in ('left', 'right', 'bottom', 'top'): - raise ValueError(f'Invalid side {side!r}.') - if not hasattr(self, 'get_subplotspec'): - return [self] - x = 'x' if side in ('left', 'right') else 'y' - idx = 0 if side in ('left', 'top') else 1 # which side to test - coord = self._range_gridspec(x)[idx] # side for a particular axes - axs = [ - ax for ax in self.figure._axes_main - if ax._range_gridspec(x)[idx] == coord - ] - if not axs: - return [self] - else: - return axs - - def _get_title(self, loc): - """ - Get the title at the corresponding location. - """ - if loc == 'abc': - return self._abc_label - else: - return getattr(self, '_' + loc.replace(' ', '_') + '_title') - - def _iter_panels(self, sides=('left', 'right', 'bottom', 'top')): - """ - Return a list of axes and child panel axes. - """ - axs = [self] if self.get_visible() else [] - if not set(sides) <= {'left', 'right', 'bottom', 'top'}: - raise ValueError(f'Invalid sides {sides!r}.') - for side in sides: - for ax in getattr(self, '_' + side + '_panels'): - if not ax or not ax.get_visible(): - continue - axs.append(ax) - return axs - - def _loc_translate(self, loc, mode=None, allow_manual=True): - """ - Return the location string `loc` translated into a standardized - form. - """ - if mode == 'legend': - valid = tuple(LOC_TRANSLATE.values()) - elif mode == 'panel': - valid = ('left', 'right', 'top', 'bottom') - elif mode == 'colorbar': - valid = ( - 'best', 'left', 'right', 'top', 'bottom', - 'upper left', 'upper right', 'lower left', 'lower right', - ) - elif mode in ('abc', 'title'): - valid = ( - 'left', 'center', 'right', - 'upper left', 'upper center', 'upper right', - 'lower left', 'lower center', 'lower right', - ) - else: - raise ValueError(f'Invalid mode {mode!r}.') - loc_translate = { - key: value for key, value in LOC_TRANSLATE.items() - if value in valid - } - if loc in (None, True): - context = mode in ('abc', 'title') - loc = rc.get(mode + '.loc', context=context) - if loc is not None: - loc = self._loc_translate(loc, mode) - elif isinstance(loc, (str, Integral)): - if loc in loc_translate.values(): # full name - pass - else: - try: - loc = loc_translate[loc] - except KeyError: - raise KeyError(f'Invalid {mode} location {loc!r}.') - elif ( - allow_manual - and mode == 'legend' - and np.iterable(loc) - and len(loc) == 2 - and all(isinstance(l, Number) for l in loc) - ): - loc = np.array(loc) - else: - raise KeyError(f'Invalid {mode} location {loc!r}.') - if mode == 'colorbar' and loc == 'best': # white lie - loc = 'lower right' - return loc - - def _make_inset_locator(self, bounds, trans): - """ - Return a locator that determines inset axes bounds. - """ - def inset_locator(ax, renderer): - bbox = mtransforms.Bbox.from_bounds(*bounds) - bb = mtransforms.TransformedBbox(bbox, trans) - tr = self.figure.transFigure.inverted() - bb = mtransforms.TransformedBbox(bb, tr) - return bb - return inset_locator - - def _range_gridspec(self, x): - """ - Return the column or row gridspec range for the axes. - """ - if not hasattr(self, 'get_subplotspec'): - raise RuntimeError(f'Axes is not a subplot.') - ss = self.get_subplotspec() - if hasattr(ss, 'get_active_rows_columns'): - func = ss.get_active_rows_columns - else: - func = ss.get_rows_columns - if x == 'x': - _, _, _, _, col1, col2 = func() - return col1, col2 - else: - _, _, row1, row2, _, _ = func() - return row1, row2 - - def _range_tightbbox(self, x): - """ - Return the tight bounding box span from the cached bounding box. - `~proplot.axes.Axes.get_tightbbox` caches bounding boxes when - `~Figure.get_tightbbox` is called. - """ - # TODO: Better testing for axes visibility - bbox = self._tightbbox - if bbox is None: - return np.nan, np.nan - if x == 'x': - return bbox.xmin, bbox.xmax - else: - return bbox.ymin, bbox.ymax - - def _reassign_suplabel(self, side): - """ - Re-assign the column and row labels to the relevant panel if - present. This is called by `~proplot.subplots.Figure._align_suplabel`. - """ - # Place column and row labels on panels instead of axes -- works when - # this is called on the main axes *or* on the relevant panel itself - # TODO: Mixed figure panels with super labels? How does that work? - if side == self._panel_side: - ax = self._panel_parent - else: - ax = self - paxs = getattr(ax, '_' + side + '_panels') - if not paxs: - return ax - kw = {} - pax = paxs[-1] # outermost is always list in list - obj = getattr(ax, '_' + side + '_label') - for key in ('color', 'fontproperties'): # TODO: add to this? - kw[key] = getattr(obj, 'get_' + key)() - pobj = getattr(pax, '_' + side + '_label') - pobj.update(kw) - text = obj.get_text() - if text: - obj.set_text('') - pobj.set_text(text) - return pax - - def _reassign_title(self): - """ - Re-assign the title to the first upper panel if present. We cannot - simply add the upper panel as a child axes, because then the title will - be offset but still belong to main axes, which messes up the tight - bounding box. - """ - # Reassign title from main axes to top panel -- works when this is - # called on the main axes *or* on the top panel itself. This is - # critical for bounding box calcs; not always clear whether draw() and - # get_tightbbox() are called on the main axes or panel first - if self._panel_side == 'top' and self._panel_parent: - ax = self._panel_parent - else: - ax = self - taxs = ax._top_panels - if not taxs or not ax._above_top_panels: - tax = ax - else: - tax = taxs[-1] - tax._title_pad = ax._title_pad - for loc in ('abc', 'left', 'center', 'right'): - kw = {} - obj = ax._get_title(loc) - if not obj.get_text(): - continue - tobj = tax._get_title(loc) - for key in ('text', 'color', 'fontproperties'): # add to this? - kw[key] = getattr(obj, 'get_' + key)() - tobj.update(kw) - obj.set_text('') - - def _sharex_setup(self, sharex): - """ - Configure x-axis sharing for panels. Main axis sharing is done in - `~XYAxes._sharex_setup`. - """ - self._share_short_axis(sharex, 'left') - self._share_short_axis(sharex, 'right') - self._share_long_axis(sharex, 'bottom') - self._share_long_axis(sharex, 'top') - - def _sharey_setup(self, sharey): - """ - Configure y-axis sharing for panels. Main axis sharing is done in - `~XYAxes._sharey_setup`. - """ - self._share_short_axis(sharey, 'bottom') - self._share_short_axis(sharey, 'top') - self._share_long_axis(sharey, 'left') - self._share_long_axis(sharey, 'right') - - def _share_setup(self): - """ - Automatically configure axis sharing based on the horizontal and - vertical extent of subplots in the figure gridspec. - """ - # Panel axes sharing, between main subplot and its panels - # NOTE: While _panel_share just means "include this panel" in the - # axis sharing between the main subplot and panel children, - # _sharex_override and _sharey_override say "override the sharing level - # for these axes, because they belong to a panel group", and may - # include the main subplot itself. - def shared(paxs): - return [ - pax for pax in paxs - if not pax._panel_filled and pax._panel_share - ] - - # Internal axis sharing; share stacks of panels and the main - # axes with each other. - # NOTE: *This* block is why, even though share[xy] are figure-wide - # settings, we still need the axes-specific _share[xy]_override attr - if not self._panel_side: # this is a main axes - # Top and bottom - bottom = self - paxs = shared(self._bottom_panels) - if paxs: - bottom = paxs[-1] - bottom._sharex_override = False - for iax in (self, *paxs[:-1]): - iax._sharex_override = True - iax._sharex_setup(bottom) # parent is bottom-most - paxs = shared(self._top_panels) - for iax in paxs: - iax._sharex_override = True - iax._sharex_setup(bottom) - # Left and right - # NOTE: Order of panel lists is always inside-to-outside - left = self - paxs = shared(self._left_panels) - if paxs: - left = paxs[-1] - left._sharey_override = False - for iax in (self, *paxs[:-1]): - iax._sharey_override = True - iax._sharey_setup(left) # parent is left-most - paxs = shared(self._right_panels) - for iax in paxs: - iax._sharey_override = True - iax._sharey_setup(left) - - # Main axes, sometimes overrides panel axes sharing - # TODO: This can get very repetitive, but probably minimal impact? - # Share x axes - parent, *children = self._get_extent_axes('x') - for child in children: - child._sharex_setup(parent) - # Share y axes - parent, *children = self._get_extent_axes('y') - for child in children: - child._sharey_setup(parent) - - def _share_short_axis(self, share, side): - """ - Share the "short" axes of panels belonging to this subplot - with panels belonging to an external subplot. - """ - if share is None or self._panel_side: - return # if this is a panel - axis = 'x' if side in ('left', 'right') else 'y' - caxs = getattr(self, '_' + side + '_panels') - paxs = getattr(share, '_' + side + '_panels') - caxs = [pax for pax in caxs if not pax._panel_filled] - paxs = [pax for pax in paxs if not pax._panel_filled] - for cax, pax in zip(caxs, paxs): # may be uneven - getattr(cax, '_share' + axis + '_setup')(pax) - - def _share_long_axis(self, share, side): - """ - Share the "long" axes of panels belonging to this subplot - with panels belonging to an external subplot. - """ - # NOTE: We do not check _panel_share because that only controls - # sharing with main subplot, not other subplots - if share is None or self._panel_side: - return # if this is a panel - axis = 'x' if side in ('top', 'bottom') else 'y' - paxs = getattr(self, '_' + side + '_panels') - paxs = [pax for pax in paxs if not pax._panel_filled] - for pax in paxs: - getattr(pax, '_share' + axis + '_setup')(share) - - def _update_axis_labels(self, x='x', **kwargs): - """ - Apply axis labels to the relevant shared axis. If spanning - labels are toggled this keeps the labels synced for all subplots in - the same row or column. Label positions will be adjusted at draw-time - with figure._align_axislabels. - """ - if x not in 'xy': - return - - # Update label on this axes - axis = getattr(self, x + 'axis') - axis.label.update(kwargs) - kwargs.pop('color', None) - - # Get "shared axes" siblings - # TODO: Share axis locators and formatters just like matplotlib shares - # axis limits and proplot shares axis labels. - ax, *_ = self._get_extent_axes(x) # the leftmost/bottommost - axs = [ax] - if getattr(ax.figure, '_span' + x): - side = axis.get_label_position() - if side in ('left', 'bottom'): - axs = ax._get_side_axes(side) - for ax in axs: - axis = getattr(ax, x + 'axis') - axis.label.update(kwargs) # apply to main axes - - def _update_title_position(self, renderer): - """ - Update the position of proplot inset titles and builtin matplotlib - titles. - """ - # Custom inset titles - width, height = self.get_size_inches() - for loc in ( - 'abc', - 'upper left', 'upper right', 'upper center', - 'lower left', 'lower right', 'lower center', - ): - obj = self._get_title(loc) - if loc == 'abc': - loc = self._abc_loc - if loc in ('left', 'right', 'center'): - continue - if loc in ('upper center', 'lower center'): - x = 0.5 - elif loc in ('upper left', 'lower left'): - pad = rc['axes.titlepad'] / (72 * width) - x = 1.5 * pad - elif loc in ('upper right', 'lower right'): - pad = rc['axes.titlepad'] / (72 * width) - x = 1 - 1.5 * pad - if loc in ('upper left', 'upper right', 'upper center'): - pad = rc['axes.titlepad'] / (72 * height) - y = 1 - 1.5 * pad - elif loc in ('lower left', 'lower right', 'lower center'): - pad = rc['axes.titlepad'] / (72 * height) - y = 1.5 * pad - obj.set_position((x, y)) - - # Push title above tick marks, since builtin algorithm used to offset - # the title seems to ignore them. This is known matplotlib problem but - # especially annoying with top panels. - # TODO: Make sure this is robust. Seems 'default' is returned usually - # when tick sides is actually *both*. - pad = self._title_pad - pos = self.xaxis.get_ticks_position() - fmt = self.xaxis.get_major_formatter() - if self.xaxis.get_visible() and ( - pos == 'default' - or (pos == 'top' and isinstance(fmt, mticker.NullFormatter)) - or ( - pos == 'unknown' - and self._panel_side == 'top' - and isinstance(fmt, mticker.NullFormatter) - ) - ): - pad += self.xaxis.get_tick_padding() - pad_active = self._title_pad_active - if pad_active is None or not np.isclose(pad_active, pad): - # Avoid doing this on every draw in case it is expensive to change - # the title Text transforms every time. - self._title_pad_active = pad - self._set_title_offset_trans(pad) - - # Adjust the title positions with builtin algorithm and match - # the a-b-c text to the relevant position. - super()._update_title_position(renderer) - if self._abc_loc in ('left', 'center', 'right'): - title = self._get_title(self._abc_loc) - self._abc_label.set_position(title.get_position()) - self._abc_label.set_transform( - self.transAxes + self.titleOffsetTrans - ) - - def format( - self, *, title=None, abovetop=None, - figtitle=None, suptitle=None, rowlabels=None, collabels=None, - leftlabels=None, rightlabels=None, toplabels=None, bottomlabels=None, - llabels=None, rlabels=None, tlabels=None, blabels=None, - ltitle=None, ctitle=None, rtitle=None, - ultitle=None, uctitle=None, urtitle=None, - lltitle=None, lctitle=None, lrtitle=None, - ): - """ - Modify the axes title(s), the a-b-c label, row and column labels, and - the figure title. Called by `XYAxes.format`, - `ProjAxes.format`, and `PolarAxes.format`. - - Parameters - ---------- - title : str, optional - The axes title. - abc : bool, optional - Whether to apply "a-b-c" subplot labelling based on the - ``number`` attribute. If ``number`` is >26, the labels will loop - around to a, ..., z, aa, ..., zz, aaa, ..., zzz, ... Default is - :rc:`abc`. - abcstyle : str, optional - String denoting the format of a-b-c labels containing the character - ``a`` or ``A``. ``'a'`` is the default, but e.g. ``'a.'``, - ``'a)'``, or ``'A'`` might also be desirable. Default is - :rc:`abc.style`. - abcloc, titleloc : str, optional - Strings indicating the location for the a-b-c label and - main title. The following locations keys are valid (defaults are - :rc:`abc.loc` and :rc:`title.loc`): - - ======================== ============================ - Location Valid keys - ======================== ============================ - center above axes ``'center'``, ``'c'`` - left above axes ``'left'``, ``'l'`` - right above axes ``'right'``, ``'r'`` - lower center inside axes ``'lower center``', ``'lc'`` - upper center inside axes ``'upper center'``, ``'uc'`` - upper right inside axes ``'upper right'``, ``'ur'`` - upper left inside axes ``'upper left'``, ``'ul'`` - lower left inside axes ``'lower left'``, ``'ll'`` - lower right inside axes ``'lower right'``, ``'lr'`` - ======================== ============================ - - abcborder, titleborder : bool, optional - Whether to draw a white border around titles and a-b-c labels - positioned inside the axes. This can help them stand out on top - of artists plotted inside the axes. Defaults are - :rc:`abc.border` and :rc:`title.border` - abovetop : bool, optional - Whether to try to put the title and a-b-c label above the top panel - (if it exists), or to always put them above the main subplot. - Default is ``True``. - ltitle, ctitle, rtitle, ultitle, uctitle, urtitle, lltitle, lctitle, \ -lrtitle : str, optional - Axes titles in specific positions (see `abcloc`). This lets you - specify multiple title-like labels for a single subplot. - leftlabels, rightlabels, toplabels, bottomlabels : list of str, \ -optional - Labels for the subplots lying along the left, right, top, and - bottom edges of the figure. The length of each list must match - the number of subplots along the corresponding edge. - rowlabels, collabels : list of str, optional - Aliases for `leftlabels`, `toplabels`. - llabels, rlabels, tlabels, blabels : list of str, optional - Aliases for `leftlabels`, `toplabels`, `rightlabels`, - `bottomlabels`. - figtitle, suptitle : str, optional - The figure "super" title, centered between the left edge of - the lefmost column of subplots and the right edge of the rightmost - column of subplots, and automatically offset above figure titles. - This is an improvement on matplotlib's "super" title, which just - centers the text between figure edges. - - Note - ---- - The `abc`, `abcstyle`, `abcloc`, and `titleloc` keyword arguments - are actually :ref:`configuration settings ` - that are temporarily changed by the call to - `~proplot.rctools.rc_configurator.context`. - They are documented here because it is very common to change - them with `~Axes.format`. They also appear in the tables in the - `~proplot.rctools` documention. - - See also - -------- - `~proplot.rctools.rc_configurator.context`, - `XYAxes.format`, - `ProjAxes.format`, - `PolarAxes.format` - """ - # Figure patch (for some reason needs to be re-asserted even if - # declared before figure is drawn) - kw = rc.fill({'facecolor': 'figure.facecolor'}, context=True) - self.figure.patch.update(kw) - if abovetop is not None: - self._above_top_panels = abovetop - pad = rc.get('axes.titlepad', context=True) - if pad is not None: - self._set_title_offset_trans(pad) - self._title_pad = pad - - # Super title - # NOTE: These are actually *figure-wide* settings, but that line - # gets blurred where we have shared axes, spanning labels, and - # whatnot. May result in redundant assignments if formatting more than - # one axes, but operations are fast so some redundancy is nbd. - # NOTE: Below workaround prevents changed *figure-wide* settings - # from getting overwritten when user makes a new axes. - fig = self.figure - suptitle = _notNone( - figtitle, suptitle, None, names=('figtitle', 'suptitle') - ) - if len(fig._axes_main) > 1 and rc._context and rc._context[-1][0] == 1: - kw = {} - else: - kw = rc.fill({ - 'fontsize': 'suptitle.size', - 'weight': 'suptitle.weight', - 'color': 'suptitle.color', - 'fontfamily': 'font.family' - }, context=True) - if suptitle or kw: - fig._update_figtitle(suptitle, **kw) - - # Labels - llabels = _notNone( - rowlabels, leftlabels, llabels, None, - names=('rowlabels', 'leftlabels', 'llabels') - ) - tlabels = _notNone( - collabels, toplabels, tlabels, None, - names=('collabels', 'toplabels', 'tlabels') - ) - rlabels = _notNone( - rightlabels, rlabels, None, - names=('rightlabels', 'rlabels') - ) - blabels = _notNone( - bottomlabels, blabels, None, - names=('bottomlabels', 'blabels') - ) - for side, labels in zip( - ('left', 'right', 'top', 'bottom'), - (llabels, rlabels, tlabels, blabels) - ): - kw = rc.fill({ - 'fontsize': side + 'label.size', - 'weight': side + 'label.weight', - 'color': side + 'label.color', - 'fontfamily': 'font.family' - }, context=True) - if labels or kw: - fig._update_labels(self, side, labels, **kw) - - # Helper function - def sanitize_kw(kw, loc): - kw = kw.copy() - if loc in ('left', 'right', 'center'): - kw.pop('border', None) - kw.pop('borderwidth', None) - return kw - - # A-b-c labels - abc = False - if not self._panel_side: - # Properties - kw = rc.fill({ - 'fontsize': 'abc.size', - 'weight': 'abc.weight', - 'color': 'abc.color', - 'border': 'abc.border', - 'borderwidth': 'abc.borderwidth', - 'fontfamily': 'font.family', - }, context=True) - # Label format - abcstyle = rc.get('abc.style', context=True) # 1st run, or changed - if abcstyle and self.number is not None: - if not isinstance(abcstyle, str) or ( - abcstyle.count('a') != 1 and abcstyle.count('A') != 1): - raise ValueError( - f'Invalid abcstyle {abcstyle!r}. ' - 'Must include letter "a" or "A".' - ) - abcedges = abcstyle.split('a' if 'a' in abcstyle else 'A') - text = abcedges[0] + _abc(self.number - 1) + abcedges[-1] - if 'A' in abcstyle: - text = text.upper() - self._abc_text = text - # Apply text - obj = self._abc_label - abc = rc.get('abc', context=True) - if abc is not None: - obj.set_text(self._abc_text if bool(abc) else '') - # Apply new settings - loc = self._loc_translate(None, 'abc') - loc_prev = self._abc_loc - if loc is None: - loc = loc_prev - kw = sanitize_kw(kw, loc) - if loc_prev is None or loc != loc_prev: - obj_ref = self._get_title(loc) - obj.set_ha(obj_ref.get_ha()) - obj.set_va(obj_ref.get_va()) - obj.set_transform(obj_ref.get_transform()) - obj.set_position(obj_ref.get_position()) - obj.update(kw) - self._abc_loc = loc - - # Titles - # Tricky because we have to reconcile two workflows: - # 1. title='name' and titleloc='position' - # 2. ltitle='name', rtitle='name', etc., arbitrarily many titles - kw = rc.fill({ - 'fontsize': 'title.size', - 'weight': 'title.weight', - 'color': 'title.color', - 'border': 'title.border', - 'borderwidth': 'title.borderwidth', - 'fontfamily': 'font.family', - }, context=True) - # Workflow 2, want this to come first so workflow 1 gets priority - for iloc, ititle in zip( - ('l', 'r', 'c', 'ul', 'uc', 'ur', 'll', 'lc', 'lr'), - ( - ltitle, rtitle, ctitle, - ultitle, uctitle, urtitle, lltitle, lctitle, lrtitle - ), - ): - iloc = self._loc_translate(iloc, 'title') - ikw = sanitize_kw(kw, iloc) - iobj = self._get_title(iloc) - iobj.update(ikw) - if ititle is not None: - iobj.set_text(ititle) - - # Workflow 1, make sure that if user calls ax.format(title='Title') - # *then* ax.format(titleloc='left') it copies over the text. - # Get current and previous location, prevent overwriting abc label - loc = self._loc_translate(None, 'title') - loc_prev = self._title_loc - if loc is None: # never None first run - loc = loc_prev # never None on subsequent runs - kw = sanitize_kw(kw, loc) - obj = self._get_title(loc) - if loc_prev is not None and loc != loc_prev: - obj_prev = self._get_title(loc_prev) - if title is None: - title = obj_prev.get_text() - obj_prev.set_text('') - obj.update(kw) - if title is not None: - obj.set_text(title) - self._title_loc = loc # assigns default loc on first run - - def area(self, *args, **kwargs): - """ - Alias for `~matplotlib.axes.Axes.fill_between`. - """ - # NOTE: *Cannot* assign area = axes.Axes.fill_between because the - # wrapper won't be applied and for some reason it messes up - # automodsumm, which tries to put the matplotlib docstring on website - return self.fill_between(*args, **kwargs) - - def areax(self, *args, **kwargs): - """ - Alias for `~matplotlib.axes.Axes.fill_betweenx`. - """ - return self.fill_betweenx(*args, **kwargs) - - def boxes(self, *args, **kwargs): - """ - Alias for `~matplotlib.axes.Axes.boxplot`. - """ - return self.boxplot(*args, **kwargs) - - def colorbar( - self, *args, loc=None, pad=None, - length=None, width=None, space=None, frame=None, frameon=None, - alpha=None, linewidth=None, edgecolor=None, facecolor=None, - **kwargs - ): - """ - Add an *inset* colorbar or *outer* colorbar along the outside edge of - the axes. See `~proplot.wrappers.colorbar_wrapper` for details. - - Parameters - ---------- - loc : str, optional - The colorbar location. Default is :rc:`colorbar.loc`. The - following location keys are valid: - - ================== ================================== - Location Valid keys - ================== ================================== - outer left ``'left'``, ``'l'`` - outer right ``'right'``, ``'r'`` - outer bottom ``'bottom'``, ``'b'`` - outer top ``'top'``, ``'t'`` - default inset ``'inset'``, ``'i'``, ``0`` - upper right inset ``'upper right'``, ``'ur'``, ``1`` - upper left inset ``'upper left'``, ``'ul'``, ``2`` - lower left inset ``'lower left'``, ``'ll'``, ``3`` - lower right inset ``'lower right'``, ``'lr'``, ``4`` - ================== ================================== - - pad : float or str, optional - The space between the axes edge and the colorbar. For inset - colorbars only. Units are interpreted by `~proplot.utils.units`. - Default is :rc:`colorbar.insetpad`. - length : float or str, optional - The colorbar length. For outer colorbars, units are relative to the - axes width or height. Default is :rc:`colorbar.length`. For inset - colorbars, units are interpreted by `~proplot.utils.units`. Default - is :rc:`colorbar.insetlength`. - width : float or str, optional - The colorbar width. Units are interpreted by - `~proplot.utils.units`. For outer colorbars, default is - :rc:`colorbar.width`. For inset colorbars, default is - :rc:`colorbar.insetwidth`. - space : float or str, optional - For outer colorbars only. The space between the colorbar and the - main axes. Units are interpreted by `~proplot.utils.units`. - When :rcraw:`tight` is ``True``, this is adjusted automatically. - Otherwise, the default is :rc:`subplots.panelpad`. - frame, frameon : bool, optional - For inset colorbars, indicates whether to draw a "frame", just - like `~matplotlib.axes.Axes.legend`. Default is - :rc:`colorbar.frameon`. - alpha, linewidth, edgecolor, facecolor : optional - Transparency, edge width, edge color, and face color for the frame - around the inset colorbar. Default is - :rc:`colorbar.framealpha`, :rc:`axes.linewidth`, - :rc:`axes.edgecolor`, and :rc:`axes.facecolor`, - respectively. - **kwargs - Passed to `~proplot.wrappers.colorbar_wrapper`. - """ - # TODO: add option to pad inset away from axes edge! - # TODO: get "best" colorbar location from legend algorithm. - kwargs.update({'edgecolor': edgecolor, 'linewidth': linewidth}) - if loc != '_fill': - loc = self._loc_translate(loc, 'colorbar') - - # Generate panel - if loc in ('left', 'right', 'top', 'bottom'): - ax = self.panel_axes(loc, width=width, space=space, filled=True) - return ax.colorbar(loc='_fill', *args, length=length, **kwargs) - - # Filled colorbar - if loc == '_fill': - # Hide content and resize panel - # NOTE: Do not run self.clear in case we want title above this - for s in self.spines.values(): - s.set_visible(False) - self.xaxis.set_visible(False) - self.yaxis.set_visible(False) - self.patch.set_alpha(0) - self._panel_filled = True - - # Get subplotspec for colorbar axes - side = self._panel_side - length = _notNone(length, rc['colorbar.length']) - subplotspec = self.get_subplotspec() - if length <= 0 or length > 1: - raise ValueError( - f'Panel colorbar length must satisfy 0 < length <= 1, ' - f'got length={length!r}.' - ) - if side in ('bottom', 'top'): - gridspec = mgridspec.GridSpecFromSubplotSpec( - nrows=1, ncols=3, wspace=0, - subplot_spec=subplotspec, - width_ratios=((1 - length) / 2, length, (1 - length) / 2), - ) - subplotspec = gridspec[1] - else: - gridspec = mgridspec.GridSpecFromSubplotSpec( - nrows=3, ncols=1, hspace=0, - subplot_spec=subplotspec, - height_ratios=((1 - length) / 2, length, (1 - length) / 2), - ) - subplotspec = gridspec[1] - - # Draw colorbar axes - # NOTE: Make this an 'xy' projection so users can do hacky stuff - # like e.g. dualx/dualy axes. - with self.figure._authorize_add_subplot(): - ax = self.figure.add_subplot(subplotspec, projection='xy') - self.add_child_axes(ax) - - # Location - # NOTE: May change loc='_fill' to 'fill' so users can manually - # fill axes but maintain proplot colorbar() features. For now - # this is just used internally by show_cmaps() and show_cycles() - if side is None: # manual - orientation = kwargs.pop('orientation', None) - if orientation == 'vertical': - side = 'left' - else: - side = 'bottom' - if side in ('bottom', 'top'): - outside, inside = 'bottom', 'top' - if side == 'top': - outside, inside = inside, outside - ticklocation = outside - orientation = 'horizontal' - else: - outside, inside = 'left', 'right' - if side == 'right': - outside, inside = inside, outside - ticklocation = outside - orientation = 'vertical' - - # Keyword args and add as child axes - orientation_user = kwargs.get('orientation', None) - if orientation_user and orientation_user != orientation: - _warn_proplot( - f'Overriding input orientation={orientation_user!r}.' - ) - ticklocation = _notNone( - kwargs.pop('ticklocation', None), - kwargs.pop('tickloc', None), - ticklocation, - names=('ticklocation', 'tickloc') - ) - kwargs.update({ - 'orientation': orientation, - 'ticklocation': ticklocation - }) - - # Inset colorbar - else: - # Default props - cbwidth, cblength = width, length - width, height = self.get_size_inches() - extend = units(_notNone( - kwargs.get('extendsize', None), rc['colorbar.insetextend'] - )) - cbwidth = units(_notNone( - cbwidth, rc['colorbar.insetwidth'] - )) / height - cblength = units(_notNone( - cblength, rc['colorbar.insetlength'] - )) / width - pad = units(_notNone(pad, rc['colorbar.insetpad'])) - xpad, ypad = pad / width, pad / height - - # Get location in axes-relative coordinates - # Bounds are x0, y0, width, height in axes-relative coordinates - xspace = rc['xtick.major.size'] / 72 - if kwargs.get('label', ''): - xspace += 2.4 * rc['font.size'] / 72 - else: - xspace += 1.2 * rc['font.size'] / 72 - xspace /= height # space for labels - if loc == 'upper right': - bounds = (1 - xpad - cblength, 1 - ypad - cbwidth) - fbounds = ( - 1 - 2 * xpad - cblength, - 1 - 2 * ypad - cbwidth - xspace - ) - elif loc == 'upper left': - bounds = (xpad, 1 - ypad - cbwidth) - fbounds = (0, 1 - 2 * ypad - cbwidth - xspace) - elif loc == 'lower left': - bounds = (xpad, ypad + xspace) - fbounds = (0, 0) - else: - bounds = (1 - xpad - cblength, ypad + xspace) - fbounds = (1 - 2 * xpad - cblength, 0) - bounds = (bounds[0], bounds[1], cblength, cbwidth) - fbounds = (fbounds[0], fbounds[1], - 2 * xpad + cblength, 2 * ypad + cbwidth + xspace) - - # Make frame - # NOTE: We do not allow shadow effects or fancy edges effect. - # Also keep zorder same as with legend. - frameon = _notNone( - frame, frameon, rc['colorbar.frameon'], - names=('frame', 'frameon')) - if frameon: - xmin, ymin, width, height = fbounds - patch = mpatches.Rectangle( - (xmin, ymin), width, height, - snap=True, zorder=4, transform=self.transAxes) - alpha = _notNone(alpha, rc['colorbar.framealpha']) - linewidth = _notNone(linewidth, rc['axes.linewidth']) - edgecolor = _notNone(edgecolor, rc['axes.edgecolor']) - facecolor = _notNone(facecolor, rc['axes.facecolor']) - patch.update({ - 'alpha': alpha, - 'linewidth': linewidth, - 'edgecolor': edgecolor, - 'facecolor': facecolor}) - self.add_artist(patch) - - # Make axes - locator = self._make_inset_locator(bounds, self.transAxes) - bbox = locator(None, None) - ax = maxes.Axes(self.figure, bbox.bounds, zorder=5) - ax.set_axes_locator(locator) - self.add_child_axes(ax) - - # Default keyword args - orient = kwargs.pop('orientation', None) - if orient is not None and orient != 'horizontal': - _warn_proplot( - f'Orientation for inset colorbars must be horizontal, ' - f'ignoring orient={orient!r}.' - ) - ticklocation = kwargs.pop('tickloc', None) - ticklocation = kwargs.pop('ticklocation', None) or ticklocation - if ticklocation is not None and ticklocation != 'bottom': - _warn_proplot( - f'Inset colorbars can only have ticks on the bottom.' - ) - kwargs.update({'orientation': 'horizontal', - 'ticklocation': 'bottom'}) - kwargs.setdefault('maxn', 5) - kwargs.setdefault('extendsize', extend) - - # Generate colorbar - return colorbar_wrapper(ax, *args, **kwargs) - - def legend(self, *args, loc=None, width=None, space=None, **kwargs): - """ - Add an *inset* legend or *outer* legend along the edge of the axes. - See `~proplot.wrappers.legend_wrapper` for details. - - Parameters - ---------- - loc : int or str, optional - The legend location. The following location keys are valid: - - ================== ======================================= - Location Valid keys - ================== ======================================= - outer left ``'left'``, ``'l'`` - outer right ``'right'``, ``'r'`` - outer bottom ``'bottom'``, ``'b'`` - outer top ``'top'``, ``'t'`` - "best" inset ``'best'``, ``'inset'``, ``'i'``, ``0`` - upper right inset ``'upper right'``, ``'ur'``, ``1`` - upper left inset ``'upper left'``, ``'ul'``, ``2`` - lower left inset ``'lower left'``, ``'ll'``, ``3`` - lower right inset ``'lower right'``, ``'lr'``, ``4`` - center left inset ``'center left'``, ``'cl'``, ``5`` - center right inset ``'center right'``, ``'cr'``, ``6`` - lower center inset ``'lower center'``, ``'lc'``, ``7`` - upper center inset ``'upper center'``, ``'uc'``, ``8`` - center inset ``'center'``, ``'c'``, ``9`` - ================== ======================================= - - width : float or str, optional - For outer legends only. The space allocated for the legend box. - This does nothing if :rcraw:`tight` is ``True``. Units are - interpreted by `~proplot.utils.units`. - space : float or str, optional - For outer legends only. The space between the axes and the legend - box. Units are interpreted by `~proplot.utils.units`. - When :rcraw:`tight` is ``True``, this is adjusted automatically. - Otherwise, the default is :rc:`subplots.panelpad`. - - Other parameters - ---------------- - *args, **kwargs - Passed to `~proplot.wrappers.legend_wrapper`. - """ - if loc != '_fill': - loc = self._loc_translate(loc, 'legend') - if isinstance(loc, np.ndarray): - loc = loc.tolist() - - # Generate panel - if loc in ('left', 'right', 'top', 'bottom'): - ax = self.panel_axes(loc, width=width, space=space, filled=True) - return ax.legend(*args, loc='_fill', **kwargs) - - # Fill - if loc == '_fill': - # Hide content - for s in self.spines.values(): - s.set_visible(False) - self.xaxis.set_visible(False) - self.yaxis.set_visible(False) - self.patch.set_alpha(0) - self._panel_filled = True - # Try to make handles and stuff flush against the axes edge - kwargs.setdefault('borderaxespad', 0) - frameon = _notNone(kwargs.get('frame', None), kwargs.get( - 'frameon', None), rc['legend.frameon']) - if not frameon: - kwargs.setdefault('borderpad', 0) - # Apply legend location - side = self._panel_side - if side == 'bottom': - loc = 'upper center' - elif side == 'right': - loc = 'center left' - elif side == 'left': - loc = 'center right' - elif side == 'top': - loc = 'lower center' - else: - raise ValueError(f'Invalid panel side {side!r}.') - - # Draw legend - return legend_wrapper(self, *args, loc=loc, **kwargs) - - def draw(self, renderer=None, *args, **kwargs): - # Perform extra post-processing steps - self._reassign_title() - super().draw(renderer, *args, **kwargs) - - def get_size_inches(self): - # Return the width and height of the axes in inches. - width, height = self.figure.get_size_inches() - bbox = self.get_position() - width = width * abs(bbox.width) - height = height * abs(bbox.height) - return width, height - - def get_tightbbox(self, renderer, *args, **kwargs): - # Perform extra post-processing steps and cache the bounding - # box as an attribute. - self._reassign_title() - bbox = super().get_tightbbox(renderer, *args, **kwargs) - self._tightbbox = bbox - return bbox - - def heatmap(self, *args, **kwargs): - """ - Pass all arguments to `~matplotlib.axes.Axes.pcolormesh` then apply - settings that are suitable for heatmaps: no gridlines, no minor ticks, - and major ticks at the center of each grid box. - """ - obj = self.pcolormesh(*args, **kwargs) - xlocator, ylocator = None, None - if hasattr(obj, '_coordinates'): - coords = obj._coordinates - coords = (coords[1:, ...] + coords[:-1, ...]) / 2 - coords = (coords[:, 1:, :] + coords[:, :-1, :]) / 2 - xlocator, ylocator = coords[0, :, 0], coords[:, 0, 1] - self.format( - xgrid=False, ygrid=False, xtickminor=False, ytickminor=False, - xlocator=xlocator, ylocator=ylocator, - ) - return obj - - def inset_axes( - self, bounds, *, transform=None, zorder=4, - zoom=True, zoom_kw=None, **kwargs - ): - """ - Return an inset `XYAxes`. This is similar to the builtin - `~matplotlib.axes.Axes.inset_axes` but includes some extra options. - - Parameters - ---------- - bounds : list of float - The bounds for the inset axes, listed as ``(x, y, width, height)``. - transform : {'data', 'axes', 'figure'} or \ -`~matplotlib.transforms.Transform`, optional - The transform used to interpret `bounds`. Can be a - `~matplotlib.transforms.Transform` object or a string representing - the `~matplotlib.axes.Axes.transData`, - `~matplotlib.axes.Axes.transAxes`, - or `~matplotlib.figure.Figure.transFigure` transforms. Default is - ``'axes'``, i.e. `bounds` is in axes-relative coordinates. - zorder : float, optional - The `zorder \ -`__ - of the axes, should be greater than the zorder of - elements in the parent axes. Default is ``4``. - zoom : bool, optional - Whether to draw lines indicating the inset zoom using - `~Axes.indicate_inset_zoom`. The lines will automatically - adjust whenever the parent axes or inset axes limits are changed. - Default is ``True``. - zoom_kw : dict, optional - Passed to `~Axes.indicate_inset_zoom`. - - Other parameters - ---------------- - **kwargs - Passed to `XYAxes`. - """ - # Carbon copy with my custom axes - if not transform: - transform = self.transAxes - else: - transform = _get_transform(self, transform) - label = kwargs.pop('label', 'inset_axes') - # This puts the rectangle into figure-relative coordinates. - locator = self._make_inset_locator(bounds, transform) - bb = locator(None, None) - ax = XYAxes(self.figure, bb.bounds, - zorder=zorder, label=label, **kwargs) - # The following locator lets the axes move if we used data coordinates, - # is called by ax.apply_aspect() - ax.set_axes_locator(locator) - self.add_child_axes(ax) - ax._inset_zoom = zoom - ax._inset_parent = self - # Zoom indicator (NOTE: Requires version >=3.0) - if zoom: - zoom_kw = zoom_kw or {} - ax.indicate_inset_zoom(**zoom_kw) - return ax - - def indicate_inset_zoom( - self, alpha=None, - lw=None, linewidth=None, zorder=3.5, - color=None, edgecolor=None, **kwargs - ): - """ - Draw lines indicating the zoom range of the inset axes. This is similar - to the builtin `~matplotlib.axes.Axes.indicate_inset_zoom` except - lines are *refreshed* at draw-time. This is also called automatically - when ``zoom=True`` is passed to `~Axes.inset_axes`. Note this method - must be called from the *inset* axes and not the parent axes. - - Parameters - ---------- - alpha : float, optional - The transparency of the zoom box fill. - lw, linewidth : float, optional - The width of the zoom lines and box outline in points. - color, edgecolor : color-spec, optional - The color of the zoom lines and box outline. - zorder : float, optional - The `zorder \ -`__ - of the axes, should be greater than the zorder of - elements in the parent axes. Default is ``3.5``. - **kwargs - Passed to `~matplotlib.axes.Axes.indicate_inset`. - """ - # Should be called from the inset axes - parent = self._inset_parent - alpha = alpha or 1.0 - linewidth = _notNone( - lw, linewidth, rc['axes.linewidth'], - names=('lw', 'linewidth')) - edgecolor = _notNone( - color, edgecolor, rc['axes.edgecolor'], - names=('color', 'edgecolor')) - if not parent: - raise ValueError(f'{self} is not an inset axes.') - xlim, ylim = self.get_xlim(), self.get_ylim() - rect = (xlim[0], ylim[0], xlim[1] - xlim[0], ylim[1] - ylim[0]) - - # Call indicate_inset - rectpatch, connects = parent.indicate_inset( - rect, self, linewidth=linewidth, edgecolor=edgecolor, alpha=alpha, - zorder=zorder, **kwargs) - - # Update zoom or adopt properties from old one - if self._inset_zoom_data: - rectpatch_old, connects_old = self._inset_zoom_data - rectpatch.update_from(rectpatch_old) - rectpatch_old.set_visible(False) - for line, line_old in zip(connects, connects_old): - visible = line.get_visible() - line.update_from(line_old) - line.set_linewidth(line_old.get_linewidth()) - line.set_visible(visible) - line_old.set_visible(False) - else: - for line in connects: - line.set_linewidth(linewidth) - line.set_color(edgecolor) - line.set_alpha(alpha) - self._inset_zoom_data = (rectpatch, connects) - return (rectpatch, connects) - - def panel_axes(self, side, **kwargs): - """ - Return a panel axes drawn along the edge of this axes. - - Parameters - ---------- - ax : `~proplot.axes.Axes` - The axes for which we are drawing a panel. - width : float or str or list thereof, optional - The panel width. Units are interpreted by `~proplot.utils.units`. - Default is :rc:`subplots.panelwidth`. - space : float or str or list thereof, optional - Empty space between the main subplot and the panel. - When :rcraw:`tight` is ``True``, this is adjusted automatically. - Otherwise, the default is :rc:`subplots.panelpad`. - share : bool, optional - Whether to enable axis sharing between the *x* and *y* axes of the - main subplot and the panel long axes for each panel in the stack. - Sharing between the panel short axis and other panel short axes - is determined by figure-wide `sharex` and `sharey` settings. - - Returns - ------- - `~proplot.axes.Axes` - The panel axes. - """ - side = self._loc_translate(side, 'panel') - return self.figure._add_axes_panel(self, side, **kwargs) - - @_standardize_1d - @_cmap_changer - def parametric( - self, *args, values=None, - cmap=None, norm=None, interp=0, - scalex=True, scaley=True, - **kwargs - ): - """ - Draw a line whose color changes as a function of the parametric - coordinate ``values`` using the input colormap ``cmap``. - Invoked when you pass the `cmap` keyword argument to - `~matplotlib.axes.Axes.plot`. - - Parameters - ---------- - *args : (y,) or (x,y) - The coordinates. If `x` is not provided, it is inferred from `y`. - cmap : colormap spec, optional - The colormap specifier, passed to `~proplot.styletools.Colormap`. - values : list of float - The parametric values used to map points on the line to colors - in the colormap. - norm : normalizer spec, optional - The normalizer, passed to `~proplot.styletools.Norm`. - interp : int, optional - If greater than ``0``, we interpolate to additional points - between the `values` coordinates. The number corresponds to the - number of additional color levels between the line joints - and the halfway points between line joints. - scalex, scaley : bool, optional - These parameters determine if the view limits are adapted to - the data limits. The values are passed on to - `~matplotlib.axes.Axes.autoscale_view`. - - Other parameters - ---------------- - **kwargs - Valid `~matplotlib.collections.LineCollection` properties. - - Returns - ------- - `~matplotlib.collections.LineCollection` - The parametric line. See `this matplotlib example \ -`__. - """ - # First error check - # WARNING: So far this only works for 1D *x* and *y* coordinates. - # Cannot draw multiple colormap lines at once - if values is None: - raise ValueError('Requires a "values" keyword arg.') - if len(args) not in (1, 2): - raise ValueError(f'Requires 1-2 arguments, got {len(args)}.') - y = np.array(args[-1]).squeeze() - x = np.arange( - y.shape[-1]) if len(args) == 1 else np.array(args[0]).squeeze() - values = np.array(values).squeeze() - if x.ndim != 1 or y.ndim != 1 or values.ndim != 1: - raise ValueError( - f'x ({x.ndim}d), y ({y.ndim}d), and values ({values.ndim}d)' - ' must be 1-dimensional.' - ) - if len(x) != len(y) or len(x) != len(values) or len(y) != len(values): - raise ValueError( - f'{len(x)} xs, {len(y)} ys, but {len(values)} ' - ' colormap values.' - ) - - # Interpolate values to allow for smooth gradations between values - # (bins=False) or color switchover halfway between points (bins=True) - # Then optionally interpolate the corresponding colormap values - if interp > 0: - xorig, yorig, vorig = x, y, values - x, y, values = [], [], [] - for j in range(xorig.shape[0] - 1): - idx = ( - slice(None, -1) if j + 1 < xorig.shape[0] - 1 - else slice(None)) - x.extend(np.linspace( - xorig[j], xorig[j + 1], interp + 2)[idx].flat) - y.extend(np.linspace( - yorig[j], yorig[j + 1], interp + 2)[idx].flat) - values.extend(np.linspace( - vorig[j], vorig[j + 1], interp + 2)[idx].flat) - x, y, values = np.array(x), np.array(y), np.array(values) - coords = [] - levels = edges(values) - for j in range(y.shape[0]): - # Get x/y coordinates and values for points to the 'left' and - # 'right' of each joint - if j == 0: - xleft, yleft = [], [] - else: - xleft = [(x[j - 1] + x[j]) / 2, x[j]] - yleft = [(y[j - 1] + y[j]) / 2, y[j]] - if j + 1 == y.shape[0]: - xright, yright = [], [] - else: - xleft = xleft[:-1] # prevent repetition when joined with right - yleft = yleft[:-1] - xright = [x[j], (x[j + 1] + x[j]) / 2] - yright = [y[j], (y[j + 1] + y[j]) / 2] - pleft = np.stack((xleft, yleft), axis=1) - pright = np.stack((xright, yright), axis=1) - coords.append(np.concatenate((pleft, pright), axis=0)) - - # Create LineCollection and update with values - hs = mcollections.LineCollection( - np.array(coords), cmap=cmap, norm=norm, - linestyles='-', capstyle='butt', joinstyle='miter' - ) - hs.set_array(np.array(values)) - hs.update({ - key: value for key, value in kwargs.items() - if key not in ('color',) - }) - - # Add collection with some custom attributes - self.add_collection(hs) - self.autoscale_view(scalex=scalex, scaley=scaley) - hs.values = values - hs.levels = levels # needed for other functions some - return hs - - def violins(self, *args, **kwargs): - """ - Alias for `~matplotlib.axes.Axes.violinplot`. - """ - return self.violinplot(*args, **kwargs) - - # For consistency with _left_title, _upper_left_title, etc. - _center_title = property(lambda self: self.title) - - # ABC location - abc = property(lambda self: getattr( - self, '_' + self._abc_loc.replace(' ', '_') + '_title' - )) - - #: Alias for `~Axes.panel_axes`. - panel = panel_axes - - #: Alias for `~Axes.inset_axes`. - inset = inset_axes - - @property - def number(self): - """ - The axes number. This controls the order of a-b-c labels and the - order of appearence in the `~proplot.subplots.subplot_grid` returned by - `~proplot.subplots.subplots`. - """ - return self._number - - @number.setter - def number(self, num): - if num is not None and (not isinstance(num, Integral) or num < 1): - raise ValueError(f'Invalid number {num!r}. Must be integer >=1.') - self._number = num - - # Wrapped by special functions - # Also support redirecting to Basemap methods - text = _text_wrapper( - maxes.Axes.text - ) - plot = _plot_wrapper(_standardize_1d(_add_errorbars(_cycle_changer( - maxes.Axes.plot - )))) - scatter = _scatter_wrapper(_standardize_1d(_add_errorbars(_cycle_changer( - maxes.Axes.scatter - )))) - bar = _bar_wrapper(_standardize_1d(_add_errorbars(_cycle_changer( - maxes.Axes.bar - )))) - barh = _barh_wrapper( - maxes.Axes.barh - ) # calls self.bar - hist = _hist_wrapper(_standardize_1d(_cycle_changer( - maxes.Axes.hist - ))) - boxplot = _boxplot_wrapper(_standardize_1d(_cycle_changer( - maxes.Axes.boxplot - ))) - violinplot = _violinplot_wrapper(_standardize_1d(_add_errorbars( - _cycle_changer(maxes.Axes.violinplot) - ))) - fill_between = _fill_between_wrapper(_standardize_1d(_cycle_changer( - maxes.Axes.fill_between - ))) - fill_betweenx = _fill_betweenx_wrapper(_standardize_1d(_cycle_changer( - maxes.Axes.fill_betweenx - ))) - - # Wrapped by cycle wrapper and standardized - pie = _standardize_1d(_cycle_changer( - maxes.Axes.pie - )) - stem = _standardize_1d(_cycle_changer( - maxes.Axes.stem - )) - step = _standardize_1d(_cycle_changer( - maxes.Axes.step - )) - - # Wrapped by cmap wrapper and standardized - # Also support redirecting to Basemap methods - hexbin = _standardize_1d(_cmap_changer( - maxes.Axes.hexbin - )) - contour = _standardize_2d(_cmap_changer( - maxes.Axes.contour - )) - contourf = _standardize_2d(_cmap_changer( - maxes.Axes.contourf - )) - pcolor = _standardize_2d(_cmap_changer( - maxes.Axes.pcolor - )) - pcolormesh = _standardize_2d(_cmap_changer( - maxes.Axes.pcolormesh - )) - quiver = _standardize_2d(_cmap_changer( - maxes.Axes.quiver - )) - streamplot = _standardize_2d(_cmap_changer( - maxes.Axes.streamplot - )) - barbs = _standardize_2d(_cmap_changer( - maxes.Axes.barbs - )) - imshow = _cmap_changer( - maxes.Axes.imshow - ) - - # Wrapped only by cmap wrapper - tripcolor = _cmap_changer( - maxes.Axes.tripcolor - ) - tricontour = _cmap_changer( - maxes.Axes.tricontour - ) - tricontourf = _cmap_changer( - maxes.Axes.tricontourf - ) - hist2d = _cmap_changer( - maxes.Axes.hist2d - ) - spy = _cmap_changer( - maxes.Axes.spy - ) - matshow = _cmap_changer( - maxes.Axes.matshow - ) - - -# TODO: More systematic approach? -_twin_kwargs = ( - 'label', 'locator', 'formatter', 'ticks', 'ticklabels', - 'minorlocator', 'minorticks', 'tickminor', - 'ticklen', 'tickrange', 'tickdir', 'ticklabeldir', 'tickrotation', - 'bounds', 'margin', 'color', 'linewidth', 'grid', 'gridminor', 'gridcolor', - 'locator_kw', 'formatter_kw', 'minorlocator_kw', 'label_kw', -) - -_dual_doc = """ -Return a secondary *%(x)s* axis for denoting equivalent *%(x)s* -coordinates in *alternate units*. - -Parameters ----------- -arg : function, (function, function), or `~matplotlib.scale.ScaleBase` - Used to transform units from the parent axis to the secondary axis. - This can be a `~proplot.axistools.FuncScale` itself or a function, - (function, function) tuple, or `~matplotlib.scale.ScaleBase` instance used - to *generate* a `~proplot.axistools.FuncScale` (see - `~proplot.axistools.FuncScale` for details). -%(args)s : optional - Prepended with ``'%(x)s'`` and passed to `Axes.format`. -""" - -_alt_doc = """ -Return an axes in the same location as this one but whose %(x)s axis is on -the %(x2)s. This is an alias and more intuitive name for -`~XYAxes.twin%(y)s`, which generates two *%(x)s* axes with -a shared ("twin") *%(y)s* axes. - -Parameters ----------- -%(xargs)s : optional - Passed to `Axes.format`. -%(args)s : optional - Prepended with ``'%(x)s'`` and passed to `Axes.format`. - -Note ----- -This function enforces the following settings: - -* Places the old *%(x)s* axis on the %(x1)s and the new *%(x)s* axis - on the %(x2)s. -* Makes the old %(x2)s spine invisible and the new %(x1)s, %(y1)s, - and %(y2)s spines invisible. -* Adjusts the *%(x)s* axis tick, tick label, and axis label positions - according to the visible spine positions. -* Locks the old and new *%(y)s* axis limits and scales, and makes the new - %(y)s axis labels invisible. - -""" - -_twin_doc = """ -Mimics the builtin `~matplotlib.axes.Axes.twin%(y)s` method. - -Parameters ----------- -%(xargs)s : optional - Passed to `Axes.format`. -%(args)s : optional - Prepended with ``'%(x)s'`` and passed to `Axes.format`. - -Note ----- -This function enforces the following settings: - -* Places the old *%(x)s* axis on the %(x1)s and the new *%(x)s* axis - on the %(x2)s. -* Makes the old %(x2)s spine invisible and the new %(x1)s, %(y1)s, - and %(y2)s spines invisible. -* Adjusts the *%(x)s* axis tick, tick label, and axis label positions - according to the visible spine positions. -* Locks the old and new *%(y)s* axis limits and scales, and makes the new - %(y)s axis labels invisible. -""" - - -def _parse_alt(x, kwargs): - """ - Interpret keyword args passed to all "twin axis" methods so they - can be passed to Axes.format. - """ - kw_bad, kw_out = {}, {} - for key, value in kwargs.items(): - if key in _twin_kwargs: - kw_out[x + key] = value - elif key[0] == x and key[1:] in _twin_kwargs: - # NOTE: We permit both e.g. 'locator' and 'xlocator' because - # while is more elegant and consistent with e.g. colorbar() syntax - # but latter is more consistent and easier to use when refactoring. - kw_out[key] = value - elif key in _rc_nodots: - kw_out[key] = value - else: - kw_bad[key] = value - if kw_bad: - raise TypeError(f'Unexpected keyword argument(s): {kw_bad!r}') - return kw_out - - -def _parse_rcloc(x, string): # figures out string location - """ - Convert the *boolean* "left", "right", "top", and "bottom" rc settings - to a location string. Returns ``None`` if settings are unchanged. - """ - if x == 'x': - top = rc.get(f'{string}.top', context=True) - bottom = rc.get(f'{string}.bottom', context=True) - if top is None and bottom is None: - return None - elif top and bottom: - return 'both' - elif top: - return 'top' - elif bottom: - return 'bottom' - else: - return 'neither' - else: - left = rc.get(f'{string}.left', context=True) - right = rc.get(f'{string}.right', context=True) - if left is None and right is None: - return None - elif left and right: - return 'both' - elif left: - return 'left' - elif right: - return 'right' - else: - return 'neither' - - -class XYAxes(Axes): - """ - Axes subclass for ordinary 2D cartesian coordinates. Adds several new - methods and overrides existing ones. - """ - #: The registered projection name. - name = 'xy' - - def __init__(self, *args, **kwargs): - """ - See also - -------- - `~proplot.subplots.subplots`, `Axes` - """ - # Impose default formatter - super().__init__(*args, **kwargs) - formatter = axistools.Formatter('auto') - self.xaxis.set_major_formatter(formatter) - self.yaxis.set_major_formatter(formatter) - self.xaxis.isDefault_majfmt = True - self.yaxis.isDefault_majfmt = True - # Custom attributes - self._datex_rotated = False # whether manual rotation has been applied - self._dualy_arg = None # for scaling units on opposite side of ax - self._dualx_arg = None - self._dualy_cache = None # prevent excess _dualy_overrides calls - self._dualx_cache = None - - def _altx_overrides(self): - """ - Apply alternate *x* axis overrides. - """ - # Unlike matplotlib API, we strong arm user into certain twin axes - # settings... doesn't really make sense to have twin axes without this - if self._altx_child is not None: # altx was called on this axes - self._shared_y_axes.join(self, self._altx_child) - self.spines['top'].set_visible(False) - self.spines['bottom'].set_visible(True) - self.xaxis.tick_bottom() - self.xaxis.set_label_position('bottom') - elif self._altx_parent is not None: # this axes is the result of altx - self.spines['bottom'].set_visible(False) - self.spines['top'].set_visible(True) - self.spines['left'].set_visible(False) - self.spines['right'].set_visible(False) - self.xaxis.tick_top() - self.xaxis.set_label_position('top') - self.yaxis.set_visible(False) - self.patch.set_visible(False) - - def _alty_overrides(self): - """ - Apply alternate *y* axis overrides. - """ - if self._alty_child is not None: - self._shared_x_axes.join(self, self._alty_child) - self.spines['right'].set_visible(False) - self.spines['left'].set_visible(True) - self.yaxis.tick_left() - self.yaxis.set_label_position('left') - elif self._alty_parent is not None: - self.spines['left'].set_visible(False) - self.spines['right'].set_visible(True) - self.spines['top'].set_visible(False) - self.spines['bottom'].set_visible(False) - self.yaxis.tick_right() - self.yaxis.set_label_position('right') - self.xaxis.set_visible(False) - self.patch.set_visible(False) - - def _datex_rotate(self): - """ - Apply default rotation to datetime axis coordinates. - """ - # NOTE: Rotation is done *before* horizontal/vertical alignment, - # cannot change alignment with set_tick_params. Must apply to text - # objects. fig.autofmt_date calls subplots_adjust, so cannot use it. - if ( - not isinstance(self.xaxis.converter, mdates.DateConverter) - or self._datex_rotated - ): - return - rotation = rc['axes.formatter.timerotation'] - kw = {'rotation': rotation} - if rotation not in (0, 90, -90): - kw['ha'] = ('right' if rotation > 0 else 'left') - for label in self.xaxis.get_ticklabels(): - label.update(kw) - self._datex_rotated = True # do not need to apply more than once - - def _dualx_overrides(self): - """ - Lock the child "dual" *x* axis limits to the parent. - """ - # NOTE: We set the scale using private API to bypass application of - # set_default_locators_and_formatters: only_if_default=True is critical - # to prevent overriding user settings! We also bypass autoscale_view - # because we set limits manually, and bypass child.stale = True - # because that is done in call to set_xlim() below. - arg = self._dualx_arg - if arg is None: - return - scale = self.xaxis._scale - olim = self.get_xlim() - if (scale, *olim) == self._dualx_cache: - return - child = self._altx_child - funcscale = axistools.Scale( - 'function', arg, invert=True, parent_scale=scale, - ) - child.xaxis._scale = funcscale - child._update_transScale() - funcscale.set_default_locators_and_formatters( - child.xaxis, only_if_default=True) - nlim = list(map(funcscale.functions[1], np.array(olim))) - if np.sign(np.diff(olim)) != np.sign(np.diff(nlim)): - nlim = nlim[::-1] # if function flips limits, so will set_xlim! - child.set_xlim(nlim, emit=False) - self._dualx_cache = (scale, *olim) - - def _dualy_overrides(self): - """ - Lock the child "dual" *y* axis limits to the parent. - """ - arg = self._dualy_arg - if arg is None: - return - scale = self.yaxis._scale - olim = self.get_ylim() - if (scale, *olim) == self._dualy_cache: - return - child = self._alty_child - funcscale = axistools.Scale( - 'function', arg, invert=True, parent_scale=scale, - ) - child.yaxis._scale = funcscale - child._update_transScale() - funcscale.set_default_locators_and_formatters( - child.yaxis, only_if_default=True) - nlim = list(map(funcscale.functions[1], np.array(olim))) - if np.sign(np.diff(olim)) != np.sign(np.diff(nlim)): - nlim = nlim[::-1] - child.set_ylim(nlim, emit=False) - self._dualy_cache = (scale, *olim) - - def _hide_labels(self): - """ - Enforce the "shared" axis labels and axis tick labels. If this is - not called at drawtime, "shared" labels can be inadvertantly turned - off e.g. when the axis scale is changed. - """ - for x in 'xy': - # "Shared" axis and tick labels - axis = getattr(self, x + 'axis') - share = getattr(self, '_share' + x) - if share is not None: - level = ( - 3 if getattr(self, '_share' + x + '_override') - else getattr(self.figure, '_share' + x) - ) - if level > 0: - axis.label.set_visible(False) - if level > 2: - axis.set_major_formatter(mticker.NullFormatter()) - # Enforce no minor ticks labels. TODO: Document? - axis.set_minor_formatter(mticker.NullFormatter()) - - def _make_twin_axes(self, *args, **kwargs): - """ - Return a twin of this axes. This is used for twinx and twiny and was - copied from matplotlib in case the private API changes. - """ - # Typically, SubplotBase._make_twin_axes is called instead of this. - # There is also an override in axes_grid1/axes_divider.py. - 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) - self.set_adjustable('datalim') - ax2.set_adjustable('datalim') - self._twinned_axes.join(self, ax2) - return ax2 - - def _sharex_setup(self, sharex): - """ - Configure shared axes accounting for panels. The input is the - 'parent' axes, from which this one will draw its properties. - """ - # Share panel across different subplots - super()._sharex_setup(sharex) - # Get sharing level - level = 3 if self._sharex_override else self.figure._sharex - if level not in range(4): - raise ValueError( - 'Invalid sharing level sharex={value!r}. ' - 'Axis sharing level can be 0 (share nothing), ' - '1 (hide axis labels), ' - '2 (share limits and hide axis labels), or ' - '3 (share limits and hide axis and tick labels).' - ) - if sharex in (None, self) or not isinstance(sharex, XYAxes): - return - # Builtin sharing features - if level > 0: - self._sharex = sharex - if level > 1: - self._shared_x_axes.join(self, sharex) - - def _sharey_setup(self, sharey): - """ - Configure shared axes accounting for panels. The input is the - 'parent' axes, from which this one will draw its properties. - """ - # Share panel across different subplots - super()._sharey_setup(sharey) - # Get sharing level - level = 3 if self._sharey_override else self.figure._sharey - if level not in range(4): - raise ValueError( - 'Invalid sharing level sharey={value!r}. ' - 'Axis sharing level can be 0 (share nothing), ' - '1 (hide axis labels), ' - '2 (share limits and hide axis labels), or ' - '3 (share limits and hide axis and tick labels).' - ) - if sharey in (None, self) or not isinstance(sharey, XYAxes): - return - # Builtin features - if level > 0: - self._sharey = sharey - if level > 1: - self._shared_y_axes.join(self, sharey) - - def format( - self, *, - aspect=None, - xloc=None, yloc=None, - xspineloc=None, yspineloc=None, - xtickloc=None, ytickloc=None, fixticks=False, - xlabelloc=None, ylabelloc=None, - xticklabelloc=None, yticklabelloc=None, - xtickdir=None, ytickdir=None, - xgrid=None, ygrid=None, - xgridminor=None, ygridminor=None, - xtickminor=None, ytickminor=None, - xticklabeldir=None, yticklabeldir=None, - xtickrange=None, ytickrange=None, - xreverse=None, yreverse=None, - xlabel=None, ylabel=None, - xlim=None, ylim=None, - xscale=None, yscale=None, - xrotation=None, yrotation=None, - xformatter=None, yformatter=None, - xticklabels=None, yticklabels=None, - xticks=None, xminorticks=None, - xlocator=None, xminorlocator=None, - yticks=None, yminorticks=None, - ylocator=None, yminorlocator=None, - xbounds=None, ybounds=None, - xmargin=None, ymargin=None, - xcolor=None, ycolor=None, - xlinewidth=None, ylinewidth=None, - xgridcolor=None, ygridcolor=None, - xticklen=None, yticklen=None, - xlabel_kw=None, ylabel_kw=None, - xscale_kw=None, yscale_kw=None, - xlocator_kw=None, ylocator_kw=None, - xformatter_kw=None, yformatter_kw=None, - xminorlocator_kw=None, yminorlocator_kw=None, - patch_kw=None, - **kwargs - ): - """ - Modify the *x* and *y* axis labels, tick locations, tick labels, - axis scales, spine settings, and more. Unknown keyword arguments - are passed to `Axes.format` and - `~proplot.rctools.rc_configurator.context`. - - Parameters - ---------- - aspect : {'auto', 'equal'}, optional - The aspect ratio mode. See `~matplotlib.axes.Axes.set_aspect` - for details. - xlabel, ylabel : str, optional - The *x* and *y* axis labels. Applied with - `~matplotlib.axes.Axes.set_xlabel` - and `~matplotlib.axes.Axes.set_ylabel`. - xlabel_kw, ylabel_kw : dict-like, optional - The *x* and *y* axis label settings. Applied with the - `~matplotlib.artist.Artist.update` method on the - `~matplotlib.text.Text` instance. Options include ``'color'``, - ``'size'``, and ``'weight'``. - xlim, ylim : (float or None, float or None), optional - The *x* and *y* axis data limits. Applied with - `~matplotlib.axes.Axes.set_xlim` and - `~matplotlib.axes.Axes.set_ylim`. - xreverse, yreverse : bool, optional - Sets whether the *x* and *y* axis are oriented in the "reverse" - direction. The "normal" direction is increasing to the right for - the *x* axis and to the top for the *y* axis. The "reverse" - direction is increasing to the left for the *x* axis and to the - bottom for the *y* axis. - xscale, yscale : axis scale spec, optional - The *x* and *y* axis scales. Passed to the - `~proplot.axistools.Scale` constructor. For example, - ``xscale='log'`` applies logarithmic scaling, and - ``xscale=('cutoff', 0.5, 2)`` applies a custom - `~proplot.axistools.CutoffScale`. - xscale_kw, yscale_kw : dict-like, optional - The *x* and *y* axis scale settings. Passed to - `~proplot.axistools.Scale`. - xspineloc, yspineloc : {'both', 'bottom', 'top', 'left', 'right', \ -'neither', 'center', 'zero'}, optional - The *x* and *y* axis spine locations. - xloc, yloc : optional - Aliases for `xspineloc`, `yspineloc`. - xtickloc, ytickloc : {'both', 'bottom', 'top', 'left', 'right', \ -'neither'}, optional - Which *x* and *y* axis spines should have major and minor tick - marks. - xtickminor, ytickminor : bool, optional - Whether to draw minor ticks on the *x* and *y* axes. - xtickdir, ytickdir : {'out', 'in', 'inout'} - Direction that major and minor tick marks point for the *x* and - *y* axis. - xgrid, ygrid : bool, optional - Whether to draw major gridlines on the *x* and *y* axis. - xgridminor, ygridminor : bool, optional - Whether to draw minor gridlines for the *x* and *y* axis. - xticklabeldir, yticklabeldir : {'in', 'out'} - Whether to place *x* and *y* axis tick label text inside - or outside the axes. - xlocator, ylocator : locator spec, optional - Used to determine the *x* and *y* axis tick mark positions. Passed - to the `~proplot.axistools.Locator` constructor. - xticks, yticks : optional - Aliases for `xlocator`, `ylocator`. - xlocator_kw, ylocator_kw : dict-like, optional - The *x* and *y* axis locator settings. Passed to - `~proplot.axistools.Locator`. - xminorlocator, yminorlocator : optional - As for `xlocator`, `ylocator`, but for the minor ticks. - xminorticks, yminorticks : optional - Aliases for `xminorlocator`, `yminorlocator`. - xminorlocator_kw, yminorlocator_kw - As for `xlocator_kw`, `ylocator_kw`, but for the minor locator. - xformatter, yformatter : formatter spec, optional - Used to determine the *x* and *y* axis tick label string format. - Passed to the `~proplot.axistools.Formatter` constructor. - Use ``[]`` or ``'null'`` for no ticks. - xticklabels, yticklabels : optional - Aliases for `xformatter`, `yformatter`. - xformatter_kw, yformatter_kw : dict-like, optional - The *x* and *y* axis formatter settings. Passed to - `~proplot.axistools.Formatter`. - xrotation, yrotation : float, optional - The rotation for *x* and *y* axis tick labels. Default is ``0`` - for normal axes, :rc:`axes.formatter.timerotation` for time - *x* axes. - xtickrange, ytickrange : (float, float), optional - The *x* and *y* axis data ranges within which major tick marks - are labelled. For example, the tick range ``(-1,1)`` with - axis range ``(-5,5)`` and a tick interval of 1 will only - label the ticks marks at -1, 0, and 1. - xmargin, ymargin : float, optional - The default margin between plotted content and the *x* and *y* axis - spines. Value is proportional to the width, height of the axes. - Use this if you want whitespace between plotted content - and the spines, but don't want to explicitly set `xlim` or `ylim`. - xbounds, ybounds : (float, float), optional - The *x* and *y* axis data bounds within which to draw the spines. - For example, the axis range ``(0, 4)`` with bounds ``(1, 4)`` - will prevent the spines from meeting at the origin. - xcolor, ycolor : color-spec, optional - Color for the *x* and *y* axis spines, ticks, tick labels, and axis - labels. Default is :rc:`color`. Use e.g. ``ax.format(color='red')`` - to set for both axes. - xlinewidth, ylinewidth : color-spec, optional - Line width for the *x* and *y* axis spines and major ticks. - Default is :rc:`linewidth`. Use e.g. ``ax.format(linewidth=2)`` - to set for both axes. - xgridcolor, ygridcolor : color-spec, optional - Color for the *x* and *y* axis major and minor gridlines. - Default is :rc:`grid.color`. Use e.g. ``ax.format(gridcolor='r')`` - to set for both axes. - xticklen, yticklen : float or str, optional - Tick lengths for the *x* and *y* axis. Units are interpreted by - `~proplot.utils.units`, with "points" as the numeric unit. Default - is :rc:`ticklen`. - - Minor tick lengths are scaled according - to :rc:`ticklenratio`. Use e.g. ``ax.format(ticklen=1)`` to - set for both axes. - fixticks : bool, optional - Whether to always transform the tick locators to a - `~matplotlib.ticker.FixedLocator` instance. Default is ``False``. - If your axis ticks are doing weird things (for example, ticks - drawn outside of the axis spine), try setting this to ``True``. - patch_kw : dict-like, optional - Keyword arguments used to update the background patch object. You - can use this, for example, to set background hatching with - ``patch_kw={'hatch':'xxx'}``. - rc_kw : dict, optional - Dictionary containing `~proplot.rctools.rc` settings applied to - this axes using `~proplot.rctools.rc_configurator.context`. - **kwargs - Passed to `Axes.format` or passed to - `~proplot.rctools.rc_configurator.context` and used to update - axes `~proplot.rctools.rc` settings. For example, - ``axestitlesize=15`` modifies the :rcraw:`axes.titlesize` setting. - - Note - ---- - If you plot something with a `datetime64 \ -`__, - `pandas.Timestamp`, `pandas.DatetimeIndex`, `datetime.date`, - `datetime.time`, or `datetime.datetime` array as the *x* or *y* axis - coordinate, the axis ticks and tick labels will be automatically - formatted as dates. - - See also - -------- - `Axes.format`, `~rctools.rc_configurator.context` - """ - rc_kw, rc_mode, kwargs = _parse_format(**kwargs) - with rc.context(rc_kw, mode=rc_mode): - # Background basics - self.patch.set_clip_on(False) - self.patch.set_zorder(-1) - kw_face = rc.fill({ - 'facecolor': 'axes.facecolor', - 'alpha': 'axes.facealpha' - }, context=True) - patch_kw = patch_kw or {} - kw_face.update(patch_kw) - self.patch.update(kw_face) - - # No mutable default args - xlabel_kw = xlabel_kw or {} - ylabel_kw = ylabel_kw or {} - xscale_kw = xscale_kw or {} - yscale_kw = yscale_kw or {} - xlocator_kw = xlocator_kw or {} - ylocator_kw = ylocator_kw or {} - xformatter_kw = xformatter_kw or {} - yformatter_kw = yformatter_kw or {} - xminorlocator_kw = xminorlocator_kw or {} - yminorlocator_kw = yminorlocator_kw or {} - - # Flexible keyword args, declare defaults - xmargin = _notNone(xmargin, rc.get('axes.xmargin', context=True)) - ymargin = _notNone(ymargin, rc.get('axes.ymargin', context=True)) - xtickdir = _notNone( - xtickdir, rc.get('xtick.direction', context=True) - ) - ytickdir = _notNone( - ytickdir, rc.get('ytick.direction', context=True) - ) - xtickminor = _notNone( - xtickminor, rc.get('xtick.minor.visible', context=True) - ) - ytickminor = _notNone( - ytickminor, rc.get('ytick.minor.visible', context=True) - ) - xformatter = _notNone( - xticklabels, xformatter, None, - names=('xticklabels', 'xformatter') - ) - yformatter = _notNone( - yticklabels, yformatter, None, - names=('yticklabels', 'yformatter') - ) - xlocator = _notNone( - xticks, xlocator, None, - names=('xticks', 'xlocator') - ) - ylocator = _notNone( - yticks, ylocator, None, - names=('yticks', 'ylocator') - ) - xminorlocator = _notNone( - xminorticks, xminorlocator, None, - names=('xminorticks', 'xminorlocator') - ) - yminorlocator = _notNone( - yminorticks, yminorlocator, None, - names=('yminorticks', 'yminorlocator') - ) - - # Grid defaults are more complicated - grid = rc.get('axes.grid', context=True) - which = rc.get('axes.grid.which', context=True) - if which is not None or grid is not None: # if *one* was changed - axis = rc['axes.grid.axis'] # always need this property - if grid is None: - grid = rc['axes.grid'] - elif which is None: - which = rc['axes.grid.which'] - xgrid = _notNone( - xgrid, grid and axis in ('x', 'both') - and which in ('major', 'both') - ) - ygrid = _notNone( - ygrid, grid and axis in ('y', 'both') - and which in ('major', 'both') - ) - xgridminor = _notNone( - xgridminor, grid and axis in ('x', 'both') - and which in ('minor', 'both') - ) - ygridminor = _notNone( - ygridminor, grid and axis in ('y', 'both') - and which in ('minor', 'both') - ) - - # Sensible defaults for spine, tick, tick label, and label locs - # NOTE: Allow tick labels to be present without ticks! User may - # want this sometimes! Same goes for spines! - xspineloc = _notNone( - xloc, xspineloc, None, - names=('xloc', 'xspineloc') - ) - yspineloc = _notNone( - yloc, yspineloc, None, - names=('yloc', 'yspineloc') - ) - xtickloc = _notNone( - xtickloc, xspineloc, _parse_rcloc('x', 'xtick') - ) - ytickloc = _notNone( - ytickloc, yspineloc, _parse_rcloc('y', 'ytick') - ) - xspineloc = _notNone( - xspineloc, _parse_rcloc('x', 'axes.spines') - ) - yspineloc = _notNone( - yspineloc, _parse_rcloc('y', 'axes.spines') - ) - if xtickloc != 'both': - xticklabelloc = _notNone(xticklabelloc, xtickloc) - xlabelloc = _notNone(xlabelloc, xticklabelloc) - if xlabelloc not in (None, 'bottom', 'top'): # e.g. "both" - xlabelloc = 'bottom' - if ytickloc != 'both': - yticklabelloc = _notNone(yticklabelloc, ytickloc) - ylabelloc = _notNone(ylabelloc, yticklabelloc) - if ylabelloc not in (None, 'left', 'right'): - ylabelloc = 'left' - - # Begin loop - for ( - x, axis, - label, color, - linewidth, gridcolor, - ticklen, - margin, bounds, - tickloc, spineloc, - ticklabelloc, labelloc, - grid, gridminor, - tickminor, minorlocator, - lim, reverse, scale, - locator, tickrange, - formatter, tickdir, - ticklabeldir, rotation, - label_kw, scale_kw, - locator_kw, minorlocator_kw, - formatter_kw - ) in zip( - ('x', 'y'), (self.xaxis, self.yaxis), - (xlabel, ylabel), (xcolor, ycolor), - (xlinewidth, ylinewidth), (xgridcolor, ygridcolor), - (xticklen, yticklen), - (xmargin, ymargin), (xbounds, ybounds), - (xtickloc, ytickloc), (xspineloc, yspineloc), - (xticklabelloc, yticklabelloc), (xlabelloc, ylabelloc), - (xgrid, ygrid), (xgridminor, ygridminor), - (xtickminor, ytickminor), (xminorlocator, yminorlocator), - (xlim, ylim), (xreverse, yreverse), (xscale, yscale), - (xlocator, ylocator), (xtickrange, ytickrange), - (xformatter, yformatter), (xtickdir, ytickdir), - (xticklabeldir, yticklabeldir), (xrotation, yrotation), - (xlabel_kw, ylabel_kw), (xscale_kw, yscale_kw), - (xlocator_kw, ylocator_kw), - (xminorlocator_kw, yminorlocator_kw), - (xformatter_kw, yformatter_kw), - ): - # Axis limits - # NOTE: 3.1+ has axis.set_inverted(), below is from source code - if lim is not None: - getattr(self, 'set_' + x + 'lim')(lim) - if reverse is not None: - lo, hi = axis.get_view_interval() - if reverse: - lim = (max(lo, hi), min(lo, hi)) - else: - lim = (min(lo, hi), max(lo, hi)) - axis.set_view_interval(*lim, ignore=True) - # Axis scale - # WARNING: This relies on monkey patch of mscale.scale_factory - # that allows it to accept a custom scale class! - # WARNING: Changing axis scale also changes default locators - # and formatters, so do it first - if scale is not None: - scale = axistools.Scale(scale, **scale_kw) - getattr(self, 'set_' + x + 'scale')(scale) - # Is this a date axis? - # NOTE: Make sure to get this *after* lims set! - # See: https://matplotlib.org/api/units_api.html - # And: https://matplotlib.org/api/dates_api.html - # Also see: https://github.com/matplotlib/matplotlib/blob/master/lib/matplotlib/axis.py # noqa - # The axis_date() method just applies DateConverter - date = isinstance(axis.converter, mdates.DateConverter) - - # Fix spines - kw = rc.fill({ - 'linewidth': 'axes.linewidth', - 'color': 'axes.edgecolor', - }, context=True) - if color is not None: - kw['color'] = color - if linewidth is not None: - kw['linewidth'] = linewidth - sides = ('bottom', 'top') if x == 'x' else ('left', 'right') - spines = [self.spines[side] for side in sides] - for spine, side in zip(spines, sides): - # Line properties - # Override if we're settings spine bounds - # In this case just have spines on edges by default - if bounds is not None and spineloc not in sides: - spineloc = sides[0] - # Eliminate sides - if spineloc == 'neither': - spine.set_visible(False) - elif spineloc == 'both': - spine.set_visible(True) - elif spineloc in sides: # make relevant spine visible - b = True if side == spineloc else False - spine.set_visible(b) - elif spineloc is not None: - # Special spine location, usually 'zero', 'center', - # or tuple with (units, location) where 'units' can - # be 'axes', 'data', or 'outward'. - if side == sides[1]: - spine.set_visible(False) - else: - spine.set_visible(True) - try: - spine.set_position(spineloc) - except ValueError: - raise ValueError( - f'Invalid {x} spine location {spineloc!r}.' - f' Options are ' - + ', '.join(map( - repr, (*sides, 'both', 'neither') - )) + '.' - ) - # Apply spine bounds - if bounds is not None and spine.get_visible(): - spine.set_bounds(*bounds) - spine.update(kw) - # Get available spines, needed for setting tick locations - spines = [ - side for side, spine in zip(sides, spines) - if spine.get_visible() - ] - - # Helper func - def _grid_dict(grid): - return { - 'grid_color': grid + '.color', - 'grid_alpha': grid + '.alpha', - 'grid_linewidth': grid + '.linewidth', - 'grid_linestyle': grid + '.linestyle', - } - - # Tick and grid settings for major and minor ticks separately - # Override is just a "new default", but user can override this - for which, igrid in zip(('major', 'minor'), (grid, gridminor)): - # Tick properties - kw_ticks = rc.category(x + 'tick.' + which, context=True) - if kw_ticks is None: - kw_ticks = {} - else: - kw_ticks.pop('visible', None) # invalid setting - if ticklen is not None: - kw_ticks['size'] = units(ticklen, 'pt') - if which == 'minor': - kw_ticks['size'] *= rc['ticklenratio'] - # Grid style and toggling - if igrid is not None: - axis.grid(igrid, which=which) - if which == 'major': - kw_grid = rc.fill( - _grid_dict('grid'), context=True - ) - else: - kw_grid = rc.fill( - _grid_dict('gridminor'), context=True - ) - # Changed rc settings - if gridcolor is not None: - kw['grid_color'] = gridcolor - axis.set_tick_params(which=which, **kw_grid, **kw_ticks) - - # Tick and ticklabel properties that apply to major and minor - # * Weird issue causes set_tick_params to reset/forget grid - # is turned on if you access tick.gridOn directly, instead of - # passing through tick_params. Since gridOn is undocumented - # feature, don't use it. So calling _format_axes() a second - # time will remove the lines. - # * Can specify whether the left/right/bottom/top spines get - # ticks; sides will be group of left/right or top/bottom. - # * Includes option to draw spines but not draw ticks on that - # spine, e.g. on the left/right edges - kw = {} - loc2sides = { - None: None, - 'both': sides, - 'none': (), - 'neither': (), - } - if bounds is not None and tickloc not in sides: - tickloc = sides[0] # override to just one side - ticklocs = loc2sides.get(tickloc, (tickloc,)) - if ticklocs is not None: - kw.update({side: side in ticklocs for side in sides}) - kw.update({ # override - side: False for side in sides if side not in spines - }) - # Tick label sides - # Will override to make sure only appear where ticks are - ticklabellocs = loc2sides.get(ticklabelloc, (ticklabelloc,)) - if ticklabellocs is not None: - kw.update({ - 'label' + side: (side in ticklabellocs) - for side in sides - }) - kw.update({ # override - 'label' + side: False for side in sides - if side not in spines - or (ticklocs is not None and side not in ticklocs) - }) # override - # The axis label side - if labelloc is None: - if ticklocs is not None: - options = [ - side for side in sides - if side in ticklocs and side in spines - ] - if len(options) == 1: - labelloc = options[0] - elif labelloc not in sides: - raise ValueError( - f'Got labelloc {labelloc!r}, valid options are ' - + ', '.join(map(repr, sides)) + '.' - ) - # Apply - axis.set_tick_params(which='both', **kw) - if labelloc is not None: - axis.set_label_position(labelloc) - - # Tick label settings - # First color and size - kw = rc.fill({ - 'labelcolor': 'tick.labelcolor', # new props - 'labelsize': 'tick.labelsize', - 'color': x + 'tick.color', - }, context=True) - if color: - kw['color'] = color - kw['labelcolor'] = color - # Tick direction and rotation - if tickdir == 'in': - kw['pad'] = 1 # ticklabels should be much closer - if ticklabeldir == 'in': # put tick labels inside the plot - tickdir = 'in' - kw['pad'] = -1 * sum( - rc[f'{x}tick.{key}'] - for key in ('major.size', 'major.pad', 'labelsize') - ) - if tickdir is not None: - kw['direction'] = tickdir - axis.set_tick_params(which='both', **kw) - - # Settings that can't be controlled by set_tick_params - # Also set rotation and alignment here - kw = rc.fill({ - 'fontfamily': 'font.family', - 'weight': 'tick.labelweight' - }, context=True) - if rotation is not None: - kw = {'rotation': rotation} - if x == 'x': - self._datex_rotated = True - if rotation not in (0, 90, -90): - kw['ha'] = ('right' if rotation > 0 else 'left') - for t in axis.get_ticklabels(): - t.update(kw) - # Margins - if margin is not None: - self.margins(**{x: margin}) - - # Axis label updates - # NOTE: This has to come after set_label_position, or ha or va - # overrides in label_kw are overwritten - kw = rc.fill({ - 'color': 'axes.edgecolor', - 'weight': 'axes.labelweight', - 'fontsize': 'axes.labelsize', - 'fontfamily': 'font.family', - }, context=True) - if label is not None: - kw['text'] = label - if color: - kw['color'] = color - kw.update(label_kw) - if kw: # NOTE: initially keep spanning labels off - self._update_axis_labels(x, **kw) - - # Major and minor locator - # NOTE: Parts of API (dualxy) rely on minor tick toggling - # preserving the isDefault_minloc setting. In future should - # override the default matplotlib API minorticks_on! - # NOTE: Unlike matplotlib API when "turning on" minor ticks - # we *always* use the scale default, thanks to scale classes - # refactoring with _ScaleBase. See Axes.minorticks_on. - if locator is not None: - locator = axistools.Locator(locator, **locator_kw) - axis.set_major_locator(locator) - if isinstance(locator, mticker.IndexLocator): - tickminor = False # 'index' minor ticks make no sense - if tickminor or minorlocator: - isdefault = minorlocator is None - if isdefault: - minorlocator = getattr( - axis._scale, '_default_minor_locator', None - ) - if not minorlocator: - minorlocator = axistools.Locator('minor') - else: - minorlocator = axistools.Locator( - minorlocator, **minorlocator_kw - ) - axis.set_minor_locator(minorlocator) - axis.isDefault_minloc = isdefault - elif tickminor is not None and not tickminor: - # NOTE: Generally if you *enable* minor ticks on a dual - # axis, want to allow FuncScale updates to change the - # minor tick locators. If you *disable* minor ticks, do - # not want FuncScale applications to turn them on. So we - # allow below to set isDefault_minloc to False. - axis.set_minor_locator(axistools.Locator('null')) - - # Major formatter - # NOTE: The only reliable way to disable ticks labels and then - # restore them is by messing with the *formatter*, rather than - # setting labelleft=False, labelright=False, etc. - if (formatter is not None or tickrange is not None) and not ( - isinstance( - axis.get_major_formatter(), mticker.NullFormatter - ) and getattr(self, '_share' + x) - ): - # Tick range - if tickrange is not None: - if formatter not in (None, 'auto'): - _warn_proplot( - 'The tickrange feature requires ' - 'proplot.AutoFormatter formatter. Overriding ' - 'input formatter.' - ) - formatter = 'auto' - formatter_kw.setdefault('tickrange', tickrange) - # Set the formatter - # Note some formatters require 'locator' as keyword arg - if formatter in ('date', 'concise'): - locator = axis.get_major_locator() - formatter_kw.setdefault('locator', locator) - formatter = axistools.Formatter( - formatter, date=date, **formatter_kw - ) - axis.set_major_formatter(formatter) - - # Ensure no out-of-bounds ticks; set_smart_bounds() can fail - # * Using set_bounds did not work, so instead just turn - # locators into fixed version. - # * Most locators take no arguments in __call__, and some do - # not have tick_values method, so we just call them. - if ( - bounds is not None - or fixticks - or isinstance(formatter, mticker.FixedFormatter) - or axis.get_scale() == 'cutoff' - ): - if bounds is None: - bounds = getattr(self, 'get_' + x + 'lim')() - locator = axistools.Locator([ - x for x in axis.get_major_locator()() - if bounds[0] <= x <= bounds[1] - ]) - axis.set_major_locator(locator) - locator = axistools.Locator([ - x for x in axis.get_minor_locator()() - if bounds[0] <= x <= bounds[1] - ]) - axis.set_minor_locator(locator) - - # Call parent - if aspect is not None: - self.set_aspect(aspect) - super().format(**kwargs) - - def altx(self, **kwargs): - # Docstring is programatically assigned below - # Cannot wrap twiny() because we want to use XYAxes, not - # matplotlib Axes. Instead use hidden method _make_twin_axes. - # See https://github.com/matplotlib/matplotlib/blob/master/lib/matplotlib/axes/_subplots.py # noqa - if self._altx_child or self._altx_parent: - raise RuntimeError('No more than *two* twin axes are allowed.') - with self.figure._authorize_add_subplot(): - ax = self._make_twin_axes(sharey=self, projection='xy') - ax.set_autoscaley_on(self.get_autoscaley_on()) - ax.grid(False) - self._altx_child = ax - ax._altx_parent = self - self._altx_overrides() - ax._altx_overrides() - self.add_child_axes(ax) # to facilitate tight layout - self.figure._axstack.remove(ax) # or gets drawn twice! - ax.format(**_parse_alt('x', kwargs)) - return ax - - def alty(self, **kwargs): - # Docstring is programatically assigned below - if self._alty_child or self._alty_parent: - raise RuntimeError('No more than *two* twin axes are allowed.') - with self.figure._authorize_add_subplot(): - ax = self._make_twin_axes(sharex=self, projection='xy') - ax.set_autoscalex_on(self.get_autoscalex_on()) - ax.grid(False) - self._alty_child = ax - ax._alty_parent = self - self._alty_overrides() - ax._alty_overrides() - self.add_child_axes(ax) # to facilitate tight layout - self.figure._axstack.remove(ax) # or gets drawn twice! - ax.format(**_parse_alt('y', kwargs)) - return ax - - def dualx(self, arg, **kwargs): - # Docstring is programatically assigned below - # NOTE: Matplotlib 3.1 has a 'secondary axis' feature. For the time - # being, our version is more robust (see FuncScale) and simpler, since - # we do not create an entirely separate _SecondaryAxis class. - ax = self.altx(**kwargs) - self._dualx_arg = arg - self._dualx_overrides() - return ax - - def dualy(self, arg, **kwargs): - # Docstring is programatically assigned below - ax = self.alty(**kwargs) - self._dualy_arg = arg - self._dualy_overrides() - return ax - - def draw(self, renderer=None, *args, **kwargs): - # Perform extra post-processing steps - # NOTE: This mimics matplotlib API, which calls identical - # post-processing steps in both draw() and get_tightbbox() - self._hide_labels() - self._altx_overrides() - self._alty_overrides() - self._dualx_overrides() - self._dualy_overrides() - self._datex_rotate() - if self._inset_parent is not None and self._inset_zoom: - self.indicate_inset_zoom() - super().draw(renderer, *args, **kwargs) - - def get_tightbbox(self, renderer, *args, **kwargs): - # Perform extra post-processing steps - self._hide_labels() - self._altx_overrides() - self._alty_overrides() - self._dualx_overrides() - self._dualy_overrides() - self._datex_rotate() - if self._inset_parent is not None and self._inset_zoom: - self.indicate_inset_zoom() - return super().get_tightbbox(renderer, *args, **kwargs) - - def twinx(self): - # Docstring is programatically assigned below - return self.alty() - - def twiny(self): - # Docstring is programatically assigned below - return self.altx() - - # Add documentation - # NOTE: Why does this work without using method.__func__.__doc__? - altx.__doc__ = _alt_doc % { - 'x': 'x', 'x1': 'bottom', 'x2': 'top', - 'y': 'y', 'y1': 'left', 'y2': 'right', - 'args': ', '.join(_twin_kwargs), - 'xargs': ', '.join('x' + key for key in _twin_kwargs), - } - alty.__doc__ = _alt_doc % { - 'x': 'y', 'x1': 'left', 'x2': 'right', - 'y': 'x', 'y1': 'bottom', 'y2': 'top', - 'args': ', '.join(_twin_kwargs), - 'xargs': ', '.join('y' + key for key in _twin_kwargs), - } - twinx.__doc__ = _twin_doc % { - 'x': 'y', 'x1': 'left', 'x2': 'right', - 'y': 'x', 'y1': 'bottom', 'y2': 'top', - 'args': ', '.join(_twin_kwargs), - 'xargs': ', '.join('y' + key for key in _twin_kwargs), - } - twiny.__doc__ = _twin_doc % { - 'x': 'x', 'x1': 'bottom', 'x2': 'top', - 'y': 'y', 'y1': 'left', 'y2': 'right', - 'args': ', '.join(_twin_kwargs), - 'xargs': ', '.join('x' + key for key in _twin_kwargs), - } - dualx.__doc__ = _dual_doc % { - 'x': 'x', - 'args': ', '.join(_twin_kwargs), - 'xargs': ', '.join('x' + key for key in _twin_kwargs), - } - dualy.__doc__ = _dual_doc % { - 'x': 'y', - 'args': ', '.join(_twin_kwargs), - 'xargs': ', '.join('y' + key for key in _twin_kwargs), - } - - -class PolarAxes(Axes, mproj.PolarAxes): - """ - Intermediate class, mixes `ProjAxes` with - `~matplotlib.projections.polar.PolarAxes`. - """ - #: The registered projection name. - name = 'polar' - - def __init__(self, *args, **kwargs): - """ - See also - -------- - `~proplot.subplots.subplots`, `Axes` - """ - # Set tick length to zero so azimuthal labels are not too offset - # Change default radial axis formatter but keep default theta one - super().__init__(*args, **kwargs) - formatter = axistools.Formatter('auto') - self.yaxis.set_major_formatter(formatter) - self.yaxis.isDefault_majfmt = True - for axis in (self.xaxis, self.yaxis): - axis.set_tick_params(which='both', size=0) - - def format( - self, *args, - r0=None, theta0=None, thetadir=None, - thetamin=None, thetamax=None, thetalim=None, - rmin=None, rmax=None, rlim=None, - rlabelpos=None, rscale=None, rborder=None, - thetalocator=None, rlocator=None, thetalines=None, rlines=None, - thetaformatter=None, rformatter=None, - thetalabels=None, rlabels=None, - thetalocator_kw=None, rlocator_kw=None, - thetaformatter_kw=None, rformatter_kw=None, - **kwargs - ): - """ - Modify radial gridline locations, gridline labels, limits, and more. - Unknown keyword arguments are passed to `Axes.format` and - `~rctools.rc_configurator.context`. All ``theta`` arguments are - specified in *degrees*, not radians. The below parameters are specific - to `PolarAxes`. - - Parameters - ---------- - r0 : float, optional - The radial origin. - theta0 : {'N', 'NW', 'W', 'SW', 'S', 'SE', 'E', 'NE'} - The zero azimuth location. - thetadir : {-1, 1, 'clockwise', 'anticlockwise', 'counterclockwise'}, \ -optional - The positive azimuth direction. Clockwise corresponds to ``-1`` - and anticlockwise corresponds to ``-1``. Default is ``-1``. - thetamin, thetamax : float, optional - The lower and upper azimuthal bounds in degrees. If - ``thetamax != thetamin + 360``, this produces a sector plot. - thetalim : (float, float), optional - Specifies `thetamin` and `thetamax` at once. - rmin, rmax : float, optional - The inner and outer radial limits. If ``r0 != rmin``, this - produces an annular plot. - rlim : (float, float), optional - Specifies `rmin` and `rmax` at once. - rborder : bool, optional - Toggles the polar axes border on and off. Visibility of the "inner" - radial spine and "start" and "end" azimuthal spines is controlled - automatically be matplotlib. - thetalocator, rlocator : float or list of float, optional - Used to determine the azimuthal and radial gridline positions. - Passed to the `~proplot.axistools.Locator` constructor. - thetalines, rlines - Aliases for `thetalocator`, `rlocator`. - thetalocator_kw, rlocator_kw : dict-like, optional - The azimuthal and radial locator settings. Passed to - `~proplot.axistools.Locator`. - rlabelpos : float, optional - The azimuth at which radial coordinates are labeled. - thetaformatter, rformatter : formatter spec, optional - Used to determine the azimuthal and radial label format. - Passed to the `~proplot.axistools.Formatter` constructor. - Use ``[]`` or ``'null'`` for no ticks. - thetalabels, rlabels : optional - Aliases for `thetaformatter`, `rformatter`. - thetaformatter_kw, rformatter_kw : dict-like, optional - The azimuthal and radial label formatter settings. Passed to - `~proplot.axistools.Formatter`. - rc_kw : dict, optional - Dictionary containing `~proplot.rctools.rc` settings applied to - this axes using `~proplot.rctools.rc_configurator.context`. - **kwargs - Passed to `Axes.format` or passed to - `~proplot.rctools.rc_configurator.context` and used to update the - axes `~proplot.rctools.rc` settings. For example, - ``axestitlesize=15`` modifies the :rcraw:`axes.titlesize` setting. - - See also - -------- - `Axes.format`, - `~proplot.rctools.rc_configurator.context` - """ - rc_kw, rc_mode, kwargs = _parse_format(**kwargs) - with rc.context(rc_kw, mode=rc_mode): - # Not mutable default args - thetalocator_kw = thetalocator_kw or {} - thetaformatter_kw = thetaformatter_kw or {} - rlocator_kw = rlocator_kw or {} - rformatter_kw = rformatter_kw or {} - # Flexible input - if rlim is not None: - if rmin is not None or rmax is not None: - _warn_proplot( - f'Conflicting keyword args rmin={rmin}, rmax={rmax}, ' - f'and rlim={rlim}. Using "rlim".' - ) - rmin, rmax = rlim - if thetalim is not None: - if thetamin is not None or thetamax is not None: - _warn_proplot( - f'Conflicting keyword args thetamin={thetamin}, ' - f'thetamax={thetamax}, and thetalim={thetalim}. ' - f'Using "thetalim".' - ) - thetamin, thetamax = thetalim - thetalocator = _notNone( - thetalines, thetalocator, None, - names=('thetalines', 'thetalocator')) - thetaformatter = _notNone( - thetalabels, thetaformatter, None, - names=('thetalabels', 'thetaformatter')) - rlocator = _notNone(rlines, rlocator, None, - names=('rlines', 'rlocator')) - rformatter = _notNone(rlabels, rformatter, - None, names=('rlabels', 'rformatter')) - - # Special radius settings - if r0 is not None: - self.set_rorigin(r0) - if rlabelpos is not None: - self.set_rlabel_position(rlabelpos) - if rscale is not None: - self.set_rscale(rscale) - if rborder is not None: - self.spines['polar'].set_visible(bool(rborder)) - # Special azimuth settings - if theta0 is not None: - self.set_theta_zero_location(theta0) - if thetadir is not None: - self.set_theta_direction(thetadir) - - # Iterate - for ( - x, r, axis, - min_, max_, - locator, formatter, - locator_kw, formatter_kw, - ) in zip( - ('x', 'y'), ('theta', 'r'), (self.xaxis, self.yaxis), - (thetamin, rmin), (thetamax, rmax), - (thetalocator, rlocator), (thetaformatter, rformatter), - (thetalocator_kw, rlocator_kw), - (thetaformatter_kw, rformatter_kw) - ): - # Axis limits - # Try to use public API where possible - if min_ is not None: - getattr(self, 'set_' + r + 'min')(min_) - else: - min_ = getattr(self, 'get_' + r + 'min')() - if max_ is not None: - getattr(self, 'set_' + r + 'max')(max_) - else: - max_ = getattr(self, 'get_' + r + 'max')() - - # Spine settings - kw = rc.fill({ - 'linewidth': 'axes.linewidth', - 'color': 'axes.edgecolor', - }, context=True) - sides = ('inner', 'polar') if r == 'r' else ('start', 'end') - spines = [self.spines[side] for side in sides] - for spine, side in zip(spines, sides): - spine.update(kw) - - # Grid and grid label settings - # NOTE: Not sure if polar lines inherit tick or grid props - kw = rc.fill({ - 'color': x + 'tick.color', - 'labelcolor': 'tick.labelcolor', # new props - 'labelsize': 'tick.labelsize', - 'grid_color': 'grid.color', - 'grid_alpha': 'grid.alpha', - 'grid_linewidth': 'grid.linewidth', - 'grid_linestyle': 'grid.linestyle', - }, context=True) - axis.set_tick_params(which='both', **kw) - # Label settings that can't be controlled with set_tick_params - kw = rc.fill({ - 'fontfamily': 'font.family', - 'weight': 'tick.labelweight' - }, context=True) - for t in axis.get_ticklabels(): - t.update(kw) - - # Tick locator, which in this case applies to gridlines - # NOTE: Must convert theta locator input to radians, then back - # to degrees. - if locator is not None: - if r == 'theta' and ( - not isinstance(locator, (str, mticker.Locator))): - # real axis limts are rad - locator = np.deg2rad(locator) - locator = axistools.Locator(locator, **locator_kw) - locator.set_axis(axis) # this is what set_locator does - grids = np.array(locator()) - if r == 'r': - grids = grids[(grids >= min_) & (grids <= max_)] - self.set_rgrids(grids) - else: - grids = np.rad2deg(grids) - grids = grids[(grids >= min_) & (grids <= max_)] - if grids[-1] == min_ + 360: # exclusive if 360 degrees - grids = grids[:-1] - self.set_thetagrids(grids) - # Tick formatter and toggling - if formatter is not None: - formatter = axistools.Formatter(formatter, **formatter_kw) - axis.set_major_formatter(formatter) - - # Parent method - super().format(*args, **kwargs) - - # Disabled methods suitable only for cartesian axes - _disable = _disable_decorator( - 'Invalid plotting method {!r} for polar axes.' - ) - twinx = _disable(Axes.twinx) - twiny = _disable(Axes.twiny) - matshow = _disable(Axes.matshow) - imshow = _disable(Axes.imshow) - spy = _disable(Axes.spy) - hist = _disable(Axes.hist) - hist2d = _disable(Axes.hist2d) - boxplot = _disable(Axes.boxplot) - violinplot = _disable(Axes.violinplot) - step = _disable(Axes.step) - stem = _disable(Axes.stem) - stackplot = _disable(Axes.stackplot) - table = _disable(Axes.table) - eventplot = _disable(Axes.eventplot) - pie = _disable(Axes.pie) - xcorr = _disable(Axes.xcorr) - acorr = _disable(Axes.acorr) - psd = _disable(Axes.psd) - csd = _disable(Axes.csd) - cohere = _disable(Axes.cohere) - specgram = _disable(Axes.specgram) - angle_spectrum = _disable(Axes.angle_spectrum) - phase_spectrum = _disable(Axes.phase_spectrum) - magnitude_spectrum = _disable(Axes.magnitude_spectrum) - - -def _circle_path(N=100): - """ - Return a circle `~matplotlib.path.Path` used as the outline - for polar stereographic, azimuthal equidistant, and Lambert - conformal projections. This was developed from `this cartopy example \ -`__. - """ # noqa - theta = np.linspace(0, 2 * np.pi, N) - center, radius = [0.5, 0.5], 0.5 - verts = np.vstack([np.sin(theta), np.cos(theta)]).T - return mpath.Path(verts * radius + center) - - -class ProjAxes(Axes): - """ - Intermediate class shared by `GeoAxes` and `BasemapAxes`. Disables - methods that are inappropriate for map projections and adds - `ProjAxes.format`, so that arguments passed to `Axes.format` are identical - for the cartopy and basemap backends. - """ - def __init__(self, *args, **kwargs): - """ - See also - -------- - `~proplot.subplots.subplots`, `Axes`, `GeoAxes`, `BasemapAxes` - """ - # Store props that let us dynamically and incrementally modify - # line locations and settings like with Cartesian axes - self._boundinglat = None - self._latmax = None - self._latlines = None - self._lonlines = None - self._lonlines_values = None - self._latlines_values = None - self._lonlines_labels = None - self._latlines_labels = None - super().__init__(*args, **kwargs) - - def format( - self, *, - lonlim=None, latlim=None, boundinglat=None, grid=None, - lonlines=None, lonlocator=None, - latlines=None, latlocator=None, latmax=None, - labels=None, latlabels=None, lonlabels=None, - patch_kw=None, **kwargs, - ): - """ - Modify the meridian and parallel labels, longitude and latitude map - limits, geographic features, and more. Unknown keyword arguments are - passed to `Axes.format` and - `~proplot.rctools.rc_configurator.context`. - - Parameters - ---------- - lonlim, latlim : (float, float), optional - Longitude and latitude limits of projection, applied - with `~cartopy.mpl.geoaxes.GeoAxes.set_extent`. - For cartopy axes only. - boundinglat : float, optional - The edge latitude for the circle bounding North Pole and - South Pole-centered projections. For cartopy axes only. - grid : bool, optional - Toggles meridian and parallel gridlines on and off. Default is - :rc:`geogrid`. - lonlines, latlines : float or list of float, optional - If float, indicates the *spacing* of meridian and parallel - gridlines. Otherwise, must be a list of floats indicating specific - meridian and parallel gridlines to draw. - lonlocator, latlocator : optional - Aliases for `lonlines`, `latlines`. - latmax : float, optional - The maximum absolute latitude for meridian gridlines. Default is - :rc:`geogrid.latmax`. - labels : bool, optional - Toggles meridian and parallel gridline labels on and off. Default - is :rc:`geogrid.labels`. - lonlabels, latlabels - Whether to label longitudes and latitudes, and on which sides - of the map. There are four different options: - - 1. Boolean ``True``. Indicates left side for latitudes, - bottom for longitudes. - 2. A string, e.g. ``'lr'`` or ``'bt'``. - 3. A boolean ``(left,right)`` tuple for longitudes, - ``(bottom,top)`` for latitudes. - 4. A boolean ``(left,right,bottom,top)`` tuple as in the - `~mpl_toolkits.basemap.Basemap.drawmeridians` and - `~mpl_toolkits.basemap.Basemap.drawparallels` methods. - - land, ocean, coast, rivers, lakes, borders, innerborders : bool, \ -optional - Toggles various geographic features. These are actually the - :rcraw:`land`, :rcraw:`ocean`, :rcraw:`coast`, :rcraw:`rivers`, - :rcraw:`lakes`, :rcraw:`borders`, and :rcraw:`innerborders` - settings passed to `~proplot.rctools.rc_configurator.context`. - The style can be modified by passing additional settings, e.g. - :rcraw:`landcolor`. - patch_kw : dict-like, optional - Keyword arguments used to update the background patch object. You - can use this, for example, to set background hatching with - ``patch_kw={'hatch':'xxx'}``. - rc_kw : dict, optional - Dictionary containing `~proplot.rctools.rc` settings applied to - this axes using `~proplot.rctools.rc_configurator.context`. - **kwargs - Passed to `Axes.format` or passed to - `~proplot.rctools.rc_configurator.context` and used to update - axes `~proplot.rctools.rc` settings. For example, - ``axestitlesize=15`` modifies the :rcraw:`axes.titlesize` setting. - - See also - -------- - :py:obj:`Axes.format`, `~proplot.rctools.rc_configurator.context` - """ - rc_kw, rc_mode, kwargs = _parse_format(**kwargs) - with rc.context(rc_kw, mode=rc_mode): - # Parse alternative keyword args - # TODO: Why isn't default latmax 80 respected sometimes? - lonlines = _notNone( - lonlines, lonlocator, rc.get('geogrid.lonstep', context=True), - names=('lonlines', 'lonlocator') - ) - latlines = _notNone( - latlines, latlocator, rc.get('geogrid.latstep', context=True), - names=('latlines', 'latlocator') - ) - latmax = _notNone(latmax, rc.get('geogrid.latmax', context=True)) - labels = _notNone(labels, rc.get('geogrid.labels', context=True)) - grid = _notNone(grid, rc.get('geogrid', context=True)) - if labels: - lonlabels = _notNone(lonlabels, 1) - latlabels = _notNone(latlabels, 1) - - # Longitude gridlines, draw relative to projection prime meridian - # NOTE: Always generate gridlines array on first format call - # because rc setting will be not None - if isinstance(self, GeoAxes): - lon_0 = self.projection.proj4_params.get('lon_0', 0) - else: - base = 5 - lon_0 = base * round( - self.projection.lonmin / base - ) + 180 # central longitude - if lonlines is not None: - if not np.iterable(lonlines): - lonlines = arange(lon_0 - 180, lon_0 + 180, lonlines) - lonlines = lonlines.astype(np.float64) - if lonlines[-1] % 360 > 0: - # Make sure the label appears on *right*, not on - # top of the leftmost label. - lonlines[-1] -= 1e-10 - else: - # Formatter formats label as 1e-10... so there is - # simply no way to put label on right. Just shift this - # location off the map edge so parallels still extend - # all the way to the edge, but label disappears. - lonlines[-1] += 1e-10 - lonlines = [*lonlines] - - # Latitudes gridlines, draw from -latmax to latmax unless result - # would be asymmetrical across equator - # NOTE: Basemap axes redraw *meridians* if they detect latmax was - # explicitly changed, so important not to overwrite 'latmax' - # with default value! Just need it for this calculation, then when - # drawparallels is called will use self._latmax - if latlines is not None or latmax is not None: - # Fill defaults - if latlines is None: - latlines = _notNone( - self._latlines_values, rc['geogrid.latstep'] - ) - ilatmax = _notNone(latmax, self._latmax, rc['geogrid.latmax']) - # Get tick locations - if not np.iterable(latlines): - if (ilatmax % latlines) == (-ilatmax % latlines): - latlines = arange(-ilatmax, ilatmax, latlines) - else: - latlines = arange(0, ilatmax, latlines) - if latlines[-1] != ilatmax: - latlines = np.concatenate((latlines, [ilatmax])) - latlines = np.concatenate( - (-latlines[::-1], latlines[1:])) - latlines = [*latlines] - - # Length-4 boolean arrays of whether and where to toggle labels - # Format is [left, right, bottom, top] - lonarray, latarray = [], [] - for labs, array in zip( - (lonlabels, latlabels), (lonarray, latarray) - ): - if labs is None: - continue # leave empty - if isinstance(labs, str): - string = labs - labs = [0] * 4 - for idx, char in zip([0, 1, 2, 3], 'lrbt'): - if char in string: - labs[idx] = 1 - elif not np.iterable(labs): - labs = np.atleast_1d(labs) - if len(labs) == 1: - labs = [*labs, 0] # default is to label bottom/left - if len(labs) == 2: - if array is lonarray: - labs = [0, 0, *labs] - else: - labs = [*labs, 0, 0] - elif len(labs) != 4: - raise ValueError(f'Invalid lon/lat label spec: {labs}.') - array[:] = labs - lonarray = lonarray or None # None so use default locations - latarray = latarray or None - - # Add attributes for redrawing lines - if latmax is not None: - self._latmax = latmax - if latlines is not None: - self._latlines_values = latlines - if lonlines is not None: - self._lonlines_values = lonlines - if latarray is not None: - self._latlines_labels = latarray - if lonarray is not None: - self._lonlines_labels = lonarray - - # Grid toggling, must come after everything else in case e.g. - # rc.geogrid is False but user passed grid=True so we need to - # recover the *default* lonlines and latlines values - if grid is not None: - if not grid: - lonlines = latlines = [] - else: - lonlines = self._lonlines_values - latlines = self._latlines_values - - # Apply formatting to basemap or cartpoy axes - patch_kw = patch_kw or {} - self._format_apply( - patch_kw, lonlim, latlim, boundinglat, - lonlines, latlines, latmax, lonarray, latarray - ) - super().format(**kwargs) - - # Disabled methods suitable only for cartesian axes - _disable = _disable_decorator( - 'Invalid plotting method {!r} for map projection axes.' - ) - bar = _disable(Axes.bar) - barh = _disable(Axes.barh) - twinx = _disable(Axes.twinx) - twiny = _disable(Axes.twiny) - matshow = _disable(Axes.matshow) - imshow = _disable(Axes.imshow) - spy = _disable(Axes.spy) - hist = _disable(Axes.hist) - hist2d = _disable(Axes.hist2d) - boxplot = _disable(Axes.boxplot) - violinplot = _disable(Axes.violinplot) - step = _disable(Axes.step) - stem = _disable(Axes.stem) - stackplot = _disable(Axes.stackplot) - table = _disable(Axes.table) - eventplot = _disable(Axes.eventplot) - pie = _disable(Axes.pie) - xcorr = _disable(Axes.xcorr) - acorr = _disable(Axes.acorr) - psd = _disable(Axes.psd) - csd = _disable(Axes.csd) - cohere = _disable(Axes.cohere) - specgram = _disable(Axes.specgram) - angle_spectrum = _disable(Axes.angle_spectrum) - phase_spectrum = _disable(Axes.phase_spectrum) - magnitude_spectrum = _disable(Axes.magnitude_spectrum) - - -def _add_gridline_label(self, value, axis, upper_end): - """ - Gridliner method monkey patch. Always print number in range (180W, 180E). - """ - # Have 3 choices (see Issue #78): - # 1. lonlines go from -180 to 180, but get double 180 labels at dateline - # 2. lonlines go from -180 to e.g. 150, but no lines from 150 to dateline - # 3. lonlines go from lon_0 - 180 to lon_0 + 180 mod 360, but results - # in non-monotonic array causing double gridlines east of dateline - # 4. lonlines go from lon_0 - 180 to lon_0 + 180 monotonic, but prevents - # labels from being drawn outside of range (-180, 180) - # These monkey patches choose #4 and permit labels being drawn - # outside of (-180 180) - if axis == 'x': - value = (value + 180) % 360 - 180 - return type(self)._add_gridline_label(self, value, axis, upper_end) - - -def _axes_domain(self, *args, **kwargs): - """ - Gridliner method monkey patch. Filter valid label coordinates to values - between lon_0 - 180 and lon_0 + 180. - """ - # See _add_gridline_label for detials - lon_0 = self.axes.projection.proj4_params.get('lon_0', 0) - x_range, y_range = type(self)._axes_domain(self, *args, **kwargs) - x_range = np.asarray(x_range) + lon_0 - return x_range, y_range - - -class GeoAxes(ProjAxes, GeoAxes): - """ - Axes subclass for plotting `cartopy \ -`__ projections. Initializes - the `cartopy.crs.Projection` instance, enforces `global extent \ -`__ - for most projections by default, and draws `circular boundaries \ -`__ - around polar azimuthal, stereographic, and Gnomonic projections bounded at - the equator by default. - """ # noqa - #: The registered projection name. - name = 'geo' - _n_points = 100 # number of points for drawing circle map boundary - - def __init__(self, *args, map_projection=None, **kwargs): - """ - Parameters - ---------- - map_projection : `~cartopy.crs.Projection` - The `~cartopy.crs.Projection` instance. - *args, **kwargs - Passed to `~cartopy.mpl.geoaxes.GeoAxes`. - - See also - -------- - `~proplot.subplots.subplots`, `Axes`, `~proplot.projs.Proj` - """ - # GeoAxes initialization. Note that critical attributes like - # outline_patch needed by _format_apply are added before it is called. - import cartopy.crs as ccrs - if not isinstance(map_projection, ccrs.Projection): - raise ValueError( - 'GeoAxes requires map_projection=cartopy.crs.Projection.' - ) - super().__init__(*args, map_projection=map_projection, **kwargs) - - # Zero out ticks so gridlines are not offset - for axis in (self.xaxis, self.yaxis): - axis.set_tick_params(which='both', size=0) - - # Set extent and boundary extent for projections - # The default bounding latitude is set in _format_apply - # NOTE: set_global does not mess up non-global projections like OSNI - if hasattr(self, 'set_boundary') and isinstance(self.projection, ( - ccrs.NorthPolarStereo, ccrs.SouthPolarStereo, - projs.NorthPolarGnomonic, projs.SouthPolarGnomonic, - projs.NorthPolarAzimuthalEquidistant, - projs.NorthPolarLambertAzimuthalEqualArea, - projs.SouthPolarAzimuthalEquidistant, - projs.SouthPolarLambertAzimuthalEqualArea)): - self.set_boundary(_circle_path(100), transform=self.transAxes) - else: - self.set_global() - - def _format_apply( # noqa: U100 - self, patch_kw, lonlim, latlim, boundinglat, - lonlines, latlines, latmax, lonarray, latarray - ): - """ - Apply formatting to cartopy axes. - """ - # NOTE: Cartopy seems to handle latmax automatically. - import cartopy.feature as cfeature - import cartopy.crs as ccrs - from cartopy.mpl import gridliner - - # Initial gridliner object, which ProPlot passively modifies - # TODO: Flexible formatter? - if not self._gridliners: - gl = self.gridlines(zorder=2.5) # below text only - gl._axes_domain = _axes_domain.__get__(gl) # apply monkey patches - gl._add_gridline_label = _add_gridline_label.__get__(gl) - gl.xlines = False - gl.ylines = False - try: - lonformat = gridliner.LongitudeFormatter # newer - latformat = gridliner.LatitudeFormatter - except AttributeError: - lonformat = gridliner.LONGITUDE_FORMATTER # older - latformat = gridliner.LATITUDE_FORMATTER - gl.xformatter = lonformat - gl.yformatter = latformat - gl.xlabels_top = False - gl.xlabels_bottom = False - gl.ylabels_left = False - gl.ylabels_right = False - - # Projection extent - # NOTE: They may add this as part of set_xlim and set_ylim in future - # See: https://github.com/SciTools/cartopy/blob/master/lib/cartopy/mpl/geoaxes.py#L638 # noqa - # WARNING: The set_extent method tries to set a *rectangle* between - # the *4* (x,y) coordinate pairs (each corner), so something like - # (-180,180,-90,90) will result in *line*, causing error! - proj = self.projection.proj4_params['proj'] - north = isinstance(self.projection, ( - ccrs.NorthPolarStereo, projs.NorthPolarGnomonic, - projs.NorthPolarAzimuthalEquidistant, - projs.NorthPolarLambertAzimuthalEqualArea - )) - south = isinstance(self.projection, ( - ccrs.SouthPolarStereo, projs.SouthPolarGnomonic, - projs.SouthPolarAzimuthalEquidistant, - projs.SouthPolarLambertAzimuthalEqualArea - )) - if north or south: - if (lonlim is not None or latlim is not None): - _warn_proplot( - f'{proj!r} extent is controlled by "boundinglat", ' - f'ignoring lonlim={lonlim!r} and latlim={latlim!r}.' - ) - if self._boundinglat is None: - if isinstance(self.projection, projs.NorthPolarGnomonic): - boundinglat = 30 - elif isinstance(self.projection, projs.SouthPolarGnomonic): - boundinglat = -30 - else: - boundinglat = 0 - if boundinglat is not None and boundinglat != self._boundinglat: - eps = 1e-10 # bug with full -180, 180 range when lon_0 != 0 - lat0 = (90 if north else -90) - lon0 = self.projection.proj4_params.get('lon_0', 0) - extent = [ - lon0 - 180 + eps, lon0 + 180 - eps, - boundinglat, lat0 - ] - self.set_extent(extent, crs=ccrs.PlateCarree()) - self._boundinglat = boundinglat - else: - if boundinglat is not None: - _warn_proplot( - f'{proj!r} extent is controlled by "lonlim" and "latlim", ' - f'ignoring boundinglat={boundinglat!r}.' - ) - if lonlim is not None or latlim is not None: - lonlim = lonlim or [None, None] - latlim = latlim or [None, None] - lonlim, latlim = [*lonlim], [*latlim] - lon_0 = self.projection.proj4_params.get('lon_0', 0) - if lonlim[0] is None: - lonlim[0] = lon_0 - 180 - if lonlim[1] is None: - lonlim[1] = lon_0 + 180 - eps = 1e-10 # bug with full -180, 180 range when lon_0 != 0 - lonlim[0] += eps - if latlim[0] is None: - latlim[0] = -90 - if latlim[1] is None: - latlim[1] = 90 - extent = [*lonlim, *latlim] - self.set_extent(extent, crs=ccrs.PlateCarree()) - - # Draw gridlines, manage them with one custom gridliner generated - # by ProPlot, user may want to use griliner API directly - gl = self._gridliners[0] - # Collection props, see GoeAxes.gridlines() source code - kw = rc.fill({ - 'alpha': 'geogrid.alpha', - 'color': 'geogrid.color', - 'linewidth': 'geogrid.linewidth', - 'linestyle': 'geogrid.linestyle', - }, context=True) - gl.collection_kwargs.update(kw) - # Grid locations - eps = 1e-10 - if lonlines is not None: - if len(lonlines) == 0: - gl.xlines = False - else: - gl.xlines = True - gl.xlocator = mticker.FixedLocator(lonlines) - if latlines is not None: - if len(latlines) == 0: - gl.ylines = False - else: - gl.ylines = True - if latlines[0] == -90: - latlines[0] += eps - if latlines[-1] == 90: - latlines[-1] -= eps - gl.ylocator = mticker.FixedLocator(latlines) - # Grid label toggling - # Issue warning instead of error! - if not isinstance(self.projection, (ccrs.Mercator, ccrs.PlateCarree)): - if latarray is not None and any(latarray): - _warn_proplot( - 'Cannot add gridline labels to cartopy ' - f'{type(self.projection).__name__} projection.' - ) - latarray = [0] * 4 - if lonarray is not None and any(lonarray): - _warn_proplot( - 'Cannot add gridline labels to cartopy ' - f'{type(self.projection).__name__} projection.' - ) - lonarray = [0] * 4 - if latarray is not None: - gl.ylabels_left = latarray[0] - gl.ylabels_right = latarray[1] - if lonarray is not None: - gl.xlabels_bottom = lonarray[2] - gl.xlabels_top = lonarray[3] - - # Geographic features - # WARNING: Seems cartopy features can't be updated! - # See: https://scitools.org.uk/cartopy/docs/v0.14/_modules/cartopy/feature.html#Feature # noqa - # Change the _kwargs property also does *nothing* - # WARNING: Changing linewidth is impossible with cfeature. Bug? - # See: https://stackoverflow.com/questions/43671240/changing-line-width-of-cartopy-borders # noqa - # TODO: Editing existing natural features? Creating natural features - # at __init__ time and hiding them? - # NOTE: The natural_earth_shp method is deprecated, use add_feature. - # See: https://cartopy-pelson.readthedocs.io/en/readthedocs/whats_new.html # noqa - # NOTE: The e.g. cfeature.COASTLINE features are just for convenience, - # hi res versions. Use cfeature.COASTLINE.name to see how it can be - # looked up with NaturalEarthFeature. - reso = rc['reso'] - if reso not in ('lo', 'med', 'hi'): - raise ValueError(f'Invalid resolution {reso!r}.') - reso = { - 'lo': '110m', - 'med': '50m', - 'hi': '10m', - }.get(reso) - features = { - 'land': ('physical', 'land'), - 'ocean': ('physical', 'ocean'), - 'lakes': ('physical', 'lakes'), - 'coast': ('physical', 'coastline'), - 'rivers': ('physical', 'rivers_lake_centerlines'), - 'borders': ('cultural', 'admin_0_boundary_lines_land'), - 'innerborders': ('cultural', 'admin_1_states_provinces_lakes'), - } - for name, args in features.items(): - # Get feature - if not rc[name]: # toggled - continue - if getattr(self, '_' + name, None): # already drawn - continue - feat = cfeature.NaturalEarthFeature(*args, reso) - # For 'lines', need to specify edgecolor and facecolor - # See: https://github.com/SciTools/cartopy/issues/803 - kw = rc.category(name) # do not omit uncached props - if name in ('coast', 'rivers', 'borders', 'innerborders'): - kw['edgecolor'] = kw.pop('color') - kw['facecolor'] = 'none' - else: - kw['linewidth'] = 0 - if name in ('ocean',): - kw['zorder'] = 0.5 # below everything! - self.add_feature(feat, **kw) - setattr(self, '_' + name, feat) - - # Update patch - kw_face = rc.fill({ - 'facecolor': 'geoaxes.facecolor', - 'alpha': 'geoaxes.facealpha', - }, context=True) - kw_edge = rc.fill({ - 'edgecolor': 'geoaxes.edgecolor', - 'linewidth': 'geoaxes.linewidth', - }, context=True) - kw_face.update(patch_kw or {}) - self.background_patch.update(kw_face) - self.outline_patch.update(kw_edge) - - def _hide_labels(self): - """ - No-op for now. In future this will hide meridian and parallel - labels for rectangular projections. - """ - pass - - def get_tightbbox(self, renderer, *args, **kwargs): - # Perform extra post-processing steps - # For now this just draws the gridliners - self._hide_labels() - if self.get_autoscale_on() and self.ignore_existing_data_limits: - self.autoscale_view() - if self.background_patch.reclip: - clipped_path = self.background_patch.orig_path.clip_to_bbox( - self.viewLim) - self.background_patch._path = clipped_path - self.apply_aspect() - for gl in self._gridliners: - try: # new versions only - gl._draw_gridliner(background_patch=self.background_patch, - renderer=renderer) - except TypeError: - gl._draw_gridliner(background_patch=self.background_patch) - self._gridliners = [] - return super().get_tightbbox(renderer, *args, **kwargs) - - # Projection property - @property - def projection(self): - """ - The `~cartopy.crs.Projection` instance associated with this axes. - """ - return self._map_projection - - @projection.setter - def projection(self, map_projection): - import cartopy.crs as ccrs - if not isinstance(map_projection, ccrs.CRS): - raise ValueError(f'Projection must be a cartopy.crs.CRS instance.') - self._map_projection = map_projection - - # Wrapped methods - # TODO: Remove this duplication! - if GeoAxes is not object: - text = _text_wrapper( - GeoAxes.text - ) - plot = _default_transform(_plot_wrapper(_standardize_1d( - _add_errorbars(_cycle_changer(GeoAxes.plot)) - ))) - scatter = _default_transform(_scatter_wrapper(_standardize_1d( - _add_errorbars(_cycle_changer(GeoAxes.scatter)) - ))) - fill_between = _fill_between_wrapper(_standardize_1d(_cycle_changer( - GeoAxes.fill_between - ))) - fill_betweenx = _fill_betweenx_wrapper(_standardize_1d(_cycle_changer( - GeoAxes.fill_betweenx - ))) - contour = _default_transform(_standardize_2d(_cmap_changer( - GeoAxes.contour - ))) - contourf = _default_transform(_standardize_2d(_cmap_changer( - GeoAxes.contourf - ))) - pcolor = _default_transform(_standardize_2d(_cmap_changer( - GeoAxes.pcolor - ))) - pcolormesh = _default_transform(_standardize_2d(_cmap_changer( - GeoAxes.pcolormesh - ))) - quiver = _default_transform(_standardize_2d(_cmap_changer( - GeoAxes.quiver - ))) - streamplot = _default_transform(_standardize_2d(_cmap_changer( - GeoAxes.streamplot - ))) - barbs = _default_transform(_standardize_2d(_cmap_changer( - GeoAxes.barbs - ))) - tripcolor = _default_transform(_cmap_changer( - GeoAxes.tripcolor - )) - tricontour = _default_transform(_cmap_changer( - GeoAxes.tricontour - )) - tricontourf = _default_transform(_cmap_changer( - GeoAxes.tricontourf - )) - get_extent = _default_crs( - GeoAxes.get_extent - ) - set_extent = _default_crs( - GeoAxes.set_extent - ) - set_xticks = _default_crs( - GeoAxes.set_xticks - ) - set_yticks = _default_crs( - GeoAxes.set_yticks - ) - - -class BasemapAxes(ProjAxes): - """ - Axes subclass for plotting `~mpl_toolkits.basemap` projections. The - `~mpl_toolkits.basemap.Basemap` projection instance is added as - the `map_projection` attribute, but this is all abstracted away -- you can - use `~matplotlib.axes.Axes` methods like `~matplotlib.axes.Axes.plot` and - `~matplotlib.axes.Axes.contour` with your raw longitude-latitude data. - """ - #: The registered projection name. - name = 'basemap' - _proj_non_rectangular = ( - 'ortho', 'geos', 'nsper', - 'moll', 'hammer', 'robin', - 'eck4', 'kav7', 'mbtfpq', - 'sinu', 'vandg', - 'npstere', 'spstere', 'nplaea', - 'splaea', 'npaeqd', 'spaeqd', - ) # do not use axes spines as boundaries - - def __init__(self, *args, map_projection=None, **kwargs): - """ - Parameters - ---------- - map_projection : `~mpl_toolkits.basemap.Basemap` - The `~mpl_toolkits.basemap.Basemap` instance. - **kwargs - Passed to `Axes`. - - See also - -------- - `~proplot.subplots.subplots`, `Axes`, `~proplot.projs.Proj` - """ - # Map boundary notes - # * Must set boundary before-hand, otherwise the set_axes_limits method - # called by mcontourf/mpcolormesh/etc draws two mapboundary Patch - # objects called "limb1" and "limb2" automatically: one for fill and - # the other for the edges - # * Then, since the patch object in _mapboundarydrawn is only the - # fill-version, calling drawmapboundary again will replace only *that - # one*, but the original visible edges are still drawn -- so e.g. you - # can't change the color - # * If you instead call drawmapboundary right away, _mapboundarydrawn - # will contain both the edges and the fill; so calling it again will - # replace *both* - import mpl_toolkits.basemap as mbasemap # verify package is available - if not isinstance(map_projection, mbasemap.Basemap): - raise ValueError( - 'BasemapAxes requires map_projection=basemap.Basemap' - ) - self._map_projection = map_projection - self._map_boundary = None - self._has_recurred = False # use this to override plotting methods - super().__init__(*args, **kwargs) - - def _format_apply( - self, patch_kw, lonlim, latlim, boundinglat, - lonlines, latlines, latmax, lonarray, latarray - ): - """ - Apply changes to the basemap axes. - """ - # Checks - if (lonlim is not None or latlim is not None - or boundinglat is not None): - _warn_proplot( - f'Got lonlim={lonlim!r}, latlim={latlim!r}, ' - f'boundinglat={boundinglat!r}, but you cannot "zoom into" a ' - 'basemap projection after creating it. Pass proj_kw in your ' - 'call to subplots with any of the following basemap keywords: ' - "'boundinglat', 'llcrnrlon', 'llcrnrlat', " - "'urcrnrlon', 'urcrnrlat', 'llcrnrx', 'llcrnry', " - "'urcrnrx', 'urcrnry', 'width', or 'height'." - ) - - # Map boundary - # * First have to *manually replace* the old boundary by just - # deleting the original one - # * If boundary is drawn successfully should be able to call - # self.projection._mapboundarydrawn.set_visible(False) and - # edges/fill color disappear - # * For now will enforce that map plots *always* have background - # whereas axes plots can have transparent background - kw_face = rc.fill({ - 'facecolor': 'geoaxes.facecolor', - 'alpha': 'geoaxes.facealpha', - }, context=True) - kw_edge = rc.fill({ - 'linewidth': 'geoaxes.linewidth', - 'edgecolor': 'geoaxes.edgecolor', - }, context=True) - kw_face.update(patch_kw or {}) - self.axesPatch = self.patch # bugfix or something - if self.projection.projection in self._proj_non_rectangular: - self.patch.set_alpha(0) # make patch invisible - if not self.projection._mapboundarydrawn: - # set fill_color to 'none' to make transparent - p = self.projection.drawmapboundary(ax=self) - else: - p = self.projection._mapboundarydrawn - p.update(kw_face) - p.update(kw_edge) - p.set_rasterized(False) - p.set_clip_on(False) # so edges denoting boundary aren't cut off - self._map_boundary = p - else: - self.patch.update({**kw_face, 'edgecolor': 'none'}) - for spine in self.spines.values(): - spine.update(kw_edge) - - # Longitude/latitude lines - # Make sure to turn off clipping by invisible axes boundary; otherwise - # get these weird flat edges where map boundaries, parallel/meridian - # markers come up to the axes bbox - lkw = rc.fill({ - 'alpha': 'geogrid.alpha', - 'color': 'geogrid.color', - 'linewidth': 'geogrid.linewidth', - 'linestyle': 'geogrid.linestyle', - }) # always apply - tkw = rc.fill({ - 'color': 'geogrid.color', - 'fontsize': 'geogrid.labelsize', - }) - # Change from left/right/bottom/top to left/right/top/bottom - if lonarray is not None: - lonarray[2:] = lonarray[2:][::-1] - if latarray is not None: - latarray[2:] = latarray[2:][::-1] - - # Parallel lines - if latlines is not None or latmax is not None or latarray is not None: - if self._latlines: - for pi in self._latlines.values(): - for obj in [i for j in pi for i in j]: # magic - obj.set_visible(False) - ilatmax = _notNone(latmax, self._latmax) - latlines = _notNone(latlines, self._latlines_values) - latarray = _notNone(latarray, self._latlines_labels, [0] * 4) - p = self.projection.drawparallels( - latlines, latmax=ilatmax, labels=latarray, ax=self - ) - for pi in p.values(): # returns dict, where each one is tuple - # Tried passing clip_on to the below, but it does nothing - # Must set for lines created after the fact - for obj in [i for j in pi for i in j]: - if isinstance(obj, mtext.Text): - obj.update(tkw) - else: - obj.update(lkw) - self._latlines = p - - # Meridian lines - if lonlines is not None or latmax is not None or lonarray is not None: - if self._lonlines: - for pi in self._lonlines.values(): - for obj in [i for j in pi for i in j]: # magic - obj.set_visible(False) - ilatmax = _notNone(latmax, self._latmax) - lonlines = _notNone(lonlines, self._lonlines_values) - lonarray = _notNone(lonarray, self._lonlines_labels, [0] * 4) - p = self.projection.drawmeridians( - lonlines, latmax=ilatmax, labels=lonarray, ax=self, - ) - for pi in p.values(): - for obj in [i for j in pi for i in j]: - if isinstance(obj, mtext.Text): - obj.update(tkw) - else: - obj.update(lkw) - self._lonlines = p - - # Geography - # TODO: Allow setting the zorder. - # NOTE: Also notable are drawcounties, blumarble, drawlsmask, - # shadedrelief, and etopo methods. - features = { - 'land': 'fillcontinents', - 'coast': 'drawcoastlines', - 'rivers': 'drawrivers', - 'borders': 'drawcountries', - 'innerborders': 'drawstates', - } - for name, method in features.items(): - if not rc[name]: # toggled - continue - if getattr(self, f'_{name}', None): # already drawn - continue - kw = rc.category(name) - feat = getattr(self.projection, method)(ax=self) - if isinstance(feat, (list, tuple)): # list of artists? - for obj in feat: - obj.update(kw) - else: - feat.update(kw) - setattr(self, '_' + name, feat) - - # Projection property - @property - def projection(self): - """ - The `~mpl_toolkits.basemap.Basemap` instance associated with this axes. - """ - return self._map_projection - - @projection.setter - def projection(self, map_projection): - import mpl_toolkits.basemap as mbasemap - if not isinstance(map_projection, mbasemap.Basemap): - raise ValueError(f'Projection must be a basemap.Basemap instance.') - self._map_projection = map_projection - - # Wrapped methods - plot = _norecurse(_default_latlon(_plot_wrapper(_standardize_1d( - _add_errorbars(_cycle_changer(_redirect(maxes.Axes.plot))) - )))) - scatter = _norecurse(_default_latlon(_scatter_wrapper(_standardize_1d( - _add_errorbars(_cycle_changer(_redirect(maxes.Axes.scatter))) - )))) - contour = _norecurse(_default_latlon(_standardize_2d(_cmap_changer( - _redirect(maxes.Axes.contour) - )))) - contourf = _norecurse(_default_latlon(_standardize_2d(_cmap_changer( - _redirect(maxes.Axes.contourf) - )))) - pcolor = _norecurse(_default_latlon(_standardize_2d(_cmap_changer( - _redirect(maxes.Axes.pcolor) - )))) - pcolormesh = _norecurse(_default_latlon(_standardize_2d(_cmap_changer( - _redirect(maxes.Axes.pcolormesh) - )))) - quiver = _norecurse(_default_latlon(_standardize_2d(_cmap_changer( - _redirect(maxes.Axes.quiver) - )))) - streamplot = _norecurse(_default_latlon(_standardize_2d(_cmap_changer( - _redirect(maxes.Axes.streamplot) - )))) - barbs = _norecurse(_default_latlon(_standardize_2d(_cmap_changer( - _redirect(maxes.Axes.barbs) - )))) - hexbin = _norecurse(_standardize_1d(_cmap_changer( - _redirect(maxes.Axes.hexbin) - ))) - imshow = _norecurse(_cmap_changer( - _redirect(maxes.Axes.imshow) - )) - - -# Register the projections -mproj.register_projection(PolarAxes) -mproj.register_projection(XYAxes) -mproj.register_projection(GeoAxes) -mproj.register_projection(BasemapAxes) diff --git a/proplot/axes/__init__.py b/proplot/axes/__init__.py new file mode 100644 index 000000000..413f28c97 --- /dev/null +++ b/proplot/axes/__init__.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +""" +The various axes classes used throughout proplot. +""" +import matplotlib.projections as mproj + +from ..internals import context +from .base import Axes # noqa: F401 +from .cartesian import CartesianAxes +from .geo import GeoAxes # noqa: F401 +from .geo import _BasemapAxes, _CartopyAxes +from .plot import PlotAxes # noqa: F401 +from .polar import PolarAxes +from .shared import _SharedAxes # noqa: F401 +from .three import ThreeAxes # noqa: F401 + +# Prevent importing module names and set order of appearance for objects +__all__ = [ + 'Axes', + 'PlotAxes', + 'CartesianAxes', + 'PolarAxes', + 'GeoAxes', + 'ThreeAxes', +] + +# Register projections with package prefix to avoid conflicts +# NOTE: We integrate with cartopy and basemap rather than using matplotlib's +# native projection system. Therefore axes names are not part of public API. +_cls_dict = {} # track valid names +for _cls in (CartesianAxes, PolarAxes, _CartopyAxes, _BasemapAxes, ThreeAxes): + for _name in (_cls._name, *_cls._name_aliases): + with context._state_context(_cls, name='proplot_' + _name): + mproj.register_projection(_cls) + _cls_dict[_name] = _cls +_cls_table = '\n'.join( + ' ' + key + ' ' * (max(map(len, _cls_dict)) - len(key) + 7) + + ('GeoAxes' if cls.__name__[:1] == '_' else cls.__name__) + for key, cls in _cls_dict.items() +) diff --git a/proplot/axes/base.py b/proplot/axes/base.py new file mode 100644 index 000000000..ae55d19d4 --- /dev/null +++ b/proplot/axes/base.py @@ -0,0 +1,2997 @@ +#!/usr/bin/env python3 +""" +The first-level axes subclass used for all proplot figures. +Implements basic shared functionality. +""" +import copy +import inspect +import re +from numbers import Integral + +import matplotlib.axes as maxes +import matplotlib.axis as maxis +import matplotlib.cm as mcm +import matplotlib.colors as mcolors +import matplotlib.container as mcontainer +import matplotlib.contour as mcontour +import matplotlib.legend as mlegend +import matplotlib.offsetbox as moffsetbox +import matplotlib.patches as mpatches +import matplotlib.projections as mproj +import matplotlib.text as mtext +import matplotlib.ticker as mticker +import matplotlib.transforms as mtransforms +import numpy as np +from matplotlib import cbook + +from .. import colors as pcolors +from .. import constructor +from .. import ticker as pticker +from ..config import rc +from ..internals import ic # noqa: F401 +from ..internals import ( + _kwargs_to_args, + _not_none, + _pop_kwargs, + _pop_params, + _pop_props, + _pop_rc, + _translate_loc, + _version_mpl, + docstring, + guides, + labels, + rcsetup, + warnings, +) +from ..utils import _fontsize_to_pt, edges, units + +try: + from cartopy.crs import CRS, PlateCarree +except Exception: + CRS = PlateCarree = object + +__all__ = ['Axes'] + + +# A-b-c label string +ABC_STRING = 'abcdefghijklmnopqrstuvwxyz' + +# Legend align options +ALIGN_OPTS = { + None: { + 'center': 'center', + 'left': 'center left', + 'right': 'center right', + 'top': 'upper center', + 'bottom': 'lower center', + }, + 'left': { + 'top': 'upper right', + 'center': 'center right', + 'bottom': 'lower right', + }, + 'right': { + 'top': 'upper left', + 'center': 'center left', + 'bottom': 'lower left', + }, + 'top': { + 'left': 'lower left', + 'center': 'lower center', + 'right': 'lower right' + }, + 'bottom': { + 'left': 'upper left', + 'center': 'upper center', + 'right': 'upper right' + }, +} + + +# Projection docstring +_proj_docstring = """ +proj, projection : \ +str, `cartopy.crs.Projection`, or `~mpl_toolkits.basemap.Basemap`, optional + The map projection specification(s). If ``'cart'`` or ``'cartesian'`` + (the default), a `~proplot.axes.CartesianAxes` is created. If ``'polar'``, + a `~proplot.axes.PolarAxes` is created. Otherwise, the argument is + interpreted by `~proplot.constructor.Proj`, and the result is used + to make a `~proplot.axes.GeoAxes` (in this case the argument can be + a `cartopy.crs.Projection` instance, a `~mpl_toolkits.basemap.Basemap` + instance, or a projection name listed in :ref:`this table `). +""" +_proj_kw_docstring = """ +proj_kw, projection_kw : dict-like, optional + Keyword arguments passed to `~mpl_toolkits.basemap.Basemap` or + cartopy `~cartopy.crs.Projection` classes on instantiation. +""" +_backend_docstring = """ +backend : {'cartopy', 'basemap'}, default: :rc:`geo.backend` + Whether to use `~mpl_toolkits.basemap.Basemap` or + `~cartopy.crs.Projection` for map projections. +""" +docstring._snippet_manager['axes.proj'] = _proj_docstring +docstring._snippet_manager['axes.proj_kw'] = _proj_kw_docstring +docstring._snippet_manager['axes.backend'] = _backend_docstring + + +# Colorbar and legend space +_space_docstring = """ +queue : bool, optional + If ``True`` and `loc` is the same as an existing {name}, the input + arguments are added to a queue and this function returns ``None``. + This is used to "update" the same {name} with successive ``ax.{name}(...)`` + calls. If ``False`` (the default) and `loc` is the same as an existing + *inset* {name}, the old {name} is removed. If ``False`` and `loc` is an + *outer* {name}, the {name}s are "stacked". +space : unit-spec, default: None + For outer {name}s only. The fixed space between the {name} and the subplot + edge. %(units.em)s + When the :ref:`tight layout algorithm ` is active for the figure, + `space` is computed automatically (see `pad`). Otherwise, `space` is set to + a suitable default. +pad : unit-spec, default: :rc:`subplots.panelpad` or :rc:`{default}` + For outer {name}s, this is the :ref:`tight layout padding ` + between the {name} and the subplot (default is :rcraw:`subplots.panelpad`). + For inset {name}s, this is the fixed space between the axes + edge and the {name} (default is :rcraw:`{default}`). + %(units.em)s +align : {{'center', 'top', 'bottom', 'left', 'right', 't', 'b', 'l', 'r'}}, optional + For outer {name}s only. How to align the {name} against the subplot edge. + The values ``'top'`` and ``'bottom'`` are valid for left and right {name}s + and ``'left'`` and ``'right'`` are valid for top and bottom {name}s. + The default is always ``'center'``. +""" +docstring._snippet_manager['axes.legend_space'] = _space_docstring.format( + name='legend', default='legend.borderaxespad' +) +docstring._snippet_manager['axes.colorbar_space'] = _space_docstring.format( + name='colorbar', default='colorbar.insetpad' +) + + +# Transform docstring +# Used for text and add_axes +_transform_docstring = """ +transform : {'data', 'axes', 'figure', 'subfigure'} \ +or `~matplotlib.transforms.Transform`, optional + The transform used to interpret the bounds. Can be a + `~matplotlib.transforms.Transform` instance or a string representing + the `~matplotlib.axes.Axes.transData`, `~matplotlib.axes.Axes.transAxes`, + `~matplotlib.figure.Figure.transFigure`, or + `~matplotlib.figure.Figure.transSubfigure`, transforms. +""" +docstring._snippet_manager['axes.transform'] = _transform_docstring + + +# Inset docstring +# NOTE: Used by SubplotGrid.inset_axes +_inset_docstring = """ +Add an inset axes. +This is similar to `matplotlib.axes.Axes.inset_axes`. + +Parameters +---------- +bounds : 4-tuple of float + The (left, bottom, width, height) coordinates for the axes. +%(axes.transform)s + Default is to use the same projection as the current axes. +%(axes.proj)s +%(axes.proj_kw)s +%(axes.backend)s +zorder : float, default: 4 + The `zorder `__ + of the axes. Should be greater than the zorder of elements in the parent axes. +zoom : bool, default: True or False + Whether to draw lines indicating the inset zoom using `~Axes.indicate_inset_zoom`. + The line positions will automatically adjust when the parent or inset axes limits + change. Default is ``True`` only if both axes are `~proplot.axes.CartesianAxes`. +zoom_kw : dict, optional + Passed to `~Axes.indicate_inset_zoom`. + +Other parameters +---------------- +**kwargs + Passed to `proplot.axes.Axes`. + +Returns +------- +proplot.axes.Axes + The inset axes. + +See also +-------- +Axes.indicate_inset_zoom +matplotlib.axes.Axes.inset_axes +matplotlib.axes.Axes.indicate_inset +matplotlib.axes.Axes.indicate_inset_zoom +""" +_indicate_inset_docstring = """ +Add indicators denoting the zoom range of the inset axes. +This will replace previously drawn zoom indicators. + +Parameters +---------- +%(artist.patch)s +zorder : float, default: 3.5 + The `zorder `__ of + the indicators. Should be greater than the zorder of elements in the parent axes. + +Other parameters +---------------- +**kwargs + Passed to `~matplotlib.patches.Patch`. + +Note +---- +This command must be called from the inset axes rather than the parent axes. +It is called automatically when ``zoom=True`` is passed to `~Axes.inset_axes` +and whenever the axes are drawn (so the line positions always track the axis +limits even if they are later changed). + +See also +-------- +matplotlib.axes.Axes.indicate_inset +matplotlib.axes.Axes.indicate_inset_zoom +""" +docstring._snippet_manager['axes.inset'] = _inset_docstring +docstring._snippet_manager['axes.indicate_inset'] = _indicate_inset_docstring + + +# Panel docstring +# NOTE: Used by SubplotGrid.panel_axes +_panel_loc_docstring = """ + ========== ===================== + Location Valid keys + ========== ===================== + left ``'left'``, ``'l'`` + right ``'right'``, ``'r'`` + bottom ``'bottom'``, ``'b'`` + top ``'top'``, ``'t'`` + ========== ===================== +""" +_panel_docstring = """ +Add a panel axes. + +Parameters +---------- +side : str, optional + The panel location. Valid location keys are as follows. + +%(axes.panel_loc)s + +width : unit-spec, default: :rc:`subplots.panelwidth` + The panel width. + %(units.in)s +space : unit-spec, default: None + The fixed space between the panel and the subplot edge. + %(units.em)s + When the :ref:`tight layout algorithm ` is active for the figure, + `space` is computed automatically (see `pad`). Otherwise, `space` is set to + a suitable default. +pad : unit-spec, default: :rc:`subplots.panelpad` + The :ref:`tight layout padding ` between the panel and the subplot. + %(units.em)s +share : bool, default: True + Whether to enable axis sharing between the *x* and *y* axes of the + main subplot and the panel long axes for each panel in the "stack". + Sharing between the panel short axis and other panel short axes + is determined by figure-wide `sharex` and `sharey` settings. + +Other parameters +---------------- +**kwargs + Passed to `proplot.axes.CartesianAxes`. Supports all valid + `~proplot.axes.CartesianAxes.format` keywords. + +Returns +------- +proplot.axes.CartesianAxes + The panel axes. +""" +docstring._snippet_manager['axes.panel_loc'] = _panel_loc_docstring +docstring._snippet_manager['axes.panel'] = _panel_docstring + + +# Format docstrings +_axes_format_docstring = """ +title : str or sequence, optional + The axes title. Can optionally be a sequence strings, in which case + the title will be selected from the sequence according to `~Axes.number`. +abc : bool or str or sequence, default: :rc:`abc` + The "a-b-c" subplot label style. Must contain the character ``a`` or ``A``, + for example ``'a.'``, or ``'A'``. If ``True`` then the default style of + ``'a'`` is used. The ``a`` or ``A`` is replaced with the alphabetic character + matching the `~Axes.number`. If `~Axes.number` is greater than 26, the + characters loop around to a, ..., z, aa, ..., zz, aaa, ..., zzz, etc. + Can also be a sequence of strings, in which case the "a-b-c" label + will simply be selected from the sequence according to `~Axes.number`. +abcloc, titleloc : str, default: :rc:`abc.loc`, :rc:`title.loc` + Strings indicating the location for the a-b-c label and main title. + The following locations are valid: + + .. _title_table: + + ======================== ============================ + Location Valid keys + ======================== ============================ + center above axes ``'center'``, ``'c'`` + left above axes ``'left'``, ``'l'`` + right above axes ``'right'``, ``'r'`` + lower center inside axes ``'lower center'``, ``'lc'`` + upper center inside axes ``'upper center'``, ``'uc'`` + upper right inside axes ``'upper right'``, ``'ur'`` + upper left inside axes ``'upper left'``, ``'ul'`` + lower left inside axes ``'lower left'``, ``'ll'`` + lower right inside axes ``'lower right'``, ``'lr'`` + ======================== ============================ + +abcborder, titleborder : bool, default: :rc:`abc.border` and :rc:`title.border` + Whether to draw a white border around titles and a-b-c labels positioned + inside the axes. This can help them stand out on top of artists + plotted inside the axes. +abcbbox, titlebbox : bool, default: :rc:`abc.bbox` and :rc:`title.bbox` + Whether to draw a white bbox around titles and a-b-c labels positioned + inside the axes. This can help them stand out on top of artists plotted + inside the axes. +abc_kw, title_kw : dict-like, optional + Additional settings used to update the a-b-c label and title + with ``text.update()``. +titlepad : float, default: :rc:`title.pad` + The padding for the inner and outer titles and a-b-c labels. + %(units.pt)s +titleabove : bool, default: :rc:`title.above` + Whether to try to put outer titles and a-b-c labels above panels, + colorbars, or legends that are above the axes. +abctitlepad : float, default: :rc:`abc.titlepad` + The horizontal padding between a-b-c labels and titles in the same location. + %(units.pt)s +ltitle, ctitle, rtitle, ultitle, uctitle, urtitle, lltitle, lctitle, lrtitle \ +: str or sequence, optional + Shorthands for the below keywords. +lefttitle, centertitle, righttitle, upperlefttitle, uppercentertitle, upperrighttitle, \ +lowerlefttitle, lowercentertitle, lowerrighttitle : str or sequence, optional + Additional titles in specific positions (see `title` for details). This works as + an alternative to the ``ax.format(title='Title', titleloc=loc)`` workflow and + permits adding more than one title-like label for a single axes. +a, alpha, fc, facecolor, ec, edgecolor, lw, linewidth, ls, linestyle : default: \ +:rc:`axes.alpha`, :rc:`axes.facecolor`, :rc:`axes.edgecolor`, :rc:`axes.linewidth`, '-' + Additional settings applied to the background patch, and their + shorthands. Their defaults values are the ``'axes'`` properties. +""" +_figure_format_docstring = """ +rowlabels, collabels, llabels, tlabels, rlabels, blabels + Aliases for `leftlabels` and `toplabels`, and for `leftlabels`, + `toplabels`, `rightlabels`, and `bottomlabels`, respectively. +leftlabels, toplabels, rightlabels, bottomlabels : sequence of str, optional + Labels for the subplots lying along the left, top, right, and + bottom edges of the figure. The length of each list must match + the number of subplots along the corresponding edge. +leftlabelpad, toplabelpad, rightlabelpad, bottomlabelpad : float or unit-spec, default\ +: :rc:`leftlabel.pad`, :rc:`toplabel.pad`, :rc:`rightlabel.pad`, :rc:`bottomlabel.pad` + The padding between the labels and the axes content. + %(units.pt)s +leftlabels_kw, toplabels_kw, rightlabels_kw, bottomlabels_kw : dict-like, optional + Additional settings used to update the labels with ``text.update()``. +figtitle + Alias for `suptitle`. +suptitle : str, optional + The figure "super" title, centered between the left edge of the leftmost + subplot and the right edge of the rightmost subplot. +suptitlepad : float, default: :rc:`suptitle.pad` + The padding between the super title and the axes content. + %(units.pt)s +suptitle_kw : optional + Additional settings used to update the super title with ``text.update()``. +includepanels : bool, default: False + Whether to include panels when aligning figure "super titles" along the top + of the subplot grid and when aligning the `spanx` *x* axis labels and + `spany` *y* axis labels along the sides of the subplot grid. +""" +_rc_init_docstring = """ +""" +_rc_format_docstring = """ +rc_mode : int, optional + The context mode passed to `~proplot.config.Configurator.context`. +rc_kw : dict-like, optional + An alternative to passing extra keyword arguments. See below. +**kwargs + {}Keyword arguments that match the name of an `~proplot.config.rc` setting are + passed to `proplot.config.Configurator.context` and used to update the axes. + If the setting name has "dots" you can simply omit the dots. For example, + ``abc='A.'`` modifies the :rcraw:`abc` setting, ``titleloc='left'`` modifies the + :rcraw:`title.loc` setting, ``gridminor=True`` modifies the :rcraw:`gridminor` + setting, and ``gridbelow=True`` modifies the :rcraw:`grid.below` setting. Many + of the keyword arguments documented above are internally applied by retrieving + settings passed to `~proplot.config.Configurator.context`. +""" +docstring._snippet_manager['rc.init'] = _rc_format_docstring.format( + 'Remaining keyword arguments are passed to `matplotlib.axes.Axes`.\n ' +) +docstring._snippet_manager['rc.format'] = _rc_format_docstring.format('') +docstring._snippet_manager['axes.format'] = _axes_format_docstring +docstring._snippet_manager['figure.format'] = _figure_format_docstring + + +# Colorbar docstrings +_colorbar_args_docstring = """ +mappable : mappable, colormap-spec, sequence of color-spec, \ +or sequence of `~matplotlib.artist.Artist` + There are four options here: + + 1. A `~matplotlib.cm.ScalarMappable` (e.g., an object returned by + `~proplot.axes.PlotAxes.contourf` or `~proplot.axes.PlotAxes.pcolormesh`). + 2. A `~matplotlib.colors.Colormap` or registered colormap name used to build a + `~matplotlib.cm.ScalarMappable` on-the-fly. The colorbar range and ticks depend + on the arguments `values`, `vmin`, `vmax`, and `norm`. The default for a + `~proplot.colors.ContinuousColormap` is ``vmin=0`` and ``vmax=1`` (note that + passing `values` will "discretize" the colormap). The default for a + `~proplot.colors.DiscreteColormap` is ``values=np.arange(0, cmap.N)``. + 3. A sequence of hex strings, color names, or RGB[A] tuples. A + `~proplot.colors.DiscreteColormap` will be generated from these colors and + used to build a `~matplotlib.cm.ScalarMappable` on-the-fly. The colorbar + range and ticks depend on the arguments `values`, `norm`, and + `norm_kw`. The default is ``values=np.arange(0, len(mappable))``. + 4. A sequence of `matplotlib.artist.Artist` instances (e.g., a list of + `~matplotlib.lines.Line2D` instances returned by `~proplot.axes.PlotAxes.plot`). + A colormap will be generated from the colors of these objects (where the + color is determined by ``get_color``, if available, or ``get_facecolor``). + The colorbar range and ticks depend on the arguments `values`, `norm`, and + `norm_kw`. The default is to infer colorbar ticks and tick labels + by calling `~matplotlib.artist.Artist.get_label` on each artist. + +values : sequence of float or str, optional + Ignored if `mappable` is a `~matplotlib.cm.ScalarMappable`. This maps the colormap + colors to numeric values using `~proplot.colors.DiscreteNorm`. If the colormap is + a `~proplot.colors.ContinuousColormap` then its colors will be "discretized". + These These can also be strings, in which case the list indices are used for + tick locations and the strings are applied as tick labels. +""" +_colorbar_kwargs_docstring = """ +orientation : {None, 'horizontal', 'vertical'}, optional + The colorbar orientation. By default this depends on the "side" of the subplot + or figure where the colorbar is drawn. Inset colorbars are always horizontal. +norm : norm-spec, optional + Ignored if `mappable` is a `~matplotlib.cm.ScalarMappable`. This is the continuous + normalizer used to scale the `~proplot.colors.ContinuousColormap` (or passed + to `~proplot.colors.DiscreteNorm` if `values` was passed). Passed to the + `~proplot.constructor.Norm` constructor function. +norm_kw : dict-like, optional + Ignored if `mappable` is a `~matplotlib.cm.ScalarMappable`. These are the + normalizer keyword arguments. Passed to `~proplot.constructor.Norm`. +vmin, vmax : float, optional + Ignored if `mappable` is a `~matplotlib.cm.ScalarMappable`. These are the minimum + and maximum colorbar values. Passed to `~proplot.constructor.Norm`. +label, title : str, optional + The colorbar label. The `title` keyword is also accepted for + consistency with `~matplotlib.axes.Axes.legend`. +reverse : bool, optional + Whether to reverse the direction of the colorbar. This is done automatically + when descending levels are used with `~proplot.colors.DiscreteNorm`. +rotation : float, default: 0 + The tick label rotation. +grid, edges, drawedges : bool, default: :rc:`colorbar.grid` + Whether to draw "grid" dividers between each distinct color. +extend : {'neither', 'both', 'min', 'max'}, optional + Direction for drawing colorbar "extensions" (i.e. color keys for out-of-bounds + data on the end of the colorbar). Default behavior is to use the value of `extend` + passed to the plotting command or use ``'neither'`` if the value is unknown. +extendfrac : float, optional + The length of the colorbar "extensions" relative to the length of the colorbar. + This is a native matplotlib `~matplotlib.figure.Figure.colorbar` keyword. +extendsize : unit-spec, default: :rc:`colorbar.extend` or :rc:`colorbar.insetextend` + The length of the colorbar "extensions" in physical units. Default is + :rcraw:`colorbar.extend` for outer colorbars and :rcraw:`colorbar.insetextend` + for inset colorbars. %(units.em)s +extendrect : bool, default: False + Whether to draw colorbar "extensions" as rectangles. If ``False`` then + the extensions are drawn as triangles. +locator, ticks : locator-spec, optional + Used to determine the colorbar tick positions. Passed to the + `~proplot.constructor.Locator` constructor function. By default + `~matplotlib.ticker.AutoLocator` is used for continuous color levels + and `~proplot.ticker.DiscreteLocator` is used for discrete color levels. +locator_kw : dict-like, optional + Keyword arguments passed to `matplotlib.ticker.Locator` class. +minorlocator, minorticks + As with `locator`, `ticks` but for the minor ticks. By default + `~matplotlib.ticker.AutoMinorLocator` is used for continuous color levels + and `~proplot.ticker.DiscreteLocator` is used for discrete color levels. +minorlocator_kw + As with `locator_kw`, but for the minor ticks. +format, formatter, ticklabels : formatter-spec, optional + The tick label format. Passed to the `~proplot.constructor.Formatter` + constructor function. +formatter_kw : dict-like, optional + Keyword arguments passed to `matplotlib.ticker.Formatter` class. +frame, frameon : bool, default: :rc:`colorbar.frameon` + For inset colorbars only. Indicates whether to draw a "frame", + just like `~matplotlib.axes.Axes.legend`. +tickminor : bool, optional + Whether to add minor ticks using `~matplotlib.colorbar.ColorbarBase.minorticks_on`. +tickloc, ticklocation : {'bottom', 'top', 'left', 'right'}, optional + Where to draw tick marks on the colorbar. Default is toward the outside + of the subplot for outer colorbars and ``'bottom'`` for inset colorbars. +tickdir, tickdirection : {'out', 'in', 'inout'}, default: :rc:`tick.dir` + Direction of major and minor colorbar ticks. +ticklen : unit-spec, default: :rc:`tick.len` + Major tick lengths for the colorbar ticks. +ticklenratio : float, default: :rc:`tick.lenratio` + Relative scaling of `ticklen` used to determine minor tick lengths. +tickwidth : unit-spec, default: `linewidth` + Major tick widths for the colorbar ticks. + or :rc:`tick.width` if `linewidth` was not passed. +tickwidthratio : float, default: :rc:`tick.widthratio` + Relative scaling of `tickwidth` used to determine minor tick widths. +ticklabelcolor, ticklabelsize, ticklabelweight \ +: default: :rc:`tick.labelcolor`, :rc:`tick.labelsize`, :rc:`tick.labelweight`. + The font color, size, and weight for colorbar tick labels +labelloc, labellocation : {'bottom', 'top', 'left', 'right'} + The colorbar label location. Inherits from `tickloc` by default. Default is toward + the outside of the subplot for outer colorbars and ``'bottom'`` for inset colorbars. +labelcolor, labelsize, labelweight \ +: default: :rc:`label.color`, :rc:`label.size`, and :rc:`label.weight`. + The font color, size, and weight for the colorbar label. +a, alpha, framealpha, fc, facecolor, framecolor, ec, edgecolor, ew, edgewidth : default\ +: :rc:`colorbar.framealpha`, :rc:`colorbar.framecolor` + For inset colorbars only. Controls the transparency and color of + the background frame. +lw, linewidth, c, color : optional + Controls the line width and edge color for both the colorbar + outline and the level dividers. +%(axes.edgefix)s +rasterize : bool, default: :rc:`colorbar.rasterize` + Whether to rasterize the colorbar solids. The matplotlib default was ``True`` + but proplot changes this to ``False`` since rasterization can cause misalignment + between the color patches and the colorbar outline. +**kwargs + Passed to `~matplotlib.figure.Figure.colorbar`. +""" +_edgefix_docstring = """ +edgefix : bool or float, default: :rc:`edgefix` + Whether to fix the common issue where white lines appear between adjacent + patches in saved vector graphics (this can slow down figure rendering). + See this `github repo `__ for a + demonstration of the problem. If ``True``, a small default linewidth of + ``0.3`` is used to cover up the white lines. If float (e.g. ``edgefix=0.5``), + this specific linewidth is used to cover up the white lines. This feature is + automatically disabled when the patches have transparency. +""" +docstring._snippet_manager['axes.edgefix'] = _edgefix_docstring +docstring._snippet_manager['axes.colorbar_args'] = _colorbar_args_docstring +docstring._snippet_manager['axes.colorbar_kwargs'] = _colorbar_kwargs_docstring + + +# Legend docstrings +_legend_args_docstring = """ +handles : list of artist, optional + List of matplotlib artists, or a list of lists of artist instances (see the `center` + keyword). If not passed, artists with valid labels (applied by passing `label` or + `labels` to a plotting command or calling `~matplotlib.artist.Artist.set_label`) + are retrieved automatically. If the object is a `~matplotlib.contour.ContourSet`, + `~matplotlib.contour.ContourSet.legend_elements` is used to select the central + artist in the list (generally useful for single-color contour plots). Note that + proplot's `~proplot.axes.PlotAxes.contour` and `~proplot.axes.PlotAxes.contourf` + accept a legend `label` keyword argument. +labels : list of str, optional + A matching list of string labels or ``None`` placeholders, or a matching list of + lists (see the `center` keyword). Wherever ``None`` appears in the list (or + if no labels were passed at all), labels are retrieved by calling + `~matplotlib.artist.Artist.get_label` on each `~matplotlib.artist.Artist` in the + handle list. If a handle consists of a tuple group of artists, labels are inferred + from the artists in the tuple (if there are multiple unique labels in the tuple + group of artists, the tuple group is expanded into unique legend entries -- + otherwise, the tuple group elements are drawn on top of eachother). For details + on matplotlib legend handlers and tuple groups, see the matplotlib `legend guide \ +`__. +""" +_legend_kwargs_docstring = """ +frame, frameon : bool, optional + Toggles the legend frame. For centered-row legends, a frame + independent from matplotlib's built-in legend frame is created. +ncol, ncols : int, optional + The number of columns. `ncols` is an alias, added + for consistency with `~matplotlib.pyplot.subplots`. +order : {'C', 'F'}, optional + Whether legend handles are drawn in row-major (``'C'``) or column-major + (``'F'``) order. Analagous to `numpy.array` ordering. The matplotlib + default was ``'F'`` but proplot changes this to ``'C'``. +center : bool, optional + Whether to center each legend row individually. If ``True``, we draw + successive single-row legends "stacked" on top of each other. If ``None``, + we infer this setting from `handles`. By default, `center` is set to ``True`` + if `handles` is a list of lists (each sublist is used as a row in the legend). +alphabetize : bool, default: False + Whether to alphabetize the legend entries according to + the legend labels. +title, label : str, optional + The legend title. The `label` keyword is also accepted, for consistency + with `~matplotlib.figure.Figure.colorbar`. +fontsize, fontweight, fontcolor : optional + The font size, weight, and color for the legend text. Font size is interpreted + by `~proplot.utils.units`. The default font size is :rcraw:`legend.fontsize`. +titlefontsize, titlefontweight, titlefontcolor : optional + The font size, weight, and color for the legend title. Font size is interpreted + by `~proplot.utils.units`. The default size is `fontsize`. +borderpad, borderaxespad, handlelength, handleheight, handletextpad, \ +labelspacing, columnspacing : unit-spec, optional + Various matplotlib `~matplotlib.axes.Axes.legend` spacing arguments. + %(units.em)s +a, alpha, framealpha, fc, facecolor, framecolor, ec, edgecolor, ew, edgewidth \ +: default: :rc:`legend.framealpha`, :rc:`legend.facecolor`, :rc:`legend.edgecolor`, \ +:rc:`axes.linewidth` + The opacity, face color, edge color, and edge width for the legend frame. +c, color, lw, linewidth, m, marker, ls, linestyle, dashes, ms, markersize : optional + Properties used to override the legend handles. For example, for a + legend describing variations in line style ignoring variations + in color, you might want to use ``color='black'``. +handle_kw : dict-like, optional + Additional properties used to override legend handles, e.g. + ``handle_kw={'edgecolor': 'black'}``. Only line properties + can be passed as keyword arguments. +handler_map : dict-like, optional + A 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`. +**kwargs + Passed to `~matplotlib.axes.Axes.legend`. +""" +docstring._snippet_manager['axes.legend_args'] = _legend_args_docstring +docstring._snippet_manager['axes.legend_kwargs'] = _legend_kwargs_docstring + + +def _align_bbox(align, length): + """ + Return a simple alignment bounding box for intersection calculations. + """ + if align in ('left', 'bottom'): + bounds = [[0, 0], [length, 0]] + elif align in ('top', 'right'): + bounds = [[1 - length, 0], [1, 0]] + elif align == 'center': + bounds = [[0.5 * (1 - length), 0], [0.5 * (1 + length), 0]] + else: + raise ValueError(f'Invalid align {align!r}.') + return mtransforms.Bbox(bounds) + + +class _TransformedBoundsLocator: + """ + Axes locator for `~Axes.inset_axes` and other axes. + """ + def __init__(self, bounds, transform): + self._bounds = bounds + self._transform = transform + + def __call__(self, ax, renderer): # noqa: U100 + transfig = getattr(ax.figure, 'transSubfigure', ax.figure.transFigure) + bbox = mtransforms.Bbox.from_bounds(*self._bounds) + bbox = mtransforms.TransformedBbox(bbox, self._transform) + bbox = mtransforms.TransformedBbox(bbox, transfig.inverted()) + return bbox + + +class Axes(maxes.Axes): + """ + The lowest-level `~matplotlib.axes.Axes` subclass used by proplot. + Implements basic universal features. + """ + _name = None # derived must override + _name_aliases = () + _make_inset_locator = _TransformedBoundsLocator + + def __repr__(self): + # Show the position in the geometry excluding panels. Panels are + # indicated by showing their parent geometry plus a 'side' argument. + # WARNING: This will not be used in matplotlib 3.3.0 (and probably next + # minor releases) because native __repr__ is defined in SubplotBase. + ax = self._get_topmost_axes() + name = type(self).__name__ + prefix = '' if ax is self else 'parent_' + params = {} + if self._name in ('cartopy', 'basemap'): + name = name.replace('_' + self._name.title(), 'Geo') + params['backend'] = self._name + if self._inset_parent: + name = re.sub('Axes(Subplot)?', 'AxesInset', name) + params['bounds'] = tuple(np.round(self._inset_bounds, 2)) + if self._altx_parent or self._alty_parent: + name = re.sub('Axes(Subplot)?', 'AxesTwin', name) + params['axis'] = 'x' if self._altx_parent else 'y' + if self._colorbar_fill: + name = re.sub('Axes(Subplot)?', 'AxesFill', name) + params['side'] = self._axes._panel_side + if self._panel_side: + name = re.sub('Axes(Subplot)?', 'AxesPanel', name) + params['side'] = self._panel_side + try: + nrows, ncols, num1, num2 = ax.get_subplotspec().get_topmost_subplotspec()._get_geometry() # noqa: E501 + params[prefix + 'index'] = (num1, num2) + except (IndexError, ValueError, AttributeError): # e.g. a loose axes + left, bottom, width, height = np.round(self._position.bounds, 2) + params['left'], params['bottom'], params['size'] = (left, bottom, (width, bottom)) # noqa: E501 + if ax.number: + params[prefix + 'number'] = ax.number + params = ', '.join(f'{key}={value!r}' for key, value in params.items()) + return f'{name}({params})' + + def __str__(self): + return self.__repr__() + + @docstring._snippet_manager + def __init__(self, *args, **kwargs): + """ + Parameters + ---------- + *args + Passed to `matplotlib.axes.Axes`. + %(axes.format)s + + Other parameters + ---------------- + %(rc.init)s + + See also + -------- + Axes.format + matplotlib.axes.Axes + proplot.axes.PlotAxes + proplot.axes.CartesianAxes + proplot.axes.PolarAxes + proplot.axes.GeoAxes + proplot.figure.Figure.subplot + proplot.figure.Figure.add_subplot + """ + # Remove subplot-related args + # NOTE: These are documented on add_subplot() + ss = kwargs.pop('_subplot_spec', None) # see below + number = kwargs.pop('number', None) + autoshare = kwargs.pop('autoshare', None) + autoshare = _not_none(autoshare, True) + + # Remove format-related args and initialize + rc_kw, rc_mode = _pop_rc(kwargs) + kw_format = _pop_props(kwargs, 'patch') # background properties + if 'zorder' in kw_format: # special case: refers to the entire axes + kwargs['zorder'] = kw_format.pop('zorder') + for cls, sig in self._format_signatures.items(): + if isinstance(self, cls): + kw_format.update(_pop_params(kwargs, sig)) + super().__init__(*args, **kwargs) + + # Varous scalar properties + self._active_cycle = rc['axes.prop_cycle'] + self._auto_format = None # manipulated by wrapper functions + self._abc_border_kwargs = {} + self._abc_loc = None + self._abc_title_pad = rc['abc.titlepad'] + self._title_above = rc['title.above'] + self._title_border_kwargs = {} # title border properties + self._title_loc = None + self._title_pad = rc['title.pad'] + self._title_pad_current = None + self._altx_parent = None # for cartesian axes only + self._alty_parent = None + self._colorbar_fill = None + self._inset_parent = None + self._inset_bounds = None # for introspection ony + self._inset_zoom = False + self._inset_zoom_artists = None + self._panel_hidden = False # True when "filled" with cbar/legend + self._panel_align = {} # store 'align' and 'length' for "filled" cbar/legend + self._panel_parent = None + self._panel_share = False + self._panel_sharex_group = False # see _apply_auto_share + self._panel_sharey_group = False # see _apply_auto_share + self._panel_side = None + self._tight_bbox = None # bounding boxes are saved + self.xaxis.isDefault_minloc = True # ensure enabled at start (needed for dual) + self.yaxis.isDefault_minloc = True + + # Various dictionary properties + # NOTE: Critical to use self.text() so they are patched with _update_label + self._legend_dict = {} + self._colorbar_dict = {} + d = self._panel_dict = {} + d['left'] = [] # NOTE: panels will be sorted inside-to-outside + d['right'] = [] + d['bottom'] = [] + d['top'] = [] + d = self._title_dict = {} + kw = {'zorder': 3.5, 'transform': self.transAxes} + d['abc'] = self.text(0, 0, '', **kw) + d['left'] = self._left_title # WARNING: track in case mpl changes this + d['center'] = self.title + d['right'] = self._right_title + d['upper left'] = self.text(0, 0, '', va='top', ha='left', **kw) + d['upper center'] = self.text(0, 0.5, '', va='top', ha='center', **kw) + d['upper right'] = self.text(0, 1, '', va='top', ha='right', **kw) + d['lower left'] = self.text(0, 0, '', va='bottom', ha='left', **kw) + d['lower center'] = self.text(0, 0.5, '', va='bottom', ha='center', **kw) + d['lower right'] = self.text(0, 1, '', va='bottom', ha='right', **kw) + + # Subplot-specific settings + # NOTE: Default number for any axes is None (i.e., no a-b-c labels allowed) + # and for subplots added with add_subplot is incremented automatically + # WARNING: For mpl>=3.4.0 subplotspec assigned *after* initialization using + # set_subplotspec. Tried to defer to setter but really messes up both format() + # and _apply_auto_share(). Instead use workaround: Have Figure.add_subplot pass + # subplotspec as a hidden keyword arg. Non-subplots don't need this arg. + # See: https://github.com/matplotlib/matplotlib/pull/18564 + self._number = None + if number: # not None or False + self.number = number + if ss is not None: # always passed from add_subplot + self.set_subplotspec(ss) + if autoshare: + self._apply_auto_share() + + # Default formatting + # NOTE: This ignores user-input rc_mode. Mode '1' applies proplot + # features which is necessary on first run. Default otherwise is mode '2' + self.format(rc_kw=rc_kw, rc_mode=1, skip_figure=True, **kw_format) + + def _add_inset_axes( + self, bounds, transform=None, *, proj=None, projection=None, + zoom=None, zoom_kw=None, zorder=None, **kwargs + ): + """ + Add an inset axes using arbitrary projection. + """ + # Converting transform to figure-relative coordinates + transform = self._get_transform(transform, 'axes') + locator = self._make_inset_locator(bounds, transform) + bounds = locator(self, None).bounds + label = kwargs.pop('label', 'inset_axes') + zorder = _not_none(zorder, 4) + + # Parse projection and inherit from the current axes by default + # NOTE: The _parse_proj method also accepts axes classes. + proj = _not_none(proj=proj, projection=projection) + if proj is None: + if self._name in ('cartopy', 'basemap'): + proj = copy.copy(self.projection) + else: + proj = self._name + kwargs = self.figure._parse_proj(proj, **kwargs) + + # Create axes and apply locator. The locator lets the axes adjust + # automatically if we used data coords. Called by ax.apply_aspect() + cls = mproj.get_projection_class(kwargs.pop('projection')) + ax = cls(self.figure, bounds, zorder=zorder, label=label, **kwargs) + ax.set_axes_locator(locator) + ax._inset_parent = self + ax._inset_bounds = bounds + self.add_child_axes(ax) + + # Add zoom indicator (NOTE: requires matplotlib >= 3.0) + zoom_default = self._name == 'cartesian' and ax._name == 'cartesian' + zoom = ax._inset_zoom = _not_none(zoom, zoom_default) + if zoom: + zoom_kw = zoom_kw or {} + ax.indicate_inset_zoom(**zoom_kw) + return ax + + def _add_queued_guides(self): + """ + Draw the queued-up legends and colorbars. Wrapper funcs and legend func let + user add handles to location lists with successive calls. + """ + # Draw queued colorbars + for (loc, align), colorbar in tuple(self._colorbar_dict.items()): + if not isinstance(colorbar, tuple): + continue + handles, labels, kwargs = colorbar + cb = self._add_colorbar(handles, labels, loc=loc, align=align, **kwargs) + self._colorbar_dict[(loc, align)] = cb + + # Draw queued legends + # WARNING: Passing empty list labels=[] to legend causes matplotlib + # _parse_legend_args to search for everything. Ensure None if empty. + for (loc, align), legend in tuple(self._legend_dict.items()): + if not isinstance(legend, tuple) or any(isinstance(_, mlegend.Legend) for _ in legend): # noqa: E501 + continue + handles, labels, kwargs = legend + leg = self._add_legend(handles, labels, loc=loc, align=align, **kwargs) + self._legend_dict[(loc, align)] = leg + + def _add_guide_frame( + self, xmin, ymin, width, height, *, fontsize, fancybox=None, **kwargs + ): + """ + Add a colorbar or multilegend frame. + """ + # TODO: Shadow patch does not seem to work. Unsure why. + # TODO: Add basic 'colorbar' and 'legend' artists with + # shared control over background frame. + shadow = kwargs.pop('shadow', None) # noqa: F841 + renderer = self.figure._get_renderer() + fontsize = _fontsize_to_pt(fontsize) + fontsize = (fontsize / 72) / self._get_size_inches()[0] # axes relative units + fontsize = renderer.points_to_pixels(fontsize) + patch = mpatches.FancyBboxPatch( + (xmin, ymin), width, height, + snap=True, + zorder=4.5, + mutation_scale=fontsize, + transform=self.transAxes + ) + patch.set_clip_on(False) + if fancybox: + patch.set_boxstyle('round', pad=0, rounding_size=0.2) + else: + patch.set_boxstyle('square', pad=0) + patch.update(kwargs) + self.add_artist(patch) + return patch + + def _add_guide_panel(self, loc='fill', align='center', length=0, **kwargs): + """ + Add a panel to be filled by an "outer" colorbar or legend. + """ + # NOTE: For colorbars we include 'length' when determining whether to allocate + # new panel but for legend just test whether that 'align' position was filled. + # WARNING: Hide content but 1) do not use ax.set_visible(False) so that + # tight layout will include legend and colorbar and 2) do not use + # ax.clear() so that top panel title and a-b-c label can remain. + bbox = _align_bbox(align, length) + if loc == 'fill': + ax = self + elif loc in ('left', 'right', 'top', 'bottom'): + ax = None + for pax in self._panel_dict[loc]: + if not pax._panel_hidden or align in pax._panel_align: + continue + if not any(bbox.overlaps(b) for b in pax._panel_align.values()): + ax = pax + break + if ax is None: + ax = self.panel_axes(loc, filled=True, **kwargs) + else: + raise ValueError(f'Invalid filled panel location {loc!r}.') + for s in ax.spines.values(): + s.set_visible(False) + ax.xaxis.set_visible(False) + ax.yaxis.set_visible(False) + ax.patch.set_facecolor('none') + ax._panel_hidden = True + ax._panel_align[align] = bbox + return ax + + @warnings._rename_kwargs('0.10', rasterize='rasterized') + def _add_colorbar( + self, mappable, values=None, *, + loc=None, align=None, space=None, pad=None, + width=None, length=None, shrink=None, + label=None, title=None, reverse=False, + rotation=None, grid=None, edges=None, drawedges=None, + extend=None, extendsize=None, extendfrac=None, + ticks=None, locator=None, locator_kw=None, + format=None, formatter=None, ticklabels=None, formatter_kw=None, + minorticks=None, minorlocator=None, minorlocator_kw=None, + tickminor=None, ticklen=None, ticklenratio=None, + tickdir=None, tickdirection=None, tickwidth=None, tickwidthratio=None, + ticklabelsize=None, ticklabelweight=None, ticklabelcolor=None, + labelloc=None, labellocation=None, labelsize=None, labelweight=None, + labelcolor=None, c=None, color=None, lw=None, linewidth=None, + edgefix=None, rasterized=None, **kwargs + ): + """ + The driver function for adding axes colorbars. + """ + # Parse input arguments and apply defaults + # TODO: Get the 'best' inset colorbar location using the legend algorithm + # and implement inset colorbars the same as inset legends. + grid = _not_none(grid=grid, edges=edges, drawedges=drawedges, default=rc['colorbar.grid']) # noqa: E501 + length = _not_none(length=length, shrink=shrink) + label = _not_none(title=title, label=label) + labelloc = _not_none(labelloc=labelloc, labellocation=labellocation) + locator = _not_none(ticks=ticks, locator=locator) + formatter = _not_none(ticklabels=ticklabels, formatter=formatter, format=format) + minorlocator = _not_none(minorticks=minorticks, minorlocator=minorlocator) + color = _not_none(c=c, color=color, default=rc['axes.edgecolor']) + linewidth = _not_none(lw=lw, linewidth=linewidth) + ticklen = units(_not_none(ticklen, rc['tick.len']), 'pt') + tickdir = _not_none(tickdir=tickdir, tickdirection=tickdirection) + tickwidth = units(_not_none(tickwidth, linewidth, rc['tick.width']), 'pt') + linewidth = units(_not_none(linewidth, default=rc['axes.linewidth']), 'pt') + ticklenratio = _not_none(ticklenratio, rc['tick.lenratio']) + tickwidthratio = _not_none(tickwidthratio, rc['tick.widthratio']) + rasterized = _not_none(rasterized, rc['colorbar.rasterized']) + if extendsize is not None and extendfrac is not None: + warnings._warn_proplot( + f'You cannot specify both an absolute extendsize={extendsize!r} ' + f"and a relative extendfrac={extendfrac!r}. Ignoring 'extendfrac'." + ) + extendfrac = None + + # Build label and locator keyword argument dicts + # NOTE: This carefully handles the 'maxn' and 'maxn_minor' deprecations + kw_label = {} + locator_kw = locator_kw or {} + formatter_kw = formatter_kw or {} + minorlocator_kw = minorlocator_kw or {} + for key, value in ( + ('size', labelsize), + ('weight', labelweight), + ('color', labelcolor), + ): + if value is not None: + kw_label[key] = value + kw_ticklabels = {} + for key, value in ( + ('size', ticklabelsize), + ('weight', ticklabelweight), + ('color', ticklabelcolor), + ('rotation', rotation), + ): + if value is not None: + kw_ticklabels[key] = value + for b, kw in enumerate((locator_kw, minorlocator_kw)): + key = 'maxn_minor' if b else 'maxn' + name = 'minorlocator' if b else 'locator' + nbins = kwargs.pop('maxn_minor' if b else 'maxn', None) + if nbins is not None: + kw['nbins'] = nbins + warnings._warn_proplot( + f'The colorbar() keyword {key!r} was deprecated in v0.10. To ' + "achieve the same effect, you can pass 'nbins' to the new default " + f"locator DiscreteLocator using {name}_kw={{'nbins': {nbins}}}." + ) + + # Generate and prepare the colorbar axes + # NOTE: The inset axes function needs 'label' to know how to pad the box + # TODO: Use seperate keywords for frame properties vs. colorbar edge properties? + # TODO: Have extendsize auto-adjust to the subplot size as it changes + fill = loc in ('fill', 'left', 'right', 'top', 'bottom') + if not fill: + extendsize = _not_none(extendsize, rc['colorbar.insetextend']) + kwargs.update({'label': label, 'length': length, 'width': width}) + cax, kwargs = self._parse_colorbar_inset(loc=loc, pad=pad, **kwargs) # noqa: E501 + else: + extendsize = _not_none(extendsize, rc['colorbar.extend']) + length = _not_none(length, rc['colorbar.length']) # for _add_guide_panel + kwargs.update({'align': align, 'length': length}) + kw = {'width': width, 'space': space, 'pad': pad} + ax = self._add_guide_panel(loc, align, length, **kw) + cax, kwargs = ax._parse_colorbar_filled(**kwargs) + vert = kwargs['orientation'] == 'vertical' + if extendfrac is None: # compute extendsize + width, height = cax._get_size_inches() + axsize = height if vert else width + extendsize = units(extendsize, 'em', 'in') + extendfrac = extendsize / max(axsize - 2 * extendsize, units(1, 'em', 'in')) + + # Parse the colorbar mappable + # NOTE: Account for special case where auto colorbar is generated from 1D + # methods that construct an 'artist list' (i.e. colormap scatter object) + if np.iterable(mappable) and len(mappable) == 1 and isinstance(mappable[0], mcm.ScalarMappable): # noqa: E501 + mappable = mappable[0] + if not isinstance(mappable, mcm.ScalarMappable): + mappable, kwargs = cax._parse_colorbar_arg(mappable, values, **kwargs) + else: + pop = _pop_params(kwargs, cax._parse_colorbar_arg, ignore_internal=True) + if pop: + warnings._warn_proplot( + f'Input is already a ScalarMappable. ' + f'Ignoring unused keyword arg(s): {pop}' + ) + + # Parse the tick locators and formatters + # NOTE: In presence of BoundaryNorm or similar handle ticks with special + # DiscreteLocator or else get issues (see mpl #22233). + norm = mappable.norm + source = getattr(norm, '_norm', None) + vcenter = getattr(source, 'vcenter', None) + vcenter = {} if vcenter is None else {'vcenter': vcenter} + formatter = _not_none(formatter, getattr(norm, '_labels', None), 'auto') + formatter = constructor.Formatter(formatter, **formatter_kw) + categorical = isinstance(formatter, mticker.FixedFormatter) + if locator is not None: + locator = constructor.Locator(locator, **locator_kw) + if minorlocator is not None: # overrides tickminor + minorlocator = constructor.Locator(minorlocator, **minorlocator_kw) + elif tickminor is None: + tickminor = False if categorical else rc['xy'[vert] + 'tick.minor.visible'] + if isinstance(norm, mcolors.BoundaryNorm): # DiscreteNorm or BoundaryNorm + ticks = getattr(norm, '_ticks', norm.boundaries) + segmented = isinstance(source, pcolors.SegmentedNorm) + if locator is None: + if categorical or segmented: + locator = mticker.FixedLocator(ticks) + else: + locator = pticker.DiscreteLocator(ticks, **vcenter) + if tickminor and minorlocator is None: + minorlocator = pticker.DiscreteLocator(ticks, minor=True, **vcenter) + + # Special handling for colorbar keyword arguments + # WARNING: Critical to not pass empty major locators in matplotlib < 3.5 + # See this issue: https://github.com/proplot-dev/proplot/issues/301 + # WARNING: Proplot 'supports' passing one extend to a mappable function + # then overwriting by passing another 'extend' to colobar. But contour + # colorbars break when you try to change its 'extend'. Matplotlib gets + # around this by just silently ignoring 'extend' passed to colorbar() but + # we issue warning. Also note ContourSet.extend existed in matplotlib 3.0. + # WARNING: Confusingly the only default way to have auto-adjusting + # colorbar ticks is to specify no locator. Then _get_ticker_locator_formatter + # uses the default ScalarFormatter on the axis that already has a set axis. + # Otherwise it sets a default axis with locator.create_dummy_axis() in + # update_ticks() which does not track axis size. Workaround is to manually + # set the locator and formatter axis... however this messes up colorbar lengths + # in matplotlib < 3.2. So we only apply this conditionally and in earlier + # verisons recognize that DiscreteLocator will behave like FixedLocator. + axis = cax.yaxis if vert else cax.xaxis + if not isinstance(mappable, mcontour.ContourSet): + extend = _not_none(extend, 'neither') + kwargs['extend'] = extend + elif extend is not None and extend != mappable.extend: + warnings._warn_proplot( + 'Ignoring extend={extend!r}. ContourSet extend cannot be changed.' + ) + if ( + isinstance(locator, mticker.NullLocator) + or hasattr(locator, 'locs') and len(locator.locs) == 0 + ): + minorlocator, tickminor = None, False # attempted fix + for ticker in (locator, formatter, minorlocator): + if _version_mpl < '3.2': + pass # see notes above + elif isinstance(ticker, mticker.TickHelper): + ticker.set_axis(axis) + + # Create colorbar and update ticks and axis direction + # NOTE: This also adds the guides._update_ticks() monkey patch that triggers + # updates to DiscreteLocator when parent axes is drawn. + obj = cax._colorbar_fill = cax.figure.colorbar( + mappable, cax=cax, ticks=locator, format=formatter, + drawedges=grid, extendfrac=extendfrac, **kwargs + ) + obj.minorlocator = minorlocator # backwards compatibility + obj.update_ticks = guides._update_ticks.__get__(obj) # backwards compatible + if minorlocator is not None: + obj.update_ticks() + elif tickminor: + obj.minorticks_on() + else: + obj.minorticks_off() + if getattr(norm, 'descending', None): + axis.set_inverted(True) + if reverse: # potentially double reverse, although that would be weird... + axis.set_inverted(True) + + # Update other colorbar settings + # WARNING: Must use the colorbar set_label to set text. Calling set_label + # on the actual axis will do nothing! + axis.set_tick_params(which='both', color=color, direction=tickdir) + axis.set_tick_params(which='major', length=ticklen, width=tickwidth) + axis.set_tick_params(which='minor', length=ticklen * ticklenratio, width=tickwidth * tickwidthratio) # noqa: E501 + if label is not None: + obj.set_label(label) + if labelloc is not None: + axis.set_label_position(labelloc) + axis.label.update(kw_label) + for label in axis.get_ticklabels(): + label.update(kw_ticklabels) + kw_outline = {'edgecolor': color, 'linewidth': linewidth} + if obj.outline is not None: + obj.outline.update(kw_outline) + if obj.dividers is not None: + obj.dividers.update(kw_outline) + if obj.solids: + from . import PlotAxes + obj.solids.set_rasterized(rasterized) + PlotAxes._fix_patch_edges(obj.solids, edgefix=edgefix) + + # Register location and return + self._register_guide('colorbar', obj, (loc, align)) # possibly replace another + return obj + + def _add_legend( + self, handles=None, labels=None, *, + loc=None, align=None, width=None, pad=None, space=None, + frame=None, frameon=None, ncol=None, ncols=None, + alphabetize=False, center=None, order=None, label=None, title=None, + fontsize=None, fontweight=None, fontcolor=None, + titlefontsize=None, titlefontweight=None, titlefontcolor=None, + handle_kw=None, handler_map=None, **kwargs + ): + """ + The driver function for adding axes legends. + """ + # Parse input argument units + ncol = _not_none(ncols=ncols, ncol=ncol) + order = _not_none(order, 'C') + frameon = _not_none(frame=frame, frameon=frameon, default=rc['legend.frameon']) + fontsize = _not_none(fontsize, rc['legend.fontsize']) + titlefontsize = _not_none( + title_fontsize=kwargs.pop('title_fontsize', None), + titlefontsize=titlefontsize, + default=rc['legend.title_fontsize'] + ) + fontsize = _fontsize_to_pt(fontsize) + titlefontsize = _fontsize_to_pt(titlefontsize) + if order not in ('F', 'C'): + raise ValueError( + f'Invalid order {order!r}. Please choose from ' + "'C' (row-major, default) or 'F' (column-major)." + ) + + # Convert relevant keys to em-widths + for setting in rcsetup.EM_KEYS: # em-width keys + pair = setting.split('legend.', 1) + if len(pair) == 1: + continue + _, key = pair + value = kwargs.pop(key, None) + if isinstance(value, str): + value = units(kwargs[key], 'em', fontsize=fontsize) + if value is not None: + kwargs[key] = value + + # Generate and prepare the legend axes + if loc in ('fill', 'left', 'right', 'top', 'bottom'): + lax = self._add_guide_panel(loc, align, width=width, space=space, pad=pad) + kwargs.setdefault('borderaxespad', 0) + if not frameon: + kwargs.setdefault('borderpad', 0) + try: + kwargs['loc'] = ALIGN_OPTS[lax._panel_side][align] + except KeyError: + raise ValueError(f'Invalid align={align!r} for legend loc={loc!r}.') + else: + lax = self + pad = kwargs.pop('borderaxespad', pad) + kwargs['loc'] = loc # simply pass to legend + kwargs['borderaxespad'] = units(pad, 'em', fontsize=fontsize) + + # Handle and text properties that are applied after-the-fact + # NOTE: Set solid_capstyle to 'butt' so line does not extend past error bounds + # shading in legend entry. This change is not noticable in other situations. + kw_frame, kwargs = lax._parse_frame('legend', **kwargs) + kw_text = {} + if fontcolor is not None: + kw_text['color'] = fontcolor + if fontweight is not None: + kw_text['weight'] = fontweight + kw_title = {} + if titlefontcolor is not None: + kw_title['color'] = titlefontcolor + if titlefontweight is not None: + kw_title['weight'] = titlefontweight + kw_handle = _pop_props(kwargs, 'line') + kw_handle.setdefault('solid_capstyle', 'butt') + kw_handle.update(handle_kw or {}) + + # Parse the legend arguments using axes for auto-handle detection + # TODO: Update this when we no longer use "filled panels" for outer legends + pairs, multi = lax._parse_legend_handles( + handles, labels, ncol=ncol, order=order, center=center, + alphabetize=alphabetize, handler_map=handler_map + ) + title = _not_none(label=label, title=title) + kwargs.update( + { + 'title': title, + 'frameon': frameon, + 'fontsize': fontsize, + 'handler_map': handler_map, + 'title_fontsize': titlefontsize, + } + ) + + # Add the legend and update patch properties + # TODO: Add capacity for categorical labels in a single legend like seaborn + # rather than manual handle overrides with multiple legends. + if multi: + objs = lax._parse_legend_centered(pairs, kw_frame=kw_frame, **kwargs) + else: + kwargs.update({key: kw_frame.pop(key) for key in ('shadow', 'fancybox')}) + objs = [lax._parse_legend_aligned(pairs, ncol=ncol, order=order, **kwargs)] + objs[0].legendPatch.update(kw_frame) + for obj in objs: + if hasattr(lax, 'legend_') and lax.legend_ is None: + lax.legend_ = obj # make first legend accessible with get_legend() + else: + lax.add_artist(obj) + + # Update legend patch and elements + # WARNING: legendHandles only contains the *first* artist per legend because + # HandlerBase.legend_artist() called in Legend._init_legend_box() only + # returns the first artist. Instead we try to iterate through offset boxes. + for obj in objs: + obj.set_clip_on(False) # needed for tight bounding box calculations + box = getattr(obj, '_legend_handle_box', None) + for obj in guides._iter_children(box): + if isinstance(obj, mtext.Text): + kw = kw_text + else: + kw = {key: val for key, val in kw_handle.items() if hasattr(obj, 'set_' + key)} # noqa: E501 + if hasattr(obj, 'set_sizes') and 'markersize' in kw_handle: + kw['sizes'] = np.atleast_1d(kw_handle['markersize']) + obj.update(kw) + + # Register location and return + if isinstance(objs[0], mpatches.FancyBboxPatch): + objs = objs[1:] + obj = objs[0] if len(objs) == 1 else tuple(objs) + self._register_guide('legend', obj, (loc, align)) # possibly replace another + + return obj + + def _apply_title_above(self): + """ + Change assignment of outer titles between main subplot and upper panels. + This is called when a panel is created or `_update_title` is called. + """ + # NOTE: Similar to how _apply_axis_sharing() is called in _align_axis_labels() + # this is called in _align_super_labels() so we get the correct offset. + paxs = self._panel_dict['top'] + if not paxs: + return + pax = paxs[-1] + names = ('left', 'center', 'right') + if self._abc_loc in names: + names += ('abc',) + if not self._title_above: + return + if pax._panel_hidden and self._title_above == 'panels': + return + pax._title_pad = self._title_pad + pax._abc_title_pad = self._abc_title_pad + for name in names: + labels._transfer_label(self._title_dict[name], pax._title_dict[name]) + + def _apply_auto_share(self): + """ + Automatically configure axis sharing based on the horizontal and + vertical extent of subplots in the figure gridspec. + """ + # Panel axes sharing, between main subplot and its panels + # NOTE: _panel_share means "include this panel in the axis sharing group" while + # _panel_sharex_group indicates the group itself and may include main axes + def shared(paxs): + return [pax for pax in paxs if not pax._panel_hidden and pax._panel_share] + + # Internal axis sharing, share stacks of panels and main axes with each other + # NOTE: This is called on the main axes whenver a panel is created. + # NOTE: This block is why, even though we have figure-wide share[xy], we + # still need the axes-specific _share[xy]_override attribute. + if not self._panel_side: # this is a main axes + # Top and bottom + bottom = self + paxs = shared(self._panel_dict['bottom']) + if paxs: + bottom = paxs[-1] + bottom._panel_sharex_group = False + for iax in (self, *paxs[:-1]): + iax._panel_sharex_group = True + iax._sharex_setup(bottom) # parent is bottom-most + paxs = shared(self._panel_dict['top']) + for iax in paxs: + iax._panel_sharex_group = True + iax._sharex_setup(bottom) + # Left and right + # NOTE: Order of panel lists is always inside-to-outside + left = self + paxs = shared(self._panel_dict['left']) + if paxs: + left = paxs[-1] + left._panel_sharey_group = False + for iax in (self, *paxs[:-1]): + iax._panel_sharey_group = True + iax._sharey_setup(left) # parent is left-most + paxs = shared(self._panel_dict['right']) + for iax in paxs: + iax._panel_sharey_group = True + iax._sharey_setup(left) + + # External axes sharing, sometimes overrides panel axes sharing + # Share x axes + parent, *children = self._get_share_axes('x') + for child in children: + child._sharex_setup(parent) + # Share y axes + parent, *children = self._get_share_axes('y') + for child in children: + child._sharey_setup(parent) + # Global sharing, use the reference subplot because why not + ref = self.figure._subplot_dict.get(self.figure._refnum, None) + if self is not ref: + if self.figure._sharex > 3: + self._sharex_setup(ref, labels=False) + if self.figure._sharey > 3: + self._sharey_setup(ref, labels=False) + + def _artist_fully_clipped(self, artist): + """ + Return a boolean flag, ``True`` if the artist is clipped to the axes + and can thus be skipped in layout calculations. + """ + clip_box = artist.get_clip_box() + clip_path = artist.get_clip_path() + types_noclip = ( + maxes.Axes, maxis.Axis, moffsetbox.AnnotationBbox, moffsetbox.OffsetBox + ) + return not isinstance(artist, types_noclip) and ( + artist.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.bbox.extents) + ) + and ( + clip_path is None + or isinstance(clip_path, mtransforms.TransformedPatchPath) + and clip_path._patch is self.patch + ) + ) + + def _get_legend_handles(self, handler_map=None): + """ + Internal implementation of matplotlib's ``get_legend_handles_labels``. + """ + if not self._panel_hidden: # this is a normal axes + axs = [self] + elif self._panel_parent: # this is an axes-wide legend + axs = list(self._panel_parent._iter_axes(hidden=False, children=True)) + else: # this is a figure-wide legend + axs = list(self.figure._iter_axes(hidden=False, children=True)) + handles = [] + handler_map_full = mlegend.Legend.get_default_handler_map() + handler_map_full = handler_map_full.copy() + handler_map_full.update(handler_map or {}) + for ax in axs: + for attr in ('lines', 'patches', 'collections', 'containers'): + for handle in getattr(ax, attr, []): # guard against API changes + label = handle.get_label() + handler = mlegend.Legend.get_legend_handler(handler_map_full, handle) # noqa: E501 + if handler and label and label[0] != '_': + handles.append(handle) + return handles + + def _get_share_axes(self, sx, panels=False): + """ + Return the axes whose horizontal or vertical extent in the main gridspec + matches the horizontal or vertical extent of this axes. + """ + # NOTE: The lefmost or bottommost axes are at the start of the list. + if not isinstance(self, maxes.SubplotBase): + return [self] + i = 0 if sx == 'x' else 1 + sy = 'y' if sx == 'x' else 'x' + argfunc = np.argmax if sx == 'x' else np.argmin + irange = self._range_subplotspec(sx) + axs = self.figure._iter_axes(hidden=False, children=False, panels=panels) + axs = [ax for ax in axs if ax._range_subplotspec(sx) == irange] + axs = list({self, *axs}) # self may be missing during initialization + pax = axs.pop(argfunc([ax._range_subplotspec(sy)[i] for ax in axs])) + return [pax, *axs] # return with leftmost or bottommost first + + def _get_span_axes(self, side, panels=False): + """ + Return the axes whose left, right, top, or bottom sides abutt against + the same row or column as this axes. Deflect to shared panels. + """ + if side not in ('left', 'right', 'bottom', 'top'): + raise ValueError(f'Invalid side {side!r}.') + if not isinstance(self, maxes.SubplotBase): + return [self] + x, y = 'xy' if side in ('left', 'right') else 'yx' + idx = 0 if side in ('left', 'top') else 1 # which side to test + coord = self._range_subplotspec(x)[idx] # side for a particular axes + axs = self.figure._iter_axes(hidden=False, children=False, panels=panels) + axs = [ax for ax in axs if ax._range_subplotspec(x)[idx] == coord] or [self] + out = [] + for ax in axs: + other = getattr(ax, '_share' + y) + if other and other._panel_parent: # this is a shared panel + ax = other + out.append(ax) + return out + + def _get_size_inches(self): + """ + Return the width and height of the axes in inches. + """ + width, height = self.figure.get_size_inches() + bbox = self.get_position() + width = width * abs(bbox.width) + height = height * abs(bbox.height) + return np.array([width, height]) + + def _get_topmost_axes(self): + """ + Return the topmost axes including panels and parents. + """ + for _ in range(5): + self = self._axes or self + self = self._panel_parent or self + return self + + def _get_transform(self, transform, default='data'): + """ + Translates user input transform. Also used in an axes method. + """ + # TODO: Can this support cartopy transforms? Seems not when this + # is used for inset axes bounds but maybe in other places? + transform = _not_none(transform, default) + if isinstance(transform, mtransforms.Transform): + return transform + elif CRS is not object and isinstance(transform, CRS): + return transform + elif PlateCarree is not object and transform == 'map': + return PlateCarree() + elif transform == 'data': + return self.transData + elif transform == 'axes': + return self.transAxes + elif transform == 'figure': + return self.figure.transFigure + elif transform == 'subfigure': + return self.figure.transSubfigure + else: + raise ValueError(f'Unknown transform {transform!r}.') + + def _register_guide(self, guide, obj, key, **kwargs): + """ + Queue up or replace objects for legends and list-of-artist style colorbars. + """ + # Initial stuff + if guide not in ('legend', 'colorbar'): + raise TypeError(f'Invalid type {guide!r}.') + dict_ = self._legend_dict if guide == 'legend' else self._colorbar_dict + + # Remove previous instances + # NOTE: No good way to remove inset colorbars right now until the bounding + # box and axes are merged into some kind of subclass. Just fine for now. + if key in dict_ and not isinstance(dict_[key], tuple): + prev = dict_.pop(key) # possibly pop a queued object + if guide == 'colorbar': + pass + elif hasattr(self, 'legend_') and prev.axes.legend_ is prev: + self.legend_ = None # was never added as artist + else: + prev.remove() # remove legends and inner colorbars + + # Replace with instance or update the queue + # NOTE: This is valid for both mappable-values pairs and handles-labels pairs + if not isinstance(obj, tuple) or any(isinstance(_, mlegend.Legend) for _ in obj): # noqa: E501 + dict_[key] = obj + else: + handles, labels = obj + if not np.iterable(handles) or type(handles) is tuple: + handles = [handles] + if not np.iterable(labels) or isinstance(labels, str): + labels = [labels] * len(handles) + length = min(len(handles), len(labels)) # mimics 'zip' behavior + handles_full, labels_full, kwargs_full = dict_.setdefault(key, ([], [], {})) + handles_full.extend(handles[:length]) + labels_full.extend(labels[:length]) + kwargs_full.update(kwargs) + + def _update_guide( + self, objs, legend=None, legend_kw=None, queue_legend=True, + colorbar=None, colorbar_kw=None, queue_colorbar=True, + ): + """ + Update queues for on-the-fly legends and colorbars or track keyword arguments. + """ + # WARNING: Important to always cache the keyword arguments so e.g. + # duplicate subsequent calls still enforce user and default behavior. + # WARNING: This should generally be last in the pipeline before calling + # the plot function or looping over data columns. The colormap parser + # and standardize functions both modify colorbar_kw and legend_kw. + legend_kw = legend_kw or {} + colorbar_kw = colorbar_kw or {} + guides._cache_guide_kw(objs, 'legend', legend_kw) + guides._cache_guide_kw(objs, 'colorbar', colorbar_kw) + if legend: + align = legend_kw.pop('align', None) + queue = legend_kw.pop('queue', queue_legend) + self.legend(objs, loc=legend, align=align, queue=queue, **legend_kw) + if colorbar: + align = colorbar_kw.pop('align', None) + queue = colorbar_kw.pop('queue', queue_colorbar) + self.colorbar(objs, loc=colorbar, align=align, queue=queue, **colorbar_kw) + + @staticmethod + def _parse_frame(guide, fancybox=None, shadow=None, **kwargs): + """ + Parse frame arguments. + """ + # NOTE: Here we permit only 'edgewidth' to avoid conflict with + # 'linewidth' used for legend handles and colorbar edge. + kw_frame = _pop_kwargs( + kwargs, + alpha=('a', 'framealpha', 'facealpha'), + facecolor=('fc', 'framecolor'), + edgecolor=('ec',), + edgewidth=('ew',), + ) + _kw_frame_default = { + 'alpha': f'{guide}.framealpha', + 'facecolor': f'{guide}.facecolor', + 'edgecolor': f'{guide}.edgecolor', + 'edgewidth': 'axes.linewidth', + } + for key, name in _kw_frame_default.items(): + kw_frame.setdefault(key, rc[name]) + for key in ('facecolor', 'edgecolor'): + if kw_frame[key] == 'inherit': + kw_frame[key] = rc['axes.' + key] + kw_frame['linewidth'] = kw_frame.pop('edgewidth') + kw_frame['fancybox'] = _not_none(fancybox, rc[f'{guide}.fancybox']) + kw_frame['shadow'] = _not_none(shadow, rc[f'{guide}.shadow']) + return kw_frame, kwargs + + @staticmethod + def _parse_colorbar_arg( + mappable, values=None, norm=None, norm_kw=None, vmin=None, vmax=None, **kwargs + ): + """ + Generate a mappable from flexible non-mappable input. Useful in bridging + the gap between legends and colorbars (e.g., creating colorbars from line + objects whose data values span a natural colormap range). + """ + # For container objects, we just assume color is the same for every item. + # Works for ErrorbarContainer, StemContainer, BarContainer. + if ( + np.iterable(mappable) + and len(mappable) > 0 + and all(isinstance(obj, mcontainer.Container) for obj in mappable) + ): + mappable = [obj[0] for obj in mappable] + + # Colormap instance + if isinstance(mappable, mcolors.Colormap) or isinstance(mappable, str): + cmap = constructor.Colormap(mappable) + if values is None and isinstance(cmap, pcolors.DiscreteColormap): + values = [None] * cmap.N # sometimes use discrete norm + + # List of colors + elif np.iterable(mappable) and all(map(mcolors.is_color_like, mappable)): + cmap = pcolors.DiscreteColormap(list(mappable), '_no_name') + if values is None: + values = [None] * len(mappable) # always use discrete norm + + # List of artists + # NOTE: Do not check for isinstance(Artist) in case it is an mpl collection + elif np.iterable(mappable) and all( + hasattr(obj, 'get_color') or hasattr(obj, 'get_facecolor') for obj in mappable # noqa: E501 + ): + # Generate colormap from colors and infer tick labels + colors = [] + for obj in mappable: + if hasattr(obj, 'update_scalarmappable'): # for e.g. pcolor + obj.update_scalarmappable() + color = obj.get_color() if hasattr(obj, 'get_color') else obj.get_facecolor() # noqa: E501 + if isinstance(color, np.ndarray): + color = color.squeeze() # e.g. single color scatter plot + if not mcolors.is_color_like(color): + raise ValueError('Cannot make colorbar from artists with more than one color.') # noqa: E501 + colors.append(color) + # Try to infer tick values and tick labels from Artist labels + cmap = pcolors.DiscreteColormap(colors, '_no_name') + if values is None: + values = [None] * len(mappable) + else: + values = list(values) + for i, (obj, val) in enumerate(zip(mappable, values)): + if val is not None: + continue + val = obj.get_label() + if val and val[0] == '_': + continue + values[i] = val + + else: + raise ValueError( + 'Input colorbar() argument must be a scalar mappable, colormap name ' + f'or object, list of colors, or list of artists. Got {mappable!r}.' + ) + + # Generate continuous normalizer, and possibly discrete normalizer. Update + # the outgoing locator and formatter if user does not override. + norm_kw = norm_kw or {} + norm = norm or 'linear' + vmin = _not_none(vmin=vmin, norm_kw_vmin=norm_kw.pop('vmin', None), default=0) + vmax = _not_none(vmax=vmax, norm_kw_vmax=norm_kw.pop('vmax', None), default=1) + norm = constructor.Norm(norm, vmin=vmin, vmax=vmax, **norm_kw) + if values is not None: + ticks = [] + labels = None + for i, val in enumerate(values): + try: + val = float(val) + except (TypeError, ValueError): + pass + if val is None: + val = i + ticks.append(val) + if any(isinstance(_, str) for _ in ticks): + labels = list(map(str, ticks)) + ticks = np.arange(len(ticks)) + if len(ticks) == 1: + levels = [ticks[0] - 1, ticks[0] + 1] + else: + levels = edges(ticks) + from . import PlotAxes + norm, cmap, _ = PlotAxes._parse_level_norm( + levels, norm, cmap, discrete_ticks=ticks, discrete_labels=labels + ) + + # Return ad hoc ScalarMappable and update locator and formatter + # NOTE: If value list doesn't match this may cycle over colors. + mappable = mcm.ScalarMappable(norm, cmap) + return mappable, kwargs + + def _parse_colorbar_filled( + self, length=None, align=None, tickloc=None, ticklocation=None, + orientation=None, **kwargs + ): + """ + Return the axes and adjusted keyword args for a panel-filling colorbar. + """ + # Parse input arguments + side = self._panel_side + side = _not_none(side, 'left' if orientation == 'vertical' else 'bottom') + align = _not_none(align, 'center') + length = _not_none(length=length, default=rc['colorbar.length']) + ticklocation = _not_none(tickloc=tickloc, ticklocation=ticklocation) + + # Calculate inset bounds for the colorbar + delta = 0.5 * (1 - length) + if side in ('bottom', 'top'): + if align == 'left': + bounds = (0, 0, length, 1) + elif align == 'center': + bounds = (delta, 0, length, 1) + elif align == 'right': + bounds = (2 * delta, 0, length, 1) + else: + raise ValueError(f'Invalid align={align!r} for colorbar loc={side!r}.') + else: + if align == 'bottom': + bounds = (0, 0, 1, length) + elif align == 'center': + bounds = (0, delta, 1, length) + elif align == 'top': + bounds = (0, 2 * delta, 1, length) + else: + raise ValueError(f'Invalid align={align!r} for colorbar loc={side!r}.') + + # Add the axes as a child of the original axes + cls = mproj.get_projection_class('proplot_cartesian') + locator = self._make_inset_locator(bounds, self.transAxes) + ax = cls(self.figure, locator(self, None).bounds, zorder=5) + ax.set_axes_locator(locator) + self.add_child_axes(ax) + ax.patch.set_facecolor('none') # ignore axes.alpha application + + # Handle default keyword args + if orientation is None: + orientation = 'horizontal' if side in ('bottom', 'top') else 'vertical' + if orientation == 'horizontal': + outside, inside = 'bottom', 'top' + if side == 'top': + outside, inside = inside, outside + ticklocation = _not_none(ticklocation, outside) + else: + outside, inside = 'left', 'right' + if side == 'right': + outside, inside = inside, outside + ticklocation = _not_none(ticklocation, outside) + kwargs.update({'orientation': orientation, 'ticklocation': ticklocation}) + return ax, kwargs + + def _parse_colorbar_inset( + self, loc=None, width=None, length=None, shrink=None, + frame=None, frameon=None, label=None, pad=None, + tickloc=None, ticklocation=None, orientation=None, **kwargs, + ): + """ + Return the axes and adjusted keyword args for an inset colorbar. + """ + # Basic colorbar properties + frame = _not_none(frame=frame, frameon=frameon, default=rc['colorbar.frameon']) + length = _not_none(length=length, shrink=shrink, default=rc['colorbar.insetlength']) # noqa: E501 + width = _not_none(width, rc['colorbar.insetwidth']) + pad = _not_none(pad, rc['colorbar.insetpad']) + length = units(length, 'em', 'ax', axes=self, width=True) # x direction + width = units(width, 'em', 'ax', axes=self, width=False) # y direction + xpad = units(pad, 'em', 'ax', axes=self, width=True) + ypad = units(pad, 'em', 'ax', axes=self, width=False) + + # Extra space accounting for colorbar label and tick labels + labspace = rc['xtick.major.size'] / 72 + fontsize = rc['xtick.labelsize'] + fontsize = _fontsize_to_pt(fontsize) + if label is not None: + labspace += 2.4 * fontsize / 72 + else: + labspace += 1.2 * fontsize / 72 + labspace /= self._get_size_inches()[1] # space for labels + + # Location in axes-relative coordinates + # Bounds are x0, y0, width, height in axes-relative coordinates + if loc == 'upper right': + bounds_inset = [1 - xpad - length, 1 - ypad - width] + bounds_frame = [1 - 2 * xpad - length, 1 - 2 * ypad - width - labspace] + elif loc == 'upper left': + bounds_inset = [xpad, 1 - ypad - width] + bounds_frame = [0, 1 - 2 * ypad - width - labspace] + elif loc == 'lower left': + bounds_inset = [xpad, ypad + labspace] + bounds_frame = [0, 0] + else: + bounds_inset = [1 - xpad - length, ypad + labspace] + bounds_frame = [1 - 2 * xpad - length, 0] + bounds_inset.extend((length, width)) # inset axes + bounds_frame.extend((2 * xpad + length, 2 * ypad + width + labspace)) + + # Make axes and frame with zorder matching default legend zorder + cls = mproj.get_projection_class('proplot_cartesian') + locator = self._make_inset_locator(bounds_inset, self.transAxes) + ax = cls(self.figure, locator(self, None).bounds, zorder=5) + ax.patch.set_facecolor('none') + ax.set_axes_locator(locator) + self.add_child_axes(ax) + kw_frame, kwargs = self._parse_frame('colorbar', **kwargs) + if frame: + frame = self._add_guide_frame(*bounds_frame, fontsize=fontsize, **kw_frame) + + # Handle default keyword args + if orientation is not None and orientation != 'horizontal': + warnings._warn_proplot( + f'Orientation for inset colorbars must be horizontal. ' + f'Ignoring orientation={orientation!r}.' + ) + ticklocation = _not_none(tickloc=tickloc, ticklocation=ticklocation) + if ticklocation is not None and ticklocation != 'bottom': + warnings._warn_proplot('Inset colorbars can only have ticks on the bottom.') + kwargs.update({'orientation': 'horizontal', 'ticklocation': 'bottom'}) + return ax, kwargs + + def _parse_legend_aligned(self, pairs, ncol=None, order=None, **kwargs): + """ + Draw an individual legend with aligned columns. Includes support + for switching legend-entries between column-major and row-major. + """ + # Potentially change the order of handles to column-major + npairs = len(pairs) + ncol = _not_none(ncol, 3) + nrow = npairs // ncol + 1 + array = np.empty((nrow, ncol), dtype=object) + for i, pair in enumerate(pairs): + array.flat[i] = pair # must be assigned individually + if order == 'C': + array = array.T + + # Return a legend + # NOTE: Permit drawing empty legend to catch edge cases + pairs = [pair for pair in array.flat if isinstance(pair, tuple)] + args = tuple(zip(*pairs)) or ([], []) + return mlegend.Legend(self, *args, ncol=ncol, **kwargs) + + def _parse_legend_centered( + self, pairs, *, fontsize, + loc=None, title=None, frameon=None, kw_frame=None, **kwargs + ): + """ + Draw "legend" with centered rows by creating separate legends for + each row. The label spacing/border spacing will be exactly replicated. + """ + # Parse input args + # NOTE: Main legend() function applies default 'legend.loc' of 'best' when + # users pass legend=True or call legend without 'loc'. Cannot issue warning. + kw_frame = kw_frame or {} + kw_frame['fontsize'] = fontsize + if loc is None or loc == 'best': # white lie + loc = 'upper center' + if not isinstance(loc, str): + raise ValueError( + f'Invalid loc={loc!r} for centered-row legend. Must be string.' + ) + keys = ('bbox_transform', 'bbox_to_anchor') + kw_ignore = {key: kwargs.pop(key) for key in keys if key in kwargs} + if kw_ignore: + warnings._warn_proplot( + f'Ignoring invalid centered-row legend keyword args: {kw_ignore!r}' + ) + + # Iterate and draw + # NOTE: Empirical testing shows spacing fudge factor necessary to + # exactly replicate the spacing of standard aligned legends. + # NOTE: We confine possible bounding box in *y*-direction, but do not + # confine it in *x*-direction. Matplotlib will automatically move + # left-to-right if you request this. + legs = [] + kwargs.update({'loc': loc, 'frameon': False}) + space = kwargs.get('labelspacing', None) or rc['legend.labelspacing'] + height = (((1 + space * 0.85) * fontsize) / 72) / self._get_size_inches()[1] + for i, ipairs in enumerate(pairs): + extra = int(i > 0 and title is not None) + if 'upper' in loc: + base, offset = 1, -extra + elif 'lower' in loc: + base, offset = 0, len(pairs) + else: # center + base, offset = 0.5, 0.5 * (len(pairs) - extra) + y0, y1 = base + (offset - np.array([i + 1, i])) * height + bb = mtransforms.Bbox([[0, y0], [1, y1]]) + leg = mlegend.Legend( + self, *zip(*ipairs), bbox_to_anchor=bb, bbox_transform=self.transAxes, + ncol=len(ipairs), title=title if i == 0 else None, **kwargs + ) + legs.append(leg) + + # Draw manual fancy bounding box for un-aligned legend + # WARNING: legendPatch uses the default transform, i.e. universal coordinates + # in points. Means we have to transform mutation scale into transAxes sizes. + # WARNING: Tempting to use legendPatch for everything but for some reason + # coordinates are messed up. In some tests all coordinates were just result + # of get window extent multiplied by 2 (???). Anyway actual box is found in + # _legend_box attribute, which is accessed by get_window_extent. + objs = tuple(legs) + if frameon and legs: + rend = self.figure._get_renderer() # arbitrary renderer + trans = self.transAxes.inverted() + bboxes = [leg.get_window_extent(rend).transformed(trans) for leg in legs] + bb = mtransforms.Bbox.union(bboxes) + bounds = (bb.xmin, bb.ymin, bb.xmax - bb.xmin, bb.ymax - bb.ymin) + self._add_guide_frame(*bounds, **kw_frame) + return objs + + @staticmethod + def _parse_legend_group(handles, labels=None): + """ + Parse possibly tuple-grouped input handles. + """ + # Helper function. Retrieve labels from a tuple group or from objects + # in a container. Multiple labels lead to multiple legend entries. + def _legend_label(*objs): # noqa: E301 + labs = [] + for obj in objs: + if hasattr(obj, 'get_label'): # e.g. silent list + lab = obj.get_label() + if lab is not None and str(lab)[:1] != '_': + labs.append(lab) + return tuple(labs) + + # Helper function. Translate handles in the input tuple group. Extracts + # legend handles from contour sets and extracts labeled elements from + # matplotlib containers (important for histogram plots). + ignore = (mcontainer.ErrorbarContainer,) + containers = (cbook.silent_list, mcontainer.Container) + def _legend_tuple(*objs): # noqa: E306 + handles = [] + for obj in objs: + if isinstance(obj, ignore) and not _legend_label(obj): + continue + if hasattr(obj, 'update_scalarmappable'): # for e.g. pcolor + obj.update_scalarmappable() + if isinstance(obj, mcontour.ContourSet): # extract single element + hs, _ = obj.legend_elements() + label = getattr(obj, '_legend_label', '_no_label') + if hs: # non-empty + obj = hs[len(hs) // 2] + obj.set_label(label) + if isinstance(obj, containers): # extract labeled elements + hs = (obj, *guides._iter_iterables(obj)) + hs = tuple(filter(_legend_label, hs)) + if hs: + handles.extend(hs) + elif obj: # fallback to first element + handles.append(obj[0]) + else: + handles.append(obj) + elif hasattr(obj, 'get_label'): + handles.append(obj) + else: + warnings._warn_proplot(f'Ignoring invalid legend handle {obj!r}.') + return tuple(handles) + + # Sanitize labels. Ignore e.g. extra hist() or hist2d() return values, + # auto-detect labels in tuple group, auto-expand tuples with diff labels + # NOTE: Allow handles and labels of different length like + # native matplotlib. Just truncate extra values with zip(). + if labels is None: + labels = [None] * len(handles) + ihandles, ilabels = [], [] + for hs, label in zip(handles, labels): + # Filter objects + if type(hs) is not tuple: # ignore Containers (tuple subclasses) + hs = (hs,) + hs = _legend_tuple(*hs) + labs = _legend_label(*hs) + if not hs: + continue + # Unfurl tuple of handles + if label is None and len(labs) > 1: + hs = tuple(filter(_legend_label, hs)) + ihandles.extend(hs) + ilabels.extend(_.get_label() for _ in hs) + # Append this handle with some name + else: + hs = hs[0] if len(hs) == 1 else hs # unfurl for better error messages + label = label if label is not None else labs[0] if labs else '_no_label' + ihandles.append(hs) + ilabels.append(label) + return ihandles, ilabels + + def _parse_legend_handles( + self, handles, labels, ncol=None, order=None, center=None, + alphabetize=None, handler_map=None, + ): + """ + Parse input handles and labels. + """ + # Handle lists of lists + # TODO: Often desirable to label a "mappable" with one data value. Maybe add a + # legend option for the *number of samples* or *sample points* when drawing + # legends for mappables. Look into "legend handlers", might just want to add + # handlers by passing handler_map to legend() and get_legend_handles_labels(). + is_list = lambda obj: ( # noqa: E731 + np.iterable(obj) and not isinstance(obj, (str, tuple)) + ) + to_list = lambda obj: ( # noqa: E731 + obj.tolist() if isinstance(obj, np.ndarray) + else obj if obj is None or is_list(obj) else [obj] + ) + handles, labels = to_list(handles), to_list(labels) + if handles and not labels and all(isinstance(h, str) for h in handles): + handles, labels = labels, handles + multi = any(is_list(h) and len(h) > 1 for h in (handles or ())) + if multi and order == 'F': + warnings._warn_proplot( + 'Column-major ordering of legend handles is not supported ' + 'for horizontally-centered legends.' + ) + if multi and ncol is not None: + warnings._warn_proplot( + 'Detected list of *lists* of legend handles. Ignoring ' + 'the user input property "ncol".' + ) + if labels and not handles: + warnings._warn_proplot( + 'Passing labels without handles is unsupported in proplot. ' + 'Please explicitly pass the handles to legend() or pass labels ' + "to plotting commands with e.g. plot(data_1d, label='label') or " + "plot(data_2d, labels=['label1', 'label2', ...]). After passing " + 'labels to plotting commands you can call legend() without any ' + 'arguments or with the handles as a sole positional argument.' + ) + ncol = _not_none(ncol, 3) + center = _not_none(center, multi) + + # Iterate over each sublist and parse independently + pairs = [] + if not multi: # temporary + handles, labels = [handles], [labels] + elif labels is None: + labels = [labels] * len(handles) + for ihandles, ilabels in zip(handles, labels): + ihandles, ilabels = to_list(ihandles), to_list(ilabels) + if ihandles is None: + ihandles = self._get_legend_handles(handler_map) + ihandles, ilabels = self._parse_legend_group(ihandles, ilabels) + ipairs = list(zip(ihandles, ilabels)) + if alphabetize: + ipairs = sorted(ipairs, key=lambda pair: pair[1]) + pairs.append(ipairs) + + # Manage (handle, label) pairs in context of the 'center' option + if not multi: + pairs = pairs[0] + if center: + multi = True + pairs = [pairs[i * ncol:(i + 1) * ncol] for i in range(len(pairs))] + else: + if not center: # standardize format based on input + multi = False # no longer is list of lists + pairs = [pair for ipairs in pairs for pair in ipairs] + + if multi: + pairs = [ipairs for ipairs in pairs if ipairs] + return pairs, multi + + def _range_subplotspec(self, s): + """ + Return the column or row range for the subplotspec. + """ + if not isinstance(self, maxes.SubplotBase): + raise RuntimeError('Axes must be a subplot.') + ss = self.get_subplotspec().get_topmost_subplotspec() + row1, row2, col1, col2 = ss._get_rows_columns() + if s == 'x': + return (col1, col2) + else: + return (row1, row2) + + def _range_tightbbox(self, s): + """ + Return the tight bounding box span from the cached bounding box. + """ + # TODO: Better testing for axes visibility + bbox = self._tight_bbox + if bbox is None: + return np.nan, np.nan + if s == 'x': + return bbox.xmin, bbox.xmax + else: + return bbox.ymin, bbox.ymax + + def _sharex_setup(self, sharex, **kwargs): + """ + Configure x-axis sharing for panels. See also `~CartesianAxes._sharex_setup`. + """ + self._share_short_axis(sharex, 'left', **kwargs) # x axis of left panels + self._share_short_axis(sharex, 'right', **kwargs) + self._share_long_axis(sharex, 'bottom', **kwargs) # x axis of bottom panels + self._share_long_axis(sharex, 'top', **kwargs) + + def _sharey_setup(self, sharey, **kwargs): + """ + Configure y-axis sharing for panels. See also `~CartesianAxes._sharey_setup`. + """ + self._share_short_axis(sharey, 'bottom', **kwargs) # y axis of bottom panels + self._share_short_axis(sharey, 'top', **kwargs) + self._share_long_axis(sharey, 'left', **kwargs) # y axis of left panels + self._share_long_axis(sharey, 'right', **kwargs) + + def _share_short_axis(self, share, side, **kwargs): + """ + Share the "short" axes of panels in this subplot with other panels. + """ + if share is None or self._panel_side: + return # if this is a panel + s = 'x' if side in ('left', 'right') else 'y' + caxs = self._panel_dict[side] + paxs = share._panel_dict[side] + caxs = [pax for pax in caxs if not pax._panel_hidden] + paxs = [pax for pax in paxs if not pax._panel_hidden] + for cax, pax in zip(caxs, paxs): # may be uneven + getattr(cax, f'_share{s}_setup')(pax, **kwargs) + + def _share_long_axis(self, share, side, **kwargs): + """ + Share the "long" axes of panels in this subplot with other panels. + """ + # NOTE: We do not check _panel_share because that only controls + # sharing with main subplot, not other subplots + if share is None or self._panel_side: + return # if this is a panel + s = 'x' if side in ('top', 'bottom') else 'y' + paxs = self._panel_dict[side] + paxs = [pax for pax in paxs if not pax._panel_hidden] + for pax in paxs: + getattr(pax, f'_share{s}_setup')(share, **kwargs) + + def _reposition_subplot(self): + """ + Reposition the subplot axes. + """ + # WARNING: In later versions self.numRows, self.numCols, and self.figbox + # are @property definitions that never go stale but in mpl < 3.4 they are + # attributes that must be updated explicitly with update_params(). + # WARNING: In early versions matplotlib only removes '_layoutbox' and + # '_poslayoutbox' when calling public set_position but in later versions it + # calls set_in_layout(False) which removes children from get_tightbbox(). + # Therefore try to use _set_position() even though it is private + if not isinstance(self, maxes.SubplotBase): + raise RuntimeError('Axes must be a subplot.') + setter = getattr(self, '_set_position', self.set_position) + if _version_mpl >= '3.4': + setter(self.get_subplotspec().get_position(self.figure)) + else: + self.update_params() + setter(self.figbox) # equivalent to above + + def _update_abc(self, **kwargs): + """ + Update the a-b-c label. + """ + # Properties + # NOTE: Border props only apply for "inner" title locations so we need to + # store on the axes whenever they are modified in case the current location + # is an 'outer' location then re-apply in case 'loc' is subsequently changed + kw = rc.fill( + { + 'size': 'abc.size', + 'weight': 'abc.weight', + 'color': 'abc.color', + 'family': 'font.family', + }, + context=True + ) + kwb = rc.fill( + { + 'border': 'abc.border', + 'borderwidth': 'abc.borderwidth', + 'bbox': 'abc.bbox', + 'bboxpad': 'abc.bboxpad', + 'bboxcolor': 'abc.bboxcolor', + 'bboxstyle': 'abc.bboxstyle', + 'bboxalpha': 'abc.bboxalpha', + }, + context=True, + ) + self._abc_border_kwargs.update(kwb) + + # A-b-c labels. Build as a...z...aa...zz...aaa...zzz + # NOTE: The abc string should already be validated here + abc = rc.find('abc', context=True) # 1st run, or changed + if abc is True: + abc = 'a' + if abc is False: + abc = '' + if abc is None or self.number is None: + pass + elif isinstance(abc, str): + nabc, iabc = divmod(self.number - 1, 26) + if abc: # should have been validated to contain 'a' or 'A' + old = re.search('[aA]', abc).group() # return first occurrence + new = (nabc + 1) * ABC_STRING[iabc] + new = new.upper() if old == 'A' else new + abc = abc.replace(old, new, 1) # replace first occurrence + kw['text'] = abc + else: + if self.number > len(abc): + raise ValueError( + f'Invalid abc list length {len(abc)} ' + f'for axes with number {self.number}.' + ) + else: + kw['text'] = abc[self._number - 1] + + # Update a-b-c label + loc = rc.find('abc.loc', context=True) + loc = self._abc_loc = _translate_loc(loc or self._abc_loc, 'text') + if loc not in ('left', 'right', 'center'): + kw.update(self._abc_border_kwargs) + kw.update(kwargs) + self._title_dict['abc'].update(kw) + + def _update_title(self, loc, title=None, **kwargs): + """ + Update the title at the specified location. + """ + # Titles, with two workflows here: + # 1. title='name' and titleloc='position' + # 2. ltitle='name', rtitle='name', etc., arbitrarily many titles + # NOTE: This always updates the *current* title and deflection to panels + # is handled later so that titles set with set_title() are deflected too. + # See notes in _update_super_labels() and _apply_title_above(). + # NOTE: Matplotlib added axes.titlecolor in version 3.2 but we still use + # custom title.size, title.weight, title.color properties for retroactive + # support in older matplotlib versions. First get params and update kwargs. + kw = rc.fill( + { + 'size': 'title.size', + 'weight': 'title.weight', + 'color': 'title.color', + 'family': 'font.family', + }, + context=True + ) + if 'color' in kw and kw['color'] == 'auto': + del kw['color'] # WARNING: matplotlib permits invalid color here + kwb = rc.fill( + { + 'border': 'title.border', + 'borderwidth': 'title.borderwidth', + 'bbox': 'title.bbox', + 'bboxpad': 'title.bboxpad', + 'bboxcolor': 'title.bboxcolor', + 'bboxstyle': 'title.bboxstyle', + 'bboxalpha': 'title.bboxalpha', + }, + context=True, + ) + self._title_border_kwargs.update(kwb) + + # Update the padding settings read at drawtime. Make sure to + # update them on the panel axes if 'title.above' is active. + pad = rc.find('abc.titlepad', context=True) + if pad is not None: + self._abc_title_pad = pad + pad = rc.find('title.pad', context=True) # title + if pad is not None: + self._title_pad = pad + self._set_title_offset_trans(pad) + + # Get the title location. If 'titleloc' was used then transfer text + # from the old location to the new location. + if loc is not None: + loc = _translate_loc(loc, 'text') + else: + old = self._title_loc + loc = rc.find('title.loc', context=True) + loc = self._title_loc = _translate_loc(loc or self._title_loc, 'text') + if loc != old and old is not None: + labels._transfer_label(self._title_dict[old], self._title_dict[loc]) + + # Update the title text. For outer panels, add text to the panel if + # necesssary. For inner panels, use the border and bbox settings. + if loc not in ('left', 'right', 'center'): + kw.update(self._title_border_kwargs) + if title is None: + pass + elif isinstance(title, str): + kw['text'] = title + elif np.iterable(title) and all(isinstance(_, str) for _ in title): + if self.number is None: + pass + elif self.number > len(title): + raise ValueError( + f'Invalid title list length {len(title)} ' + f'for axes with number {self.number}.' + ) + else: + kw['text'] = title[self.number - 1] + else: + raise ValueError(f'Invalid title {title!r}. Must be string(s).') + kw.update(kwargs) + self._title_dict[loc].update(kw) + + def _update_title_position(self, renderer): + """ + Update the position of inset titles and outer titles. This is called + by matplotlib at drawtime. + """ + # Update title positions + # NOTE: Critical to do this every time in case padding changes or + # we added or removed an a-b-c label in the same position as a title + width, height = self._get_size_inches() + x_pad = self._title_pad / (72 * width) + y_pad = self._title_pad / (72 * height) + for loc, obj in self._title_dict.items(): + x, y = (0, 1) + if loc == 'abc': # redirect + loc = self._abc_loc + if loc == 'left': + x = 0 + elif loc == 'center': + x = 0.5 + elif loc == 'right': + x = 1 + if loc in ('upper center', 'lower center'): + x = 0.5 + elif loc in ('upper left', 'lower left'): + x = x_pad + elif loc in ('upper right', 'lower right'): + x = 1 - x_pad + if loc in ('upper left', 'upper right', 'upper center'): + y = 1 - y_pad + elif loc in ('lower left', 'lower right', 'lower center'): + y = y_pad + obj.set_position((x, y)) + + # Get title padding. Push title above tick marks since matplotlib ignores them. + # This is known matplotlib problem but especially annoying with top panels. + # NOTE: See axis.get_ticks_position for inspiration + pad = self._title_pad + abcpad = self._abc_title_pad + if self.xaxis.get_visible() and any( + tick.tick2line.get_visible() and not tick.label2.get_visible() + for tick in self.xaxis.majorTicks + ): + pad += self.xaxis.get_tick_padding() + + # Avoid applying padding on every draw in case it is expensive to change + # the title Text transforms every time. + pad_current = self._title_pad_current + if pad_current is None or not np.isclose(pad, pad_current): + self._title_pad_current = pad + self._set_title_offset_trans(pad) + + # Adjust the above-axes positions with builtin algorithm + # WARNING: Make sure the name of this private function doesn't change + super()._update_title_position(renderer) + + # Sync the title position with the a-b-c label position + aobj = self._title_dict['abc'] + tobj = self._title_dict[self._abc_loc] + aobj.set_transform(tobj.get_transform()) + aobj.set_position(tobj.get_position()) + aobj.set_ha(tobj.get_ha()) + aobj.set_va(tobj.get_va()) + + # Offset title away from a-b-c label + # NOTE: Title texts all use axes transform in x-direction + if not tobj.get_text() or not aobj.get_text(): + return + awidth, twidth = ( + obj.get_window_extent(renderer).transformed(self.transAxes.inverted()) + .width for obj in (aobj, tobj) + ) + ha = aobj.get_ha() + pad = (abcpad / 72) / self._get_size_inches()[0] + aoffset = toffset = 0 + if ha == 'left': + toffset = awidth + pad + elif ha == 'right': + aoffset = -(twidth + pad) + else: # guaranteed center, there are others + toffset = 0.5 * (awidth + pad) + aoffset = -0.5 * (twidth + pad) + aobj.set_x(aobj.get_position()[0] + aoffset) + tobj.set_x(tobj.get_position()[0] + toffset) + + def _update_super_title(self, suptitle=None, **kwargs): + """ + Update the figure super title. + """ + # NOTE: This is actually *figure-wide* setting, but that line gets blurred + # where we have shared axes, spanning labels, etc. May cause redundant + # assignments if using SubplotGrid.format() but this is fast so nbd. + if self.number is None: + # NOTE: Kludge prevents changed *figure-wide* settings from getting + # overwritten when user makes a new panels or insets. Funky limitation but + # kind of makes sense to make these inaccessible from panels. + return + kw = rc.fill( + { + 'size': 'suptitle.size', + 'weight': 'suptitle.weight', + 'color': 'suptitle.color', + 'family': 'font.family' + }, + context=True, + ) + kw.update(kwargs) + if suptitle or kw: + self.figure._update_super_title(suptitle, **kw) + + def _update_super_labels(self, side, labels=None, **kwargs): + """ + Update the figure super labels. + """ + fig = self.figure + if self.number is None: + return # NOTE: see above + kw = rc.fill( + { + 'color': side + 'label.color', + 'rotation': side + 'label.rotation', + 'size': side + 'label.size', + 'weight': side + 'label.weight', + 'family': 'font.family' + }, + context=True, + ) + kw.update(kwargs) + if labels or kw: + fig._update_super_labels(side, labels, **kw) + + @docstring._snippet_manager + def format( + self, *, title=None, title_kw=None, abc_kw=None, + ltitle=None, lefttitle=None, + ctitle=None, centertitle=None, + rtitle=None, righttitle=None, + ultitle=None, upperlefttitle=None, + uctitle=None, uppercentertitle=None, + urtitle=None, upperrighttitle=None, + lltitle=None, lowerlefttitle=None, + lctitle=None, lowercentertitle=None, + lrtitle=None, lowerrighttitle=None, + **kwargs + ): + """ + Modify the a-b-c label, axes title(s), and background patch, + and call `proplot.figure.Figure.format` on the axes figure. + + Parameters + ---------- + %(axes.format)s + + Important + --------- + `abc`, `abcloc`, `titleloc`, `titleabove`, `titlepad`, and + `abctitlepad` are actually :ref:`configuration settings `. + We explicitly document these arguments here because it is common to + change them for specific axes. But many :ref:`other configuration + settings ` can be passed to ``format`` too. + + Other parameters + ---------------- + %(figure.format)s + %(rc.format)s + + See also + -------- + proplot.axes.CartesianAxes.format + proplot.axes.PolarAxes.format + proplot.axes.GeoAxes.format + proplot.figure.Figure.format + proplot.gridspec.SubplotGrid.format + proplot.config.Configurator.context + """ + skip_figure = kwargs.pop('skip_figure', False) # internal keyword arg + params = _pop_params(kwargs, self.figure._format_signature) + + # Initiate context block + rc_kw, rc_mode = _pop_rc(kwargs) + with rc.context(rc_kw, mode=rc_mode): + # Behavior of titles in presence of panels + above = rc.find('title.above', context=True) + if above is not None: + self._title_above = above # used for future titles + + # Update a-b-c label and titles + abc_kw = abc_kw or {} + title_kw = title_kw or {} + self._update_abc(**abc_kw) + self._update_title( + None, + title, + **title_kw + ) + self._update_title( + 'left', + _not_none(ltitle=ltitle, lefttitle=lefttitle), + **title_kw, + ) + self._update_title( + 'center', + _not_none(ctitle=ctitle, centertitle=centertitle), + **title_kw, + ) + self._update_title( + 'right', + _not_none(rtitle=rtitle, righttitle=righttitle), + **title_kw, + ) + self._update_title( + 'upper left', + _not_none(ultitle=ultitle, upperlefttitle=upperlefttitle), + **title_kw, + ) + self._update_title( + 'upper center', + _not_none(uctitle=uctitle, uppercentertitle=uppercentertitle), + **title_kw + ) + self._update_title( + 'upper right', + _not_none(urtitle=urtitle, upperrighttitle=upperrighttitle), + **title_kw + ) + self._update_title( + 'lower left', + _not_none(lltitle=lltitle, lowerlefttitle=lowerlefttitle), + **title_kw + ) + self._update_title( + 'lower center', + _not_none(lctitle=lctitle, lowercentertitle=lowercentertitle), + **title_kw + ) + self._update_title( + 'lower right', + _not_none(lrtitle=lrtitle, lowerrighttitle=lowerrighttitle), + **title_kw + ) + + # Update the axes style + # NOTE: This will also raise an error if unknown args are encountered + cycle = rc.find('axes.prop_cycle', context=True) + if cycle is not None: + self.set_prop_cycle(cycle) + self._update_background(**kwargs) + + # Update super labels and super title + # NOTE: To avoid resetting figure-wide settings when new axes are created + # we only proceed if using the default context mode. Simliar to geo.py + if skip_figure: # avoid recursion + return + if rc_mode == 1: # avoid resetting + return + self.figure.format(rc_kw=rc_kw, rc_mode=rc_mode, skip_axes=True, **params) + + def draw(self, renderer=None, *args, **kwargs): + # Perform extra post-processing steps + # NOTE: In *principle* these steps go here but should already be complete + # because auto_layout() (called by figure pre-processor) has to run them + # before aligning labels. So these are harmless no-ops. + self._add_queued_guides() + self._apply_title_above() + if self._colorbar_fill: + self._colorbar_fill.update_ticks(manual_only=True) # only if needed + if self._inset_parent is not None and self._inset_zoom: + self.indicate_inset_zoom() + super().draw(renderer, *args, **kwargs) + + def get_tightbbox(self, renderer, *args, **kwargs): + # Perform extra post-processing steps + # NOTE: This should be updated alongside draw(). We also cache the resulting + # bounding box to speed up tight layout calculations (see _range_tightbbox). + self._add_queued_guides() + self._apply_title_above() + if self._colorbar_fill: + self._colorbar_fill.update_ticks(manual_only=True) # only if needed + if self._inset_parent is not None and self._inset_zoom: + self.indicate_inset_zoom() + self._tight_bbox = super().get_tightbbox(renderer, *args, **kwargs) + return self._tight_bbox + + def get_default_bbox_extra_artists(self): + # Further restrict artists to those with disabled clipping + # or use the axes bounding box or patch path for clipping. + # NOTE: Critical to ignore x and y axis, spines, and all child axes. + # For some reason these have clipping 'enabled' but it is not respected. + # NOTE: Matplotlib already tries to do this inside get_tightbbox() but + # their approach fails for cartopy axes clipped by paths and not boxes. + return [ + artist for artist in super().get_default_bbox_extra_artists() + if not self._artist_fully_clipped(artist) + ] + + def set_prop_cycle(self, *args, **kwargs): + # Silent override. This is a strict superset of matplotlib functionality. + # Includes both proplot syntax with positional arguments interpreted as + # color arguments and oldschool matplotlib cycler(key, value) syntax. + if len(args) == 2 and isinstance(args[0], str) and np.iterable(args[1]): + if _pop_props({args[0]: object()}, 'line'): # if a valid line property + kwargs = {args[0]: args[1]} # pass as keyword argument + args = () + cycle = self._active_cycle = constructor.Cycle(*args, **kwargs) + return super().set_prop_cycle(cycle) # set the property cycler after validation + + @docstring._snippet_manager + def inset(self, *args, **kwargs): + """ + %(axes.inset)s + """ + return self._add_inset_axes(*args, **kwargs) + + @docstring._snippet_manager + def inset_axes(self, *args, **kwargs): + """ + %(axes.inset)s + """ + return self._add_inset_axes(*args, **kwargs) + + @docstring._snippet_manager + def indicate_inset_zoom(self, **kwargs): + """ + %(axes.indicate_inset)s + """ + # Add the inset indicators + parent = self._inset_parent + if not parent: + raise ValueError('This command can only be called from an inset axes.') + kwargs.update(_pop_props(kwargs, 'patch')) # impose alternative defaults + if not self._inset_zoom_artists: + kwargs.setdefault('zorder', 3.5) + kwargs.setdefault('linewidth', rc['axes.linewidth']) + kwargs.setdefault('edgecolor', rc['axes.edgecolor']) + xlim, ylim = self.get_xlim(), self.get_ylim() + rect = (xlim[0], ylim[0], xlim[1] - xlim[0], ylim[1] - ylim[0]) + rectpatch, connects = parent.indicate_inset(rect, self) + + # Update indicator properties + # NOTE: Unlike matplotlib we sync zoom box properties with connection lines. + if self._inset_zoom_artists: + rectpatch_prev, connects_prev = self._inset_zoom_artists + rectpatch.update_from(rectpatch_prev) + rectpatch.set_zorder(rectpatch_prev.get_zorder()) + rectpatch_prev.remove() + for line, line_prev in zip(connects, connects_prev): + line.update_from(line_prev) + line.set_zorder(line_prev.get_zorder()) # not included in update_from + line_prev.remove() + rectpatch.update(kwargs) + for line in connects: + line.update(kwargs) + self._inset_zoom_artists = (rectpatch, connects) + return rectpatch, connects + + @docstring._snippet_manager + def panel(self, side=None, **kwargs): + """ + %(axes.panel)s + """ + return self.figure._add_axes_panel(self, side, **kwargs) + + @docstring._snippet_manager + def panel_axes(self, side=None, **kwargs): + """ + %(axes.panel)s + """ + return self.figure._add_axes_panel(self, side, **kwargs) + + @docstring._obfuscate_params + @docstring._snippet_manager + def colorbar(self, mappable, values=None, loc=None, location=None, **kwargs): + """ + Add an inset colorbar or an outer colorbar along the edge of the axes. + + Parameters + ---------- + %(axes.colorbar_args)s + loc, location : int or str, default: :rc:`colorbar.loc` + The colorbar location. Valid location keys are shown in the below table. + + .. _colorbar_table: + + ================== ======================================= + Location Valid keys + ================== ======================================= + outer left ``'left'``, ``'l'`` + outer right ``'right'``, ``'r'`` + outer bottom ``'bottom'``, ``'b'`` + outer top ``'top'``, ``'t'`` + default inset ``'best'``, ``'inset'``, ``'i'``, ``0`` + upper right inset ``'upper right'``, ``'ur'``, ``1`` + upper left inset ``'upper left'``, ``'ul'``, ``2`` + lower left inset ``'lower left'``, ``'ll'``, ``3`` + lower right inset ``'lower right'``, ``'lr'``, ``4`` + "filled" ``'fill'`` + ================== ======================================= + + shrink + Alias for `length`. This is included for consistency with + `matplotlib.figure.Figure.colorbar`. + length \ +: float or unit-spec, default: :rc:`colorbar.length` or :rc:`colorbar.insetlength` + The colorbar length. For outer colorbars, units are relative to the axes + width or height (default is :rcraw:`colorbar.length`). For inset + colorbars, floats interpreted as em-widths and strings interpreted + by `~proplot.utils.units` (default is :rcraw:`colorbar.insetlength`). + width : unit-spec, default: :rc:`colorbar.width` or :rc:`colorbar.insetwidth + The colorbar width. For outer colorbars, floats are interpreted as inches + (default is :rcraw:`colorbar.width`). For inset colorbars, floats are + interpreted as em-widths (default is :rcraw:`colorbar.insetwidth`). + Strings are interpreted by `~proplot.utils.units`. + %(axes.colorbar_space)s + Has no visible effect if `length` is ``1``. + + Other parameters + ---------------- + %(axes.colorbar_kwargs)s + + See also + -------- + proplot.figure.Figure.colorbar + matplotlib.figure.Figure.colorbar + """ + # Translate location and possibly infer from orientation. Also optionally + # infer align setting from keywords stored on object. + orientation = kwargs.get('orientation', None) + kwargs = guides._flush_guide_kw(mappable, 'colorbar', kwargs) + loc = _not_none(loc=loc, location=location) + if orientation is not None: # possibly infer loc from orientation + if orientation not in ('vertical', 'horizontal'): + raise ValueError(f"Invalid colorbar orientation {orientation!r}. Must be 'vertical' or 'horizontal'.") # noqa: E501 + if loc is None: + loc = {'vertical': 'right', 'horizontal': 'bottom'}[orientation] + loc = _translate_loc(loc, 'colorbar', default=rc['colorbar.loc']) + align = kwargs.pop('align', None) + align = _translate_loc(align, 'align', default='center') + + # Either draw right now or queue up for later. The queue option lets us + # successively append objects (e.g. lines) to a colorbar artist list. + queue = kwargs.pop('queue', False) + if queue: + self._register_guide('colorbar', (mappable, values), (loc, align), **kwargs) + else: + return self._add_colorbar(mappable, values, loc=loc, align=align, **kwargs) + + @docstring._concatenate_inherited # also obfuscates params + @docstring._snippet_manager + def legend(self, handles=None, labels=None, loc=None, location=None, **kwargs): + """ + Add an inset legend or outer legend along the edge of the axes. + + Parameters + ---------- + %(axes.legend_args)s + loc, location : int or str, default: :rc:`legend.loc` + The legend location. Valid location keys are shown in the below table. + + .. _legend_table: + + ================== ======================================= + Location Valid keys + ================== ======================================= + outer left ``'left'``, ``'l'`` + outer right ``'right'``, ``'r'`` + outer bottom ``'bottom'``, ``'b'`` + outer top ``'top'``, ``'t'`` + "best" inset ``'best'``, ``'inset'``, ``'i'``, ``0`` + upper right inset ``'upper right'``, ``'ur'``, ``1`` + upper left inset ``'upper left'``, ``'ul'``, ``2`` + lower left inset ``'lower left'``, ``'ll'``, ``3`` + lower right inset ``'lower right'``, ``'lr'``, ``4`` + center left inset ``'center left'``, ``'cl'``, ``5`` + center right inset ``'center right'``, ``'cr'``, ``6`` + lower center inset ``'lower center'``, ``'lc'``, ``7`` + upper center inset ``'upper center'``, ``'uc'``, ``8`` + center inset ``'center'``, ``'c'``, ``9`` + "filled" ``'fill'`` + ================== ======================================= + + width : unit-spec, optional + For outer legends only. The space allocated for the legend + box. This does nothing if the :ref:`tight layout algorithm + ` is active for the figure. + %(units.in)s + %(axes.legend_space)s + + Other parameters + ---------------- + %(axes.legend_kwargs)s + + See also + -------- + proplot.figure.Figure.legend + matplotlib.axes.Axes.legend + """ + # Translate location and possibly infer from orientation. Also optionally + # infer align setting from keywords stored on object. + kwargs = guides._flush_guide_kw(handles, 'legend', kwargs) + loc = _not_none(loc=loc, location=location) + loc = _translate_loc(loc, 'legend', default=rc['legend.loc']) + align = kwargs.pop('align', None) + align = _translate_loc(align, 'align', default='center') + + # Either draw right now or queue up for later. Handles can be successively + # added to a single location this way. Used for on-the-fly legends. + queue = kwargs.pop('queue', False) + if queue: + self._register_guide('legend', (handles, labels), (loc, align), **kwargs) + else: + return self._add_legend(handles, labels, loc=loc, align=align, **kwargs) + + @docstring._concatenate_inherited + @docstring._snippet_manager + def text( + self, *args, border=False, bbox=False, + bordercolor='w', borderwidth=2, borderinvert=False, borderstyle='miter', + bboxcolor='w', bboxstyle='round', bboxalpha=0.5, bboxpad=None, **kwargs + ): + """ + Add text to the axes. + + Parameters + ---------- + x, y, [z] : float + The coordinates for the text. `~proplot.axes.ThreeAxes` accept an + optional third coordinate. If only two are provided this automatically + redirects to the `~mpl_toolkits.mplot3d.Axes3D.text2D` method. + s, text : str + The string for the text. + %(axes.transform)s + + Other parameters + ---------------- + border : bool, default: False + Whether to draw border around text. + borderwidth : float, default: 2 + The width of the text border. + bordercolor : color-spec, default: 'w' + The color of the text border. + borderinvert : bool, optional + If ``True``, the text and border colors are swapped. + borderstyle : {'miter', 'round', 'bevel'}, optional + The `line join style \ +`__ + used for the border. + bbox : bool, default: False + Whether to draw a bounding box around text. + bboxcolor : color-spec, default: 'w' + The color of the text bounding box. + bboxstyle : boxstyle, default: 'round' + The style of the bounding box. + bboxalpha : float, default: 0.5 + The alpha for the bounding box. + bboxpad : float, default: :rc:`title.bboxpad` + The padding for the bounding box. + %(artist.text)s + + **kwargs + Passed to `matplotlib.axes.Axes.text`. + + See also + -------- + matplotlib.axes.Axes.text + """ + # Translate positional args + # Audo-redirect to text2D for 3D axes if not enough arguments passed + # NOTE: The transform must be passed positionally for 3D axes with 2D coords + keys = 'xy' + func = super().text + if self._name == 'three': + if len(args) >= 4 or 'z' in kwargs: + keys += 'z' + else: + func = self.text2D + keys = (*keys, ('s', 'text'), 'transform') + args, kwargs = _kwargs_to_args(keys, *args, **kwargs) + *args, transform = args + if any(arg is None for arg in args): + raise TypeError('Missing required positional argument.') + if transform is None: + transform = self.transData + else: + transform = self._get_transform(transform) + with warnings.catch_warnings(): # ignore duplicates (internal issues?) + warnings.simplefilter('ignore', warnings.ProplotWarning) + kwargs.update(_pop_props(kwargs, 'text')) + + # Update the text object using a monkey patch + obj = func(*args, transform=transform, **kwargs) + obj.update = labels._update_label.__get__(obj) + obj.update( + { + 'border': border, + 'bordercolor': bordercolor, + 'borderinvert': borderinvert, + 'borderwidth': borderwidth, + 'borderstyle': borderstyle, + 'bbox': bbox, + 'bboxcolor': bboxcolor, + 'bboxstyle': bboxstyle, + 'bboxalpha': bboxalpha, + 'bboxpad': bboxpad, + } + ) + return obj + + def _iter_axes(self, hidden=False, children=False, panels=True): + """ + Return a list of visible axes, panel axes, and child axes of both. + + Parameters + ---------- + hidden : bool, optional + Whether to include "hidden" panels. + children : bool, optional + Whether to include children. Note this now includes "twin" axes. + panels : bool or str or sequence of str, optional + Whether to include panels or the panels to include. + """ + # Parse panels + if panels is False: + panels = () + elif panels is True or panels is None: + panels = ('left', 'right', 'bottom', 'top') + elif isinstance(panels, str): + panels = (panels,) + if not set(panels) <= {'left', 'right', 'bottom', 'top'}: + raise ValueError(f'Invalid sides {panels!r}.') + # Iterate + axs = (self, *(ax for side in panels for ax in self._panel_dict[side])) + for iax in axs: + if not hidden and iax._panel_hidden: + continue # ignore hidden panel and its colorbar/legend child + iaxs = (iax, *(iax.child_axes if children else ())) + for jax in iaxs: + if not jax.get_visible(): + continue # safety first + yield jax + + @property + def number(self): + """ + The axes number. This controls the order of a-b-c labels and the + order of appearance in the `~proplot.gridspec.SubplotGrid` returned + by `~proplot.figure.Figure.subplots`. + """ + return self._number + + @number.setter + def number(self, num): + if num is None or isinstance(num, Integral) and num > 0: + self._number = num + else: + raise ValueError(f'Invalid number {num!r}. Must be integer >=1.') + + +# Apply signature obfuscation after storing previous signature +# NOTE: This is needed for __init__ +Axes._format_signatures = {Axes: inspect.signature(Axes.format)} +Axes.format = docstring._obfuscate_kwargs(Axes.format) diff --git a/proplot/axes/cartesian.py b/proplot/axes/cartesian.py new file mode 100644 index 000000000..521f2dbaa --- /dev/null +++ b/proplot/axes/cartesian.py @@ -0,0 +1,1296 @@ +#!/usr/bin/env python3 +""" +The standard Cartesian axes used for most proplot figures. +""" +import copy +import inspect + +import matplotlib.dates as mdates +import matplotlib.ticker as mticker +import numpy as np + +from .. import constructor +from .. import scale as pscale +from .. import ticker as pticker +from ..config import rc +from ..internals import ic # noqa: F401 +from ..internals import _not_none, _pop_rc, _version_mpl, docstring, labels, warnings +from . import plot, shared + +__all__ = ['CartesianAxes'] + + +# Tuple of date converters +DATE_CONVERTERS = (mdates.DateConverter,) +if hasattr(mdates, '_SwitchableDateConverter'): + DATE_CONVERTERS += (mdates._SwitchableDateConverter,) + +# Opposite side keywords +OPPOSITE_SIDE = { + 'left': 'right', + 'right': 'left', + 'bottom': 'top', + 'top': 'bottom', +} + + +# Format docstring +_format_docstring = """ +aspect : {'auto', 'equal'} or float, optional + The data aspect ratio. See `~matplotlib.axes.Axes.set_aspect` + for details. +xlabel, ylabel : str, optional + The x and y axis labels. Applied with `~matplotlib.axes.Axes.set_xlabel` + and `~matplotlib.axes.Axes.set_ylabel`. +xlabel_kw, ylabel_kw : dict-like, optional + Additional axis label settings applied with `~matplotlib.axes.Axes.set_xlabel` + and `~matplotlib.axes.Axes.set_ylabel`. See also `labelpad`, `labelcolor`, + `labelsize`, and `labelweight` below. +xlim, ylim : 2-tuple of floats or None, optional + The x and y axis data limits. Applied with `~matplotlib.axes.Axes.set_xlim` + and `~matplotlib.axes.Axes.set_ylim`. +xmin, ymin : float, optional + The x and y minimum data limits. Useful if you do not want + to set the maximum limits. +xmax, ymax : float, optional + The x and y maximum data limits. Useful if you do not want + to set the minimum limits. +xreverse, yreverse : bool, optional + Whether to "reverse" the x and y axis direction. Makes the x and + y axes ascend left-to-right and top-to-bottom, respectively. +xscale, yscale : scale-spec, optional + The x and y axis scales. Passed to the `~proplot.scale.Scale` constructor. + For example, ``xscale='log'`` applies logarithmic scaling, and + ``xscale=('cutoff', 100, 2)`` applies a `~proplot.scale.CutoffScale`. +xscale_kw, yscale_kw : dict-like, optional + The x and y axis scale settings. Passed to `~proplot.scale.Scale`. +xmargin, ymargin, margin : float, default: :rc:`margin` + The default margin between plotted content and the x and y axis spines in + axes-relative coordinates. This is useful if you don't witch to explicitly set + axis limits. Use the keyword `margin` to set both at once. +xbounds, ybounds : 2-tuple of float, optional + The x and y axis data bounds within which to draw the spines. For example, + ``xlim=(0, 4)`` combined with ``xbounds=(2, 4)`` will prevent the spines + from meeting at the origin. This also applies ``xspineloc='bottom'`` and + ``yspineloc='left'`` by default if both spines are currently visible. +xtickrange, ytickrange : 2-tuple of float, optional + The x and y axis data ranges within which major tick marks are labelled. + For example, ``xlim=(-5, 5)`` combined with ``xtickrange=(-1, 1)`` and a + tick interval of 1 will only label the ticks marks at -1, 0, and 1. See + `~proplot.ticker.AutoFormatter` for details. +xwraprange, ywraprange : 2-tuple of float, optional + The x and y axis data ranges with which major tick mark values are wrapped. For + example, ``xwraprange=(0, 3)`` causes the values 0 through 9 to be formatted as + 0, 1, 2, 0, 1, 2, 0, 1, 2, 0. See `~proplot.ticker.AutoFormatter` for details. This + can be combined with `xtickrange` and `ytickrange` to make "stacked" line plots. +xloc, yloc : optional + Shorthands for `xspineloc`, `yspineloc`. +xspineloc, yspineloc : {'b', 't', 'l', 'r', 'bottom', 'top', 'left', 'right', \ +'both', 'neither', 'none', 'zero', 'center'} or 2-tuple, optional + The x and y spine locations. Applied with `~matplotlib.spines.Spine.set_position`. + Propagates to `tickloc` unless specified otherwise. +xtickloc, ytickloc : {'b', 't', 'l', 'r', 'bottom', 'top', 'left', 'right', \ +'both', 'neither', 'none'}, optional + Which x and y axis spines should have major and minor tick marks. Inherits from + `spineloc` by default and propagates to `ticklabelloc` unless specified otherwise. +xticklabelloc, yticklabelloc : {'b', 't', 'l', 'r', 'bottom', 'top', 'left', 'right', \ +'both', 'neither', 'none'}, optional + Which x and y axis spines should have major tick labels. Inherits from `tickloc` + by default and propagates to `labelloc` and `offsetloc` unless specified otherwise. +xlabelloc, ylabelloc : \ +{'b', 't', 'l', 'r', 'bottom', 'top', 'left', 'right'}, optional + Which x and y axis spines should have axis labels. Inherits from + `ticklabelloc` by default (if `ticklabelloc` is a single side). +xoffsetloc, yoffsetloc : \ +{'b', 't', 'l', 'r', 'bottom', 'top', 'left', 'right'}, optional + Which x and y axis spines should have the axis offset indicator. Inherits from + `ticklabelloc` by default (if `ticklabelloc` is a single side). +xtickdir, ytickdir, tickdir : {'out', 'in', 'inout'}, optional + Direction that major and minor tick marks point for the x and y axis. + Use the keyword `tickdir` to control both. +xticklabeldir, yticklabeldir : {'in', 'out'}, optional + Whether to place x and y axis tick label text inside or outside the axes. + Propagates to `xtickdir` and `ytickdir` unless specified otherwise. +xrotation, yrotation : float, default: 0 + The rotation for x and y axis tick labels. + for normal axes, :rc:`formatter.timerotation` for time x axes. +xgrid, ygrid, grid : bool, default: :rc:`grid` + Whether to draw major gridlines on the x and y axis. + Use the keyword `grid` to toggle both. +xgridminor, ygridminor, gridminor : bool, default: :rc:`gridminor` + Whether to draw minor gridlines for the x and y axis. + Use the keyword `gridminor` to toggle both. +xtickminor, ytickminor, tickminor : bool, default: :rc:`tick.minor` + Whether to draw minor ticks on the x and y axes. + Use the keyword `tickminor` to toggle both. +xticks, yticks : optional + Aliases for `xlocator`, `ylocator`. +xlocator, ylocator : locator-spec, optional + Used to determine the x and y axis tick mark positions. Passed + to the `~proplot.constructor.Locator` constructor. Can be float, + list of float, string, or `matplotlib.ticker.Locator` instance. + Use ``[]``, ``'null'``, or ``'none'`` for no ticks. +xlocator_kw, ylocator_kw : dict-like, optional + Keyword arguments passed to the `matplotlib.ticker.Locator` class. +xminorticks, yminorticks : optional + Aliases for `xminorlocator`, `yminorlocator`. +xminorlocator, yminorlocator : optional + As for `xlocator`, `ylocator`, but for the minor ticks. +xminorlocator_kw, yminorlocator_kw + As for `xlocator_kw`, `ylocator_kw`, but for the minor locator. +xticklabels, yticklabels : optional + Aliases for `xformatter`, `yformatter`. +xformatter, yformatter : formatter-spec, optional + Used to determine the x and y axis tick label string format. + Passed to the `~proplot.constructor.Formatter` constructor. + Can be string, list of strings, or `matplotlib.ticker.Formatter` instance. + Use ``[]``, ``'null'``, or ``'none'`` for no labels. +xformatter_kw, yformatter_kw : dict-like, optional + Keyword arguments passed to the `matplotlib.ticker.Formatter` class. +xcolor, ycolor, color : color-spec, default: :rc:`meta.color` + Color for the x and y axis spines, ticks, tick labels, and axis labels. + Use the keyword `color` to set both at once. +xgridcolor, ygridcolor, gridcolor : color-spec, default: :rc:`grid.color` + Color for the x and y axis major and minor gridlines. + Use the keyword `gridcolor` to set both at once. +xlinewidth, ylinewidth, linewidth : color-spec, default: :rc:`meta.width` + Line width for the x and y axis spines and major ticks. Propagates to `tickwidth` + unless specified otherwise. Use the keyword `linewidth` to set both at once. +xtickcolor, ytickcolor, tickcolor : color-spec, default: :rc:`tick.color` + Color for the x and y axis ticks. Defaults are `xcolor`, `ycolor`, and `color` + if they were passed. Use the keyword `tickcolor` to set both at once. +xticklen, yticklen, ticklen : unit-spec, default: :rc:`tick.len` + Major tick lengths for the x and y axis. + %(units.pt)s + Use the keyword `ticklen` to set both at once. +xticklenratio, yticklenratio, ticklenratio : float, default: :rc:`tick.lenratio` + Relative scaling of `xticklen` and `yticklen` used to determine minor + tick lengths. Use the keyword `ticklenratio` to set both at once. +xtickwidth, ytickwidth, tickwidth, : unit-spec, default: :rc:`tick.width` + Major tick widths for the x ans y axis. Default is `linewidth` if it was passed. + %(units.pt)s + Use the keyword `tickwidth` to set both at once. +xtickwidthratio, ytickwidthratio, tickwidthratio : float, default: :rc:`tick.widthratio` + Relative scaling of `xtickwidth` and `ytickwidth` used to determine + minor tick widths. Use the keyword `tickwidthratio` to set both at once. +xticklabelpad, yticklabelpad, ticklabelpad : unit-spec, default: :rc:`tick.labelpad` + The padding between the x and y axis ticks and tick labels. Use the + keyword `ticklabelpad` to set both at once. + %(units.pt)s +xticklabelcolor, yticklabelcolor, ticklabelcolor \ +: color-spec, default: :rc:`tick.labelcolor` + Color for the x and y tick labels. Defaults are `xcolor`, `ycolor`, and `color` + if they were passed. Use the keyword `ticklabelcolor` to set both at once. +xticklabelsize, yticklabelsize, ticklabelsize \ +: unit-spec or str, default: :rc:`tick.labelsize` + Font size for the x and y tick labels. + %(units.pt)s + Use the keyword `ticklabelsize` to set both at once. +xticklabelweight, yticklabelweight, ticklabelweight \ +: str, default: :rc:`tick.labelweight` + Font weight for the x and y tick labels. + Use the keyword `ticklabelweight` to set both at once. +xlabelpad, ylabelpad : unit-spec, default: :rc:`label.pad` + The padding between the x and y axis bounding box and the x and y axis labels. + %(units.pt)s +xlabelcolor, ylabelcolor, labelcolor : color-spec, default: :rc:`label.color` + Color for the x and y axis labels. Defaults are `xcolor`, `ycolor`, and `color` + if they were passed. Use the keyword `labelcolor` to set both at once. +xlabelsize, ylabelsize, labelsize : unit-spec or str, default: :rc:`label.size` + Font size for the x and y axis labels. + %(units.pt)s + Use the keyword `labelsize` to set both at once. +xlabelweight, ylabelweight, labelweight : str, default: :rc:`label.weight` + Font weight for the x and y axis labels. + Use the keyword `labelweight` to set both at once. +fixticks : bool, default: False + Whether to transform the tick locators to a `~matplotlib.ticker.FixedLocator`. + If your axis ticks are doing weird things (for example, ticks are drawn + outside of the axis spine) you can try setting this to ``True``. +""" +docstring._snippet_manager['cartesian.format'] = _format_docstring + + +# Shared docstring +_shared_x_keys = { + 'x': 'x', 'x1': 'bottom', 'x2': 'top', + 'y': 'y', 'y1': 'left', 'y2': 'right', +} +_shared_y_keys = { + 'x': 'y', 'x1': 'left', 'x2': 'right', + 'y': 'x', 'y1': 'bottom', 'y2': 'top', +} +_shared_docstring = """ +%(descrip)s +Parameters +---------- +%(extra)s**kwargs + Passed to `~proplot.axes.CartesianAxes`. Supports all valid + `~proplot.axes.CartesianAxes.format` keywords. You can optionally + omit the {x} from keywords beginning with ``{x}`` -- for example + ``ax.alt{x}(lim=(0, 10))`` is equivalent to ``ax.alt{x}({x}lim=(0, 10))``. + You can also change the default side for the axis spine, axis tick marks, + axis tick labels, and/or axis labels by passing ``loc`` keywords. For example, + ``ax.alt{x}(loc='{x1}')`` changes the default side from {x2} to {x1}. + +Returns +------- +proplot.axes.CartesianAxes + The resulting axes. + +Note +---- +This enforces the following default settings: + +* Places the old {x} axis on the {x1} and the new {x} + axis on the {x2}. +* Makes the old {x2} spine invisible and the new {x1}, {y1}, + and {y2} spines invisible. +* Adjusts the {x} axis tick, tick label, and axis label positions + according to the visible spine positions. +* Syncs the old and new {y} axis limits and scales, and makes the + new {y} axis labels invisible. +""" + +# Alt docstrings +# NOTE: Used by SubplotGrid.altx +_alt_descrip = """ +Add an axes locked to the same location with a +distinct {x} axis. +This is an alias and arguably more intuitive name for +`~proplot.axes.CartesianAxes.twin{y}`, which generates +two {x} axes with a shared ("twin") {y} axes. +""" +_alt_docstring = _shared_docstring % {'descrip': _alt_descrip, 'extra': ''} +docstring._snippet_manager['axes.altx'] = _alt_docstring.format(**_shared_x_keys) +docstring._snippet_manager['axes.alty'] = _alt_docstring.format(**_shared_y_keys) + +# Twin docstrings +# NOTE: Used by SubplotGrid.twinx +_twin_descrip = """ +Add an axes locked to the same location with a +distinct {x} axis. +This builds upon `matplotlib.axes.Axes.twin{y}`. +""" +_twin_docstring = _shared_docstring % {'descrip': _twin_descrip, 'extra': ''} +docstring._snippet_manager['axes.twinx'] = _twin_docstring.format(**_shared_y_keys) +docstring._snippet_manager['axes.twiny'] = _twin_docstring.format(**_shared_x_keys) + +# Dual docstrings +# NOTE: Used by SubplotGrid.dualx +_dual_descrip = """ +Add an axes locked to the same location whose {x} axis denotes +equivalent coordinates in alternate units. +This is an alternative to `matplotlib.axes.Axes.secondary_{x}axis` with +additional convenience features. +""" +_dual_extra = """ +funcscale : callable, 2-tuple of callables, or scale-spec + The scale used to transform units from the parent axis to the secondary + axis. This can be a `~proplot.scale.FuncScale` itself or a function, + (function, function) tuple, or an axis scale specification interpreted + by the `~proplot.constructor.Scale` constructor function, any of which + will be used to build a `~proplot.scale.FuncScale` and applied + to the dual axis (see `~proplot.scale.FuncScale` for details). +""" +_dual_docstring = _shared_docstring % {'descrip': _dual_descrip, 'extra': _dual_extra.lstrip()} # noqa: E501 +docstring._snippet_manager['axes.dualx'] = _dual_docstring.format(**_shared_x_keys) +docstring._snippet_manager['axes.dualy'] = _dual_docstring.format(**_shared_y_keys) + + +class CartesianAxes(shared._SharedAxes, plot.PlotAxes): + """ + Axes subclass for plotting in ordinary Cartesian coordinates. Adds the + `~CartesianAxes.format` method and overrides several existing methods. + + Important + --------- + This is the default axes subclass. It can be specified explicitly by passing + ``proj='cart'``, ``proj='cartesian'``, ``proj='rect'``, or ``proj='rectilinear'`` + to axes-creation commands like `~proplot.figure.Figure.add_axes`, + `~proplot.figure.Figure.add_subplot`, and `~proplot.figure.Figure.subplots`. + """ + _name = 'cartesian' + _name_aliases = ('cart', 'rect', 'rectilinar') # include matplotlib name + + @docstring._snippet_manager + def __init__(self, *args, **kwargs): + """ + Parameters + ---------- + *args + Passed to `matplotlib.axes.Axes`. + %(cartesian.format)s + + Other parameters + ---------------- + %(axes.format)s + %(rc.init)s + + See also + -------- + CartesianAxes.format + proplot.axes.Axes + proplot.axes.PlotAxes + proplot.figure.Figure.subplot + proplot.figure.Figure.add_subplot + """ + # Initialize axes + self._xaxis_current_rotation = 'horizontal' # current rotation + self._yaxis_current_rotation = 'horizontal' + self._xaxis_isdefault_rotation = True # whether to auto rotate the axis + self._yaxis_isdefault_rotation = True + super().__init__(*args, **kwargs) + + # Apply default formatter + if self.xaxis.isDefault_majfmt: + self.xaxis.set_major_formatter(pticker.AutoFormatter()) + self.xaxis.isDefault_majfmt = True + if self.yaxis.isDefault_majfmt: + self.yaxis.set_major_formatter(pticker.AutoFormatter()) + self.yaxis.isDefault_majfmt = True + + # Dual axes utilities + self._dualx_funcscale = None # for scaling units on dual axes + self._dualx_prevstate = None # prevent excess _dualy_scale calls + self._dualy_funcscale = None + self._dualy_prevstate = None + + def _apply_axis_sharing(self): + """ + Enforce the "shared" axis labels and axis tick labels. If this is not + called at drawtime, "shared" labels can be inadvertantly turned off. + """ + # X axis + # NOTE: Critical to apply labels to *shared* axes attributes rather + # than testing extents or we end up sharing labels with twin axes. + # NOTE: Similar to how _align_super_labels() calls _apply_title_above() this + # is called inside _align_axis_labels() so we align the correct text. + # NOTE: The "panel sharing group" refers to axes and panels *above* the + # bottommost or to the *right* of the leftmost panel. But the sharing level + # used for the leftmost and bottommost is the *figure* sharing level. + axis = self.xaxis + if self._sharex is not None and axis.get_visible(): + level = 3 if self._panel_sharex_group else self.figure._sharex + if level > 0: + labels._transfer_label(axis.label, self._sharex.xaxis.label) + axis.label.set_visible(False) + if level > 2: + # WARNING: Cannot set NullFormatter because shared axes share the + # same Ticker(). Instead use approach copied from mpl subplots(). + axis.set_tick_params(which='both', labelbottom=False, labeltop=False) + # Y axis + axis = self.yaxis + if self._sharey is not None and axis.get_visible(): + level = 3 if self._panel_sharey_group else self.figure._sharey + if level > 0: + labels._transfer_label(axis.label, self._sharey.yaxis.label) + axis.label.set_visible(False) + if level > 2: + axis.set_tick_params(which='both', labelleft=False, labelright=False) + axis.set_minor_formatter(mticker.NullFormatter()) + + def _add_alt(self, sx, **kwargs): + """ + Add an alternate axes. + """ + # Parse keyword arguments. Optionally omit redundant leading 'x' and 'y' + # WARNING: We add axes as children for tight layout algorithm convenience and + # to support eventual paradigm of arbitrarily many duplicates with spines + # arranged in an edge stack. However this means all artists drawn there take + # on zorder of their axes when drawn inside the "parent" (see Axes.draw()). + # To restore matplotlib behavior, which draws "child" artists on top simply + # because the axes was created after the "parent" one, use the inset_axes + # zorder of 4 and make the background transparent. + sy = 'y' if sx == 'x' else 'x' + sig = self._format_signatures[CartesianAxes] + keys = tuple(key[1:] for key in sig.parameters if key[0] == sx) + kwargs = {(sx + key if key in keys else key): val for key, val in kwargs.items()} # noqa: E501 + if f'{sy}spineloc' not in kwargs: # acccount for aliases + kwargs.setdefault(f'{sy}loc', 'neither') + if f'{sx}spineloc' not in kwargs: # account for aliases + kwargs.setdefault(f'{sx}loc', 'top' if sx == 'x' else 'right') + kwargs.setdefault(f'autoscale{sy}_on', getattr(self, f'get_autoscale{sy}_on')()) + kwargs.setdefault(f'share{sy}', self) + + # Initialize child axes + kwargs.setdefault('grid', False) # note xgrid=True would override this + kwargs.setdefault('zorder', 4) # increased default zorder + kwargs.setdefault('number', None) + kwargs.setdefault('autoshare', False) + if 'sharex' in kwargs and 'sharey' in kwargs: + raise ValueError('Twinned axes may share only one axis.') + locator = self._make_inset_locator([0, 0, 1, 1], self.transAxes) + ax = CartesianAxes(self.figure, locator(self, None).bounds, **kwargs) + ax.set_axes_locator(locator) + ax.set_adjustable('datalim') + self.add_child_axes(ax) # to facilitate tight layout + self.set_adjustable('datalim') + self._twinned_axes.join(self, ax) + + # Format parent and child axes + self.format(**{f'{sx}loc': OPPOSITE_SIDE.get(kwargs[f'{sx}loc'], None)}) + setattr(ax, f'_alt{sx}_parent', self) + getattr(ax, f'{sy}axis').set_visible(False) + getattr(ax, 'patch').set_visible(False) + return ax + + def _dual_scale(self, s, funcscale=None): + """ + Lock the child "dual" axis limits to the parent. + """ + # NOTE: We bypass autoscale_view because we set limits manually, and bypass + # child.stale = True because that is done in call to set_xlim() below. + # NOTE: We set the scale using private API to bypass application of + # set_default_locators_and_formatters: only_if_default=True is critical + # to prevent overriding user settings! + # NOTE: Dual axis only needs to be constrained if the parent axis scale + # and limits have changed, and limits are always applied before we reach + # the child.draw() because always called after parent.draw() + child = self + parent = getattr(self, f'_alt{s}_parent') + if funcscale is not None: + setattr(self, f'_dual{s}_funcscale', funcscale) + else: + funcscale = getattr(self, f'_dual{s}_funcscale') + if parent is None or funcscale is None: + return + olim = getattr(parent, f'get_{s}lim')() + scale = getattr(parent, f'{s}axis')._scale + if (scale, *olim) == getattr(child, f'_dual{s}_prevstate'): + return + funcscale = pscale.FuncScale(funcscale, invert=True, parent_scale=scale) + caxis = getattr(child, f'{s}axis') + caxis._scale = funcscale + child._update_transScale() + funcscale.set_default_locators_and_formatters(caxis, only_if_default=True) + nlim = list(map(funcscale.functions[1], np.array(olim))) + if np.sign(np.diff(olim)) != np.sign(np.diff(nlim)): + nlim = nlim[::-1] # if function flips limits, so will set_xlim! + getattr(child, f'set_{s}lim')(nlim, emit=False) + setattr(child, f'_dual{s}_prevstate', (scale, *olim)) + + def _fix_ticks(self, s, fixticks=False): + """ + Ensure there are no out-of-bounds ticks. Mostly a brute-force version of + `~matplotlib.axis.Axis.set_smart_bounds` (which I couldn't get to work). + """ + # NOTE: Previously triggered this every time FixedFormatter was found + # on axis but 1) that seems heavy-handed + strange and 2) internal + # application of FixedFormatter by boxplot resulted in subsequent format() + # successfully calling this and messing up the ticks for some reason. + # So avoid using this when possible, and try to make behavior consistent + # by cacheing the locators before we use them for ticks. + axis = getattr(self, f'{s}axis') + sides = ('bottom', 'top') if s == 'x' else ('left', 'right') + l0, l1 = getattr(self, f'get_{s}lim')() + bounds = tuple(self.spines[side].get_bounds() or (None, None) for side in sides) + skipticks = lambda ticks: [ # noqa: E731 + x for x in ticks + if not any(x < _not_none(b0, l0) or x > _not_none(b1, l1) for (b0, b1) in bounds) # noqa: E501 + ] + if fixticks or any(x is not None for b in bounds for x in b): + # Major locator + locator = getattr(axis, '_major_locator_cached', None) + if locator is None: + locator = axis._major_locator_cached = axis.get_major_locator() + locator = constructor.Locator(skipticks(locator())) + axis.set_major_locator(locator) + # Minor locator + locator = getattr(axis, '_minor_locator_cached', None) + if locator is None: + locator = axis._minor_locator_cached = axis.get_minor_locator() + locator = constructor.Locator(skipticks(locator())) + axis.set_minor_locator(locator) + + def _get_spine_side(self, s, loc): + """ + Get the spine side implied by the input location or position. This + propagates to tick mark, tick label, and axis label positions. + """ + # NOTE: Could defer error to CartesianAxes.format but instead use our + # own error message with info on coordinate position options. + sides = ('bottom', 'top') if s == 'x' else ('left', 'right') + centers = ('zero', 'center') + options = (*(s[0] for s in sides), *sides, 'both', 'neither', 'none') + if np.iterable(loc) and len(loc) == 2 and loc[0] in ('axes', 'data', 'outward'): + lim = getattr(self, f'get_{s}lim')() + if loc[0] == 'outward': # ambiguous so just choose first side + side = sides[0] + elif loc[0] == 'axes': + side = sides[int(loc[1] > 0.5)] + else: + side = sides[int(loc[1] > lim[0] + 0.5 * (lim[1] - lim[0]))] + elif loc in centers: # ambiguous so just choose the first side + side = sides[0] + elif loc is None or loc in options: + side = loc + else: + raise ValueError( + f'Invalid {s} spine location {loc!r}. Options are: ' + + ', '.join(map(repr, (*options, *centers))) + + " or a coordinate position ('axes', coord), " + + " ('data', coord), or ('outward', coord)." + ) + return side + + def _is_panel_group_member(self, other): + """ + Return whether the axes belong in a panel sharing stack.. + """ + return ( + self._panel_parent is other # other is child panel + or other._panel_parent is self # other is main subplot + or other._panel_parent and self._panel_parent # ... + and other._panel_parent is self._panel_parent # other is sibling panel + ) + + def _sharex_limits(self, sharex): + """ + Safely share limits and tickers without resetting things. + """ + # Copy non-default limits and scales. Either this axes or the input + # axes could be a newly-created subplot while the other is a subplot + # with possibly-modified user settings we are careful to preserve. + for (ax1, ax2) in ((self, sharex), (sharex, self)): + if ax1.get_xscale() == 'linear' and ax2.get_xscale() != 'linear': + ax1.set_xscale(ax2.get_xscale()) # non-default scale + if ax1.get_autoscalex_on() and not ax2.get_autoscalex_on(): + ax1.set_xlim(ax2.get_xlim()) # non-default limits + # Copy non-default locators and formatters + self.get_shared_x_axes().join(self, sharex) # share limit/scale changes + if sharex.xaxis.isDefault_majloc and not self.xaxis.isDefault_majloc: + sharex.xaxis.set_major_locator(self.xaxis.get_major_locator()) + if sharex.xaxis.isDefault_minloc and not self.xaxis.isDefault_minloc: + sharex.xaxis.set_minor_locator(self.xaxis.get_minor_locator()) + if sharex.xaxis.isDefault_majfmt and not self.xaxis.isDefault_majfmt: + sharex.xaxis.set_major_formatter(self.xaxis.get_major_formatter()) + if sharex.xaxis.isDefault_minfmt and not self.xaxis.isDefault_minfmt: + sharex.xaxis.set_minor_formatter(self.xaxis.get_minor_formatter()) + self.xaxis.major = sharex.xaxis.major + self.xaxis.minor = sharex.xaxis.minor + + def _sharey_limits(self, sharey): + """ + Safely share limits and tickers without resetting things. + """ + # NOTE: See _sharex_limits for notes + for (ax1, ax2) in ((self, sharey), (sharey, self)): + if ax1.get_yscale() == 'linear' and ax2.get_yscale() != 'linear': + ax1.set_yscale(ax2.get_yscale()) + if ax1.get_autoscaley_on() and not ax2.get_autoscaley_on(): + ax1.set_ylim(ax2.get_ylim()) + self.get_shared_y_axes().join(self, sharey) # share limit/scale changes + if sharey.yaxis.isDefault_majloc and not self.yaxis.isDefault_majloc: + sharey.yaxis.set_major_locator(self.yaxis.get_major_locator()) + if sharey.yaxis.isDefault_minloc and not self.yaxis.isDefault_minloc: + sharey.yaxis.set_minor_locator(self.yaxis.get_minor_locator()) + if sharey.yaxis.isDefault_majfmt and not self.yaxis.isDefault_majfmt: + sharey.yaxis.set_major_formatter(self.yaxis.get_major_formatter()) + if sharey.yaxis.isDefault_minfmt and not self.yaxis.isDefault_minfmt: + sharey.yaxis.set_minor_formatter(self.yaxis.get_minor_formatter()) + self.yaxis.major = sharey.yaxis.major + self.yaxis.minor = sharey.yaxis.minor + + def _sharex_setup(self, sharex, *, labels=True, limits=True): + """ + Configure shared axes accounting. Input is the 'parent' axes from which this + one will draw its properties. Use keyword args to override settings. + """ + # Share panels across *different* subplots + super()._sharex_setup(sharex) + # Get the axis sharing level + level = ( + 3 if self._panel_sharex_group and self._is_panel_group_member(sharex) + else self.figure._sharex + ) + if level not in range(5): # must be internal error + raise ValueError(f'Invalid sharing level sharex={level!r}.') + if sharex in (None, self) or not isinstance(sharex, CartesianAxes): + return + # Share future axis label changes. Implemented in _apply_axis_sharing(). + # Matplotlib only uses these attributes in __init__() and cla() to share + # tickers -- all other builtin sharing features derives from shared x axes + if level > 0 and labels: + self._sharex = sharex + # Share future axis tickers, limits, and scales + # NOTE: Only difference between levels 2 and 3 is level 3 hides tick + # labels. But this is done after the fact -- tickers are still shared. + if level > 1 and limits: + self._sharex_limits(sharex) + + def _sharey_setup(self, sharey, *, labels=True, limits=True): + """ + Configure shared axes accounting for panels. The input is the + 'parent' axes, from which this one will draw its properties. + """ + # NOTE: See _sharex_setup for notes + super()._sharey_setup(sharey) + level = ( + 3 if self._panel_sharey_group and self._is_panel_group_member(sharey) + else self.figure._sharey + ) + if level not in range(5): # must be internal error + raise ValueError(f'Invalid sharing level sharey={level!r}.') + if sharey in (None, self) or not isinstance(sharey, CartesianAxes): + return + if level > 0 and labels: + self._sharey = sharey + if level > 1 and limits: + self._sharey_limits(sharey) + + def _update_formatter( + self, s, formatter=None, *, formatter_kw=None, + tickrange=None, wraprange=None, + ): + """ + Update the axis formatter. Passes `formatter` through `Formatter` with kwargs. + """ + # Test if this is date axes + # See: https://matplotlib.org/api/units_api.html + # And: https://matplotlib.org/api/dates_api.html + axis = getattr(self, f'{s}axis') + date = isinstance(axis.converter, DATE_CONVERTERS) + + # Major formatter + # NOTE: The default axis formatter accepts lots of keywords. So unlike + # everywhere else that uses constructor functions we also allow only + # formatter_kw input without formatter and use 'auto' as the default. + formatter_kw = formatter_kw or {} + formatter_kw = formatter_kw.copy() + if formatter is not None or tickrange is not None or wraprange is not None or formatter_kw: # noqa: E501 + # Tick range + formatter = _not_none(formatter, 'auto') + if tickrange is not None or wraprange is not None: + if formatter != 'auto': + warnings._warn_proplot( + 'The tickrange and autorange features require ' + 'proplot.AutoFormatter formatter. Overriding the input.' + ) + if tickrange is not None: + formatter_kw.setdefault('tickrange', tickrange) + if wraprange is not None: + formatter_kw.setdefault('wraprange', wraprange) + + # Set the formatter + # Note some formatters require 'locator' as keyword arg + if formatter in ('date', 'concise'): + locator = axis.get_major_locator() + formatter_kw.setdefault('locator', locator) + formatter = constructor.Formatter(formatter, date=date, **formatter_kw) + axis.set_major_formatter(formatter) + + def _update_labels(self, s, *args, **kwargs): + """ + Apply axis labels to the relevant shared axis. If spanning labels are toggled + this keeps the labels synced for all subplots in the same row or column. Label + positions will be adjusted at draw-time with figure._align_axislabels. + """ + # NOTE: Critical to test whether arguments are None or else this + # will set isDefault_label to False every time format() is called. + # NOTE: This always updates the *current* labels and sharing is handled + # later so that labels set with set_xlabel() and set_ylabel() are shared too. + # See notes in _align_axis_labels() and _apply_axis_sharing(). + kwargs = rc._get_label_props(**kwargs) + no_args = all(a is None for a in args) + no_kwargs = all(v is None for v in kwargs.values()) + if no_args and no_kwargs: + return # also returns if args and kwargs are empty + setter = getattr(self, f'set_{s}label') + getter = getattr(self, f'get_{s}label') + if no_args: # otherwise label text is reset! + args = (getter(),) + setter(*args, **kwargs) + + def _update_locators( + self, s, locator=None, minorlocator=None, *, + tickminor=None, locator_kw=None, minorlocator_kw=None, + ): + """ + Update the locators. Requires `Locator` instances. + """ + # Apply input major locator + axis = getattr(self, f'{s}axis') + locator_kw = locator_kw or {} + if locator is not None: + locator = constructor.Locator(locator, **locator_kw) + axis.set_major_locator(locator) + if isinstance(locator, (mticker.IndexLocator, pticker.IndexLocator)): + tickminor = _not_none(tickminor, False) # disable 'index' minor ticks + + # Apply input or default minor locator + # NOTE: Parts of API (dualxy) rely on minor tick toggling preserving the + # isDefault_minloc setting. In future should override mpl minorticks_on() + # NOTE: Unlike matplotlib when "turning on" minor ticks we *always* use the + # scale default, thanks to scale classes refactoring with _ScaleBase. + isdefault = minorlocator is None + minorlocator_kw = minorlocator_kw or {} + if not isdefault: + minorlocator = constructor.Locator(minorlocator, **minorlocator_kw) + elif tickminor: + minorlocator = getattr(axis._scale, '_default_minor_locator', None) + minorlocator = copy.copy(minorlocator) + minorlocator = constructor.Locator(minorlocator or 'minor') + if minorlocator is not None: + axis.set_minor_locator(minorlocator) + axis.isDefault_minloc = isdefault + + # Disable minor ticks + # NOTE: Generally if you *enable* minor ticks on a dual axis, want to + # allow FuncScale updates to change the minor tick locators. If you + # *disable* minor ticks, do not want FuncScale applications to turn them + # on. So we allow below to set isDefault_minloc to False. + if tickminor is not None and not tickminor: + axis.set_minor_locator(constructor.Locator('null')) + + def _update_limits(self, s, *, min_=None, max_=None, lim=None, reverse=None): + """ + Update the axis limits. + """ + # Set limits for just one side or both at once + lim = self._min_max_lim(s, min_, max_, lim) + if any(_ is not None for _ in lim): + getattr(self, f'set_{s}lim')(lim) + + # Reverse direction + # NOTE: 3.1+ has axis.set_inverted(), below is from source code + if reverse is not None: + axis = getattr(self, f'{s}axis') + lo, hi = axis.get_view_interval() + if reverse: + lim = (max(lo, hi), min(lo, hi)) + else: + lim = (min(lo, hi), max(lo, hi)) + axis.set_view_interval(*lim, ignore=True) + + def _update_rotation(self, s, *, rotation=None): + """ + Rotate the tick labels. Rotate 90 degrees by default for datetime *x* axes. + """ + # Apply rotation for datetime axes. + # NOTE: Rotation is done *before* horizontal/vertical alignment. Cannot + # change alignment with set_tick_params so we must apply to text objects. + # Note fig.autofmt_date calls subplots_adjust, so we cannot use it. + current = f'_{s}axis_current_rotation' + default = f'_{s}axis_isdefault_rotation' + axis = getattr(self, f'{s}axis') + if rotation is not None: + setattr(self, default, False) + elif not getattr(self, default): + return # do not rotate + elif s == 'x' and isinstance(axis.converter, DATE_CONVERTERS): + rotation = rc['formatter.timerotation'] + else: + rotation = 'horizontal' + + # Apply tick label rotation if necessary + if rotation != getattr(self, current): + rotation = {'horizontal': 0, 'vertical': 90}.get(rotation, rotation) + kw = {'rotation': rotation} + if rotation not in (0, 90, -90): + kw['ha'] = 'right' if rotation > 0 else 'left' + for label in axis.get_ticklabels(): + label.update(kw) + setattr(self, current, rotation) + + def _update_spines(self, s, *, loc=None, bounds=None): + """ + Update the spine settings. + """ + # Change default spine location from 'both' to the first + # relevant side if the user passes 'bounds'. + sides = ('bottom', 'top') if s == 'x' else ('left', 'right') + opts = (*(s[0] for s in sides), *sides) # see _get_spine_side() + side = self._get_spine_side(s, loc) # side for set_position() + if bounds is not None and all(self.spines[s].get_visible() for s in sides): + loc = _not_none(loc, sides[0]) + for key in sides: + # Simple spine location that just toggles the side(s). Do not bother + # with the _get_spine_side stuff. + spine = self.spines[key] + if loc is None: + pass + elif loc == 'neither' or loc == 'none': + spine.set_visible(False) + elif loc == 'both': + spine.set_visible(True) + elif loc in opts: + spine.set_visible(key[0] == loc[0]) + # Special spine location, usually 'zero', 'center', or tuple with + # (units, location) where 'units' can be 'axes', 'data', or 'outward'. + elif key != side: + spine.set_visible(False) # special position is for other spine + else: + spine.set_visible(True) # special position uses this spine + spine.set_position(loc) + # Apply spine bounds + if bounds is not None: + spine.set_bounds(*bounds) + + def _update_locs( + self, s, *, tickloc=None, ticklabelloc=None, labelloc=None, offsetloc=None + ): + """ + Update the tick, tick label, and axis label locations. + """ + # Helper function and initial stuff + def _validate_loc(loc, opts, descrip): + try: + return opts[loc] + except KeyError: + raise ValueError( + f'Invalid {descrip} location {loc!r}. Options are ' + + ', '.join(map(repr, sides + tuple(opts))) + '.' + ) + sides = ('bottom', 'top') if s == 'x' else ('left', 'right') + sides_active = tuple(side for side in sides if self.spines[side].get_visible()) + label_opts = {s[:i]: s for s in sides for i in (1, None)} + tick_opts = {'both': sides, 'neither': (), 'none': (), None: None} + tick_opts.update({k: (v,) for k, v in label_opts.items()}) + + # Apply the tick mark and tick label locations + kw = {} + kw.update({side: False for side in sides if side not in sides_active}) + kw.update({'label' + side: False for side in sides if side not in sides_active}) + if ticklabelloc is not None: + ticklabelloc = _validate_loc(ticklabelloc, tick_opts, 'tick label') + kw.update({'label' + side: side in ticklabelloc for side in sides}) + if tickloc is not None: # possibly overrides ticklabelloc + tickloc = _validate_loc(tickloc, tick_opts, 'tick mark') + kw.update({side: side in tickloc for side in sides}) + kw.update({'label' + side: False for side in sides if side not in tickloc}) + self.tick_params(axis=s, which='both', **kw) + + # Apply the axis label and offset label locations + # Uses ugly mpl 3.3+ tick_top() tick_bottom() kludge for offset location + # See: https://matplotlib.org/3.3.1/users/whats_new.html + axis = getattr(self, f'{s}axis') + options = tuple(_ for _ in sides if tickloc and _ in tickloc and _ in sides_active) # noqa: E501 + if tickloc is not None and len(options) == 1: + labelloc = _not_none(labelloc, options[0]) + offsetloc = _not_none(offsetloc, options[0]) + if labelloc is not None: + labelloc = _validate_loc(labelloc, label_opts, 'axis label') + axis.set_label_position(labelloc) + if offsetloc is not None: + offsetloc = _not_none(offsetloc, options[0]) + if hasattr(axis, 'set_offset_position'): # y axis (and future x axis?) + axis.set_offset_position(offsetloc) + elif s == 'x' and _version_mpl >= '3.3': # ugly x axis kludge + axis._tick_position = offsetloc + axis.offsetText.set_verticalalignment(OPPOSITE_SIDE[offsetloc]) + + @docstring._snippet_manager + def format( + self, *, + aspect=None, + xloc=None, yloc=None, + xspineloc=None, yspineloc=None, + xoffsetloc=None, yoffsetloc=None, + xwraprange=None, ywraprange=None, + xreverse=None, yreverse=None, + xlim=None, ylim=None, + xmin=None, ymin=None, + xmax=None, ymax=None, + xscale=None, yscale=None, + xbounds=None, ybounds=None, + xmargin=None, ymargin=None, + xrotation=None, yrotation=None, + xformatter=None, yformatter=None, + xticklabels=None, yticklabels=None, + xticks=None, yticks=None, + xlocator=None, ylocator=None, + xminorticks=None, yminorticks=None, + xminorlocator=None, yminorlocator=None, + xcolor=None, ycolor=None, + xlinewidth=None, ylinewidth=None, + xtickloc=None, ytickloc=None, fixticks=False, + xtickdir=None, ytickdir=None, + xtickminor=None, ytickminor=None, + xtickrange=None, ytickrange=None, + xtickcolor=None, ytickcolor=None, + xticklen=None, yticklen=None, + xticklenratio=None, yticklenratio=None, + xtickwidth=None, ytickwidth=None, + xtickwidthratio=None, ytickwidthratio=None, + xticklabelloc=None, yticklabelloc=None, + xticklabeldir=None, yticklabeldir=None, + xticklabelpad=None, yticklabelpad=None, + xticklabelcolor=None, yticklabelcolor=None, + xticklabelsize=None, yticklabelsize=None, + xticklabelweight=None, yticklabelweight=None, + xlabel=None, ylabel=None, + xlabelloc=None, ylabelloc=None, + xlabelpad=None, ylabelpad=None, + xlabelcolor=None, ylabelcolor=None, + xlabelsize=None, ylabelsize=None, + xlabelweight=None, ylabelweight=None, + xgrid=None, ygrid=None, + xgridminor=None, ygridminor=None, + xgridcolor=None, ygridcolor=None, + xlabel_kw=None, ylabel_kw=None, + xscale_kw=None, yscale_kw=None, + xlocator_kw=None, ylocator_kw=None, + xformatter_kw=None, yformatter_kw=None, + xminorlocator_kw=None, yminorlocator_kw=None, + **kwargs + ): + """ + Modify axes limits, axis scales, axis labels, spine locations, + tick locations, tick labels, and more. + + Parameters + ---------- + %(cartesian.format)s + + Other parameters + ---------------- + %(axes.format)s + %(figure.format)s + %(rc.format)s + + See also + -------- + proplot.axes.Axes.format + proplot.figure.Figure.format + proplot.config.Configurator.context + + Note + ---- + If you plot something with a `datetime64 \ +`__, + `pandas.Timestamp`, `pandas.DatetimeIndex`, `datetime.date`, `datetime.time`, + or `datetime.datetime` array as the x or y axis coordinate, the axis ticks + and tick labels will be automatically formatted as dates. + """ + rc_kw, rc_mode = _pop_rc(kwargs) + with rc.context(rc_kw, mode=rc_mode): + # No mutable default args + xlabel_kw = xlabel_kw or {} + ylabel_kw = ylabel_kw or {} + xscale_kw = xscale_kw or {} + yscale_kw = yscale_kw or {} + xlocator_kw = xlocator_kw or {} + ylocator_kw = ylocator_kw or {} + xformatter_kw = xformatter_kw or {} + yformatter_kw = yformatter_kw or {} + xminorlocator_kw = xminorlocator_kw or {} + yminorlocator_kw = yminorlocator_kw or {} + + # Color keyword arguments. Inherit from 'color' when necessary + color = kwargs.pop('color', None) + xcolor = _not_none(xcolor, color) + ycolor = _not_none(ycolor, color) + if 'tick.color' not in rc_kw: + xtickcolor = _not_none(xtickcolor, xcolor) + ytickcolor = _not_none(ytickcolor, ycolor) + if 'tick.labelcolor' not in rc_kw: + xticklabelcolor = _not_none(xticklabelcolor, xcolor) + yticklabelcolor = _not_none(yticklabelcolor, ycolor) + if 'label.color' not in rc_kw: + xlabelcolor = _not_none(xlabelcolor, xcolor) + ylabelcolor = _not_none(ylabelcolor, ycolor) + + # Flexible keyword args, declare defaults + # NOTE: 'xtickdir' and 'ytickdir' read from 'tickdir' arguments here + xmargin = _not_none(xmargin, rc.find('axes.xmargin', context=True)) + ymargin = _not_none(ymargin, rc.find('axes.ymargin', context=True)) + xtickdir = _not_none(xtickdir, rc.find('xtick.direction', context=True)) + ytickdir = _not_none(ytickdir, rc.find('ytick.direction', context=True)) + xlocator = _not_none(xlocator=xlocator, xticks=xticks) + ylocator = _not_none(ylocator=ylocator, yticks=yticks) + xminorlocator = _not_none(xminorlocator=xminorlocator, xminorticks=xminorticks) # noqa: E501 + yminorlocator = _not_none(yminorlocator=yminorlocator, yminorticks=yminorticks) # noqa: E501 + xformatter = _not_none(xformatter=xformatter, xticklabels=xticklabels) + yformatter = _not_none(yformatter=yformatter, yticklabels=yticklabels) + xtickminor_default = ytickminor_default = None + if isinstance(xformatter, mticker.FixedFormatter) or np.iterable(xformatter) and not isinstance(xformatter, str): # noqa: E501 + xtickminor_default = False if xminorlocator is None else None + if isinstance(yformatter, mticker.FixedFormatter) or np.iterable(yformatter) and not isinstance(yformatter, str): # noqa: E501 + ytickminor_default = False if yminorlocator is None else None + xtickminor = _not_none(xtickminor, xtickminor_default, rc.find('xtick.minor.visible', context=True)) # noqa: E501 + ytickminor = _not_none(ytickminor, ytickminor_default, rc.find('ytick.minor.visible', context=True)) # noqa: E501 + ticklabeldir = kwargs.pop('ticklabeldir', None) + xticklabeldir = _not_none(xticklabeldir, ticklabeldir) + yticklabeldir = _not_none(yticklabeldir, ticklabeldir) + xtickdir = _not_none(xtickdir, xticklabeldir) + ytickdir = _not_none(ytickdir, yticklabeldir) + + # Sensible defaults for spine, tick, tick label, and label locs + # NOTE: Allow tick labels to be present without ticks! User may + # want this sometimes! Same goes for spines! + xspineloc = _not_none(xloc=xloc, xspineloc=xspineloc) + yspineloc = _not_none(yloc=yloc, yspineloc=yspineloc) + xside = self._get_spine_side('x', xspineloc) + yside = self._get_spine_side('y', yspineloc) + if xside is not None and xside not in ('zero', 'center', 'both'): + xtickloc = _not_none(xtickloc, xside) + if yside is not None and yside not in ('zero', 'center', 'both'): + ytickloc = _not_none(ytickloc, yside) + if xtickloc != 'both': # then infer others + xticklabelloc = _not_none(xticklabelloc, xtickloc) + if xticklabelloc in ('bottom', 'top'): + xlabelloc = _not_none(xlabelloc, xticklabelloc) + xoffsetloc = _not_none(xoffsetloc, yticklabelloc) + if ytickloc != 'both': # then infer others + yticklabelloc = _not_none(yticklabelloc, ytickloc) + if yticklabelloc in ('left', 'right'): + ylabelloc = _not_none(ylabelloc, yticklabelloc) + yoffsetloc = _not_none(yoffsetloc, yticklabelloc) + xtickloc = _not_none(xtickloc, rc._get_loc_string('x', 'xtick')) + ytickloc = _not_none(ytickloc, rc._get_loc_string('y', 'ytick')) + xspineloc = _not_none(xspineloc, rc._get_loc_string('x', 'axes.spines')) + yspineloc = _not_none(yspineloc, rc._get_loc_string('y', 'axes.spines')) + + # Loop over axes + for ( + s, + min_, + max_, + lim, + reverse, + margin, + bounds, + tickrange, + wraprange, + scale, + scale_kw, + spineloc, + tickloc, + ticklabelloc, + labelloc, + offsetloc, + grid, + gridminor, + locator, + locator_kw, + minorlocator, + minorlocator_kw, + formatter, + formatter_kw, + label, + label_kw, + color, + gridcolor, + linewidth, + rotation, + tickminor, + tickdir, + tickcolor, + ticklen, + ticklenratio, + tickwidth, + tickwidthratio, + ticklabeldir, + ticklabelpad, + ticklabelcolor, + ticklabelsize, + ticklabelweight, + labelpad, + labelcolor, + labelsize, + labelweight, + ) in zip( + ('x', 'y'), + (xmin, ymin), + (xmax, ymax), + (xlim, ylim), + (xreverse, yreverse), + (xmargin, ymargin), + (xbounds, ybounds), + (xtickrange, ytickrange), + (xwraprange, ywraprange), + (xscale, yscale), + (xscale_kw, yscale_kw), + (xspineloc, yspineloc), + (xtickloc, ytickloc), + (xticklabelloc, yticklabelloc), + (xlabelloc, ylabelloc), + (xoffsetloc, yoffsetloc), + (xgrid, ygrid), + (xgridminor, ygridminor), + (xlocator, ylocator), + (xlocator_kw, ylocator_kw), + (xminorlocator, yminorlocator), + (xminorlocator_kw, yminorlocator_kw), + (xformatter, yformatter), + (xformatter_kw, yformatter_kw), + (xlabel, ylabel), + (xlabel_kw, ylabel_kw), + (xcolor, ycolor), + (xgridcolor, ygridcolor), + (xlinewidth, ylinewidth), + (xrotation, yrotation), + (xtickminor, ytickminor), + (xtickdir, ytickdir), + (xtickcolor, ytickcolor), + (xticklen, yticklen), + (xticklenratio, yticklenratio), + (xtickwidth, ytickwidth), + (xtickwidthratio, ytickwidthratio), + (xticklabeldir, yticklabeldir), + (xticklabelpad, yticklabelpad), + (xticklabelcolor, yticklabelcolor), + (xticklabelsize, yticklabelsize), + (xticklabelweight, yticklabelweight), + (xlabelpad, ylabelpad), + (xlabelcolor, ylabelcolor), + (xlabelsize, ylabelsize), + (xlabelweight, ylabelweight), + ): + # Axis scale + # WARNING: This relies on monkey patch of mscale.scale_factory + # that allows it to accept a custom scale class! + # WARNING: Changing axis scale also changes default locators + # and formatters, and restricts possible range of axis limits, + # so critical to do it first. + if scale is not None: + scale = constructor.Scale(scale, **scale_kw) + getattr(self, f'set_{s}scale')(scale) + + # Axis limits + self._update_limits( + s, min_=min_, max_=max_, lim=lim, reverse=reverse + ) + if margin is not None: + self.margins(**{s: margin}) + + # Axis spine settings + # NOTE: This sets spine-specific color and linewidth settings. For + # non-specific settings _update_background is called in Axes.format() + self._update_spines( + s, loc=spineloc, bounds=bounds + ) + self._update_background( + s, edgecolor=color, linewidth=linewidth, + tickwidth=tickwidth, tickwidthratio=tickwidthratio, + ) + + # Axis tick settings + self._update_locs( + s, tickloc=tickloc, ticklabelloc=ticklabelloc, + labelloc=labelloc, offsetloc=offsetloc, + ) + self._update_rotation( + s, rotation=rotation + ) + self._update_ticks( + s, grid=grid, gridminor=gridminor, + ticklen=ticklen, ticklenratio=ticklenratio, + tickdir=tickdir, labeldir=ticklabeldir, labelpad=ticklabelpad, + tickcolor=tickcolor, gridcolor=gridcolor, labelcolor=ticklabelcolor, + labelsize=ticklabelsize, labelweight=ticklabelweight, + ) + + # Axis label settings + # NOTE: This must come after set_label_position, or any ha and va + # overrides in label_kw are overwritten. + kw = dict( + labelpad=labelpad, + color=labelcolor, + size=labelsize, + weight=labelweight, + **label_kw + ) + self._update_labels(s, label, **kw) + + # Axis locator + if minorlocator is True or minorlocator is False: # must test identity + warnings._warn_proplot( + f'You passed {s}minorticks={minorlocator}, but this argument ' + 'is used to specify the tick locations. If you just want to ' + f'toggle minor ticks, please use {s}tickminor={minorlocator}.' + ) + minorlocator = None + self._update_locators( + s, locator, minorlocator, tickminor=tickminor, + locator_kw=locator_kw, minorlocator_kw=minorlocator_kw, + ) + + # Axis formatter + self._update_formatter( + s, formatter, formatter_kw=formatter_kw, + tickrange=tickrange, wraprange=wraprange, + ) + + # Ensure ticks are within axis bounds + self._fix_ticks(s, fixticks=fixticks) + + # Parent format method + if aspect is not None: + self.set_aspect(aspect) + super().format(rc_kw=rc_kw, rc_mode=rc_mode, **kwargs) + + @docstring._snippet_manager + def altx(self, **kwargs): + """ + %(axes.altx)s + """ + return self._add_alt('x', **kwargs) + + @docstring._snippet_manager + def alty(self, **kwargs): + """ + %(axes.alty)s + """ + return self._add_alt('y', **kwargs) + + @docstring._snippet_manager + def dualx(self, funcscale, **kwargs): + """ + %(axes.dualx)s + """ + # NOTE: Matplotlib 3.1 has a 'secondary axis' feature. For the time + # being, our version is more robust (see FuncScale) and simpler, since + # we do not create an entirely separate _SecondaryAxis class. + ax = self._add_alt('x', **kwargs) + ax._dual_scale('x', funcscale) + return ax + + @docstring._snippet_manager + def dualy(self, funcscale, **kwargs): + """ + %(axes.dualy)s + """ + ax = self._add_alt('y', **kwargs) + ax._dual_scale('y', funcscale) + return ax + + @docstring._snippet_manager + def twinx(self, **kwargs): + """ + %(axes.twinx)s + """ + return self._add_alt('y', **kwargs) + + @docstring._snippet_manager + def twiny(self, **kwargs): + """ + %(axes.twiny)s + """ + return self._add_alt('x', **kwargs) + + def draw(self, renderer=None, *args, **kwargs): + # Perform extra post-processing steps + # NOTE: In *principle* axis sharing application step goes here. But should + # already be complete because auto_layout() (called by figure pre-processor) + # has to run it before aligning labels. So this is harmless no-op. + self._dual_scale('x') + self._dual_scale('y') + self._apply_axis_sharing() + self._update_rotation('x') + super().draw(renderer, *args, **kwargs) + + def get_tightbbox(self, renderer, *args, **kwargs): + # Perform extra post-processing steps + self._dual_scale('x') + self._dual_scale('y') + self._apply_axis_sharing() + self._update_rotation('x') + return super().get_tightbbox(renderer, *args, **kwargs) + + +# Apply signature obfuscation after storing previous signature +# NOTE: This is needed for __init__, altx, and alty +CartesianAxes._format_signatures[CartesianAxes] = inspect.signature(CartesianAxes.format) # noqa: E501 +CartesianAxes.format = docstring._obfuscate_kwargs(CartesianAxes.format) diff --git a/proplot/axes/geo.py b/proplot/axes/geo.py new file mode 100644 index 000000000..dc98551c0 --- /dev/null +++ b/proplot/axes/geo.py @@ -0,0 +1,1547 @@ +#!/usr/bin/env python3 +""" +Axes filled with cartographic projections. +""" +import copy +import inspect + +import matplotlib.axis as maxis +import matplotlib.path as mpath +import matplotlib.text as mtext +import matplotlib.ticker as mticker +import numpy as np + +from .. import constructor +from .. import proj as pproj +from ..config import rc +from ..internals import ic # noqa: F401 +from ..internals import _not_none, _pop_rc, _version_cartopy, docstring, warnings +from . import plot + +try: + import cartopy.crs as ccrs + import cartopy.feature as cfeature + import cartopy.mpl.gridliner as cgridliner + from cartopy.crs import Projection + from cartopy.mpl.geoaxes import GeoAxes as _GeoAxes +except ModuleNotFoundError: + ccrs = cfeature = cgridliner = None + _GeoAxes = Projection = object + +try: + from mpl_toolkits.basemap import Basemap +except ModuleNotFoundError: + Basemap = object + +__all__ = ['GeoAxes'] + + +# Format docstring +_format_docstring = """ +round : bool, default: :rc:`geo.round` + *For polar cartopy axes only*. + Whether to bound polar projections with circles rather than squares. Note that outer + gridline labels cannot be added to circle-bounded polar projections. When basemap + is the backend this argument must be passed to `~proplot.constructor.Proj` instead. +extent : {'globe', 'auto'}, default: :rc:`geo.extent` + *For cartopy axes only*. + Whether to auto adjust the map bounds based on plotted content. If ``'globe'`` then + non-polar projections are fixed with `~cartopy.mpl.geoaxes.GeoAxes.set_global`, + non-Gnomonic polar projections are bounded at the equator, and Gnomonic polar + projections are bounded at 30 degrees latitude. If ``'auto'`` nothing is done. +lonlim, latlim : 2-tuple of float, optional + *For cartopy axes only.* + The approximate longitude and latitude boundaries of the map, applied + with `~cartopy.mpl.geoaxes.GeoAxes.set_extent`. When basemap is the backend + this argument must be passed to `~proplot.constructor.Proj` instead. +boundinglat : float, optional + *For cartopy axes only.* + The edge latitude for the circle bounding North Pole and South Pole-centered + projections. When basemap is the backend this argument must be passed to + `~proplot.constructor.Proj` instead. +longrid, latgrid, grid : bool, default: :rc:`grid` + Whether to draw longitude and latitude gridlines. + Use the keyword `grid` to toggle both at once. +longridminor, latgridminor, gridminor : bool, default: :rc:`gridminor` + Whether to draw "minor" longitude and latitude lines. + Use the keyword `gridminor` to toggle both at once. +latmax : float, default: 80 + The maximum absolute latitude for gridlines. Longitude gridlines are cut off + poleward of this value (note this feature does not work in cartopy 0.18). +nsteps : int, default: :rc:`grid.nsteps` + *For cartopy axes only.* + The number of interpolation steps used to draw gridlines. +lonlines, latlines : optional + Aliases for `lonlocator`, `latlocator`. +lonlocator, latlocator : locator-spec, optional + Used to determine the longitude and latitude gridline locations. + Passed to the `~proplot.constructor.Locator` constructor. Can be + string, float, list of float, or `matplotlib.ticker.Locator` instance. + + For basemap or cartopy < 0.18, the defaults are ``'deglon'`` and + ``'deglat'``, which correspond to the `~proplot.ticker.LongitudeLocator` + and `~proplot.ticker.LatitudeLocator` locators (adapted from cartopy). + For cartopy >= 0.18, the defaults are ``'dmslon'`` and ``'dmslat'``, + which uses the same locators with ``dms=True``. This selects gridlines + at nice degree-minute-second intervals when the map extent is very small. +lonlines_kw, latlines_kw : optional + Aliases for `lonlocator_kw`, `latlocator_kw`. +lonlocator_kw, latlocator_kw : dict-like, optional + Keyword arguments passed to the `matplotlib.ticker.Locator` class. +lonminorlocator, latminorlocator, lonminorlines, latminorlines : optional + As with `lonlocator` and `latlocator` but for the "minor" gridlines. +lonminorlines_kw, latminorlines_kw : optional + Aliases for `lonminorlocator_kw`, `latminorlocator_kw`. +lonminorlocator_kw, latminorlocator_kw : optional + As with `lonlocator_kw`, and `latlocator_kw` but for the "minor" gridlines. +lonlabels, latlabels, labels : str, bool, or sequence, :rc:`grid.labels` + Whether to add non-inline longitude and latitude gridline labels, and on + which sides of the map. Use the keyword `labels` to set both at once. The + argument must conform to one of the following options: + + * A boolean. ``True`` indicates the bottom side for longitudes and + the left side for latitudes, and ``False`` disables all labels. + * A string or sequence of strings indicating the side names, e.g. + ``'top'`` for longitudes or ``('left', 'right')`` for latitudes. + * A string indicating the side names with single characters, e.g. + ``'bt'`` for longitudes or ``'lr'`` for latitudes. + * A string matching ``'neither'`` (no labels), ``'both'`` (equivalent + to ``'bt'`` for longitudes and ``'lr'`` for latitudes), or ``'all'`` + (equivalent to ``'lrbt'``, i.e. all sides). + * A boolean 2-tuple indicating whether to draw labels + on the ``(bottom, top)`` sides for longitudes, + and the ``(left, right)`` sides for latitudes. + * A boolean 4-tuple indicating whether to draw labels on the + ``(left, right, bottom, top)`` sides, as with the basemap + `~mpl_toolkits.basemap.Basemap.drawmeridians` and + `~mpl_toolkits.basemap.Basemap.drawparallels` `labels` keyword. + +loninline, latinline, inlinelabels : bool, default: :rc:`grid.inlinelabels` + *For cartopy axes only.* + Whether to add inline longitude and latitude gridline labels. Use + the keyword `inlinelabels` to set both at once. +rotatelabels : bool, default: :rc:`grid.rotatelabels` + *For cartopy axes only.* + Whether to rotate non-inline gridline labels so that they automatically + follow the map boundary curvature. +labelpad : unit-spec, default: :rc:`grid.labelpad` + *For cartopy axes only.* + The padding between non-inline gridline labels and the map boundary. + %(units.pt)s +dms : bool, default: :rc:`grid.dmslabels` + *For cartopy axes only.* + Whether the default locators and formatters should use "minutes" and "seconds" + for gridline labels on small scales rather than decimal degrees. Setting this to + ``False`` is equivalent to ``ax.format(lonlocator='deglon', latlocator='deglat')`` + and ``ax.format(lonformatter='deglon', latformatter='deglat')``. +lonformatter, latformatter : formatter-spec, optional + Formatter used to style longitude and latitude gridline labels. + Passed to the `~proplot.constructor.Formatter` constructor. Can be + string, list of string, or `matplotlib.ticker.Formatter` instance. + + For basemap or cartopy < 0.18, the defaults are ``'deglon'`` and + ``'deglat'``, which correspond to `~proplot.ticker.SimpleFormatter` + presets with degree symbols and cardinal direction suffixes. + For cartopy >= 0.18, the defaults are ``'dmslon'`` and ``'dmslat'``, + which uses cartopy's `~cartopy.mpl.ticker.LongitudeFormatter` and + `~cartopy.mpl.ticker.LatitudeFormatter` formatters with ``dms=True``. + This formats gridlines that do not fall on whole degrees as "minutes" and + "seconds" rather than decimal degrees. Use ``dms=False`` to disable this. +lonformatter_kw, latformatter_kw : dict-like, optional + Keyword arguments passed to the `matplotlib.ticker.Formatter` class. +land, ocean, coast, rivers, lakes, borders, innerborders : bool, optional + Toggles various geographic features. These are actually the + :rcraw:`land`, :rcraw:`ocean`, :rcraw:`coast`, :rcraw:`rivers`, + :rcraw:`lakes`, :rcraw:`borders`, and :rcraw:`innerborders` + settings passed to `~proplot.config.Configurator.context`. + The style can be modified using additional `rc` settings. + + For example, to change :rcraw:`land.color`, use + ``ax.format(landcolor='green')``, and to change + :rcraw:`land.zorder`, use ``ax.format(landzorder=4)``. +reso : {'lo', 'med', 'hi', 'x-hi', 'xx-hi'}, optional + *For cartopy axes only.* + The resolution of geographic features. When basemap is the backend this + must be passed to `~proplot.constructor.Proj` instead. +color : color-spec, default: :rc:`meta.color` + The color for the axes edge. Propagates to `labelcolor` unless specified + otherwise (similar to `proplot.axes.CartesianAxes.format`). +gridcolor : color-spec, default: :rc:`grid.color` + The color for the gridline labels. +labelcolor : color-spec, default: `color` or :rc:`grid.labelcolor` + The color for the gridline labels (`gridlabelcolor` is also allowed). +labelsize : unit-spec or str, default: :rc:`grid.labelsize` + The font size for the gridline labels (`gridlabelsize` is also allowed). + %(units.pt)s +labelweight : str, default: :rc:`grid.labelweight` + The font weight for the gridline labels (`gridlabelweight` is also allowed). +""" +docstring._snippet_manager['geo.format'] = _format_docstring + + +class _GeoLabel(object): + """ + Optionally omit overlapping check if an rc setting is disabled. + """ + def check_overlapping(self, *args, **kwargs): + if rc['grid.checkoverlap']: + return super().check_overlapping(*args, **kwargs) + else: + return False + + +# Add monkey patch to gridliner module +if cgridliner is not None and hasattr(cgridliner, 'Label'): # only recent versions + _cls = type('Label', (_GeoLabel, cgridliner.Label), {}) + cgridliner.Label = _cls + + +class _GeoAxis(object): + """ + Dummy axis used by longitude and latitude locators and for storing view limits on + longitude and latitude coordinates. Modeled after how `matplotlib.ticker._DummyAxis` + and `matplotlib.ticker.TickHelper` are used to control tick locations and labels. + """ + # NOTE: Due to cartopy bug (https://github.com/SciTools/cartopy/issues/1564) + # we store presistent longitude and latitude locators on axes, then *call* + # them whenever set_extent is called and apply *fixed* locators. + def __init__(self, axes): + self.axes = axes + self.major = maxis.Ticker() + self.minor = maxis.Ticker() + self.isDefault_majfmt = True + self.isDefault_majloc = True + self.isDefault_minloc = True + self._interval = None + self._use_dms = ( + ccrs is not None + and isinstance(axes.projection, (ccrs._RectangularProjection, ccrs.Mercator)) # noqa: E501 + and _version_cartopy >= '0.18' + ) + + def _get_extent(self): + # Try to get extent but bail out for projections where this is + # impossible. So far just transverse Mercator + try: + return self.axes.get_extent() + except Exception: + lon0 = self.axes._get_lon0() + return (-180 + lon0, 180 + lon0, -90, 90) + + @staticmethod + def _pad_ticks(ticks, vmin, vmax): + # Wrap up to the longitude/latitude range to avoid + # giant lists of 10,000 gridline locations. + if len(ticks) == 0: + return ticks + range_ = np.max(ticks) - np.min(ticks) + vmin = max(vmin, ticks[0] - range_) + vmax = min(vmax, ticks[-1] + range_) + + # Pad the reported tick range up to specified range + step = ticks[1] - ticks[0] # MaxNLocator/AutoMinorLocator steps are equal + ticks_lo = np.arange(ticks[0], vmin, -step)[1:][::-1] + ticks_hi = np.arange(ticks[-1], vmax, step)[1:] + ticks = np.concatenate((ticks_lo, ticks, ticks_hi)) + return ticks + + def get_scale(self): + return 'linear' + + def get_tick_space(self): + return 9 # longstanding default of nbins=9 + + def get_major_formatter(self): + return self.major.formatter + + def get_major_locator(self): + return self.major.locator + + def get_minor_locator(self): + return self.minor.locator + + def get_majorticklocs(self): + return self._get_ticklocs(self.major.locator) + + def get_minorticklocs(self): + return self._get_ticklocs(self.minor.locator) + + def set_major_formatter(self, formatter, default=False): + # NOTE: Cartopy formatters check Formatter.axis.axes.projection + # in order to implement special projection-dependent behavior. + self.major.formatter = formatter + formatter.set_axis(self) + self.isDefault_majfmt = default + + def set_major_locator(self, locator, default=False): + self.major.locator = locator + if self.major.formatter: + self.major.formatter._set_locator(locator) + locator.set_axis(self) + self.isDefault_majloc = default + + def set_minor_locator(self, locator, default=False): + self.minor.locator = locator + locator.set_axis(self) + self.isDefault_majfmt = default + + def set_view_interval(self, vmin, vmax): + self._interval = (vmin, vmax) + + +class _LonAxis(_GeoAxis): + """ + Axis with default longitude locator. + """ + # NOTE: Basemap accepts tick formatters with drawmeridians(fmt=Formatter()) + # Try to use cartopy formatter if cartopy installed. Otherwise use + # default builtin basemap formatting. + def __init__(self, axes): + super().__init__(axes) + if self._use_dms: + locator = formatter = 'dmslon' + else: + locator = formatter = 'deglon' + self.set_major_formatter(constructor.Formatter(formatter), default=True) + self.set_major_locator(constructor.Locator(locator), default=True) + self.set_minor_locator(mticker.AutoMinorLocator(), default=True) + + def _get_ticklocs(self, locator): + # Prevent ticks from looping around + # NOTE: Cartopy 0.17 formats numbers offset by eps with the cardinal indicator + # (e.g. 0 degrees for map centered on 180 degrees). So skip in that case. + # NOTE: Common strange issue is e.g. MultipleLocator(60) starts out at + # -60 degrees for a map from 0 to 360 degrees. If always trimmed circular + # locations from right then would cut off rightmost gridline. Workaround is + # to trim on the side closest to central longitude (in this case the left). + eps = 1e-10 + lon0 = self.axes._get_lon0() + ticks = np.sort(locator()) + while ticks.size: + if np.isclose(ticks[0] + 360, ticks[-1]): + if _version_cartopy >= '0.18' or not np.isclose(ticks[0] % 360, 0): + ticks[-1] -= eps # ensure label appears on *right* not left + break + elif ticks[0] + 360 < ticks[-1]: + idx = (1, None) if lon0 - ticks[0] > ticks[-1] - lon0 else (None, -1) + ticks = ticks[slice(*idx)] # cut off ticks looped over globe + else: + break + + # Append extra ticks in case longitude/latitude limits do not encompass + # the entire view range of map, e.g. for Lambert Conformal sectors. + # NOTE: Try to avoid making 10,000 element lists. Just wrap extra ticks + # up to the width of *reported* longitude range. + if isinstance(locator, (mticker.MaxNLocator, mticker.AutoMinorLocator)): + ticks = self._pad_ticks(ticks, lon0 - 180 + eps, lon0 + 180 - eps) + + return ticks + + def get_view_interval(self): + # NOTE: Proplot tries to set its *own* view intervals to avoid dateline + # weirdness, but if rc['geo.extent'] is 'auto' the interval will be unset. + # In this case we use _get_extent() as a backup. + interval = self._interval + if interval is None: + extent = self._get_extent() + interval = extent[:2] # longitude extents + return interval + + +class _LatAxis(_GeoAxis): + """ + Axis with default latitude locator. + """ + def __init__(self, axes, latmax=90): + # NOTE: Need to pass projection because lataxis/lonaxis are + # initialized before geoaxes is initialized, because format() needs + # the axes and format() is called by proplot.axes.Axes.__init__() + self._latmax = latmax + super().__init__(axes) + if self._use_dms: + locator = formatter = 'dmslat' + else: + locator = formatter = 'deglat' + self.set_major_formatter(constructor.Formatter(formatter), default=True) + self.set_major_locator(constructor.Locator(locator), default=True) + self.set_minor_locator(mticker.AutoMinorLocator(), default=True) + + def _get_ticklocs(self, locator): + # Adjust latitude ticks to fix bug in some projections. Harmless for basemap. + # NOTE: Maybe this was fixed by cartopy 0.18? + eps = 1e-10 + ticks = np.sort(locator()) + if ticks.size: + if ticks[0] == -90: + ticks[0] += eps + if ticks[-1] == 90: + ticks[-1] -= eps + + # Append extra ticks in case longitude/latitude limits do not encompass + # the entire view range of map, e.g. for Lambert Conformal sectors. + if isinstance(locator, (mticker.MaxNLocator, mticker.AutoMinorLocator)): + ticks = self._pad_ticks(ticks, -90 + eps, 90 - eps) + + # Filter ticks to latmax range + latmax = self.get_latmax() + ticks = ticks[(ticks >= -latmax) & (ticks <= latmax)] + + return ticks + + def get_latmax(self): + return self._latmax + + def get_view_interval(self): + interval = self._interval + if interval is None: + extent = self._get_extent() + interval = extent[2:] # latitudes + return interval + + def set_latmax(self, latmax): + self._latmax = latmax + + +class GeoAxes(plot.PlotAxes): + """ + Axes subclass for plotting in geographic projections. Uses either cartopy + or basemap as a "backend". + + Note + ---- + This subclass uses longitude and latitude as the default coordinate system for all + plotting commands by internally passing ``transform=cartopy.crs.PlateCarree()`` to + cartopy commands and ``latlon=True`` to basemap commands. Also, when using basemap + as the "backend", plotting is still done "cartopy-style" by calling methods from + the axes instance rather than the `~mpl_toolkits.basemap.Basemap` instance. + + Important + --------- + This axes subclass can be used by passing ``proj='proj_name'`` + to axes-creation commands like `~proplot.figure.Figure.add_axes`, + `~proplot.figure.Figure.add_subplot`, and `~proplot.figure.Figure.subplots`, + where ``proj_name`` is a registered :ref:`PROJ projection name `. + You can also pass a `~cartopy.crs.Projection` or `~mpl_toolkits.basemap.Basemap` + instance instead of a projection name. Alternatively, you can pass any of the + matplotlib-recognized axes subclass names ``proj='cartopy'``, ``proj='geo'``, or + ``proj='geographic'`` with a `~cartopy.crs.Projection` `map_projection` keyword + argument, or pass ``proj='basemap'`` with a `~mpl_toolkits.basemap.Basemap` + `map_projection` keyword argument. + """ + @docstring._snippet_manager + def __init__(self, *args, **kwargs): + """ + Parameters + ---------- + *args + Passed to `matplotlib.axes.Axes`. + map_projection : `~cartopy.crs.Projection` or `~mpl_toolkits.basemap.Basemap` + The cartopy or basemap projection instance. This is + passed automatically when calling axes-creation + commands like `~proplot.figure.Figure.add_subplot`. + %(geo.format)s + + Other parameters + ---------------- + %(axes.format)s + %(rc.init)s + + See also + -------- + GeoAxes.format + proplot.constructor.Proj + proplot.axes.Axes + proplot.axes.PlotAxes + proplot.figure.Figure.subplot + proplot.figure.Figure.add_subplot + """ + super().__init__(*args, **kwargs) + + def _get_lonticklocs(self, which='major'): + """ + Retrieve longitude tick locations. + """ + # Get tick locations from dummy axes + # NOTE: This is workaround for: https://github.com/SciTools/cartopy/issues/1564 + # Since _axes_domain is wrong we determine tick locations ourselves with + # more accurate extent tracked by _LatAxis and _LonAxis. + axis = self._lonaxis + if which == 'major': + lines = axis.get_majorticklocs() + else: + lines = axis.get_minorticklocs() + return lines + + def _get_latticklocs(self, which='major'): + """ + Retrieve latitude tick locations. + """ + axis = self._lataxis + if which == 'major': + lines = axis.get_majorticklocs() + else: + lines = axis.get_minorticklocs() + return lines + + def _set_view_intervals(self, extent): + """ + Update view intervals for lon and lat axis. + """ + self._lonaxis.set_view_interval(*extent[:2]) + self._lataxis.set_view_interval(*extent[2:]) + + @staticmethod + def _to_label_array(arg, lon=True): + """ + Convert labels argument to length-5 boolean array. + """ + array = arg + which = 'lon' if lon else 'lat' + array = np.atleast_1d(array).tolist() + if len(array) == 1 and array[0] is None: + array = [None] * 5 + elif all(isinstance(_, str) for _ in array): + strings = array # iterate over list of strings + array = [False] * 5 + opts = ('left', 'right', 'bottom', 'top', 'geo') + for string in strings: + if string == 'all': + string = 'lrbt' + elif string == 'both': + string = 'bt' if lon else 'lr' + elif string == 'neither': + string = '' + elif string in opts: + string = string[0] + if set(string) - set('lrbtg'): + raise ValueError( + f'Invalid {which}label string {string!r}. Must be one of ' + + ', '.join(map(repr, (*opts, 'neither', 'both', 'all'))) + + " or a string of single-letter characters like 'lr'." + ) + for char in string: + array['lrbtg'.index(char)] = True + if rc['grid.geolabels'] and any(array): + array[4] = True # possibly toggle geo spine labels + elif not any(isinstance(_, str) for _ in array): + if len(array) == 1: + array.append(False) # default is to label bottom or left + if len(array) == 2: + array = [False, False, *array] if lon else [*array, False, False] + if len(array) == 4: + b = any(array) if rc['grid.geolabels'] else False + array.append(b) # possibly toggle geo spine labels + if len(array) != 5: + raise ValueError(f'Invald boolean label array length {len(array)}.') + array = list(map(bool, array)) + else: + raise ValueError(f'Invalid {which}label spec: {arg}.') + return array + + @docstring._snippet_manager + def format( + self, *, + extent=None, round=None, + lonlim=None, latlim=None, boundinglat=None, + longrid=None, latgrid=None, longridminor=None, latgridminor=None, + latmax=None, nsteps=None, + lonlocator=None, lonlines=None, + latlocator=None, latlines=None, + lonminorlocator=None, lonminorlines=None, + latminorlocator=None, latminorlines=None, + lonlocator_kw=None, lonlines_kw=None, + latlocator_kw=None, latlines_kw=None, + lonminorlocator_kw=None, lonminorlines_kw=None, + latminorlocator_kw=None, latminorlines_kw=None, + lonformatter=None, latformatter=None, + lonformatter_kw=None, latformatter_kw=None, + labels=None, latlabels=None, lonlabels=None, rotatelabels=None, + loninline=None, latinline=None, inlinelabels=None, dms=None, + labelpad=None, labelcolor=None, labelsize=None, labelweight=None, + **kwargs, + ): + """ + Modify map limits, longitude and latitude + gridlines, geographic features, and more. + + Parameters + ---------- + %(geo.format)s + + Other parameters + ---------------- + %(axes.format)s + %(figure.format)s + %(rc.format)s + + See also + -------- + proplot.axes.Axes.format + proplot.config.Configurator.context + """ + # Initialize map boundary + # WARNING: Normal workflow is Axes.format() does 'universal' tasks including + # updating the map boundary (in the future may also handle gridlines). However + # drawing gridlines before basemap map boundary will call set_axes_limits() + # which initializes a boundary hidden from external access. So we must call + # it here. Must do this between mpl.Axes.__init__() and base.Axes.format(). + if self._name == 'basemap' and self._map_boundary is None: + if self.projection.projection in self._proj_non_rectangular: + patch = self.projection.drawmapboundary(ax=self) + self._map_boundary = patch + else: + self.projection.set_axes_limits(self) # initialize aspect ratio + self._map_boundary = object() # sentinel + + # Initiate context block + rc_kw, rc_mode = _pop_rc(kwargs) + lonlabels = _not_none(lonlabels, labels) + latlabels = _not_none(latlabels, labels) + if '0.18' <= _version_cartopy < '0.20': + lonlabels = _not_none(lonlabels, loninline, inlinelabels) + latlabels = _not_none(latlabels, latinline, inlinelabels) + labelcolor = _not_none(labelcolor, kwargs.get('color', None)) + if labelcolor is not None: + rc_kw['grid.labelcolor'] = labelcolor + if labelsize is not None: + rc_kw['grid.labelsize'] = labelsize + if labelweight is not None: + rc_kw['grid.labelweight'] = labelweight + with rc.context(rc_kw, mode=rc_mode): + # Apply extent mode first + # NOTE: We deprecate autoextent on _CartopyAxes with _rename_kwargs which + # does not translate boolean flag. So here apply translation. + if extent is not None and not isinstance(extent, str): + extent = ('globe', 'auto')[int(bool(extent))] + self._update_boundary(round) + self._update_extent_mode(extent, boundinglat) + + # Retrieve label toggles + # NOTE: Cartopy 0.18 and 0.19 inline labels require any of + # top, bottom, left, or right to be toggled then ignores them. + # Later versions of cartopy permit both or neither labels. + labels = _not_none(labels, rc.find('grid.labels', context=True)) + lonlabels = _not_none(lonlabels, labels) + latlabels = _not_none(latlabels, labels) + lonarray = self._to_label_array(lonlabels, lon=True) + latarray = self._to_label_array(latlabels, lon=False) + + # Update max latitude + latmax = _not_none(latmax, rc.find('grid.latmax', context=True)) + if latmax is not None: + self._lataxis.set_latmax(latmax) + + # Update major locators + lonlocator = _not_none(lonlocator=lonlocator, lonlines=lonlines) + latlocator = _not_none(latlocator=latlocator, latlines=latlines) + if lonlocator is not None: + lonlocator_kw = _not_none( + lonlocator_kw=lonlocator_kw, lonlines_kw=lonlines_kw, default={}, + ) + locator = constructor.Locator(lonlocator, **lonlocator_kw) + self._lonaxis.set_major_locator(locator) + if latlocator is not None: + latlocator_kw = _not_none( + latlocator_kw=latlocator_kw, latlines_kw=latlines_kw, default={}, + ) + locator = constructor.Locator(latlocator, **latlocator_kw) + self._lataxis.set_major_locator(locator) + + # Update minor locators + lonminorlocator = _not_none( + lonminorlocator=lonminorlocator, lonminorlines=lonminorlines + ) + latminorlocator = _not_none( + latminorlocator=latminorlocator, latminorlines=latminorlines + ) + if lonminorlocator is not None: + lonminorlocator_kw = _not_none( + lonminorlocator_kw=lonminorlocator_kw, + lonminorlines_kw=lonminorlines_kw, + default={}, + ) + locator = constructor.Locator(lonminorlocator, **lonminorlocator_kw) + self._lonaxis.set_minor_locator(locator) + if latminorlocator is not None: + latminorlocator_kw = _not_none( + latminorlocator_kw=latminorlocator_kw, + latminorlines_kw=latminorlines_kw, + default={}, + ) + locator = constructor.Locator(latminorlocator, **latminorlocator_kw) + self._lataxis.set_minor_locator(locator) + + # Update formatters + loninline = _not_none(loninline, inlinelabels, rc.find('grid.inlinelabels', context=True)) # noqa: E501 + latinline = _not_none(latinline, inlinelabels, rc.find('grid.inlinelabels', context=True)) # noqa: E501 + rotatelabels = _not_none(rotatelabels, rc.find('grid.rotatelabels', context=True)) # noqa: E501 + labelpad = _not_none(labelpad, rc.find('grid.labelpad', context=True)) + dms = _not_none(dms, rc.find('grid.dmslabels', context=True)) + nsteps = _not_none(nsteps, rc.find('grid.nsteps', context=True)) + if lonformatter is not None: + lonformatter_kw = lonformatter_kw or {} + formatter = constructor.Formatter(lonformatter, **lonformatter_kw) + self._lonaxis.set_major_formatter(formatter) + if latformatter is not None: + latformatter_kw = latformatter_kw or {} + formatter = constructor.Formatter(latformatter, **latformatter_kw) + self._lataxis.set_major_formatter(formatter) + if dms is not None: # harmless if these are not GeoLocators + self._lonaxis.get_major_formatter()._dms = dms + self._lataxis.get_major_formatter()._dms = dms + self._lonaxis.get_major_locator()._dms = dms + self._lataxis.get_major_locator()._dms = dms + + # Apply worker extent, feature, and gridline functions + lonlim = _not_none(lonlim, default=(None, None)) + latlim = _not_none(latlim, default=(None, None)) + self._update_extent(lonlim=lonlim, latlim=latlim, boundinglat=boundinglat) + self._update_features() + self._update_major_gridlines( + longrid=longrid, latgrid=latgrid, # gridline toggles + lonarray=lonarray, latarray=latarray, # label toggles + loninline=loninline, latinline=latinline, rotatelabels=rotatelabels, + labelpad=labelpad, nsteps=nsteps, + ) + self._update_minor_gridlines( + longrid=longridminor, latgrid=latgridminor, nsteps=nsteps, + ) + + # Parent format method + super().format(rc_kw=rc_kw, rc_mode=rc_mode, **kwargs) + + @property + def gridlines_major(self): + """ + The cartopy `~cartopy.mpl.gridliner.Gridliner` + used for major gridlines or a 2-tuple containing the + (longitude, latitude) major gridlines returned by + basemap's `~mpl_toolkits.basemap.Basemap.drawmeridians` + and `~mpl_toolkits.basemap.Basemap.drawparallels`. + This can be used for customization and debugging. + """ + if self._name == 'basemap': + return (self._lonlines_major, self._latlines_major) + else: + return self._gridlines_major + + @property + def gridlines_minor(self): + """ + The cartopy `~cartopy.mpl.gridliner.Gridliner` + used for minor gridlines or a 2-tuple containing the + (longitude, latitude) minor gridlines returned by + basemap's `~mpl_toolkits.basemap.Basemap.drawmeridians` + and `~mpl_toolkits.basemap.Basemap.drawparallels`. + This can be used for customization and debugging. + """ + if self._name == 'basemap': + return (self._lonlines_minor, self._latlines_minor) + else: + return self._gridlines_minor + + @property + def projection(self): + """ + The cartopy `~cartopy.crs.Projection` or basemap `~mpl_toolkits.basemap.Basemap` + instance associated with this axes. + """ + return self._map_projection + + @projection.setter + def projection(self, map_projection): + cls = self._proj_class + if not isinstance(map_projection, cls): + raise ValueError(f'Projection must be a {cls} instance.') + self._map_projection = map_projection + + +class _CartopyAxes(GeoAxes, _GeoAxes): + """ + Axes subclass for plotting cartopy projections. + """ + _name = 'cartopy' + _name_aliases = ('geo', 'geographic') # default 'geographic' axes + _proj_class = Projection + _proj_north = ( + pproj.NorthPolarStereo, + pproj.NorthPolarGnomonic, + pproj.NorthPolarAzimuthalEquidistant, + pproj.NorthPolarLambertAzimuthalEqualArea, + ) + _proj_south = ( + pproj.SouthPolarStereo, + pproj.SouthPolarGnomonic, + pproj.SouthPolarAzimuthalEquidistant, + pproj.SouthPolarLambertAzimuthalEqualArea + ) + _proj_polar = _proj_north + _proj_south + + # NOTE: The rename argument wrapper belongs here instead of format() because + # these arguments were previously only accepted during initialization. + @warnings._rename_kwargs('0.10', circular='round', autoextent='extent') + def __init__(self, *args, map_projection=None, **kwargs): + """ + Parameters + ---------- + map_projection : ~cartopy.crs.Projection + The map projection. + *args, **kwargs + Passed to `GeoAxes`. + """ + # Initialize axes. Note that critical attributes like outline_patch + # needed by _format_apply are added before it is called. + import cartopy # noqa: F401 verify package is available + self.projection = map_projection # verify + polar = isinstance(self.projection, self._proj_polar) + latmax = 80 if polar else 90 # default latmax + self._is_round = False + self._boundinglat = None # NOTE: must start at None so _update_extent acts + self._gridlines_major = None + self._gridlines_minor = None + self._lonaxis = _LonAxis(self) + self._lataxis = _LatAxis(self, latmax=latmax) + super().__init__(*args, map_projection=self.projection, **kwargs) + for axis in (self.xaxis, self.yaxis): + axis.set_tick_params(which='both', size=0) # prevent extra label offset + + def _apply_axis_sharing(self): # noqa: U100 + """ + No-op for now. In future will hide labels on certain subplots. + """ + pass + + @staticmethod + def _get_circle_path(N=100): + """ + Return a circle `~matplotlib.path.Path` used as the outline for polar + stereographic, azimuthal equidistant, Lambert conformal, and gnomonic + projections. This was developed from `this cartopy example \ + `__. + """ + theta = np.linspace(0, 2 * np.pi, N) + center, radius = [0.5, 0.5], 0.5 + verts = np.vstack([np.sin(theta), np.cos(theta)]).T + return mpath.Path(verts * radius + center) + + def _get_global_extent(self): + """ + Return the global extent with meridian properly shifted. + """ + lon0 = self._get_lon0() + return [-180 + lon0, 180 + lon0, -90, 90] + + def _get_lon0(self): + """ + Get the central longitude. Default is ``0``. + """ + return self.projection.proj4_params.get('lon_0', 0) + + def _init_gridlines(self): + """ + Create monkey patched "major" and "minor" gridliners managed by proplot. + """ + # Cartopy < 0.18 monkey patch. Helps filter valid coordates to lon_0 +/- 180 + def _axes_domain(self, *args, **kwargs): + x_range, y_range = type(self)._axes_domain(self, *args, **kwargs) + if _version_cartopy < '0.18': + lon_0 = self.axes.projection.proj4_params.get('lon_0', 0) + x_range = np.asarray(x_range) + lon_0 + return x_range, y_range + # Cartopy >= 0.18 monkey patch. Fixes issue where cartopy draws an overlapping + # dateline gridline (e.g. polar maps). See the nx -= 1 line in _draw_gridliner + def _draw_gridliner(self, *args, **kwargs): # noqa: E306 + result = type(self)._draw_gridliner(self, *args, **kwargs) + if _version_cartopy >= '0.18': + lon_lim, _ = self._axes_domain() + if abs(np.diff(lon_lim)) == abs(np.diff(self.crs.x_limits)): + for collection in self.xline_artists: + if not getattr(collection, '_cartopy_fix', False): + collection.get_paths().pop(-1) + collection._cartopy_fix = True + return result + # Return the gridliner with monkey patch + gl = self.gridlines(crs=ccrs.PlateCarree()) + gl._axes_domain = _axes_domain.__get__(gl) + gl._draw_gridliner = _draw_gridliner.__get__(gl) + gl.xlines = gl.ylines = False + self._toggle_gridliner_labels(gl, False, False, False, False, False) + return gl + + @staticmethod + def _toggle_gridliner_labels( + gl, left=None, right=None, bottom=None, top=None, geo=None + ): + """ + Toggle gridliner labels across different cartopy versions. + """ + if _version_cartopy >= '0.18': + left_labels = 'left_labels' + right_labels = 'right_labels' + bottom_labels = 'bottom_labels' + top_labels = 'top_labels' + else: # cartopy < 0.18 + left_labels = 'ylabels_left' + right_labels = 'ylabels_right' + bottom_labels = 'xlabels_bottom' + top_labels = 'xlabels_top' + if left is not None: + setattr(gl, left_labels, left) + if right is not None: + setattr(gl, right_labels, right) + if bottom is not None: + setattr(gl, bottom_labels, bottom) + if top is not None: + setattr(gl, top_labels, top) + if geo is not None: # only cartopy 0.20 supported but harmless + setattr(gl, 'geo_labels', geo) + + def _update_background(self, **kwargs): + """ + Update the map background patches. This is called in `Axes.format`. + """ + # TODO: Understand issue where setting global linewidth puts map boundary on + # top of land patches, but setting linewidth with format() (even with separate + # format() calls) puts map boundary underneath. Zorder seems to be totally + # ignored and using spines vs. patch makes no difference. + # NOTE: outline_patch is redundant, use background_patch instead + kw_face, kw_edge = rc._get_background_props(native=False, **kwargs) + kw_face['linewidth'] = 0 + kw_edge['facecolor'] = 'none' + if _version_cartopy >= '0.18': + self.patch.update(kw_face) + self.spines['geo'].update(kw_edge) + else: + self.background_patch.update(kw_face) + self.outline_patch.update(kw_edge) + + def _update_boundary(self, round=None): + """ + Update the map boundary path. + """ + round = _not_none(round, rc.find('geo.round', context=True)) + if round is None or not isinstance(self.projection, self._proj_polar): + pass + elif round: + self._is_round = True + self.set_boundary(self._get_circle_path(), transform=self.transAxes) + elif not round and self._is_round: + if hasattr(self, '_boundary'): + self._boundary() + else: + warnings._warn_proplot('Failed to reset round map boundary.') + + def _update_extent_mode(self, extent=None, boundinglat=None): + """ + Update the extent mode. + """ + # NOTE: Use set_global rather than set_extent() or _update_extent() for + # simplicity. Uses projection.[xy]_limits which may not be strictly global. + # NOTE: For some reason initial call to _set_view_intervals may change the + # default boundary with extent='auto'. Try this in a robinson projection: + # ax.contour(np.linspace(-90, 180, N), np.linspace(0, 90, N), np.zeros(N, N)) + extent = _not_none(extent, rc.find('geo.extent', context=True)) + if extent is None: + return + if extent not in ('globe', 'auto'): + raise ValueError( + f"Invalid extent mode {extent!r}. Must be 'auto' or 'globe'." + ) + polar = isinstance(self.projection, self._proj_polar) + if not polar: + self.set_global() + else: + if isinstance(self.projection, pproj.NorthPolarGnomonic): + default_boundinglat = 30 + elif isinstance(self.projection, pproj.SouthPolarGnomonic): + default_boundinglat = -30 + else: + default_boundinglat = 0 + boundinglat = _not_none(boundinglat, default_boundinglat) + self._update_extent(boundinglat=boundinglat) + if extent == 'auto': + # NOTE: This will work even if applied after plotting stuff + # and fixing the limits. Very easy to toggle on and off. + self.set_autoscalex_on(True) + self.set_autoscaley_on(True) + + def _update_extent(self, lonlim=None, latlim=None, boundinglat=None): + """ + Set the projection extent. + """ + # Projection extent + # NOTE: Lon axis and lat axis extents are updated by set_extent. + # WARNING: The set_extent method tries to set a *rectangle* between the *4* + # (x, y) coordinate pairs (each corner), so something like (-180, 180, -90, 90) + # will result in *line*, causing error! We correct this here. + eps = 1e-10 # bug with full -180, 180 range when lon_0 != 0 + lon0 = self._get_lon0() + proj = type(self.projection).__name__ + north = isinstance(self.projection, self._proj_north) + south = isinstance(self.projection, self._proj_south) + lonlim = _not_none(lonlim, (None, None)) + latlim = _not_none(latlim, (None, None)) + if north or south: + if any(_ is not None for _ in (*lonlim, *latlim)): + warnings._warn_proplot( + f'{proj!r} extent is controlled by "boundinglat", ' + f'ignoring lonlim={lonlim!r} and latlim={latlim!r}.' + ) + if boundinglat is not None and boundinglat != self._boundinglat: + lat0 = 90 if north else -90 + lon0 = self._get_lon0() + extent = [lon0 - 180 + eps, lon0 + 180 - eps, boundinglat, lat0] + self.set_extent(extent, crs=ccrs.PlateCarree()) + self._boundinglat = boundinglat + + # Rectangular extent + else: + if boundinglat is not None: + warnings._warn_proplot( + f'{proj!r} extent is controlled by "lonlim" and "latlim", ' + f'ignoring boundinglat={boundinglat!r}.' + ) + if any(_ is not None for _ in (*lonlim, *latlim)): + lonlim = list(lonlim) + if lonlim[0] is None: + lonlim[0] = lon0 - 180 + if lonlim[1] is None: + lonlim[1] = lon0 + 180 + lonlim[0] += eps + latlim = list(latlim) + if latlim[0] is None: + latlim[0] = -90 + if latlim[1] is None: + latlim[1] = 90 + extent = lonlim + latlim + self.set_extent(extent, crs=ccrs.PlateCarree()) + + def _update_features(self): + """ + Update geographic features. + """ + # NOTE: The e.g. cfeature.COASTLINE features are just for convenience, + # lo res versions. Use NaturalEarthFeature instead. + # WARNING: Seems cartopy features cannot be updated! Updating _kwargs + # attribute does *nothing*. + reso = rc['reso'] # resolution cannot be changed after feature created + try: + reso = constructor.RESOS_CARTOPY[reso] + except KeyError: + raise ValueError( + f'Invalid resolution {reso!r}. Options are: ' + + ', '.join(map(repr, constructor.RESOS_CARTOPY)) + '.' + ) + for name, args in constructor.FEATURES_CARTOPY.items(): + # Draw feature or toggle feature off + b = rc.find(name, context=True) + attr = f'_{name}_feature' + feat = getattr(self, attr, None) + drawn = feat is not None # if exists, apply *updated* settings + if b is not None: + if not b: + if drawn: # toggle existing feature off + feat.set_visible(False) + else: + if not drawn: + feat = cfeature.NaturalEarthFeature(*args, reso) + feat = self.add_feature(feat) # convert to FeatureArtist + + # Update artist attributes (FeatureArtist._kwargs used back to v0.5). + # For 'lines', need to specify edgecolor and facecolor + # See: https://github.com/SciTools/cartopy/issues/803 + if feat is not None: + kw = rc.category(name, context=drawn) + if name in ('coast', 'rivers', 'borders', 'innerborders'): + kw.update({'edgecolor': kw.pop('color'), 'facecolor': 'none'}) + else: + kw.update({'linewidth': 0}) + if 'zorder' in kw: + # NOTE: Necessary to update zorder directly because _kwargs + # attributes are not applied until draw()... at which point + # matplotlib is drawing in the order based on the *old* zorder. + feat.set_zorder(kw['zorder']) + if hasattr(feat, '_kwargs'): + feat._kwargs.update(kw) + + def _update_gridlines( + self, gl, which='major', longrid=None, latgrid=None, nsteps=None, + ): + """ + Update gridliner object with axis locators, and toggle gridlines on and off. + """ + # Update gridliner collection properties + # WARNING: Here we use native matplotlib 'grid' rc param for geographic + # gridlines. If rc mode is 1 (first format call) use context=False + kwlines = rc._get_gridline_props(which=which, native=False) + kwtext = rc._get_ticklabel_props(native=False) + gl.collection_kwargs.update(kwlines) + gl.xlabel_style.update(kwtext) + gl.ylabel_style.update(kwtext) + + # Apply tick locations from dummy _LonAxis and _LatAxis axes + # NOTE: This will re-apply existing gridline locations if unchanged. + if nsteps is not None: + gl.n_steps = nsteps + latmax = self._lataxis.get_latmax() + if _version_cartopy >= '0.19': + gl.ylim = (-latmax, latmax) + longrid = rc._get_gridline_bool(longrid, axis='x', which=which, native=False) + if longrid is not None: + gl.xlines = longrid + latgrid = rc._get_gridline_bool(latgrid, axis='y', which=which, native=False) + if latgrid is not None: + gl.ylines = latgrid + lonlines = self._get_lonticklocs(which=which) + latlines = self._get_latticklocs(which=which) + if _version_cartopy >= '0.18': # see lukelbd/proplot#208 + lonlines = (np.asarray(lonlines) + 180) % 360 - 180 # only for cartopy + gl.xlocator = mticker.FixedLocator(lonlines) + gl.ylocator = mticker.FixedLocator(latlines) + + def _update_major_gridlines( + self, + longrid=None, latgrid=None, + lonarray=None, latarray=None, + loninline=None, latinline=None, labelpad=None, + rotatelabels=None, nsteps=None, + ): + """ + Update major gridlines. + """ + # Update gridline locations and style + gl = self._gridlines_major + if gl is None: + gl = self._gridlines_major = self._init_gridlines() + self._update_gridlines( + gl, which='major', longrid=longrid, latgrid=latgrid, nsteps=nsteps, + ) + gl.xformatter = self._lonaxis.get_major_formatter() + gl.yformatter = self._lataxis.get_major_formatter() + + # Update gridline label parameters + # NOTE: Cartopy 0.18 and 0.19 can not draw both edge and inline labels. Instead + # requires both a set 'side' and 'x_inline' is True (applied in GeoAxes.format). + # NOTE: The 'xpadding' and 'ypadding' props were introduced in v0.16 + # with default 5 points, then set to default None in v0.18. + # TODO: Cartopy has had two formatters for a while but we use the newer one. + # See https://github.com/SciTools/cartopy/pull/1066 + if labelpad is not None: + gl.xpadding = gl.ypadding = labelpad + if loninline is not None: + gl.x_inline = bool(loninline) + if latinline is not None: + gl.y_inline = bool(latinline) + if rotatelabels is not None: + gl.rotate_labels = bool(rotatelabels) # ignored in cartopy < 0.18 + if latinline is not None or loninline is not None: + lon, lat = loninline, latinline + b = True if lon and lat else 'x' if lon else 'y' if lat else None + gl.inline_labels = b # ignored in cartopy < 0.20 + + # Gridline label toggling + # Issue warning instead of error! + if ( + _version_cartopy < '0.18' + and not isinstance(self.projection, (ccrs.Mercator, ccrs.PlateCarree)) + ): + if any(latarray): + warnings._warn_proplot( + 'Cannot add gridline labels to cartopy ' + f'{type(self.projection).__name__} projection.' + ) + latarray = [False] * 5 + if any(lonarray): + warnings._warn_proplot( + 'Cannot add gridline labels to cartopy ' + f'{type(self.projection).__name__} projection.' + ) + lonarray = [False] * 5 + array = [ + True if lon and lat + else 'x' if lon + else 'y' if lat + else False if lon is not None or lon is not None + else None + for lon, lat in zip(lonarray, latarray) + ] + self._toggle_gridliner_labels(gl, *array[:2], *array[2:4], array[4]) + + def _update_minor_gridlines(self, longrid=None, latgrid=None, nsteps=None): + """ + Update minor gridlines. + """ + gl = self._gridlines_minor + if gl is None: + gl = self._gridlines_minor = self._init_gridlines() + self._update_gridlines( + gl, which='minor', longrid=longrid, latgrid=latgrid, nsteps=nsteps, + ) + + def get_extent(self, crs=None): + # Get extent and try to repair longitude bounds. + if crs is None: + crs = ccrs.PlateCarree() + extent = super().get_extent(crs=crs) + if isinstance(crs, ccrs.PlateCarree): + if np.isclose(extent[0], -180) and np.isclose(extent[-1], 180): + # Repair longitude bounds to reflect dateline position + # NOTE: This is critical so we can prevent duplicate gridlines + # on dateline. See _update_gridlines. + lon0 = self._get_lon0() + extent[:2] = [lon0 - 180, lon0 + 180] + return extent + + def get_tightbbox(self, renderer, *args, **kwargs): + # Perform extra post-processing steps + # For now this just draws the gridliners + self._apply_axis_sharing() + if self.get_autoscale_on() and self.ignore_existing_data_limits: + self.autoscale_view() + + # Adjust location + if _version_cartopy >= '0.18': + self.patch._adjust_location() # this does the below steps + elif ( + getattr(self.background_patch, 'reclip', None) + and hasattr(self.background_patch, 'orig_path') + ): + clipped_path = self.background_patch.orig_path.clip_to_bbox(self.viewLim) + self.outline_patch._path = clipped_path + self.background_patch._path = clipped_path + + # Apply aspect + self.apply_aspect() + for gl in self._gridliners: + if _version_cartopy >= '0.18': + gl._draw_gridliner(renderer=renderer) + else: + gl._draw_gridliner(background_patch=self.background_patch) + + # Remove gridliners + if _version_cartopy < '0.18': + self._gridliners = [] + + return super().get_tightbbox(renderer, *args, **kwargs) + + def set_extent(self, extent, crs=None): + # Fix paths, so axes tight bounding box gets correct box! From this issue: + # https://github.com/SciTools/cartopy/issues/1207#issuecomment-439975083 + # Also record the requested longitude latitude extent so we can use these + # values for LongitudeLocator and LatitudeLocator. Otherwise if longitude + # extent is across dateline LongitudeLocator fails because get_extent() + # reports -180 to 180: https://github.com/SciTools/cartopy/issues/1564 + # NOTE: This is *also* not perfect because if set_extent() was called + # and extent crosses map boundary of rectangular projection, the *actual* + # resulting extent is the opposite. But that means user has messed up anyway + # so probably doesn't matter if gridlines are also wrong. + if crs is None: + crs = ccrs.PlateCarree() + if isinstance(crs, ccrs.PlateCarree): + self._set_view_intervals(extent) + with rc.context(mode=2): # do not reset gridline properties! + if self._gridlines_major is not None: + self._update_gridlines(self._gridlines_major, which='major') + if self._gridlines_minor is not None: + self._update_gridlines(self._gridlines_minor, which='minor') + if _version_cartopy < '0.18': + clipped_path = self.outline_patch.orig_path.clip_to_bbox(self.viewLim) + self.outline_patch._path = clipped_path + self.background_patch._path = clipped_path + return super().set_extent(extent, crs=crs) + + def set_global(self): + # Set up "global" extent and update _LatAxis and _LonAxis view intervals + result = super().set_global() + self._set_view_intervals(self._get_global_extent()) + return result + + +class _BasemapAxes(GeoAxes): + """ + Axes subclass for plotting basemap projections. + """ + _name = 'basemap' + _proj_class = Basemap + _proj_north = ('npaeqd', 'nplaea', 'npstere') + _proj_south = ('spaeqd', 'splaea', 'spstere') + _proj_polar = _proj_north + _proj_south + _proj_non_rectangular = _proj_polar + ( # do not use axes spines as boundaries + 'ortho', 'geos', 'nsper', + 'moll', 'hammer', 'robin', + 'eck4', 'kav7', 'mbtfpq', + 'sinu', 'vandg', + ) + + def __init__(self, *args, map_projection=None, **kwargs): + """ + Parameters + ---------- + map_projection : ~mpl_toolkits.basemap.Basemap + The map projection. + *args, **kwargs + Passed to `GeoAxes`. + """ + # First assign projection and set axis bounds for locators + # WARNING: Unlike cartopy projections basemaps cannot normally be reused. + # To make syntax similar we make a copy. + # WARNING: Investigated whether Basemap.__init__() could be called + # twice with updated proj kwargs to modify map bounds after creation + # and python immmediately crashes. Do not try again. + import mpl_toolkits.basemap # noqa: F401 verify package is available + self.projection = copy.copy(map_projection) # verify + lon0 = self._get_lon0() + if self.projection.projection in self._proj_polar: + latmax = 80 # default latmax for gridlines + extent = [-180 + lon0, 180 + lon0] + bound = getattr(self.projection, 'boundinglat', 0) + north = self.projection.projection in self._proj_north + extent.extend([bound, 90] if north else [-90, bound]) + else: + latmax = 90 + attrs = ('lonmin', 'lonmax', 'latmin', 'latmax') + extent = [getattr(self.projection, attr, None) for attr in attrs] + if any(_ is None for _ in extent): + extent = [180 - lon0, 180 + lon0, -90, 90] # fallback + + # Initialize axes + self._map_boundary = None # see format() + self._has_recurred = False # use this to override plotting methods + self._lonlines_major = None # store gridliner objects this way + self._lonlines_minor = None + self._latlines_major = None + self._latlines_minor = None + self._lonarray = 4 * [False] # cached label toggles + self._latarray = 4 * [False] # cached label toggles + self._lonaxis = _LonAxis(self) + self._lataxis = _LatAxis(self, latmax=latmax) + self._set_view_intervals(extent) + super().__init__(*args, **kwargs) + + def _get_lon0(self): + """ + Get the central longitude. + """ + return getattr(self.projection, 'projparams', {}).get('lon_0', 0) + + @staticmethod + def _iter_gridlines(dict_): + """ + Iterate over longitude latitude lines. + """ + dict_ = dict_ or {} + for pi in dict_.values(): + for pj in pi: + for obj in pj: + yield obj + + def _update_background(self, **kwargs): + """ + Update the map boundary patches. This is called in `Axes.format`. + """ + # Non-rectangular projections + # WARNING: Map boundary must be drawn before all other tasks. See __init__. + # WARNING: With clipping on boundary lines are clipped by the axes bbox. + if self.projection.projection in self._proj_non_rectangular: + self.patch.set_facecolor('none') # make sure main patch is hidden + kw_face, kw_edge = rc._get_background_props(native=False, **kwargs) + kw = {**kw_face, **kw_edge, 'rasterized': False, 'clip_on': False} + self._map_boundary.update(kw) + # Rectangular projections + else: + kw_face, kw_edge = rc._get_background_props(native=False, **kwargs) + self.patch.update({**kw_face, 'edgecolor': 'none'}) + for spine in self.spines.values(): + spine.update(kw_edge) + + def _update_boundary(self, round=None): + """ + No-op. Boundary mode cannot be changed in basemap. + """ + # NOTE: Unlike the cartopy method we do not look up the rc setting here. + if round is None: + return + else: + warnings._warn_proplot( + f'Got round={round!r}, but you cannot change the bounds of a polar ' + "basemap projection after creating it. Please pass 'round' to pplt.Proj " # noqa: E501 + "instead (e.g. using the pplt.subplots() dictionary keyword 'proj_kw')." + ) + + def _update_extent_mode(self, extent=None, boundinglat=None): # noqa: U100 + """ + No-op. Extent mode cannot be changed in basemap. + """ + # NOTE: Unlike the cartopy method we do not look up the rc setting here. + if extent is None: + return + if extent not in ('globe', 'auto'): + raise ValueError( + f"Invalid extent mode {extent!r}. Must be 'auto' or 'globe'." + ) + if extent == 'auto': + warnings._warn_proplot( + f'Got extent={extent!r}, but you cannot use auto extent mode ' + 'in basemap projections. Please consider switching to cartopy.' + ) + + def _update_extent(self, lonlim=None, latlim=None, boundinglat=None): + """ + No-op. Map bounds cannot be changed in basemap. + """ + lonlim = _not_none(lonlim, (None, None)) + latlim = _not_none(latlim, (None, None)) + if boundinglat is not None or any(_ is not None for _ in (*lonlim, *latlim)): + warnings._warn_proplot( + f'Got lonlim={lonlim!r}, latlim={latlim!r}, boundinglat={boundinglat!r}' + ', but you cannot "zoom into" a basemap projection after creating it. ' + 'Please pass any of the following keyword arguments to pplt.Proj ' + "instead (e.g. using the pplt.subplots() dictionary keyword 'proj_kw'):" + "'boundinglat', 'lonlim', 'latlim', 'llcrnrlon', 'llcrnrlat', " + "'urcrnrlon', 'urcrnrlat', 'llcrnrx', 'llcrnry', 'urcrnrx', 'urcrnry', " + "'width', or 'height'." + ) + + def _update_features(self): + """ + Update geographic features. + """ + # NOTE: Also notable are drawcounties, blumarble, drawlsmask, + # shadedrelief, and etopo methods. + for name, method in constructor.FEATURES_BASEMAP.items(): + # Draw feature or toggle on and off + b = rc.find(name, context=True) + attr = f'_{name}_feature' + feat = getattr(self, attr, None) + drawn = feat is not None # if exists, apply *updated* settings + if b is not None: + if not b: + if drawn: # toggle existing feature off + for obj in feat: + feat.set_visible(False) + else: + if not drawn: + feat = getattr(self.projection, method)(ax=self) + if not isinstance(feat, (list, tuple)): # list of artists? + feat = (feat,) + setattr(self, attr, feat) + + # Update settings + if feat is not None: + kw = rc.category(name, context=drawn) + for obj in feat: + obj.update(kw) + + def _update_gridlines( + self, which='major', longrid=None, latgrid=None, lonarray=None, latarray=None, + ): + """ + Apply changes to the basemap axes. + """ + latmax = self._lataxis.get_latmax() + for axis, name, grid, array, method in zip( + ('x', 'y'), + ('lon', 'lat'), + (longrid, latgrid), + (lonarray, latarray), + ('drawmeridians', 'drawparallels'), + ): + # Correct lonarray and latarray label toggles by changing from lrbt to lrtb. + # Then update the cahced toggle array. This lets us change gridline locs + # while preserving the label toggle setting from a previous format() call. + grid = rc._get_gridline_bool(grid, axis=axis, which=which, native=False) + axis = getattr(self, f'_{name}axis') + if len(array) == 5: # should be always + array = array[:4] + bools = 4 * [False] if which == 'major' else getattr(self, f'_{name}array') + array = [*array[:2], *array[2:4][::-1]] # flip lrbt to lrtb and skip geo + for i, b in enumerate(array): + if b is not None: + bools[i] = b # update toggles + + # Get gridlines + # NOTE: This may re-apply existing gridlines. + lines = list(getattr(self, f'_get_{name}ticklocs')(which=which)) + if name == 'lon' and np.isclose(lines[0] + 360, lines[-1]): + lines = lines[:-1] # prevent double labels + + # Figure out whether we have to redraw meridians/parallels + # NOTE: Always update minor gridlines if major locator also changed + attr = f'_{name}lines_{which}' + objs = getattr(self, attr) # dictionary of previous objects + attrs = ['isDefault_majloc'] # always check this one + attrs.append('isDefault_majfmt' if which == 'major' else 'isDefault_minloc') + rebuild = lines and ( + not objs + or any(_ is not None for _ in array) # user-input or initial toggles + or any(not getattr(axis, attr) for attr in attrs) # none tracked yet + ) + if rebuild and objs and grid is None: # get *previous* toggle state + grid = all(obj.get_visible() for obj in self._iter_gridlines(objs)) + + # Draw or redraw meridian or parallel lines + # Also mark formatters and locators as 'default' + if rebuild: + kwdraw = {} + formatter = axis.get_major_formatter() + if formatter is not None: # use functional formatter + kwdraw['fmt'] = formatter + for obj in self._iter_gridlines(objs): + obj.set_visible(False) + objs = getattr(self.projection, method)( + lines, ax=self, latmax=latmax, labels=bools, **kwdraw + ) + setattr(self, attr, objs) + + # Update gridline settings + # We use native matplotlib 'grid' rc param for geographic gridlines + kwlines = rc._get_gridline_props(which=which, native=False, rebuild=rebuild) + kwtext = rc._get_ticklabel_props(native=False, rebuild=rebuild) + for obj in self._iter_gridlines(objs): + if isinstance(obj, mtext.Text): + obj.update(kwtext) + else: + obj.update(kwlines) + + # Toggle existing gridlines on and off + if grid is not None: + for obj in self._iter_gridlines(objs): + obj.set_visible(grid) + + def _update_major_gridlines( + self, + longrid=None, latgrid=None, lonarray=None, latarray=None, + loninline=None, latinline=None, rotatelabels=None, labelpad=None, nsteps=None, + ): + """ + Update major gridlines. + """ + loninline, latinline, labelpad, rotatelabels, nsteps # avoid U100 error + self._update_gridlines( + which='major', + longrid=longrid, latgrid=latgrid, lonarray=lonarray, latarray=latarray, + ) + + def _update_minor_gridlines(self, longrid=None, latgrid=None, nsteps=None): + """ + Update minor gridlines. + """ + # Update gridline locations + nsteps # avoid U100 error + array = [None] * 4 # NOTE: must be None not False (see _update_gridlines) + self._update_gridlines( + which='minor', + longrid=longrid, latgrid=latgrid, lonarray=array, latarray=array, + ) + # Set isDefault_majloc, etc. to True for both axes + # NOTE: This cannot be done inside _update_gridlines or minor gridlines + # will not update to reflect new major gridline locations. + for axis in (self._lonaxis, self._lataxis): + axis.isDefault_majfmt = True + axis.isDefault_majloc = True + axis.isDefault_minloc = True + + +# Apply signature obfuscation after storing previous signature +GeoAxes._format_signatures[GeoAxes] = inspect.signature(GeoAxes.format) +GeoAxes.format = docstring._obfuscate_kwargs(GeoAxes.format) diff --git a/proplot/axes/plot.py b/proplot/axes/plot.py new file mode 100644 index 000000000..74de279ec --- /dev/null +++ b/proplot/axes/plot.py @@ -0,0 +1,4231 @@ +#!/usr/bin/env python3 +""" +The second-level axes subclass used for all proplot figures. +Implements plotting method overrides. +""" +import contextlib +import inspect +import itertools +import re +import sys +from numbers import Integral + +import matplotlib.artist as martist +import matplotlib.axes as maxes +import matplotlib.cbook as cbook +import matplotlib.cm as mcm +import matplotlib.collections as mcollections +import matplotlib.colors as mcolors +import matplotlib.contour as mcontour +import matplotlib.image as mimage +import matplotlib.lines as mlines +import matplotlib.patches as mpatches +import matplotlib.ticker as mticker +import numpy as np +import numpy.ma as ma + +from .. import colors as pcolors +from .. import constructor, utils +from ..config import rc +from ..internals import ic # noqa: F401 +from ..internals import ( + _get_aliases, + _not_none, + _pop_kwargs, + _pop_params, + _pop_props, + context, + docstring, + guides, + inputs, + warnings, +) +from . import base + +try: + from cartopy.crs import PlateCarree +except ModuleNotFoundError: + PlateCarree = object + +__all__ = ['PlotAxes'] + + +# Constants +# NOTE: Increased from native linewidth of 0.25 matplotlib uses for grid box edges. +# This is half of rc['patch.linewidth'] of 0.6. Half seems like a nice default. +EDGEWIDTH = 0.3 + +# Data argument docstrings +_args_1d_docstring = """ +*args : {y} or {x}, {y} + The data passed as positional or keyword arguments. Interpreted as follows: + + * If only `{y}` coordinates are passed, try to infer the `{x}` coordinates + from the `~pandas.Series` or `~pandas.DataFrame` indices or the + `~xarray.DataArray` coordinates. Otherwise, the `{x}` coordinates + are ``np.arange(0, {y}.shape[0])``. + * If the `{y}` coordinates are a 2D array, plot each column of data in succession + (except where each column of data represents a statistical distribution, as with + ``boxplot``, ``violinplot``, or when using ``means=True`` or ``medians=True``). + * If any arguments are `pint.Quantity`, auto-add the pint unit registry + to matplotlib's unit registry using `~pint.UnitRegistry.setup_matplotlib`. + A `pint.Quantity` embedded in an `xarray.DataArray` is also supported. +""" +_args_1d_multi_docstring = """ +*args : {y}2 or {x}, {y}2, or {x}, {y}1, {y}2 + The data passed as positional or keyword arguments. Interpreted as follows: + + * If only `{y}` coordinates are passed, try to infer the `{x}` coordinates from + the `~pandas.Series` or `~pandas.DataFrame` indices or the `~xarray.DataArray` + coordinates. Otherwise, the `{x}` coordinates are ``np.arange(0, {y}2.shape[0])``. + * If only `{x}` and `{y}2` coordinates are passed, set the `{y}1` coordinates + to zero. This draws elements originating from the zero line. + * If both `{y}1` and `{y}2` are provided, draw elements between these points. If + either are 2D, draw elements by iterating over each column. + * If any arguments are `pint.Quantity`, auto-add the pint unit registry + to matplotlib's unit registry using `~pint.UnitRegistry.setup_matplotlib`. + A `pint.Quantity` embedded in an `xarray.DataArray` is also supported. +""" +_args_2d_docstring = """ +*args : {z} or x, y, {z} + The data passed as positional or keyword arguments. Interpreted as follows: + + * If only {zvar} coordinates are passed, try to infer the `x` and `y` coordinates + from the `~pandas.DataFrame` indices and columns or the `~xarray.DataArray` + coordinates. Otherwise, the `y` coordinates are ``np.arange(0, y.shape[0])`` + and the `x` coordinates are ``np.arange(0, y.shape[1])``. + * For ``pcolor`` and ``pcolormesh``, calculate coordinate *edges* using + `~proplot.utils.edges` or `~proplot.utils.edges2d` if *centers* were provided. + For all other methods, calculate coordinate *centers* if *edges* were provided. + * If the `x` or `y` coordinates are `pint.Quantity`, auto-add the pint unit registry + to matplotlib's unit registry using `~pint.UnitRegistry.setup_matplotlib`. If the + {zvar} coordinates are `pint.Quantity`, pass the magnitude to the plotting + command. A `pint.Quantity` embedded in an `xarray.DataArray` is also supported. +""" +docstring._snippet_manager['plot.args_1d_y'] = _args_1d_docstring.format(x='x', y='y') +docstring._snippet_manager['plot.args_1d_x'] = _args_1d_docstring.format(x='y', y='x') +docstring._snippet_manager['plot.args_1d_multiy'] = _args_1d_multi_docstring.format(x='x', y='y') # noqa: E501 +docstring._snippet_manager['plot.args_1d_multix'] = _args_1d_multi_docstring.format(x='y', y='x') # noqa: E501 +docstring._snippet_manager['plot.args_2d'] = _args_2d_docstring.format(z='z', zvar='`z`') # noqa: E501 +docstring._snippet_manager['plot.args_2d_flow'] = _args_2d_docstring.format(z='u, v', zvar='`u` and `v`') # noqa: E501 + + +# Shared docstrings +_args_1d_shared_docstring = """ +data : dict-like, optional + A dict-like dataset container (e.g., `~pandas.DataFrame` or + `~xarray.Dataset`). If passed, each data argument can optionally + be a string `key` and the arrays used for plotting are retrieved + with ``data[key]``. This is a `native matplotlib feature + `__. +autoformat : bool, default: :rc:`autoformat` + Whether the `x` axis labels, `y` axis labels, axis formatters, axes titles, + legend titles, and colorbar labels are automatically configured when a + `~pandas.Series`, `~pandas.DataFrame`, `~xarray.DataArray`, or `~pint.Quantity` + is passed to the plotting command. Formatting of `pint.Quantity` + unit strings is controlled by :rc:`unitformat`. +""" +_args_2d_shared_docstring = """ +%(plot.args_1d_shared)s +transpose : bool, default: False + Whether to transpose the input data. This should be used when + passing datasets with column-major dimension order ``(x, y)``. + Otherwise row-major dimension order ``(y, x)`` is expected. +order : {'C', 'F'}, default: 'C' + Alternative to `transpose`. ``'C'`` corresponds to the default C-cyle + row-major ordering (equivalent to ``transpose=False``). ``'F'`` corresponds + to Fortran-style column-major ordering (equivalent to ``transpose=True``). +globe : bool, default: False + For `proplot.axes.GeoAxes` only. Whether to enforce global + coverage. When set to ``True`` this does the following: + + #. Interpolates input data to the North and South poles by setting the data + values at the poles to the mean from latitudes nearest each pole. + #. Makes meridional coverage "circular", i.e. the last longitude coordinate + equals the first longitude coordinate plus 360\N{DEGREE SIGN}. + #. When basemap is the backend, cycles 1D longitude vectors to fit within + the map edges. For example, if the central longitude is 90\N{DEGREE SIGN}, + the data is shifted so that it spans -90\N{DEGREE SIGN} to 270\N{DEGREE SIGN}. +""" +docstring._snippet_manager['plot.args_1d_shared'] = _args_1d_shared_docstring +docstring._snippet_manager['plot.args_2d_shared'] = _args_2d_shared_docstring + + +# Auto colorbar and legend docstring +_guide_docstring = """ +colorbar : bool, int, or str, optional + If not ``None``, this is a location specifying where to draw an + *inset* or *outer* colorbar from the resulting object(s). If ``True``, + the default :rc:`colorbar.loc` is used. If the same location is + used in successive plotting calls, object(s) will be added to the + existing colorbar in that location (valid for colorbars built from lists + of artists). Valid locations are shown in in `~proplot.axes.Axes.colorbar`. +colorbar_kw : dict-like, optional + Extra keyword args for the call to `~proplot.axes.Axes.colorbar`. +legend : bool, int, or str, optional + Location specifying where to draw an *inset* or *outer* legend from the + resulting object(s). If ``True``, the default :rc:`legend.loc` is used. + If the same location is used in successive plotting calls, object(s) + will be added to existing legend in that location. Valid locations + are shown in `~proplot.axes.Axes.legend`. +legend_kw : dict-like, optional + Extra keyword args for the call to `~proplot.axes.Axes.legend`. +""" +docstring._snippet_manager['plot.guide'] = _guide_docstring + + +# Misc shared 1D plotting docstrings +_inbounds_docstring = """ +inbounds : bool, default: :rc:`axes.inbounds` + Whether to restrict the default `y` (`x`) axis limits to account for only + in-bounds data when the `x` (`y`) axis limits have been locked. + See also :rcraw:`axes.inbounds` and :rcraw:`cmap.inbounds`. +""" +_error_means_docstring = """ +mean, means : bool, default: False + Whether to plot the means of each column for 2D `{y}` coordinates. Means + are calculated with `numpy.nanmean`. If no other arguments are specified, + this also sets ``barstd=True`` (and ``boxstd=True`` for violin plots). +median, medians : bool, default: False + Whether to plot the medians of each column for 2D `{y}` coordinates. Medians + are calculated with `numpy.nanmedian`. If no other arguments arguments are + specified, this also sets ``barstd=True`` (and ``boxstd=True`` for violin plots). +""" +_error_bars_docstring = """ +bars : bool, default: None + Shorthand for `barstd`, `barstds`. +barstd, barstds : bool, float, or 2-tuple of float, optional + Valid only if `mean` or `median` is ``True``. Standard deviation multiples for + *thin error bars* with optional whiskers (i.e., caps). If scalar, then +/- that + multiple is used. If ``True``, the default standard deviation range of +/-3 is used. +barpctile, barpctiles : bool, float, or 2-tuple of float, optional + Valid only if `mean` or `median` is ``True``. As with `barstd`, but instead + using percentiles for the error bars. If scalar, that percentile range is + used (e.g., ``90`` shows the 5th to 95th percentiles). If ``True``, the default + percentile range of 0 to 100 is used. +bardata : array-like, optional + Valid only if `mean` and `median` are ``False``. If shape is 2 x N, these + are the lower and upper bounds for the thin error bars. If shape is N, these + are the absolute, symmetric deviations from the central points. +boxes : bool, default: None + Shorthand for `boxstd`, `boxstds`. +boxstd, boxstds, boxpctile, boxpctiles, boxdata : optional + As with `barstd`, `barpctile`, and `bardata`, but for *thicker error bars* + representing a smaller interval than the thin error bars. If `boxstds` is + ``True``, the default standard deviation range of +/-1 is used. If `boxpctiles` + is ``True``, the default percentile range of 25 to 75 is used (i.e., the + interquartile range). When "boxes" and "bars" are combined, this has the + effect of drawing miniature box-and-whisker plots. +capsize : float, default: :rc:`errorbar.capsize` + The cap size for thin error bars in points. +barz, barzorder, boxz, boxzorder : float, default: 2.5 + The "zorder" for the thin and thick error bars. +barc, barcolor, boxc, boxcolor \ +: color-spec, default: :rc:`boxplot.whiskerprops.color` + Colors for the thin and thick error bars. +barlw, barlinewidth, boxlw, boxlinewidth \ +: float, default: :rc:`boxplot.whiskerprops.linewidth` + Line widths for the thin and thick error bars, in points. The default for boxes + is 4 times :rcraw:`boxplot.whiskerprops.linewidth`. +boxm, boxmarker : bool or marker-spec, default: 'o' + Whether to draw a small marker in the middle of the box denoting + the mean or median position. Ignored if `boxes` is ``False``. +boxms, boxmarkersize : size-spec, default: ``(2 * boxlinewidth) ** 2`` + The marker size for the `boxmarker` marker in points ** 2. +boxmc, boxmarkercolor, boxmec, boxmarkeredgecolor : color-spec, default: 'w' + Color, face color, and edge color for the `boxmarker` marker. +""" +_error_shading_docstring = """ +shade : bool, default: None + Shorthand for `shadestd`. +shadestd, shadestds, shadepctile, shadepctiles, shadedata : optional + As with `barstd`, `barpctile`, and `bardata`, but using *shading* to indicate + the error range. If `shadestds` is ``True``, the default standard deviation + range of +/-2 is used. If `shadepctiles` is ``True``, the default + percentile range of 10 to 90 is used. +fade : bool, default: None + Shorthand for `fadestd`. +fadestd, fadestds, fadepctile, fadepctiles, fadedata : optional + As with `shadestd`, `shadepctile`, and `shadedata`, but for an additional, + more faded, *secondary* shaded region. If `fadestds` is ``True``, the default + standard deviation range of +/-3 is used. If `fadepctiles` is ``True``, + the default percentile range of 0 to 100 is used. +shadec, shadecolor, fadec, fadecolor : color-spec, default: None + Colors for the different shaded regions. The parent artist color is used by default. +shadez, shadezorder, fadez, fadezorder : float, default: 1.5 + The "zorder" for the different shaded regions. +shadea, shadealpha, fadea, fadealpha : float, default: 0.4, 0.2 + The opacity for the different shaded regions. +shadelw, shadelinewidth, fadelw, fadelinewidth : float, default: :rc:`patch.linewidth`. + The edge line width for the shading patches. +shdeec, shadeedgecolor, fadeec, fadeedgecolor : float, default: 'none' + The edge color for the shading patches. +shadelabel, fadelabel : bool or str, optional + Labels for the shaded regions to be used as separate legend entries. To toggle + labels "on" and apply a *default* label, use e.g. ``shadelabel=True``. To apply + a *custom* label, use e.g. ``shadelabel='label'``. Otherwise, the shading is + drawn underneath the line and/or marker in the legend entry. +""" +docstring._snippet_manager['plot.inbounds'] = _inbounds_docstring +docstring._snippet_manager['plot.error_means_y'] = _error_means_docstring.format(y='y') +docstring._snippet_manager['plot.error_means_x'] = _error_means_docstring.format(y='x') +docstring._snippet_manager['plot.error_bars'] = _error_bars_docstring +docstring._snippet_manager['plot.error_shading'] = _error_shading_docstring + + +# Color docstrings +_cycle_docstring = """ +cycle : cycle-spec, optional + The cycle specifer, passed to the `~proplot.constructor.Cycle` constructor. + If the returned cycler is unchanged from the current cycler, the axes + cycler will not be reset to its first position. To disable property cycling + and just use black for the default color, use ``cycle=False``, ``cycle='none'``, + or ``cycle=()`` (analogous to disabling ticks with e.g. ``xformatter='none'``). + To restore the default property cycler, use ``cycle=True``. +cycle_kw : dict-like, optional + Passed to `~proplot.constructor.Cycle`. +""" +_cmap_norm_docstring = """ +cmap : colormap-spec, default: \ +:rc:`cmap.sequential` or :rc:`cmap.diverging` + The colormap specifer, passed to the `~proplot.constructor.Colormap` constructor + function. If :rcraw:`cmap.autodiverging` is ``True`` and the normalization + range contains negative and positive values then :rcraw:`cmap.diverging` is used. + Otherwise :rcraw:`cmap.sequential` is used. +cmap_kw : dict-like, optional + Passed to `~proplot.constructor.Colormap`. +c, color, colors : color-spec or sequence of color-spec, optional + The color(s) used to create a `~proplot.colors.DiscreteColormap`. + If not passed, `cmap` is used. +norm : norm-spec, default: \ +`~matplotlib.colors.Normalize` or `~proplot.colors.DivergingNorm` + The data value normalizer, passed to the `~proplot.constructor.Norm` + constructor function. If `discrete` is ``True`` then 1) this affects the default + level-generation algorithm (e.g. ``norm='log'`` builds levels in log-space) and + 2) this is passed to `~proplot.colors.DiscreteNorm` to scale the colors before they + are discretized (if `norm` is not already a `~proplot.colors.DiscreteNorm`). + If :rcraw:`cmap.autodiverging` is ``True`` and the normalization range contains + negative and positive values then `~proplot.colors.DivergingNorm` is used. + Otherwise `~matplotlib.colors.Normalize` is used. +norm_kw : dict-like, optional + Passed to `~proplot.constructor.Norm`. +extend : {'neither', 'both', 'min', 'max'}, default: 'neither' + Direction for drawing colorbar "extensions" indicating + out-of-bounds data on the end of the colorbar. +discrete : bool, default: :rc:`cmap.discrete` + If ``False``, then `~proplot.colors.DiscreteNorm` is not applied to the + colormap. Instead, for non-contour plots, the number of levels will be + roughly controlled by :rcraw:`cmap.lut`. This has a similar effect to + using `levels=large_number` but it may improve rendering speed. Default is + ``True`` only for contouring commands like `~proplot.axes.Axes.contourf` + and pseudocolor commands like `~proplot.axes.Axes.pcolor`. +sequential, diverging, cyclic, qualitative : bool, default: None + Boolean arguments used if `cmap` is not passed. Set these to ``True`` + to use the default :rcraw:`cmap.sequential`, :rcraw:`cmap.diverging`, + :rcraw:`cmap.cyclic`, and :rcraw:`cmap.qualitative` colormaps. + The `diverging` option also applies `~proplot.colors.DivergingNorm` + as the default continuous normalizer. +""" +docstring._snippet_manager['plot.cycle'] = _cycle_docstring +docstring._snippet_manager['plot.cmap_norm'] = _cmap_norm_docstring + + +# Levels docstrings +# NOTE: In some functions we only need some components +_vmin_vmax_docstring = """ +vmin, vmax : float, optional + The minimum and maximum color scale values used with the `norm` normalizer. + If `discrete` is ``False`` these are the absolute limits, and if `discrete` + is ``True`` these are the approximate limits used to automatically determine + `levels` or `values` lists at "nice" intervals. If `levels` or `values` were + already passed as lists, these are ignored, and `vmin` and `vmax` are set to + the minimum and maximum of the lists. If `robust` was passed, the default `vmin` + and `vmax` are some percentile range of the data values. Otherwise, the default + `vmin` and `vmax` are the minimum and maximum of the data values. +""" +_manual_levels_docstring = """ +N + Shorthand for `levels`. +levels : int or sequence of float, default: :rc:`cmap.levels` + The number of level edges or a sequence of level edges. If the former, `locator` + is used to generate this many level edges at "nice" intervals. If the latter, + the levels should be monotonically increasing or decreasing (note decreasing + levels fail with ``contour`` plots). +values : int or sequence of float, default: None + The number of level centers or a sequence of level centers. If the former, + `locator` is used to generate this many level centers at "nice" intervals. + If the latter, levels are inferred using `~proplot.utils.edges`. + This will override any `levels` input. +""" +_auto_levels_docstring = """ +robust : bool, float, or 2-tuple, default: :rc:`cmap.robust` + If ``True`` and `vmin` or `vmax` were not provided, they are + determined from the 2nd and 98th data percentiles rather than the + minimum and maximum. If float, this percentile range is used (for example, + ``90`` corresponds to the 5th to 95th percentiles). If 2-tuple of float, + these specific percentiles should be used. This feature is useful + when your data has large outliers. +inbounds : bool, default: :rc:`cmap.inbounds` + If ``True`` and `vmin` or `vmax` were not provided, when axis limits + have been explicitly restricted with `~matplotlib.axes.Axes.set_xlim` + or `~matplotlib.axes.Axes.set_ylim`, out-of-bounds data is ignored. + See also :rcraw:`cmap.inbounds` and :rcraw:`axes.inbounds`. +locator : locator-spec, default: `matplotlib.ticker.MaxNLocator` + The locator used to determine level locations if `levels` or `values` were not + already passed as lists. Passed to the `~proplot.constructor.Locator` constructor. + Default is `~matplotlib.ticker.MaxNLocator` with `levels` integer levels. +locator_kw : dict-like, optional + Keyword arguments passed to `matplotlib.ticker.Locator` class. +symmetric : bool, default: False + If ``True``, the normalization range or discrete colormap levels are + symmetric about zero. +positive : bool, default: False + If ``True``, the normalization range or discrete colormap levels are + positive with a minimum at zero. +negative : bool, default: False + If ``True``, the normaliation range or discrete colormap levels are + negative with a minimum at zero. +nozero : bool, default: False + If ``True``, ``0`` is removed from the level list. This is mainly useful for + single-color `~matplotlib.axes.Axes.contour` plots. +""" +docstring._snippet_manager['plot.vmin_vmax'] = _vmin_vmax_docstring +docstring._snippet_manager['plot.levels_manual'] = _manual_levels_docstring +docstring._snippet_manager['plot.levels_auto'] = _auto_levels_docstring + + +# Labels docstrings +_label_docstring = """ +label, value : float or str, optional + The single legend label or colorbar coordinate to be used for + this plotted element. Can be numeric or string. This is generally + used with 1D positional arguments. +""" +_labels_1d_docstring = """ +%(plot.label)s +labels, values : sequence of float or sequence of str, optional + The legend labels or colorbar coordinates used for each plotted element. + Can be numeric or string, and must match the number of plotted elements. + This is generally used with 2D positional arguments. +""" +_labels_2d_docstring = """ +label : str, optional + The legend label to be used for this object. In the case of + contours, this is paired with the the central artist in the artist + list returned by `matplotlib.contour.ContourSet.legend_elements`. +labels : bool, optional + Whether to apply labels to contours and grid boxes. The text will be + white when the luminance of the underlying filled contour or grid box + is less than 50 and black otherwise. +labels_kw : dict-like, optional + Ignored if `labels` is ``False``. Extra keyword args for the labels. + For contour plots, this is passed to `~matplotlib.axes.Axes.clabel`. + Otherwise, this is passed to `~matplotlib.axes.Axes.text`. +formatter, fmt : formatter-spec, optional + The `~matplotlib.ticker.Formatter` used to format number labels. + Passed to the `~proplot.constructor.Formatter` constructor. +formatter_kw : dict-like, optional + Keyword arguments passed to `matplotlib.ticker.Formatter` class. +precision : int, optional + The maximum number of decimal places for number labels generated + with the default formatter `~proplot.ticker.Simpleformatter`. +""" +docstring._snippet_manager['plot.label'] = _label_docstring +docstring._snippet_manager['plot.labels_1d'] = _labels_1d_docstring +docstring._snippet_manager['plot.labels_2d'] = _labels_2d_docstring + + +# Negative-positive colors +_negpos_docstring = """ +negpos : bool, default: False + Whether to shade {objects} where ``{pos}`` with `poscolor` + and where ``{neg}`` with `negcolor`. If ``True`` this + function will return a length-2 silent list of handles. +negcolor, poscolor : color-spec, default: :rc:`negcolor`, :rc:`poscolor` + Colors to use for the negative and positive {objects}. Ignored if + `negpos` is ``False``. +""" +docstring._snippet_manager['plot.negpos_fill'] = _negpos_docstring.format( + objects='patches', neg='y2 < y1', pos='y2 >= y1' +) +docstring._snippet_manager['plot.negpos_lines'] = _negpos_docstring.format( + objects='lines', neg='ymax < ymin', pos='ymax >= ymin' +) +docstring._snippet_manager['plot.negpos_bar'] = _negpos_docstring.format( + objects='bars', neg='height < 0', pos='height >= 0' +) + + +# Plot docstring +_plot_docstring = """ +Plot standard lines. + +Parameters +---------- +%(plot.args_1d_{y})s +%(plot.args_1d_shared)s + +Other parameters +---------------- +%(plot.cycle)s +%(artist.line)s +%(plot.error_means_{y})s +%(plot.error_bars)s +%(plot.error_shading)s +%(plot.inbounds)s +%(plot.labels_1d)s +%(plot.guide)s +**kwargs + Passed to `~matplotlib.axes.Axes.plot`. + +See also +-------- +PlotAxes.plot +PlotAxes.plotx +matplotlib.axes.Axes.plot +""" +docstring._snippet_manager['plot.plot'] = _plot_docstring.format(y='y') +docstring._snippet_manager['plot.plotx'] = _plot_docstring.format(y='x') + + +# Step docstring +# NOTE: Internally matplotlib implements step with thin wrapper of plot +_step_docstring = """ +Plot step lines. + +Parameters +---------- +%(plot.args_1d_{y})s +%(plot.args_1d_shared)s + +Other parameters +---------------- +%(plot.cycle)s +%(artist.line)s +%(plot.inbounds)s +%(plot.labels_1d)s +%(plot.guide)s +**kwargs + Passed to `~matplotlib.axes.Axes.step`. + +See also +-------- +PlotAxes.step +PlotAxes.stepx +matplotlib.axes.Axes.step +""" +docstring._snippet_manager['plot.step'] = _step_docstring.format(y='y') +docstring._snippet_manager['plot.stepx'] = _step_docstring.format(y='x') + + +# Stem docstring +_stem_docstring = """ +Plot stem lines. + +Parameters +---------- +%(plot.args_1d_{y})s +%(plot.args_1d_shared)s + +Other parameters +---------------- +%(plot.cycle)s +%(plot.inbounds)s +%(plot.guide)s +**kwargs + Passed to `~matplotlib.axes.Axes.stem`. +""" +docstring._snippet_manager['plot.stem'] = _stem_docstring.format(y='x') +docstring._snippet_manager['plot.stemx'] = _stem_docstring.format(y='x') + + +# Lines docstrings +_lines_docstring = """ +Plot {orientation} lines. + +Parameters +---------- +%(plot.args_1d_multi{y})s +%(plot.args_1d_shared)s + +Other parameters +---------------- +stack, stacked : bool, default: False + Whether to "stack" lines from successive columns of {y} data + or plot lines on top of each other. +%(plot.cycle)s +%(artist.line)s +%(plot.negpos_lines)s +%(plot.inbounds)s +%(plot.labels_1d)s +%(plot.guide)s +**kwargs + Passed to `~matplotlib.axes.Axes.{prefix}lines`. + +See also +-------- +PlotAxes.vlines +PlotAxes.hlines +matplotlib.axes.Axes.vlines +matplotlib.axes.Axes.hlines +""" +docstring._snippet_manager['plot.vlines'] = _lines_docstring.format( + y='y', prefix='v', orientation='vertical' +) +docstring._snippet_manager['plot.hlines'] = _lines_docstring.format( + y='x', prefix='h', orientation='horizontal' +) + + +# Scatter docstring +_parametric_docstring = """ +Plot a parametric line. + +Parameters +---------- +%(plot.args_1d_y)s +c, color, colors, values, labels : sequence of float, str, or color-spec, optional + The parametric coordinate(s). These can be passed as a third positional + argument or as a keyword argument. If they are float, the colors will be + determined from `norm` and `cmap`. If they are strings, the color values + will be ``np.arange(len(colors))`` and eventual colorbar ticks will + be labeled with the strings. If they are colors, they are used for the + line segments and `cmap` is ignored -- for example, ``colors='blue'`` + makes a monochromatic "parametric" line. +interp : int, default: 0 + Interpolate to this many additional points between the parametric + coordinates. This can be increased to make the color gradations + between a small number of coordinates appear "smooth". +%(plot.args_1d_shared)s + +Other parameters +---------------- +%(plot.cmap_norm)s +%(plot.vmin_vmax)s +%(plot.inbounds)s +scalex, scaley : bool, optional + Whether the view limits are adapted to the data limits. The values are + passed on to `~matplotlib.axes.Axes.autoscale_view`. +%(plot.label)s +%(plot.guide)s +**kwargs + Valid `~matplotlib.collections.LineCollection` properties. + +Returns +------- +`~matplotlib.collections.LineCollection` + The parametric line. See `this matplotlib example \ +`__. + +See also +-------- +PlotAxes.plot +PlotAxes.plotx +matplotlib.collections.LineCollection +""" +docstring._snippet_manager['plot.parametric'] = _parametric_docstring + + +# Scatter function docstring +_scatter_docstring = """ +Plot markers with flexible keyword arguments. + +Parameters +---------- +%(plot.args_1d_{y})s +s, size, ms, markersize : float or array-like or unit-spec, optional + The marker size area(s). If this is an array matching the shape of `x` and `y`, + the units are scaled by `smin` and `smax`. If this contains unit string(s), it + is processed by `~proplot.utils.units` and represents the width rather than area. +c, color, colors, mc, markercolor, markercolors, fc, facecolor, facecolors \ +: array-like or color-spec, optional + The marker color(s). If this is an array matching the shape of `x` and `y`, + the colors are generated using `cmap`, `norm`, `vmin`, and `vmax`. Otherwise, + this should be a valid matplotlib color. +smin, smax : float, optional + The minimum and maximum marker size area in units ``points ** 2``. Ignored + if `absolute_size` is ``True``. Default value for `smin` is ``1`` and for + `smax` is the square of :rc:`lines.markersize`. +area_size : bool, default: True + Whether the marker sizes `s` are scaled by area or by radius. The default + ``True`` is consistent with matplotlib. When `absolute_size` is ``True``, + the `s` units are ``points ** 2`` if `area_size` is ``True`` and ``points`` + if `area_size` is ``False``. +absolute_size : bool, default: True or False + Whether `s` should be taken to represent "absolute" marker sizes in units + ``points`` or ``points ** 2`` or "relative" marker sizes scaled by `smin` + and `smax`. Default is ``True`` if `s` is scalar and ``False`` if `s` is + array-like or `smin` or `smax` were passed. +%(plot.vmin_vmax)s +%(plot.args_1d_shared)s + +Other parameters +---------------- +%(plot.cmap_norm)s +%(plot.levels_manual)s +%(plot.levels_auto)s +%(plot.cycle)s +lw, linewidth, linewidths, mew, markeredgewidth, markeredgewidths \ +: float or sequence, optional + The marker edge width(s). +edgecolors, markeredgecolor, markeredgecolors \ +: color-spec or sequence, optional + The marker edge color(s). +%(plot.error_means_{y})s +%(plot.error_bars)s +%(plot.error_shading)s +%(plot.inbounds)s +%(plot.labels_1d)s +%(plot.guide)s +**kwargs + Passed to `~matplotlib.axes.Axes.scatter`. + +See also +-------- +PlotAxes.scatter +PlotAxes.scatterx +matplotlib.axes.Axes.scatter +""" +docstring._snippet_manager['plot.scatter'] = _scatter_docstring.format(y='y') +docstring._snippet_manager['plot.scatterx'] = _scatter_docstring.format(y='x') + + +# Bar function docstring +_bar_docstring = """ +Plot individual, grouped, or stacked bars. + +Parameters +---------- +%(plot.args_1d_{y})s +width : float or array-like, default: 0.8 + The width(s) of the bars. Can be passed as a third positional argument. If + `absolute_width` is ``True`` (the default) these are in units relative to the + {x} coordinate step size. Otherwise these are in {x} coordinate units. +{bottom} : float or array-like, default: 0 + The coordinate(s) of the {bottom} edge of the bars. + Can be passed as a fourth positional argument. +absolute_width : bool, default: False + Whether to make the `width` units *absolute*. If ``True``, + this restores the default matplotlib behavior. +stack, stacked : bool, default: False + Whether to "stack" bars from successive columns of {y} + data or plot bars side-by-side in groups. +%(plot.args_1d_shared)s + +Other parameters +---------------- +%(plot.cycle)s +%(artist.patch)s +%(plot.negpos_bar)s +%(axes.edgefix)s +%(plot.error_means_{y})s +%(plot.error_bars)s +%(plot.inbounds)s +%(plot.labels_1d)s +%(plot.guide)s +**kwargs + Passed to `~matplotlib.axes.Axes.bar{suffix}`. + +See also +-------- +PlotAxes.bar +PlotAxes.barh +matplotlib.axes.Axes.bar +matplotlib.axes.Axes.barh +""" +docstring._snippet_manager['plot.bar'] = _bar_docstring.format( + x='x', y='y', bottom='bottom', suffix='' +) +docstring._snippet_manager['plot.barh'] = _bar_docstring.format( + x='y', y='x', bottom='left', suffix='h' +) + + +# Area plot docstring +_fill_docstring = """ +Plot individual, grouped, or overlaid shading patches. + +Parameters +---------- +%(plot.args_1d_multi{y})s +stack, stacked : bool, default: False + Whether to "stack" area patches from successive columns of {y} + data or plot area patches on top of each other. +%(plot.args_1d_shared)s + +Other parameters +---------------- +where : ndarray, optional + A boolean mask for the points that should be shaded. + See `this matplotlib example \ +`__. +%(plot.cycle)s +%(artist.patch)s +%(plot.negpos_fill)s +%(axes.edgefix)s +%(plot.inbounds)s +%(plot.labels_1d)s +%(plot.guide)s +**kwargs + Passed to `~matplotlib.axes.Axes.fill_between{suffix}`. + +See also +-------- +PlotAxes.area +PlotAxes.areax +PlotAxes.fill_between +PlotAxes.fill_betweenx +matplotlib.axes.Axes.fill_between +matplotlib.axes.Axes.fill_betweenx +""" +docstring._snippet_manager['plot.fill_between'] = _fill_docstring.format( + x='x', y='y', suffix='' +) +docstring._snippet_manager['plot.fill_betweenx'] = _fill_docstring.format( + x='y', y='x', suffix='x' +) + + +# Box plot docstrings +_boxplot_docstring = """ +Plot {orientation} boxes and whiskers with a nice default style. + +Parameters +---------- +%(plot.args_1d_{y})s +%(plot.args_1d_shared)s + +Other parameters +---------------- +fill : bool, default: True + Whether to fill the box with a color. +mean, means : bool, default: False + If ``True``, this passes ``showmeans=True`` and ``meanline=True`` to + `matplotlib.axes.Axes.boxplot`. Adds mean lines alongside the median. +%(plot.cycle)s +%(artist.patch_black)s +m, marker, ms, markersize : float or str, optional + Marker style and size for the 'fliers', i.e. outliers. See the + ``boxplot.flierprops`` `~matplotlib.rcParams` settings. +meanls, medianls, meanlinestyle, medianlinestyle, meanlinestyles, medianlinestyles \ +: str, optional + Line style for the mean and median lines drawn across the box. + See the ``boxplot.meanprops`` and ``boxplot.medianprops`` + `~matplotlib.rcParams` settings. +boxc, capc, whiskerc, flierc, meanc, medianc, \ +boxcolor, capcolor, whiskercolor, fliercolor, meancolor, mediancolor \ +boxcolors, capcolors, whiskercolors, fliercolors, meancolors, mediancolors \ +: color-spec or sequence, optional + Color of various boxplot components. If a sequence, should be the same length as + the number of boxes. These are shorthands so you don't have to pass e.g. a + `boxprops` dictionary keyword. See the ``boxplot.boxprops``, ``boxplot.capprops``, + ``boxplot.whiskerprops``, ``boxplot.flierprops``, ``boxplot.meanprops``, and + ``boxplot.medianprops`` `~matplotlib.rcParams` settings. +boxlw, caplw, whiskerlw, flierlw, meanlw, medianlw, boxlinewidth, caplinewidth, \ +meanlinewidth, medianlinewidth, whiskerlinewidth, flierlinewidth, boxlinewidths, \ +caplinewidths, meanlinewidths, medianlinewidths, whiskerlinewidths, flierlinewidths \ +: float, optional + Line width of various boxplot components. These are shorthands so + you don't have to pass e.g. a `boxprops` dictionary keyword. + See the ``boxplot.boxprops``, ``boxplot.capprops``, ``boxplot.whiskerprops``, + ``boxplot.flierprops``, ``boxplot.meanprops``, and ``boxplot.medianprops`` + `~matplotlib.rcParams` settings. +%(plot.labels_1d)s +**kwargs + Passed to `matplotlib.axes.Axes.boxplot`. + +See also +-------- +PlotAxes.boxes +PlotAxes.boxesh +PlotAxes.boxplot +PlotAxes.boxploth +matplotlib.axes.Axes.boxplot +""" +docstring._snippet_manager['plot.boxplot'] = _boxplot_docstring.format( + y='y', orientation='vertical' +) +docstring._snippet_manager['plot.boxploth'] = _boxplot_docstring.format( + y='x', orientation='horizontal' +) + + +# Violin plot docstrings +_violinplot_docstring = """ +Plot {orientation} violins with a nice default style matching +`this matplotlib example \ +`__. + +Parameters +---------- +%(plot.args_1d_{y})s +%(plot.args_1d_shared)s + +Other parameters +---------------- +%(plot.cycle)s +%(artist.patch_black)s +%(plot.labels_1d)s +showmeans, showmedians : bool, optional + Interpreted as ``means=True`` and ``medians=True`` when passed. +showextrema : bool, optional + Interpreted as ``barpctiles=True`` when passed (i.e. shows minima and maxima). +%(plot.error_bars)s +**kwargs + Passed to `matplotlib.axes.Axes.violinplot`. + +See also +-------- +PlotAxes.violin +PlotAxes.violinh +PlotAxes.violinplot +PlotAxes.violinploth +matplotlib.axes.Axes.violinplot +""" +docstring._snippet_manager['plot.violinplot'] = _violinplot_docstring.format( + y='y', orientation='vertical' +) +docstring._snippet_manager['plot.violinploth'] = _violinplot_docstring.format( + y='x', orientation='horizontal' +) + + +# 1D histogram docstrings +_hist_docstring = """ +Plot {orientation} histograms. + +Parameters +---------- +%(plot.args_1d_{y})s +bins : int or sequence of float, optional + The bin count or exact bin edges. +%(plot.weights)s +histtype : {{'bar', 'barstacked', 'step', 'stepfilled'}}, optional + The histogram type. See `matplotlib.axes.Axes.hist` for details. +width, rwidth : float, default: 0.8 or 1 + The bar width(s) for bar-type histograms relative to the bin size. Default + is ``0.8`` for multiple columns of unstacked data and ``1`` otherwise. +stack, stacked : bool, optional + Whether to "stack" successive columns of {y} data for bar-type histograms + or show side-by-side in groups. Setting this to ``False`` is equivalent to + ``histtype='bar'`` and to ``True`` is equivalent to ``histtype='barstacked'``. +fill, filled : bool, optional + Whether to "fill" step-type histograms or just plot the edges. Setting + this to ``False`` is equivalent to ``histtype='step'`` and to ``True`` + is equivalent to ``histtype='stepfilled'``. +%(plot.args_1d_shared)s + +Other parameters +---------------- +%(plot.cycle)s +%(artist.patch)s +%(axes.edgefix)s +%(plot.labels_1d)s +%(plot.guide)s +**kwargs + Passed to `~matplotlib.axes.Axes.hist`. + +See also +-------- +PlotAxes.hist +PlotAxes.histh +matplotlib.axes.Axes.hist +""" +_weights_docstring = """ +weights : array-like, optional + The weights associated with each point. If string this + can be retrieved from `data` (see below). +""" +docstring._snippet_manager['plot.weights'] = _weights_docstring +docstring._snippet_manager['plot.hist'] = _hist_docstring.format( + y='x', orientation='vertical' +) +docstring._snippet_manager['plot.histh'] = _hist_docstring.format( + y='x', orientation='horizontal' +) + + +# 2D histogram docstrings +_hist2d_docstring = """ +Plot a {descrip}. +standard 2D histogram. + +Parameters +---------- +%(plot.args_1d_y)s{bins} +%(plot.weights)s +%(plot.args_1d_shared)s + +Other parameters +---------------- +%(plot.cmap_norm)s +%(plot.vmin_vmax)s +%(plot.levels_manual)s +%(plot.levels_auto)s +%(plot.labels_2d)s +%(plot.guide)s +**kwargs + Passed to `~matplotlib.axes.Axes.{command}`. + +See also +-------- +PlotAxes.hist2d +PlotAxes.hexbin +matplotlib.axes.Axes.{command} +""" +_bins_docstring = """ +bins : int or 2-tuple of int, or array-like or 2-tuple of array-like, optional + The bin count or exact bin edges for each dimension or both dimensions. +""".rstrip() +docstring._snippet_manager['plot.hist2d'] = _hist2d_docstring.format( + command='hist2d', descrip='standard 2D histogram', bins=_bins_docstring +) +docstring._snippet_manager['plot.hexbin'] = _hist2d_docstring.format( + command='hexbin', descrip='2D hexagonally binned histogram', bins='' +) + + +# Pie chart docstring +_pie_docstring = """ +Plot a pie chart. + +Parameters +---------- +%(plot.args_1d_y)s +%(plot.args_1d_shared)s + +Other parameters +---------------- +%(plot.cycle)s +%(artist.patch)s +%(axes.edgefix)s +%(plot.labels_1d)s +labelpad, labeldistance : float, optional + The distance at which labels are drawn in radial coordinates. + +See also +-------- +matplotlib.axes.Axes.pie +""" +docstring._snippet_manager['plot.pie'] = _pie_docstring + + +# Contour docstrings +_contour_docstring = """ +Plot {descrip}. + +Parameters +---------- +%(plot.args_2d)s + +%(plot.args_2d_shared)s + +Other parameters +---------------- +%(plot.cmap_norm)s +%(plot.vmin_vmax)s +%(plot.levels_manual)s +%(plot.levels_auto)s +%(artist.collection_contour)s{edgefix} +%(plot.labels_2d)s +%(plot.guide)s +**kwargs + Passed to `matplotlib.axes.Axes.{command}`. + +See also +-------- +PlotAxes.contour +PlotAxes.contourf +PlotAxes.tricontour +PlotAxes.tricontourf +matplotlib.axes.Axes.{command} +""" +docstring._snippet_manager['plot.contour'] = _contour_docstring.format( + descrip='contour lines', command='contour', edgefix='' +) +docstring._snippet_manager['plot.contourf'] = _contour_docstring.format( + descrip='filled contours', command='contourf', edgefix='%(axes.edgefix)s\n', +) +docstring._snippet_manager['plot.tricontour'] = _contour_docstring.format( + descrip='contour lines on a triangular grid', command='tricontour', edgefix='' +) +docstring._snippet_manager['plot.tricontourf'] = _contour_docstring.format( + descrip='filled contours on a triangular grid', command='tricontourf', edgefix='\n%(axes.edgefix)s' # noqa: E501 +) + + +# Pcolor docstring +_pcolor_docstring = """ +Plot {descrip}. + +Parameters +---------- +%(plot.args_2d)s + +%(plot.args_2d_shared)s{aspect} + +Other parameters +---------------- +%(plot.cmap_norm)s +%(plot.vmin_vmax)s +%(plot.levels_manual)s +%(plot.levels_auto)s +%(artist.collection_pcolor)s +%(axes.edgefix)s +%(plot.labels_2d)s +%(plot.guide)s +**kwargs + Passed to `matplotlib.axes.Axes.{command}`. + +See also +-------- +PlotAxes.pcolor +PlotAxes.pcolormesh +PlotAxes.pcolorfast +PlotAxes.heatmap +PlotAxes.tripcolor +matplotlib.axes.Axes.{command} +""" +_heatmap_descrip = """ +grid boxes with formatting suitable for heatmaps. Ensures square grid +boxes, adds major ticks to the center of each grid box, disables minor +ticks and gridlines, and sets :rcraw:`cmap.discrete` to ``False`` by default +""".strip() +_heatmap_aspect = """ +aspect : {'equal', 'auto'} or float, default: :rc:`image.aspet` + Modify the axes aspect ratio. The aspect ratio is of particular relevance for + heatmaps since it may lead to non-square grid boxes. This parameter is a shortcut + for calling `~matplotlib.axes.set_aspect`. The options are as follows: + + * Number: The data aspect ratio. + * ``'equal'``: A data aspect ratio of 1. + * ``'auto'``: Allows the data aspect ratio to change depending on + the layout. In general this results in non-square grid boxes. +""".rstrip() +docstring._snippet_manager['plot.pcolor'] = _pcolor_docstring.format( + descrip='irregular grid boxes', command='pcolor', aspect='' +) +docstring._snippet_manager['plot.pcolormesh'] = _pcolor_docstring.format( + descrip='regular grid boxes', command='pcolormesh', aspect='' +) +docstring._snippet_manager['plot.pcolorfast'] = _pcolor_docstring.format( + descrip='grid boxes quickly', command='pcolorfast', aspect='' +) +docstring._snippet_manager['plot.tripcolor'] = _pcolor_docstring.format( + descrip='triangular grid boxes', command='tripcolor', aspect='' +) +docstring._snippet_manager['plot.heatmap'] = _pcolor_docstring.format( + descrip=_heatmap_descrip, command='pcolormesh', aspect=_heatmap_aspect +) + + +# Image docstring +_show_docstring = """ +Plot {descrip}. + +Parameters +---------- +z : array-like + The data passed as a positional argument or keyword argument. +%(plot.args_1d_shared)s + +Other parameters +---------------- +%(plot.cmap_norm)s +%(plot.vmin_vmax)s +%(plot.levels_manual)s +%(plot.levels_auto)s +%(plot.guide)s +**kwargs + Passed to `matplotlib.axes.Axes.{command}`. + +See also +-------- +proplot.axes.PlotAxes +matplotlib.axes.Axes.{command} +""" +docstring._snippet_manager['plot.imshow'] = _show_docstring.format( + descrip='an image', command='imshow' +) +docstring._snippet_manager['plot.matshow'] = _show_docstring.format( + descrip='a matrix', command='matshow' +) +docstring._snippet_manager['plot.spy'] = _show_docstring.format( + descrip='a sparcity pattern', command='spy' +) + + +# Flow function docstring +_flow_docstring = """ +Plot {descrip}. + +Parameters +---------- +%(plot.args_2d_flow)s + +c, color, colors : array-like or color-spec, optional + The colors of the {descrip} passed as either a keyword argument + or a fifth positional argument. This can be a single color or + a color array to be scaled by `cmap` and `norm`. +%(plot.args_2d_shared)s + +Other parameters +---------------- +%(plot.cmap_norm)s +%(plot.vmin_vmax)s +%(plot.levels_manual)s +%(plot.levels_auto)s +**kwargs + Passed to `matplotlib.axes.Axes.{command}` + +See also +-------- +PlotAxes.barbs +PlotAxes.quiver +PlotAxes.stream +PlotAxes.streamplot +matplotlib.axes.Axes.{command} +""" +docstring._snippet_manager['plot.barbs'] = _flow_docstring.format( + descrip='wind barbs', command='barbs' +) +docstring._snippet_manager['plot.quiver'] = _flow_docstring.format( + descrip='quiver arrows', command='quiver' +) +docstring._snippet_manager['plot.stream'] = _flow_docstring.format( + descrip='streamlines', command='streamplot' +) + + +def _get_vert(vert=None, orientation=None, **kwargs): + """ + Get the orientation specified as either `vert` or `orientation`. This is + used internally by various helper functions. + """ + if vert is not None: + return kwargs, vert + elif orientation is not None: + return kwargs, orientation != 'horizontal' # should already be validated + else: + return kwargs, True # fallback + + +def _parse_vert( + vert=None, orientation=None, default_vert=None, default_orientation=None, **kwargs +): + """ + Interpret both 'vert' and 'orientation' and add to outgoing keyword args + if a default is provided. + """ + # NOTE: Users should only pass these to hist, boxplot, or violinplot. To change + # the plot, scatter, area, or bar orientation users should use the differently + # named functions. Internally, however, they use these keyword args. + if default_vert is not None: + kwargs['vert'] = _not_none( + vert=vert, + orientation=None if orientation is None else orientation == 'vertical', + default=default_vert, + ) + if default_orientation is not None: + kwargs['orientation'] = _not_none( + orientation=orientation, + vert=None if vert is None else 'vertical' if vert else 'horizontal', + default=default_orientation, + ) + if kwargs.get('orientation', None) not in (None, 'horizontal', 'vertical'): + raise ValueError("Orientation must be either 'horizontal' or 'vertical'.") + return kwargs + + +def _inside_seaborn_call(): + """ + Try to detect `seaborn` calls to `scatter` and `bar` and then automatically + apply `absolute_size` and `absolute_width`. + """ + frame = sys._getframe() + absolute_names = ( + 'seaborn.distributions', + 'seaborn.categorical', + 'seaborn.relational', + 'seaborn.regression', + ) + while frame is not None: + if frame.f_globals.get('__name__', '') in absolute_names: + return True + frame = frame.f_back + return False + + +class PlotAxes(base.Axes): + """ + The second lowest-level `~matplotlib.axes.Axes` subclass used by proplot. + Implements all plotting overrides. + """ + def __init__(self, *args, **kwargs): + """ + Parameters + ---------- + *args, **kwargs + Passed to `proplot.axes.Axes`. + + See also + -------- + matplotlib.axes.Axes + proplot.axes.Axes + proplot.axes.CartesianAxes + proplot.axes.PolarAxes + proplot.axes.GeoAxes + """ + super().__init__(*args, **kwargs) + + def _call_native(self, name, *args, **kwargs): + """ + Call the plotting method and redirect internal calls to native methods. + """ + # NOTE: Previously allowed internal matplotlib plotting function calls to run + # through proplot overrides then avoided awkward conflicts in piecemeal fashion. + # Now prevent internal calls from running through overrides using preprocessor + kwargs.pop('distribution', None) # remove stat distributions + with context._state_context(self, _internal_call=True): + if self._name == 'basemap': + obj = getattr(self.projection, name)(*args, ax=self, **kwargs) + else: + obj = getattr(super(), name)(*args, **kwargs) + return obj + + def _call_negpos( + self, name, x, *ys, negcolor=None, poscolor=None, colorkey='facecolor', + use_where=False, use_zero=False, **kwargs + ): + """ + Call the plotting method separately for "negative" and "positive" data. + """ + if use_where: + kwargs.setdefault('interpolate', True) # see fill_between docs + for key in ('color', 'colors', 'facecolor', 'facecolors', 'where'): + value = kwargs.pop(key, None) + if value is not None: + warnings._warn_proplot( + f'{name}() argument {key}={value!r} is incompatible with negpos=True. Ignoring.' # noqa: E501 + ) + # Negative component + yneg = list(ys) # copy + if use_zero: # filter bar heights + yneg[0] = inputs._safe_mask(ys[0] < 0, ys[0]) + elif use_where: # apply fill_between mask + kwargs['where'] = ys[1] < ys[0] + else: + yneg = inputs._safe_mask(ys[1] < ys[0], *ys) + kwargs[colorkey] = _not_none(negcolor, rc['negcolor']) + negobj = self._call_native(name, x, *yneg, **kwargs) + # Positive component + ypos = list(ys) # copy + if use_zero: # filter bar heights + ypos[0] = inputs._safe_mask(ys[0] >= 0, ys[0]) + elif use_where: # apply fill_between mask + kwargs['where'] = ys[1] >= ys[0] + else: + ypos = inputs._safe_mask(ys[1] >= ys[0], *ys) + kwargs[colorkey] = _not_none(poscolor, rc['poscolor']) + posobj = self._call_native(name, x, *ypos, **kwargs) + return cbook.silent_list(type(negobj).__name__, (negobj, posobj)) + + def _add_auto_labels( + self, obj, cobj=None, labels=False, labels_kw=None, + fmt=None, formatter=None, formatter_kw=None, precision=None, + ): + """ + Add number labels. Default formatter is `~proplot.ticker.SimpleFormatter` + with a default maximum precision of ``3`` decimal places. + """ + # TODO: Add quiverkey to this! + if not labels: + return + labels_kw = labels_kw or {} + formatter_kw = formatter_kw or {} + formatter = _not_none( + fmt_labels_kw=labels_kw.pop('fmt', None), + formatter_labels_kw=labels_kw.pop('formatter', None), + fmt=fmt, + formatter=formatter, + default='simple' + ) + precision = _not_none( + formatter_kw_precision=formatter_kw.pop('precision', None), + precision=precision, + default=3, # should be lower than the default intended for tick labels + ) + formatter = constructor.Formatter(formatter, precision=precision, **formatter_kw) # noqa: E501 + if isinstance(obj, mcontour.ContourSet): + self._add_contour_labels(obj, cobj, formatter, **labels_kw) + elif isinstance(obj, mcollections.Collection): + self._add_collection_labels(obj, formatter, **labels_kw) + else: + raise RuntimeError(f'Not possible to add labels to object {obj!r}.') + + def _add_collection_labels( + self, obj, fmt, *, c=None, color=None, colors=None, + size=None, fontsize=None, **kwargs + ): + """ + Add labels to pcolor boxes with support for shade-dependent text colors. + Values are inferred from the unnormalized grid box color. + """ + # Parse input args + # NOTE: This function also hides grid boxes filled with NaNs to avoid ugly + # issue where edge colors surround NaNs. Should maybe move this somewhere else. + obj.update_scalarmappable() # update 'edgecolors' list + color = _not_none(c=c, color=color, colors=colors) + fontsize = _not_none(size=size, fontsize=fontsize, default=rc['font.smallsize']) + kwargs.setdefault('ha', 'center') + kwargs.setdefault('va', 'center') + + # Apply colors and hide edge colors for empty grids + labs = [] + array = obj.get_array() + paths = obj.get_paths() + edgecolors = inputs._to_numpy_array(obj.get_edgecolors()) + if len(edgecolors) == 1: + edgecolors = np.repeat(edgecolors, len(array), axis=0) + for i, (path, value) in enumerate(zip(paths, array)): + # Round to the number corresponding to the *color* rather than + # the exact data value. Similar to contour label numbering. + if value is ma.masked or not np.isfinite(value): + edgecolors[i, :] = 0 + continue + if isinstance(obj.norm, pcolors.DiscreteNorm): + value = obj.norm._norm.inverse(obj.norm(value)) + icolor = color + if color is None: + _, _, lum = utils.to_xyz(obj.cmap(obj.norm(value)), 'hcl') + icolor = 'w' if lum < 50 else 'k' + bbox = path.get_extents() + x = (bbox.xmin + bbox.xmax) / 2 + y = (bbox.ymin + bbox.ymax) / 2 + lab = self.text(x, y, fmt(value), color=icolor, size=fontsize, **kwargs) + labs.append(lab) + + obj.set_edgecolors(edgecolors) + return labs + + def _add_contour_labels( + self, obj, cobj, fmt, *, c=None, color=None, colors=None, + size=None, fontsize=None, inline_spacing=None, **kwargs + ): + """ + Add labels to contours with support for shade-dependent filled contour labels. + Text color is inferred from filled contour object and labels are always drawn + on unfilled contour object (otherwise errors crop up). + """ + # Parse input args + zorder = max((h.get_zorder() for h in obj.collections), default=3) + zorder = max(3, zorder + 1) + kwargs.setdefault('zorder', zorder) + colors = _not_none(c=c, color=color, colors=colors) + fontsize = _not_none(size=size, fontsize=fontsize, default=rc['font.smallsize']) + inline_spacing = _not_none(inline_spacing, 2.5) + + # Separate clabel args from text Artist args + text_kw = {} + clabel_keys = ('levels', 'inline', 'manual', 'rightside_up', 'use_clabeltext') + for key in tuple(kwargs): # allow dict to change size + if key not in clabel_keys: + text_kw[key] = kwargs.pop(key) + + # Draw hidden additional contour for filled contour labels + cobj = _not_none(cobj, obj) + if obj.filled and colors is None: + colors = [] + for level in obj.levels: + _, _, lum = utils.to_xyz(obj.cmap(obj.norm(level))) + colors.append('w' if lum < 50 else 'k') + + # Draw the labels + labs = cobj.clabel( + fmt=fmt, colors=colors, fontsize=fontsize, + inline_spacing=inline_spacing, **kwargs + ) + if labs is not None: # returns None if no contours + for lab in labs: + lab.update(text_kw) + + return labs + + def _add_error_bars( + self, x, y, *_, distribution=None, + default_barstds=False, default_boxstds=False, + default_barpctiles=False, default_boxpctiles=False, default_marker=False, + bars=None, boxes=None, + barstd=None, barstds=None, barpctile=None, barpctiles=None, bardata=None, + boxstd=None, boxstds=None, boxpctile=None, boxpctiles=None, boxdata=None, + capsize=None, **kwargs, + ): + """ + Add up to 2 error indicators: thick "boxes" and thin "bars". The ``default`` + keywords toggle default range indicators when distributions are passed. + """ + # Parse input args + # NOTE: Want to keep _add_error_bars() and _add_error_shading() separate. + # But also want default behavior where some default error indicator is shown + # if user requests means/medians only. Result is the below kludge. + kwargs, vert = _get_vert(**kwargs) + barstds = _not_none(bars=bars, barstd=barstd, barstds=barstds) + boxstds = _not_none(boxes=boxes, boxstd=boxstd, boxstds=boxstds) + barpctiles = _not_none(barpctile=barpctile, barpctiles=barpctiles) + boxpctiles = _not_none(boxpctile=boxpctile, boxpctiles=boxpctiles) + if distribution is not None and not any( + typ + mode in key for key in kwargs + for typ in ('shade', 'fade') for mode in ('', 'std', 'pctile', 'data') + ): # ugly kludge to check for shading + if all(_ is None for _ in (bardata, barstds, barpctiles)): + barstds, barpctiles = default_barstds, default_barpctiles + if all(_ is None for _ in (boxdata, boxstds, boxpctiles)): + boxstds, boxpctiles = default_boxstds, default_boxpctiles + showbars = any( + _ is not None and _ is not False for _ in (barstds, barpctiles, bardata) + ) + showboxes = any( + _ is not None and _ is not False for _ in (boxstds, boxpctiles, boxdata) + ) + + # Error bar properties + edgecolor = kwargs.get('edgecolor', rc['boxplot.whiskerprops.color']) + barprops = _pop_props(kwargs, 'line', ignore='marker', prefix='bar') + barprops['capsize'] = _not_none(capsize, rc['errorbar.capsize']) + barprops['linestyle'] = 'none' + barprops.setdefault('color', edgecolor) + barprops.setdefault('zorder', 2.5) + barprops.setdefault('linewidth', rc['boxplot.whiskerprops.linewidth']) + + # Error box properties + # NOTE: Includes 'markerfacecolor' and 'markeredgecolor' props + boxprops = _pop_props(kwargs, 'line', prefix='box') + boxprops['capsize'] = 0 + boxprops['linestyle'] = 'none' + boxprops.setdefault('color', barprops['color']) + boxprops.setdefault('zorder', barprops['zorder']) + boxprops.setdefault('linewidth', 4 * barprops['linewidth']) + + # Box marker properties + boxmarker = {key: boxprops.pop(key) for key in tuple(boxprops) if 'marker' in key} # noqa: E501 + boxmarker['c'] = _not_none(boxmarker.pop('markerfacecolor', None), 'white') + boxmarker['s'] = _not_none(boxmarker.pop('markersize', None), boxprops['linewidth'] ** 0.5) # noqa: E501 + boxmarker['zorder'] = boxprops['zorder'] + boxmarker['edgecolor'] = boxmarker.pop('markeredgecolor', None) + boxmarker['linewidth'] = boxmarker.pop('markerlinewidth', None) + if boxmarker.get('marker') is True: + boxmarker['marker'] = 'o' + elif default_marker: + boxmarker.setdefault('marker', 'o') + + # Draw thin or thick error bars from distributions or explicit errdata + # NOTE: Now impossible to make thin bar width different from cap width! + # NOTE: Boxes must go after so scatter point can go on top + sy = 'y' if vert else 'x' # yerr + ex, ey = (x, y) if vert else (y, x) + eobjs = [] + if showbars: # noqa: E501 + edata, _ = inputs._dist_range( + y, distribution, + stds=barstds, pctiles=barpctiles, errdata=bardata, + stds_default=(-3, 3), pctiles_default=(0, 100), + ) + if edata is not None: + obj = self.errorbar(ex, ey, **barprops, **{sy + 'err': edata}) + eobjs.append(obj) + if showboxes: # noqa: E501 + edata, _ = inputs._dist_range( + y, distribution, + stds=boxstds, pctiles=boxpctiles, errdata=boxdata, + stds_default=(-1, 1), pctiles_default=(25, 75), + ) + if edata is not None: + obj = self.errorbar(ex, ey, **boxprops, **{sy + 'err': edata}) + if boxmarker.get('marker', None): + self.scatter(ex, ey, **boxmarker) + eobjs.append(obj) + + kwargs['distribution'] = distribution + return (*eobjs, kwargs) + + def _add_error_shading( + self, x, y, *_, distribution=None, color_key='color', + shade=None, shadestd=None, shadestds=None, + shadepctile=None, shadepctiles=None, shadedata=None, + fade=None, fadestd=None, fadestds=None, + fadepctile=None, fadepctiles=None, fadedata=None, + shadelabel=False, fadelabel=False, **kwargs + ): + """ + Add up to 2 error indicators: more opaque "shading" and less opaque "fading". + """ + kwargs, vert = _get_vert(**kwargs) + shadestds = _not_none(shade=shade, shadestd=shadestd, shadestds=shadestds) + fadestds = _not_none(fade=fade, fadestd=fadestd, fadestds=fadestds) + shadepctiles = _not_none(shadepctile=shadepctile, shadepctiles=shadepctiles) + fadepctiles = _not_none(fadepctile=fadepctile, fadepctiles=fadepctiles) + drawshade = any( + _ is not None and _ is not False + for _ in (shadestds, shadepctiles, shadedata) + ) + drawfade = any( + _ is not None and _ is not False + for _ in (fadestds, fadepctiles, fadedata) + ) + + # Shading properties + shadeprops = _pop_props(kwargs, 'patch', prefix='shade') + shadeprops.setdefault('alpha', 0.4) + shadeprops.setdefault('zorder', 1.5) + shadeprops.setdefault('linewidth', rc['patch.linewidth']) + shadeprops.setdefault('edgecolor', 'none') + # Fading properties + fadeprops = _pop_props(kwargs, 'patch', prefix='fade') + fadeprops.setdefault('zorder', shadeprops['zorder']) + fadeprops.setdefault('alpha', 0.5 * shadeprops['alpha']) + fadeprops.setdefault('linewidth', shadeprops['linewidth']) + fadeprops.setdefault('edgecolor', 'none') + # Get default color then apply to outgoing keyword args so + # that plotting function will not advance to next cycler color. + # TODO: More robust treatment of 'color' vs. 'facecolor' + if ( + drawshade and shadeprops.get('facecolor', None) is None + or drawfade and fadeprops.get('facecolor', None) is None + ): + color = kwargs.get(color_key, None) + if color is None: # add to outgoing + color = kwargs[color_key] = self._get_lines.get_next_color() + shadeprops.setdefault('facecolor', color) + fadeprops.setdefault('facecolor', color) + + # Draw dark and light shading from distributions or explicit errdata + eobjs = [] + fill = self.fill_between if vert else self.fill_betweenx + if drawfade: + edata, label = inputs._dist_range( + y, distribution, + stds=fadestds, pctiles=fadepctiles, errdata=fadedata, + stds_default=(-3, 3), pctiles_default=(0, 100), + label=fadelabel, absolute=True, + ) + if edata is not None: + eobj = fill(x, *edata, label=label, **fadeprops) + eobjs.append(eobj) + if drawshade: + edata, label = inputs._dist_range( + y, distribution, + stds=shadestds, pctiles=shadepctiles, errdata=shadedata, + stds_default=(-2, 2), pctiles_default=(10, 90), + label=shadelabel, absolute=True, + ) + if edata is not None: + eobj = fill(x, *edata, label=label, **shadeprops) + eobjs.append(eobj) + + kwargs['distribution'] = distribution + return (*eobjs, kwargs) + + def _fix_contour_edges(self, method, *args, **kwargs): + """ + Fix the filled contour edges by secretly adding solid contours with + the same input data. + """ + # NOTE: This is used to provide an object that can be used by 'clabel' for + # auto-labels. Filled contours create strange artifacts. + # NOTE: Make the default 'line width' identical to one used for pcolor plots + # rather than rc['contour.linewidth']. See mpl pcolor() source code + if not any(key in kwargs for key in ('linewidths', 'linestyles', 'edgecolors')): + kwargs['linewidths'] = 0 # for clabel + kwargs.setdefault('linewidths', EDGEWIDTH) + kwargs.pop('cmap', None) + kwargs['colors'] = kwargs.pop('edgecolors', 'k') + return self._call_native(method, *args, **kwargs) + + def _fix_sticky_edges(self, objs, axis, *args, only=None): + """ + Fix sticky edges for the input artists using the minimum and maximum of the + input coordinates. This is used to copy `bar` behavior to `area` and `lines`. + """ + for array in args: + min_, max_ = inputs._safe_range(array) + if min_ is None or max_ is None: + continue + for obj in guides._iter_iterables(objs): + if only and not isinstance(obj, only): + continue # e.g. ignore error bars + convert = getattr(self, 'convert_' + axis + 'units') + edges = getattr(obj.sticky_edges, axis) + edges.extend(convert((min_, max_))) + + @staticmethod + def _fix_patch_edges(obj, edgefix=None, **kwargs): + """ + Fix white lines between between filled patches and fix issues + with colormaps that are transparent. If keyword args passed by user + include explicit edge properties then we skip this step. + """ + # NOTE: Use default edge width used for pcolor grid box edges. This is thick + # enough to hide lines but thin enough to not add 'nubs' to corners of boxes. + # See: https://github.com/jklymak/contourfIssues + # See: https://stackoverflow.com/q/15003353/4970632 + edgefix = _not_none(edgefix, rc.edgefix, True) + linewidth = EDGEWIDTH if edgefix is True else 0 if edgefix is False else edgefix + if not linewidth: + return + keys = ('linewidth', 'linestyle', 'edgecolor') # patches and collections + if any(key + suffix in kwargs for key in keys for suffix in ('', 's')): + return + rasterized = obj.get_rasterized() if isinstance(obj, martist.Artist) else False + if rasterized: + return + + # Skip when cmap has transparency + if hasattr(obj, 'get_alpha'): # collections and contour sets use singular + alpha = obj.get_alpha() + if alpha is not None and alpha < 1: + return + if isinstance(obj, mcm.ScalarMappable): + cmap = obj.cmap + if not cmap._isinit: + cmap._init() + if not all(cmap._lut[:-1, 3] == 1): # skip for cmaps with transparency + return + + # Apply fixes + # NOTE: This also covers TriContourSet returned by tricontour + if isinstance(obj, mcontour.ContourSet): + if obj.filled: + for contour in obj.collections: + contour.set_linestyle('-') + contour.set_linewidth(linewidth) + contour.set_edgecolor('face') + elif isinstance(obj, mcollections.Collection): # e.g. QuadMesh, PolyCollection + obj.set_linewidth(linewidth) + obj.set_edgecolor('face') + elif isinstance(obj, mpatches.Patch): # e.g. Rectangle + obj.set_linewidth(linewidth) + obj.set_edgecolor(obj.get_facecolor()) + elif np.iterable(obj): # e.g. silent_list of BarContainer + for element in obj: + PlotAxes._fix_patch_edges(element, edgefix=edgefix) + else: + warnings._warn_proplot(f'Unexpected obj {obj} passed to _fix_patch_edges.') + + @contextlib.contextmanager + def _keep_grid_bools(self): + """ + Preserve the gridline booleans during the operation. This prevents `pcolor` + methods from disabling grids (mpl < 3.5) and emitting warnings (mpl >= 3.5). + """ + # NOTE: Modern matplotlib uses _get_axis_list() but this is only to support + # Axes3D which PlotAxes does not subclass. Safe to use xaxis and yaxis. + bools = [] + for axis, which in itertools.product( + (self.xaxis, self.yaxis), ('major', 'minor') + ): + kw = getattr(axis, f'_{which}_tick_kw', {}) + bools.append(kw.get('gridOn', None)) + kw['gridOn'] = False # prevent deprecation warning + yield + for b, (axis, which) in zip( + bools, itertools.product('xy', ('major', 'minor')) + ): + if b is not None: + self.grid(b, axis=axis, which=which) + + def _inbounds_extent(self, *, inbounds=None, **kwargs): + """ + Capture the `inbounds` keyword arg and return data limit + extents if it is ``True``. Otherwise return ``None``. When + ``_inbounds_xylim`` gets ``None`` it will silently exit. + """ + extents = None + inbounds = _not_none(inbounds, rc['axes.inbounds']) + if inbounds: + extents = list(self.dataLim.extents) # ensure modifiable + return kwargs, extents + + def _inbounds_vlim(self, x, y, z, *, to_centers=False): + """ + Restrict the sample data used for automatic `vmin` and `vmax` selection + based on the existing x and y axis limits. + """ + # Get masks + # WARNING: Experimental, seems robust but this is not mission-critical so + # keep this in a try-except clause for now. However *internally* we should + # not reach this block unless everything is an array so raise that error. + xmask = ymask = None + if self._name != 'cartesian': + return z # TODO: support geographic projections when input is PlateCarree() + if not all(getattr(a, 'ndim', None) in (1, 2) for a in (x, y, z)): + raise ValueError('Invalid input coordinates. Must be 1D or 2D arrays.') + try: + # Get centers and masks + if to_centers and z.ndim == 2: + x, y = inputs._to_centers(x, y, z) + if not self.get_autoscalex_on(): + xlim = self.get_xlim() + xmask = (x >= min(xlim)) & (x <= max(xlim)) + if not self.get_autoscaley_on(): + ylim = self.get_ylim() + ymask = (y >= min(ylim)) & (y <= max(ylim)) + # Get subsample + if xmask is not None and ymask is not None: + z = z[np.ix_(ymask, xmask)] if z.ndim == 2 and xmask.ndim == 1 else z[ymask & xmask] # noqa: E501 + elif xmask is not None: + z = z[:, xmask] if z.ndim == 2 and xmask.ndim == 1 else z[xmask] + elif ymask is not None: + z = z[ymask, :] if z.ndim == 2 and ymask.ndim == 1 else z[ymask] + return z + except Exception as err: + warnings._warn_proplot( + 'Failed to restrict automatic colormap normalization ' + f'to in-bounds data only. Error message: {err}' + ) + return z + + def _inbounds_xylim(self, extents, x, y, **kwargs): + """ + Restrict the `dataLim` to exclude out-of-bounds data when x (y) limits + are fixed and we are determining default y (x) limits. This modifies + the mutable input `extents` to support iteration over columns. + """ + # WARNING: This feature is still experimental. But seems obvious. Matplotlib + # updates data limits in ad hoc fashion differently for each plotting command + # but since proplot standardizes inputs we can easily use them for dataLim. + if extents is None: + return + if self._name != 'cartesian': + return + if not x.size or not y.size: + return + kwargs, vert = _get_vert(**kwargs) + if not vert: + x, y = y, x + trans = self.dataLim + autox, autoy = self.get_autoscalex_on(), self.get_autoscaley_on() + try: + if autoy and not autox and x.shape == y.shape: + # Reset the y data limits + xmin, xmax = sorted(self.get_xlim()) + mask = (x >= xmin) & (x <= xmax) + ymin, ymax = inputs._safe_range(inputs._safe_mask(mask, y)) + convert = self.convert_yunits # handle datetime, pint units + if ymin is not None: + trans.y0 = extents[1] = min(convert(ymin), extents[1]) + if ymax is not None: + trans.y1 = extents[3] = max(convert(ymax), extents[3]) + getattr(self, '_request_autoscale_view', self.autoscale_view)() + if autox and not autoy and y.shape == x.shape: + # Reset the x data limits + ymin, ymax = sorted(self.get_ylim()) + mask = (y >= ymin) & (y <= ymax) + xmin, xmax = inputs._safe_range(inputs._safe_mask(mask, x)) + convert = self.convert_xunits # handle datetime, pint units + if xmin is not None: + trans.x0 = extents[0] = min(convert(xmin), extents[0]) + if xmax is not None: + trans.x1 = extents[2] = max(convert(xmax), extents[2]) + getattr(self, '_request_autoscale_view', self.autoscale_view)() + except Exception as err: + warnings._warn_proplot( + 'Failed to restrict automatic y (x) axis limit algorithm to ' + f'data within locked x (y) limits only. Error message: {err}' + ) + + def _parse_1d_args(self, x, *ys, **kwargs): + """ + Interpret positional arguments for all 1D plotting commands. + """ + # Standardize values + zerox = not ys + if zerox or all(y is None for y in ys): # pad with remaining Nones + x, *ys = None, x, *ys[1:] + if len(ys) == 2: # 'lines' or 'fill_between' + if ys[1] is None: + ys = (np.array([0.0]), ys[0]) # user input 1 or 2 positional args + elif ys[0] is None: + ys = (np.array([0.0]), ys[1]) # user input keyword 'y2' but no y1 + if any(y is None for y in ys): + raise ValueError('Missing required data array argument.') + ys = tuple(map(inputs._to_duck_array, ys)) + if x is not None: + x = inputs._to_duck_array(x) + x, *ys, kwargs = self._parse_1d_format(x, *ys, zerox=zerox, **kwargs) + + # Geographic corrections + if self._name == 'cartopy' and isinstance(kwargs.get('transform'), PlateCarree): # noqa: E501 + x, *ys = inputs._geo_cartopy_1d(x, *ys) + elif self._name == 'basemap' and kwargs.get('latlon', None): + xmin, xmax = self._lonaxis.get_view_interval() + x, *ys = inputs._geo_basemap_1d(x, *ys, xmin=xmin, xmax=xmax) + + return (x, *ys, kwargs) + + def _parse_1d_format( + self, x, *ys, zerox=False, autox=True, autoy=True, autoformat=None, + autoreverse=True, autolabels=True, autovalues=False, autoguide=True, + label=None, labels=None, value=None, values=None, **kwargs + ): + """ + Try to retrieve default coordinates from array-like objects and apply default + formatting. Also update the keyword arguments. + """ + # Parse input + y = max(ys, key=lambda y: y.size) # find a non-scalar y for inferring metadata + autox = autox and not zerox # so far just relevant for hist() + autoformat = _not_none(autoformat, rc['autoformat']) + kwargs, vert = _get_vert(**kwargs) + labels = _not_none( + label=label, + labels=labels, + value=value, + values=values, + legend_kw_labels=kwargs.get('legend_kw', {}).pop('labels', None), + colorbar_kw_values=kwargs.get('colorbar_kw', {}).pop('values', None), + ) + + # Retrieve the x coords + # NOTE: Where columns represent distributions, like for box and violinplot or + # where we use 'means' or 'medians', columns coords (axis 1) are 'x' coords. + # Otherwise, columns represent e.g. lines and row coords (axis 0) are 'x' + # coords. Exception is passing "ragged arrays" to boxplot and violinplot. + dists = any(kwargs.get(s) for s in ('mean', 'means', 'median', 'medians')) + raggd = any(getattr(y, 'dtype', None) == 'object' for y in ys) + xaxis = 0 if raggd else 1 if dists or not autoy else 0 + if autox and x is None: + x = inputs._meta_labels(y, axis=xaxis) # use the first one + + # Retrieve the labels. We only want default legend labels if this is an + # object with 'title' metadata and/or the coords are string. + # WARNING: Confusing terminology differences here -- for box and violin plots + # labels refer to indices along x axis. + if autolabels and labels is None: + laxis = 0 if not autox and not autoy else xaxis if not autoy else xaxis + 1 + if laxis >= y.ndim: + labels = inputs._meta_title(y) + else: + labels = inputs._meta_labels(y, axis=laxis, always=False) + notitle = not inputs._meta_title(labels) + if labels is None: + pass + elif notitle and not any(isinstance(_, str) for _ in labels): + labels = None + + # Apply the labels or values + if labels is not None: + if autovalues: + kwargs['values'] = inputs._to_numpy_array(labels) + elif autolabels: + kwargs['labels'] = inputs._to_numpy_array(labels) + + # Apply title for legend or colorbar that uses the labels or values + if autoguide and autoformat: + title = inputs._meta_title(labels) + if title: # safely update legend_kw and colorbar_kw + guides._add_guide_kw('legend', kwargs, title=title) + guides._add_guide_kw('colorbar', kwargs, title=title) + + # Apply the basic x and y settings + autox = autox and self._name == 'cartesian' + autoy = autoy and self._name == 'cartesian' + sx, sy = 'xy' if vert else 'yx' + kw_format = {} + if autox and autoformat: # 'x' axis + title = inputs._meta_title(x) + if title: + axis = getattr(self, sx + 'axis') + if axis.isDefault_label: + kw_format[sx + 'label'] = title + if autoy and autoformat: # 'y' axis + sy = sx if zerox else sy # hist() 'y' values are along 'x' axis + title = inputs._meta_title(y) + if title: + axis = getattr(self, sy + 'axis') + if axis.isDefault_label: + kw_format[sy + 'label'] = title + + # Convert string-type coordinates to indices + # NOTE: This should even allow qualitative string input to hist() + if autox: + x, kw_format = inputs._meta_coords(x, which=sx, **kw_format) + if autoy: + *ys, kw_format = inputs._meta_coords(*ys, which=sy, **kw_format) + if autox and autoreverse and inputs._is_descending(x): + if getattr(self, f'get_autoscale{sx}_on')(): + kw_format[sx + 'reverse'] = True + + # Finally apply formatting and strip metadata + # WARNING: Most methods that accept 2D arrays use columns of data, but when + # pandas DataFrame specifically is passed to hist, boxplot, or violinplot, rows + # of data assumed! Converting to ndarray necessary. + if kw_format: + self.format(**kw_format) + ys = tuple(map(inputs._to_numpy_array, ys)) + if x is not None: # pie() and hist() + x = inputs._to_numpy_array(x) + return (x, *ys, kwargs) + + def _parse_2d_args( + self, x, y, *zs, globe=False, edges=False, allow1d=False, + transpose=None, order=None, **kwargs + ): + """ + Interpret positional arguments for all 2D plotting commands. + """ + # Standardize values + # NOTE: Functions pass two 'zs' at most right now + if all(z is None for z in zs): + x, y, zs = None, None, (x, y)[:len(zs)] + if any(z is None for z in zs): + raise ValueError('Missing required data array argument(s).') + zs = tuple(inputs._to_duck_array(z, strip_units=True) for z in zs) + if x is not None: + x = inputs._to_duck_array(x) + if y is not None: + y = inputs._to_duck_array(y) + if order is not None: + if not isinstance(order, str) or order not in 'CF': + raise ValueError(f"Invalid order={order!r}. Options are 'C' or 'F'.") + transpose = _not_none( + transpose=transpose, transpose_order=bool('CF'.index(order)) + ) + if transpose: + zs = tuple(z.T for z in zs) + if x is not None: + x = x.T + if y is not None: + y = y.T + x, y, *zs, kwargs = self._parse_2d_format(x, y, *zs, **kwargs) + if edges: + # NOTE: These functions quitely pass through 1D inputs, e.g. barb data + x, y = inputs._to_edges(x, y, zs[0]) + else: + x, y = inputs._to_centers(x, y, zs[0]) + + # Geographic corrections + if allow1d: + pass + elif self._name == 'cartopy' and isinstance(kwargs.get('transform'), PlateCarree): # noqa: E501 + x, y, *zs = inputs._geo_cartopy_2d(x, y, *zs, globe=globe) + elif self._name == 'basemap' and kwargs.get('latlon', None): + xmin, xmax = self._lonaxis.get_view_interval() + x, y, *zs = inputs._geo_basemap_2d(x, y, *zs, xmin=xmin, xmax=xmax, globe=globe) # noqa: E501 + x, y = np.meshgrid(x, y) # WARNING: required always + + return (x, y, *zs, kwargs) + + def _parse_2d_format( + self, x, y, *zs, autoformat=None, autoguide=True, autoreverse=True, **kwargs + ): + """ + Try to retrieve default coordinates from array-like objects and apply default + formatting. Also apply optional transpose and update the keyword arguments. + """ + # Retrieve coordinates + autoformat = _not_none(autoformat, rc['autoformat']) + if x is None and y is None: + z = zs[0] + if z.ndim == 1: + x = inputs._meta_labels(z, axis=0) + y = np.zeros(z.shape) # default barb() and quiver() behavior in mpl + else: + x = inputs._meta_labels(z, axis=1) + y = inputs._meta_labels(z, axis=0) + + # Apply labels and XY axis settings + if self._name == 'cartesian': + # Apply labels + # NOTE: Do not overwrite existing labels! + kw_format = {} + if autoformat: + for s, d in zip('xy', (x, y)): + title = inputs._meta_title(d) + if title: + axis = getattr(self, s + 'axis') + if axis.isDefault_label: + kw_format[s + 'label'] = title + + # Handle string-type coordinates + x, kw_format = inputs._meta_coords(x, which='x', **kw_format) + y, kw_format = inputs._meta_coords(y, which='y', **kw_format) + for s, d in zip('xy', (x, y)): + if autoreverse and inputs._is_descending(d): + if getattr(self, f'get_autoscale{s}_on')(): + kw_format[s + 'reverse'] = True + + # Apply formatting + if kw_format: + self.format(**kw_format) + + # Apply title for legend or colorbar + if autoguide and autoformat: + title = inputs._meta_title(zs[0]) + if title: # safely update legend_kw and colorbar_kw + guides._add_guide_kw('legend', kwargs, title=title) + guides._add_guide_kw('colorbar', kwargs, title=title) + + # Finally strip metadata + x = inputs._to_numpy_array(x) + y = inputs._to_numpy_array(y) + zs = tuple(map(inputs._to_numpy_array, zs)) + return (x, y, *zs, kwargs) + + def _parse_color(self, x, y, c, *, apply_cycle=True, infer_rgb=False, **kwargs): + """ + Parse either a colormap or color cycler. Colormap will be discrete and fade + to subwhite luminance by default. Returns a HEX string if needed so we don't + get ambiguous color warnings. Used with scatter, streamplot, quiver, barbs. + """ + # NOTE: This function is positioned above the _parse_cmap and _parse_cycle + # functions and helper functions. + parsers = (self._parse_cmap, *self._level_parsers) + if c is None or mcolors.is_color_like(c): + if infer_rgb and c is not None: + c = pcolors.to_hex(c) # avoid scatter() ambiguous color warning + if apply_cycle: # False for scatter() so we can wait to get correct 'N' + kwargs = self._parse_cycle(**kwargs) + else: + c = np.atleast_1d(c) # should only have effect on 'scatter' input + if infer_rgb and (inputs._is_categorical(c) or c.ndim == 2 and c.shape[1] in (3, 4)): # noqa: E501 + c = list(map(pcolors.to_hex, c)) # avoid iterating over columns + else: + kwargs = self._parse_cmap(x, y, c, plot_lines=True, default_discrete=False, **kwargs) # noqa: E501 + parsers = (self._parse_cycle,) + pop = _pop_params(kwargs, *parsers, ignore_internal=True) + if pop: + warnings._warn_proplot(f'Ignoring unused keyword arg(s): {pop}') + return (c, kwargs) + + @warnings._rename_kwargs('0.6.0', centers='values') + def _parse_cmap( + self, *args, + cmap=None, cmap_kw=None, c=None, color=None, colors=None, + norm=None, norm_kw=None, extend=None, vmin=None, vmax=None, vcenter=None, + discrete=None, default_discrete=True, default_cmap=None, skip_autolev=False, + min_levels=None, plot_lines=False, plot_contours=False, **kwargs + ): + """ + Parse colormap and normalizer arguments. + + Parameters + ---------- + c, color, colors : sequence of color-spec, optional + Build a `DiscreteColormap` from the input color(s). + cmap, cmap_kw : optional + Colormap specs. + norm, norm_kw : optional + Normalize specs. + extend : optional + The colormap extend setting. + vmin, vmax : float, optional + The normalization range. + vcenter : float, optional + The central value for diverging colormaps. + sequential, diverging, cyclic, qualitative : bool, optional + Toggle various colormap types. + discrete : bool, optional + Whether to apply `DiscreteNorm` to the colormap. + default_discrete : bool, optional + The default `discrete`. Depends on plotting method. + skip_autolev : bool, optional + Whether to skip automatic level generation. + min_levels : int, optional + The minimum number of valid levels. 1 for line contour plots 2 otherwise. + plot_lines : bool, optional + Whether these are lines. If so the default monochromatic luminance is 90. + plot_contours : bool, optional + Whether these are contours. If so then a discrete of `True` is required. + """ + # Parse keyword args + cmap_kw = cmap_kw or {} + norm_kw = norm_kw or {} + vmin = _not_none(vmin=vmin, norm_kw_vmin=norm_kw.pop('vmin', None)) + vmax = _not_none(vmax=vmax, norm_kw_vmax=norm_kw.pop('vmax', None)) + vcenter = _not_none(vcenter=vcenter, norm_kw_vcenter=norm_kw.get('vcenter')) + colors = _not_none(c=c, color=color, colors=colors) # in case untranslated + extend = 'both' if extend is True else 'neither' if extend is False else extend + extend = _not_none(extend, 'neither') + modes = ('sequential', 'diverging', 'cyclic', 'qualitative') + modes = {mode: kwargs.pop(mode, None) for mode in modes} + if vcenter is not None: # shorthand to ensure diverging colormap + norm = _not_none(norm, 'div') + modes['diverging'] = True + norm_kw.setdefault('vcenter', vcenter) + if sum(map(bool, modes.values())) > 1: # noqa: E501 + warnings._warn_proplot( + f'Conflicting colormap arguments: {modes!r}. Using the first one.' + ) + keys = tuple(key for key, b in modes.items() if b) + for key in keys[1:]: + modes[key] = None + + # Create user-input colormap and potentially disable autodiverging + # NOTE: Let people use diverging=False with diverging cmaps because some + # use them (wrongly IMO but to each their own) for increased color contrast. + # WARNING: Previously 'colors' set the edgecolors. To avoid all-black + # colormap make sure to ignore 'colors' if 'cmap' was also passed. + # WARNING: Previously tried setting number of levels to len(colors), but this + # makes single-level single-color contour plots, and since _parse_level_num is + # only generates approximate level counts, the idea failed anyway. Users should + # pass their own levels to avoid truncation/cycling in these very special cases. + autodiverging = rc['cmap.autodiverging'] + if colors is not None: + if cmap is not None: + warnings._warn_proplot( + f'You specified both cmap={cmap!s} and the qualitative-colormap ' + f"colors={colors!r}. Ignoring 'colors'. If you meant to specify " + f'the edge color please use e.g. edgecolor={colors!r} instead.' + ) + else: + if mcolors.is_color_like(colors): + colors = [colors] # RGB[A] tuple possibly + cmap = colors = np.atleast_1d(colors) + cmap_kw['listmode'] = 'discrete' + if cmap is not None: + if plot_lines: + cmap_kw['default_luminance'] = constructor.DEFAULT_CYCLE_LUMINANCE + cmap = constructor.Colormap(cmap, **cmap_kw) + name = re.sub(r'\A_*(.*?)(?:_r|_s|_copy)*\Z', r'\1', cmap.name.lower()) + if not any(name in opts for opts in pcolors.CMAPS_DIVERGING.items()): + autodiverging = False # avoid auto-truncation of sequential colormaps + + # Force default options in special cases + # NOTE: Delay application of 'sequential', 'diverging', 'cyclic', 'qualitative' + # until after level generation so 'diverging' can be automatically applied. + if modes['cyclic'] or getattr(cmap, '_cyclic', None): + if extend is not None and extend != 'neither': + warnings._warn_proplot( + f"Cyclic colormaps require extend='neither'. Ignoring extend={extend!r}" # noqa: E501 + ) + extend = 'neither' + if modes['qualitative'] or isinstance(cmap, pcolors.DiscreteColormap): + if discrete is not None and not discrete: # noqa: E501 + warnings._warn_proplot( + 'Qualitative colormaps require discrete=True. Ignoring discrete=False.' # noqa: E501 + ) + discrete = True + if plot_contours: + if discrete is not None and not discrete: + warnings._warn_proplot( + 'Contoured plots require discrete=True. Ignoring discrete=False.' + ) + discrete = True + keys = ('levels', 'values', 'locator', 'negative', 'positive', 'symmetric') + if any(key in kwargs for key in keys): # override + discrete = _not_none(discrete, True) + else: # use global boolean rc['cmap.discrete'] or command-specific default + discrete = _not_none(discrete, rc['cmap.discrete'], default_discrete) + + # Determine the appropriate 'vmin', 'vmax', and/or 'levels' + # NOTE: Unlike xarray, but like matplotlib, vmin and vmax only approximately + # determine level range. Levels are selected with Locator.tick_values(). + levels = None # unused + isdiverging = False + if not discrete and not skip_autolev: + vmin, vmax, kwargs = self._parse_level_lim( + *args, vmin=vmin, vmax=vmax, **kwargs + ) + if autodiverging and vmin is not None and vmax is not None: + if abs(np.sign(vmax) - np.sign(vmin)) == 2: + isdiverging = True + if discrete: + levels, vmin, vmax, norm, norm_kw, kwargs = self._parse_level_vals( + *args, vmin=vmin, vmax=vmax, norm=norm, norm_kw=norm_kw, extend=extend, + min_levels=min_levels, skip_autolev=skip_autolev, **kwargs + ) + if autodiverging and levels is not None: + _, counts = np.unique(np.sign(levels), return_counts=True) + if counts[counts > 1].size > 1: + isdiverging = True + if not any(modes.values()) and isdiverging and modes['diverging'] is None: + modes['diverging'] = True + + # Create the continuous normalizer. + isdiverging = modes['diverging'] + default = 'div' if isdiverging else 'linear' + norm = _not_none(norm, default) + if isdiverging and isinstance(norm, str) and norm in ('segments', 'segmented'): + norm_kw.setdefault('vcenter', 0) + if isinstance(norm, mcolors.Normalize): + norm.vmin, norm.vmax = vmin, vmax + else: + norm = constructor.Norm(norm, vmin=vmin, vmax=vmax, **norm_kw) + if autodiverging and isinstance(norm, pcolors.DivergingNorm): + isdiverging = True + if not any(modes.values()) and isdiverging and modes['diverging'] is None: + modes['diverging'] = True + + # Create the final colormap + if cmap is None: + if default_cmap is not None: # used internally + cmap = default_cmap + elif any(modes.values()): + cmap = rc['cmap.' + tuple(key for key, b in modes.items() if b)[0]] + else: + cmap = rc['image.cmap'] + cmap = constructor.Colormap(cmap, **cmap_kw) + + # Create the discrete normalizer + # Then finally warn and remove unused args + if levels is not None: + norm, cmap, kwargs = self._parse_level_norm( + levels, norm, cmap, extend=extend, min_levels=min_levels, **kwargs + ) + params = _pop_params(kwargs, *self._level_parsers, ignore_internal=True) + if 'N' in params: # use this for lookup table N instead of levels N + cmap = cmap.copy(N=params.pop('N')) + if params: + warnings._warn_proplot(f'Ignoring unused keyword args(s): {params}') + + # Update outgoing args + # NOTE: ContourSet natively stores 'extend' on the result but for other + # classes we need to hide it on the object. + kwargs.update({'cmap': cmap, 'norm': norm}) + if plot_contours: + kwargs.update({'levels': levels, 'extend': extend}) + else: + guides._add_guide_kw('colorbar', kwargs, extend=extend) + + return kwargs + + def _parse_cycle( + self, ncycle=None, *, cycle=None, cycle_kw=None, + cycle_manually=None, return_cycle=False, **kwargs + ): + """ + Parse property cycle-related arguments. + + Parameters + ---------- + ncycle : int, optional + The number of samples to draw for the cycle. + cycle : cycle-spec, optional + The property cycle specifier. + cycle_kw : dict-like, optional + The property cycle keyword arguments + cycle_manually : dict-like, optional + Mapping of property cycle keys to plotting function keys. Used + to translate property cycle line properties to scatter properties. + return_cycle : bool, optional + Whether to simply return the property cycle or apply it. The cycle is + only applied (and therefore reset) if it differs from the current one. + """ + # Create the property cycler and update it if necessary + # NOTE: Matplotlib Cycler() objects have built-in __eq__ operator + # so really easy to check if the cycler has changed! + if cycle is not None or cycle_kw: + cycle_kw = cycle_kw or {} + if ncycle != 1: # ignore for column-by-column plotting commands + cycle_kw.setdefault('N', ncycle) # if None then filled in Colormap() + if isinstance(cycle, str) and cycle.lower() == 'none': + cycle = False + if not cycle: + args = () + elif cycle is True: # consistency with 'False' ('reactivate' the cycler) + args = (rc['axes.prop_cycle'],) + else: + args = (cycle,) + cycle = constructor.Cycle(*args, **cycle_kw) + with warnings.catch_warnings(): # hide 'elementwise-comparison failed' + warnings.simplefilter('ignore', FutureWarning) + if return_cycle: + pass + elif cycle != self._active_cycle: + self.set_prop_cycle(cycle) + + # Manually extract and apply settings to outgoing keyword arguments + # if native matplotlib function does not include desired properties + cycle_manually = cycle_manually or {} + parser = self._get_lines # the _process_plot_var_args instance + props = {} # which keys to apply from property cycler + for prop, key in cycle_manually.items(): + if kwargs.get(key, None) is None and prop in parser._prop_keys: + props[prop] = key + if props: + dict_ = next(parser.prop_cycler) + for prop, key in props.items(): + value = dict_[prop] + if key == 'c': # special case: scatter() color must be converted to hex + value = pcolors.to_hex(value) + kwargs[key] = value + + if return_cycle: + return cycle, kwargs # needed for stem() to apply in a context() + else: + return kwargs + + def _parse_level_lim( + self, *args, vmin=None, vmax=None, vcenter=None, robust=None, inbounds=None, + negative=None, positive=None, symmetric=None, to_centers=False, **kwargs + ): + """ + Return a suitable vmin and vmax based on the input data. + + Parameters + ---------- + *args + The sample data. + vmin, vmax, vcenter : float, optional + The user input minimum, maximum, and center. + robust : bool, optional + Whether to limit the default range to exclude outliers. + inbounds : bool, optional + Whether to filter to in-bounds data. + negative, positive, symmetric : bool, optional + Whether limits should be negative, positive, or symmetric. + to_centers : bool, optional + Whether to convert coordinates to 'centers'. + + Returns + ------- + vmin, vmax : float + The minimum and maximum. + **kwargs + Unused arguemnts. + """ + # Parse vmin and vmax + automin = vmin is None + automax = vmax is None + vcenter = vcenter or 0.0 + if not automin and not automax: + return vmin, vmax, kwargs + + # Parse input args + inbounds = _not_none(inbounds, rc['cmap.inbounds']) + robust = _not_none(robust, rc['cmap.robust'], False) + robust = 96 if robust is True else 100 if robust is False else robust + robust = np.atleast_1d(robust) + if robust.size == 1: + pmin, pmax = 50 + 0.5 * np.array([-robust.item(), robust.item()]) + elif robust.size == 2: + pmin, pmax = robust.flat # pull out of array + else: + raise ValueError(f'Unexpected robust={robust!r}. Must be bool, float, or 2-tuple.') # noqa: E501 + + # Get sample data + # NOTE: Try to get reasonable *count* levels for hexbin/hist2d, but in general + # have no way to select nice ones a priori (why we disable discretenorm). + # NOTE: Currently we only ever use this function with *single* array input + # but in future could make this public as a way for users (me) to get + # automatic synced contours for a bunch of arrays in a grid. + vmins, vmaxs = [], [] + if len(args) > 2: + x, y, *zs = args + else: + x, y, *zs = None, None, *args + for z in zs: + if z is None: # e.g. empty scatter color + continue + if z.ndim > 2: # e.g. imshow data + continue + z = inputs._to_numpy_array(z) # critical since not always standardized + if inbounds and x is not None and y is not None: # ignore if None coords + z = self._inbounds_vlim(x, y, z, to_centers=to_centers) + imin, imax = inputs._safe_range(z, pmin, pmax) + if automin and imin is not None: + vmins.append(imin) + if automax and imax is not None: + vmaxs.append(imax) + if automin: + vmin = min(vmins, default=0) + if automax: + vmax = max(vmaxs, default=1) + + # Apply modifications + # NOTE: This is also applied to manual input levels lists in _parse_level_vals + if negative: + if automax: + vmax = vcenter + else: + warnings._warn_proplot( + f'Incompatible arguments vmax={vmax!r} and negative=True. ' + 'Ignoring the latter.' + ) + if positive: + if automin: + vmin = vcenter + else: + warnings._warn_proplot( + f'Incompatible arguments vmin={vmin!r} and positive=True. ' + 'Ignoring the latter.' + ) + if symmetric: + vmin, vmax = vmin - vcenter, vmax - vcenter + if automin and not automax: + vmin = -vmax + elif automax and not automin: + vmax = -vmin + elif automin and automax: + vmin, vmax = -np.max(np.abs((vmin, vmax))), np.max(np.abs((vmin, vmax))) + else: + warnings._warn_proplot( + f'Incompatible arguments vmin={vmin!r}, vmax={vmax!r}, and ' + 'symmetric=True. Ignoring the latter.' + ) + vmin, vmax = vmin + vcenter, vmax + vcenter + + return vmin, vmax, kwargs + + def _parse_level_num( + self, *args, levels=None, locator=None, locator_kw=None, vmin=None, vmax=None, + norm=None, norm_kw=None, extend=None, symmetric=None, **kwargs + ): + """ + Return a suitable level list given the input data, normalizer, + locator, and vmin and vmax. + + Parameters + ---------- + *args + The sample data. Passed to `_parse_level_lim`. + levels : int + The approximate number of levels. + locator, locator_kw + The tick locator used to draw levels. + vmin, vmax : float, optional + The minimum and maximum values passed to the tick locator. + norm, norm_kw : optional + The continuous normalizer. Affects the default locator used to draw levels. + extend : str, optional + The extend setting. Affects level trimming settings. + symmetric : bool, optional + Whether the resulting levels should be symmetric about zero. + + Returns + ------- + levels : list of float + The level edges. + **kwargs + Unused arguments. + """ + # Input args + # NOTE: Some of this is adapted from contour.ContourSet._autolev + # NOTE: We use 'symmetric' with MaxNLocator to ensure boundaries + # include a zero level but may trim many of these levels below. + norm_kw = norm_kw or {} + locator_kw = locator_kw or {} + extend = _not_none(extend, 'neither') + levels = _not_none(levels, rc['cmap.levels']) + symmetric = _not_none( + symmetric=symmetric, + locator_kw_symmetric=locator_kw.pop('symmetric', None), + default=False, + ) + + # Get default locator from input norm + # NOTE: This normalizer is only temporary for inferring level locs + norm = norm or 'linear' + norm = constructor.Norm(norm, **norm_kw) + if locator is not None: + locator = constructor.Locator(locator, **locator_kw) + elif isinstance(norm, mcolors.LogNorm): + locator = mticker.LogLocator(**locator_kw) + elif isinstance(norm, mcolors.SymLogNorm): + for key, default in (('base', 10), ('linthresh', 1)): + val = _not_none(getattr(norm, key, None), getattr(norm, '_' + key, None), default) # noqa: E501 + locator_kw.setdefault(key, val) + locator = mticker.SymmetricalLogLocator(**locator_kw) + else: + locator_kw['symmetric'] = symmetric + locator = mticker.MaxNLocator(levels, min_n_ticks=1, **locator_kw) + + # Get default level locations + # NOTE: Critical to adjust ticks with vcenter + nlevs = levels + vcenter = getattr(norm, 'vcenter', None) + automin = vmin is None + automax = vmax is None + vmin, vmax, kwargs = self._parse_level_lim( + *args, vmin=vmin, vmax=vmax, vcenter=vcenter, symmetric=symmetric, **kwargs + ) + if vcenter is not None: + vmin, vmax = vmin - vcenter, vmax - vcenter + try: + levels = locator.tick_values(vmin, vmax) + except TypeError: # e.g. due to datetime arrays + return None, kwargs + except RuntimeError: # too-many-ticks error + levels = np.linspace(vmin, vmax, levels) # TODO: _autolev used N + 1 + if vcenter is not None: + vmin, vmax, levels = vmin + vcenter, vmax + vcenter, levels + vcenter + + # Possibly trim levels far outside of 'vmin' and 'vmax' + # NOTE: This part is mostly copied from matplotlib _autolev + if not symmetric: + i0, i1 = 0, len(levels) # defaults + under, = np.where(levels < vmin) + if len(under): + i0 = under[-1] + if not automin or extend in ('min', 'both'): + i0 += 1 # permit out-of-bounds data + over, = np.where(levels > vmax) + if len(over): + i1 = over[0] + 1 if len(over) else len(levels) + if not automax or extend in ('max', 'both'): + i1 -= 1 # permit out-of-bounds data + if i1 - i0 < 3: + i0, i1 = 0, len(levels) # revert + levels = levels[i0:i1] + + # Compare the no. of levels we got (levels) to what we wanted (nlevs) + # If we wanted more than 2 times the result, then add nn - 1 extra + # levels in-between the returned levels in normalized space (e.g. LogNorm). + nn = nlevs // len(levels) + if nn >= 2: + olevels = norm(levels) + nlevels = [] + for i in range(len(levels) - 1): + l1, l2 = olevels[i], olevels[i + 1] + nlevels.extend(np.linspace(l1, l2, nn + 1)[:-1]) + nlevels.append(olevels[-1]) + levels = norm.inverse(nlevels) + + return levels, kwargs + + def _parse_level_vals( + self, *args, N=None, levels=None, values=None, extend=None, + positive=False, negative=False, nozero=False, norm=None, norm_kw=None, + skip_autolev=False, min_levels=None, **kwargs, + ): + """ + Return levels resulting from a wide variety of keyword options. + + Parameters + ---------- + *args + The sample data. Passed to `_parse_level_num`. + N, levels : int or sequence of float, optional + The levels list or (approximate) number of levels to create. + values : int or sequence of float, optional + The level center list or (approximate) number of level centers to create. + positive, negative, nozero : bool, optional + Whether to remove out non-positive, non-negative, and zero-valued + levels. The latter is useful for single-color contour plots. + norm, norm_kw : optional + Passed to `Norm`. Used to possibly infer levels or to convert values. + skip_autolev : bool, optional + Whether to skip automatic level generation. + min_levels : int, optional + The minimum number of levels allowed. + **kwargs + Passed to `_parse_level_num`. + + Returns + ------- + levels : list of float + The level edges. + vmin, vmax : float + The minimum and maximum. + norm : `matplotlib.colors.Normalize` + The normalizer. + **kwargs + Unused arguments. + """ + # Generate levels so that ticks will be centered between edges + # Solve: (x1 + x2) / 2 = y --> x2 = 2 * y - x1 with arbitrary init x1 + # NOTE: Used for e.g. parametric plots with logarithmic coordinates + def _convert_values(values): + descending = values[1] < values[0] + if descending: # e.g. [100, 50, 20, 10, 5, 2, 1] successful if reversed + values = values[::-1] + levels = [1.5 * values[0] - 0.5 * values[1]] # arbitrary starting point + for value in values: + levels.append(2 * value - levels[-1]) + if np.any(np.diff(levels) < 0): # never happens for evenly spaced levs + levels = utils.edges(values) + if descending: # then revert back below + levels = levels[::-1] + return levels + + # Helper function that restricts levels + # NOTE: This should have no effect if levels were generated automatically. + # However want to apply these to manual-input levels as well. + def _restrict_levels(levels): + kw = {} + levels = np.asarray(levels) + if len(levels) > 2: + kw['atol'] = 1e-5 * np.min(np.diff(levels)) + if nozero: + levels = levels[~np.isclose(levels, 0, **kw)] + if positive: + levels = levels[(levels > 0) | np.isclose(levels, 0, **kw)] + if negative: + levels = levels[(levels < 0) | np.isclose(levels, 0, **kw)] + return levels + + # Helper function to sanitize input levels + # NOTE: Include special case where color levels are referenced by string labels + def _sanitize_levels(key, array, minsize): + if np.iterable(array): + array, _ = pcolors._sanitize_levels(array, minsize) + elif isinstance(array, Integral): + pass + elif array is not None: + raise ValueError(f'Invalid {key}={array}. Must be list or integer.') + if isinstance(norm, (mcolors.BoundaryNorm, pcolors.SegmentedNorm)): + if isinstance(array, Integral): + warnings._warn_proplot( + f'Ignoring {key}={array}. Using norm={norm!r} {key} instead.' + ) + array = norm.boundaries if key == 'levels' else None + return array + + # Parse input arguments and infer edges from centers + # NOTE: The only way for user to manually impose BoundaryNorm is by + # passing one -- users cannot create one using Norm constructor key. + vmin = vmax = None + levels = _not_none(N=N, levels=levels, norm_kw_levs=norm_kw.pop('levels', None)) + if positive and negative: + warnings._warn_proplot( + 'Incompatible args positive=True and negative=True. Using former.' + ) + negative = False + if levels is not None and values is not None: + warnings._warn_proplot( + f'Incompatible args levels={levels!r} and values={values!r}. Using former.' # noqa: E501 + ) + values = None + if isinstance(values, Integral): + levels = values + 1 + values = None + if values is None: + levels = _sanitize_levels('levels', levels, _not_none(min_levels, 2)) + levels = _not_none(levels, rc['cmap.levels']) + else: + values = _sanitize_levels('values', values, 1) + kwargs['discrete_ticks'] = values # passed to _parse_level_norm + if len(values) == 1: # special case (see also DiscreteNorm) + levels = [values[0] - 1, values[0] + 1] + elif norm is not None and norm not in ('segments', 'segmented'): + convert = constructor.Norm(norm, **(norm_kw or {})) + levels = convert.inverse(utils.edges(convert(values))) + else: + levels = _convert_values(values) + + # Process level edges and infer defaults + # NOTE: Matplotlib colorbar algorithm *cannot* handle descending levels so + # this function reverses them and adds special attribute to the normalizer. + # Then colorbar() reads this attr and flips the axis and the colormap direction + if np.iterable(levels): + pop = _pop_params(kwargs, self._parse_level_num, ignore_internal=True) + if pop: + warnings._warn_proplot(f'Ignoring unused keyword arg(s): {pop}') + elif not skip_autolev: + levels, kwargs = self._parse_level_num( + *args, levels=levels, norm=norm, norm_kw=norm_kw, extend=extend, + negative=negative, positive=positive, **kwargs + ) + else: + levels = values = None + + # Determine default colorbar locator and norm and apply filters + # NOTE: DiscreteNorm does not currently support vmin and + # vmax different from level list minimum and maximum. + # NOTE: The level restriction should have no effect if levels were generated + # automatically. However want to apply these to manual-input levels as well. + if levels is not None: + levels = _restrict_levels(levels) + if len(levels) == 0: # skip + pass + elif len(levels) == 1: # use central colormap color + vmin, vmax = levels[0] - 1, levels[0] + 1 + else: # use minimum and maximum + vmin, vmax = np.min(levels), np.max(levels) + if not np.allclose(levels[1] - levels[0], np.diff(levels)): + norm = _not_none(norm, 'segmented') + if norm in ('segments', 'segmented'): + norm_kw['levels'] = levels + + return levels, vmin, vmax, norm, norm_kw, kwargs + + @staticmethod + def _parse_level_norm( + levels, norm, cmap, *, extend=None, min_levels=None, + discrete_ticks=None, discrete_labels=None, **kwargs + ): + """ + Create a `~proplot.colors.DiscreteNorm` or `~proplot.colors.BoundaryNorm` + from the input levels, normalizer, and colormap. + + Parameters + ---------- + levels : sequence of float + The level boundaries. + norm : `~matplotlib.colors.Normalize` + The continuous normalizer. + cmap : `~matplotlib.colors.Colormap` + The colormap. + extend : str, optional + The extend setting. + min_levels : int, optional + The minimum number of levels. + discrete_ticks : array-like, optional + The colorbar locations to tick. + discrete_labels : array-like, optional + The colorbar tick labels. + + Returns + ------- + norm : `~proplot.colors.DiscreteNorm` + The discrete normalizer. + cmap : `~matplotlib.colors.Colormap` + The possibly-modified colormap. + kwargs + Unused arguments. + """ + # Reverse the colormap if input levels or values were descending + # See _parse_level_vals for details + min_levels = _not_none(min_levels, 2) # 1 for contour plots + unique = extend = _not_none(extend, 'neither') + under = cmap._rgba_under + over = cmap._rgba_over + cyclic = getattr(cmap, '_cyclic', None) + qualitative = isinstance(cmap, pcolors.DiscreteColormap) # see _parse_cmap + if len(levels) < min_levels: + raise ValueError( + f'Invalid levels={levels!r}. Must be at least length {min_levels}.' + ) + + # Ensure end colors are unique by scaling colors as if extend='both' + # NOTE: Inside _parse_cmap should have enforced extend='neither' + if cyclic: + step = 0.5 # try to allocate space for unique end colors + unique = 'both' + + # Ensure color list length matches level list length using rotation + # NOTE: No harm if not enough colors, we just end up with the same + # color for out-of-bounds extensions. This is a gentle failure + elif qualitative: + step = 0.5 # try to sample the central index for safety + unique = 'both' + auto_under = under is None and extend in ('min', 'both') + auto_over = over is None and extend in ('max', 'both') + ncolors = len(levels) - min_levels + 1 + auto_under + auto_over + colors = list(itertools.islice(itertools.cycle(cmap.colors), ncolors)) + if auto_under and len(colors) > 1: + under, *colors = colors + if auto_over and len(colors) > 1: + *colors, over = colors + cmap = cmap.copy(colors, N=len(colors)) + if under is not None: + cmap.set_under(under) + if over is not None: + cmap.set_over(over) + + # Ensure middle colors sample full range when extreme colors are present + # by scaling colors as if extend='neither' + else: + step = 1.0 + if over is not None and under is not None: + unique = 'neither' + elif over is not None: # turn off over-bounds unique bin + if extend == 'both': + unique = 'min' + elif extend == 'max': + unique = 'neither' + elif under is not None: # turn off under-bounds unique bin + if extend == 'both': + unique = 'min' + elif extend == 'max': + unique = 'neither' + + # Generate DiscreteNorm and update "child" norm with vmin and vmax from + # levels. This lets the colorbar set tick locations properly! + if len(levels) == 1: + pass # e.g. contours + elif isinstance(norm, mcolors.BoundaryNorm): + pass # override with native matplotlib normalizer + else: + norm = pcolors.DiscreteNorm( + levels, norm=norm, unique=unique, step=step, + ticks=discrete_ticks, labels=discrete_labels, + ) + + return norm, cmap, kwargs + + def _apply_plot(self, *pairs, vert=True, **kwargs): + """ + Plot standard lines. + """ + # Plot the lines + objs, xsides = [], [] + kws = kwargs.copy() + kws.update(_pop_props(kws, 'line')) + kws, extents = self._inbounds_extent(**kws) + for xs, ys, fmt in self._iter_arg_pairs(*pairs): + xs, ys, kw = self._parse_1d_args(xs, ys, vert=vert, **kws) + ys, kw = inputs._dist_reduce(ys, **kw) + guide_kw = _pop_params(kw, self._update_guide) # after standardize + for _, n, x, y, kw in self._iter_arg_cols(xs, ys, **kw): + kw = self._parse_cycle(n, **kw) + *eb, kw = self._add_error_bars(x, y, vert=vert, default_barstds=True, **kw) # noqa: E501 + *es, kw = self._add_error_shading(x, y, vert=vert, **kw) + xsides.append(x) + if not vert: + x, y = y, x + a = [x, y] + if fmt is not None: # x1, y1, fmt1, x2, y2, fm2... style input + a.append(fmt) + obj, = self._call_native('plot', *a, **kw) + self._inbounds_xylim(extents, x, y) + objs.append((*eb, *es, obj) if eb or es else obj) + + # Add sticky edges + self._fix_sticky_edges(objs, 'x' if vert else 'y', *xsides, only=mlines.Line2D) + self._update_guide(objs, **guide_kw) + return cbook.silent_list('Line2D', objs) # always return list + + @docstring._snippet_manager + def line(self, *args, **kwargs): + """ + %(plot.plot)s + """ + return self.plot(*args, **kwargs) + + @docstring._snippet_manager + def linex(self, *args, **kwargs): + """ + %(plot.plotx)s + """ + return self.plotx(*args, **kwargs) + + @inputs._preprocess_or_redirect('x', 'y', allow_extra=True) + @docstring._concatenate_inherited + @docstring._snippet_manager + def plot(self, *args, **kwargs): + """ + %(plot.plot)s + """ + kwargs = _parse_vert(default_vert=True, **kwargs) + return self._apply_plot(*args, **kwargs) + + @inputs._preprocess_or_redirect('y', 'x', allow_extra=True) + @docstring._snippet_manager + def plotx(self, *args, **kwargs): + """ + %(plot.plotx)s + """ + kwargs = _parse_vert(default_vert=False, **kwargs) + return self._apply_plot(*args, **kwargs) + + def _apply_step(self, *pairs, vert=True, **kwargs): + """ + Plot the steps. + """ + # Plot the steps + # NOTE: Internally matplotlib plot() calls step() so we could use that + # approach... but instead repeat _apply_plot internals here so we can + # disable error indications that make no sense for 'step' plots. + kws = kwargs.copy() + kws.update(_pop_props(kws, 'line')) + kws, extents = self._inbounds_extent(**kws) + objs = [] + for xs, ys, fmt in self._iter_arg_pairs(*pairs): + xs, ys, kw = self._parse_1d_args(xs, ys, vert=vert, **kws) + guide_kw = _pop_params(kw, self._update_guide) # after standardize + if fmt is not None: + kw['fmt'] = fmt + for _, n, x, y, *a, kw in self._iter_arg_cols(xs, ys, **kw): + kw = self._parse_cycle(n, **kw) + if not vert: + x, y = y, x + obj, = self._call_native('step', x, y, *a, **kw) + self._inbounds_xylim(extents, x, y) + objs.append(obj) + + self._update_guide(objs, **guide_kw) + return cbook.silent_list('Line2D', objs) # always return list + + @inputs._preprocess_or_redirect('x', 'y', allow_extra=True) + @docstring._concatenate_inherited + @docstring._snippet_manager + def step(self, *args, **kwargs): + """ + %(plot.step)s + """ + kwargs = _parse_vert(default_vert=True, **kwargs) + return self._apply_step(*args, **kwargs) + + @inputs._preprocess_or_redirect('y', 'x', allow_extra=True) + @docstring._snippet_manager + def stepx(self, *args, **kwargs): + """ + %(plot.stepx)s + """ + kwargs = _parse_vert(default_vert=False, **kwargs) + return self._apply_step(*args, **kwargs) + + def _apply_stem( + self, x, y, *, + linefmt=None, markerfmt=None, basefmt=None, orientation=None, **kwargs + ): + """ + Plot stem lines and markers. + """ + # Parse input + kw = kwargs.copy() + kw, extents = self._inbounds_extent(**kw) + x, y, kw = self._parse_1d_args(x, y, **kw) + guide_kw = _pop_params(kw, self._update_guide) + + # Set default colors + # NOTE: 'fmt' strings can only be 2 to 3 characters and include color + # shorthands like 'r' or cycle colors like 'C0'. Cannot use full color names. + # NOTE: Matplotlib defaults try to make a 'reddish' color the base and 'bluish' + # color the stems. To make this more robust we temporarily replace the cycler. + # Bizarrely stem() only reads from the global cycler() so have to update it. + fmts = (linefmt, basefmt, markerfmt) + orientation = _not_none(orientation, 'vertical') + if not any(isinstance(fmt, str) and re.match(r'\AC[0-9]', fmt) for fmt in fmts): + cycle = constructor.Cycle((rc['negcolor'], rc['poscolor']), name='_no_name') + kw.setdefault('cycle', cycle) + kw['basefmt'] = _not_none(basefmt, 'C1-') # red base + kw['linefmt'] = linefmt = _not_none(linefmt, 'C0-') # blue stems + kw['markerfmt'] = _not_none(markerfmt, linefmt[:-1] + 'o') # blue marker + sig = inspect.signature(maxes.Axes.stem) + if 'use_line_collection' in sig.parameters: + kw.setdefault('use_line_collection', True) + + # Call function then restore property cycle + # WARNING: Horizontal stem plots are only supported in recent versions of + # matplotlib. Let matplotlib raise an error if need be. + ctx = {} + cycle, kw = self._parse_cycle(return_cycle=True, **kw) # allow re-application + if cycle is not None: + ctx['axes.prop_cycle'] = cycle + if orientation == 'horizontal': # may raise error + kw['orientation'] = orientation + with rc.context(ctx): + obj = self._call_native('stem', x, y, **kw) + self._inbounds_xylim(extents, x, y, orientation=orientation) + self._update_guide(obj, **guide_kw) + return obj + + @inputs._preprocess_or_redirect('x', 'y') + @docstring._concatenate_inherited + @docstring._snippet_manager + def stem(self, *args, **kwargs): + """ + %(plot.stem)s + """ + kwargs = _parse_vert(default_orientation='vertical', **kwargs) + return self._apply_stem(*args, **kwargs) + + @inputs._preprocess_or_redirect('x', 'y') + @docstring._snippet_manager + def stemx(self, *args, **kwargs): + """ + %(plot.stemx)s + """ + kwargs = _parse_vert(default_orientation='horizontal', **kwargs) + return self._apply_stem(*args, **kwargs) + + @inputs._preprocess_or_redirect('x', 'y', ('c', 'color', 'colors', 'values')) + @docstring._snippet_manager + def parametric(self, x, y, c, *, interp=0, scalex=True, scaley=True, **kwargs): + """ + %(plot.parametric)s + """ + # Standardize arguments + # NOTE: Values are inferred in _auto_format() the same way legend labels are + # inferred. Will not always return an array like inferred coordinates do. + # NOTE: We want to be able to think of 'c' as a scatter color array and + # as a colormap color list. Try to support that here. + kw = kwargs.copy() + kw.update(_pop_props(kw, 'collection')) + kw, extents = self._inbounds_extent(**kw) + label = _not_none(**{key: kw.pop(key, None) for key in ('label', 'value')}) + x, y, kw = self._parse_1d_args( + x, y, values=c, autovalues=True, autoreverse=False, **kw + ) + c = kw.pop('values', None) # permits auto-inferring values + c = np.arange(y.size) if c is None else inputs._to_numpy_array(c) + if ( + c.size in (3, 4) + and y.size not in (3, 4) + and mcolors.is_color_like(tuple(c.flat)) + or all(map(mcolors.is_color_like, c)) + ): + c, kw['colors'] = np.arange(c.shape[0]), c # convert color specs + + # Interpret color values + # NOTE: This permits string label input for 'values' + c, guide_kw = inputs._meta_coords(c, which='') # convert string labels + if c.size == 1 and y.size != 1: + c = np.arange(y.size) # convert dummy label for single color + if guide_kw: + guides._add_guide_kw('colorbar', kw, **guide_kw) + else: + guides._add_guide_kw('colorbar', kw, locator=c) + + # Interpolate values to allow for smooth gradations between values or just + # to color siwtchover halfway between points (interp True, False respectively) + if interp > 0: + x_orig, y_orig, v_orig = x, y, c + x, y, c = [], [], [] + for j in range(x_orig.shape[0] - 1): + idx = slice(None) + if j + 1 < x_orig.shape[0] - 1: + idx = slice(None, -1) + x.extend(np.linspace(x_orig[j], x_orig[j + 1], interp + 2)[idx].flat) + y.extend(np.linspace(y_orig[j], y_orig[j + 1], interp + 2)[idx].flat) + c.extend(np.linspace(v_orig[j], v_orig[j + 1], interp + 2)[idx].flat) + x, y, c = np.array(x), np.array(y), np.array(c) + + # Get coordinates and values for points to the 'left' and 'right' of joints + coords = [] + for i in range(y.shape[0]): + icoords = np.empty((3, 2)) + for j, arr in enumerate((x, y)): + icoords[:, j] = ( + arr[0] if i == 0 else 0.5 * (arr[i - 1] + arr[i]), + arr[i], + arr[-1] if i + 1 == y.shape[0] else 0.5 * (arr[i + 1] + arr[i]), + ) + coords.append(icoords) + coords = np.array(coords) + + # Get the colormap accounting for 'discrete' mode + discrete = kw.get('discrete', None) + if discrete is not None and not discrete: + a = (x, y, c) # pick levels from vmin and vmax, possibly limiting range + else: + a, kw['values'] = (), c + kw = self._parse_cmap(*a, plot_lines=True, **kw) + cmap, norm = kw.pop('cmap'), kw.pop('norm') + + # Add collection with some custom attributes + # NOTE: Modern API uses self._request_autoscale_view but this is + # backwards compatible to earliest matplotlib versions. + guide_kw = _pop_params(kw, self._update_guide) + obj = mcollections.LineCollection( + coords, cmap=cmap, norm=norm, label=label, + linestyles='-', capstyle='butt', joinstyle='miter', + ) + obj.set_array(c) # the ScalarMappable method + obj.update({key: value for key, value in kw.items() if key not in ('color',)}) + self.add_collection(obj) # also adjusts label + self.autoscale_view(scalex=scalex, scaley=scaley) + self._update_guide(obj, **guide_kw) + return obj + + def _apply_lines( + self, xs, ys1, ys2, colors, *, + vert=True, stack=None, stacked=None, negpos=False, **kwargs + ): + """ + Plot vertical or hotizontal lines at each point. + """ + # Parse input arguments + kw = kwargs.copy() + name = 'vlines' if vert else 'hlines' + if colors is not None: + kw['colors'] = colors + kw.update(_pop_props(kw, 'collection')) + kw, extents = self._inbounds_extent(**kw) + stack = _not_none(stack=stack, stacked=stacked) + xs, ys1, ys2, kw = self._parse_1d_args(xs, ys1, ys2, vert=vert, **kw) + guide_kw = _pop_params(kw, self._update_guide) + + # Support "negative" and "positive" lines + # TODO: Ensure 'linewidths' etc. are applied! For some reason + # previously thought they had to be manually applied. + y0 = 0 + objs, sides = [], [] + for _, n, x, y1, y2, kw in self._iter_arg_cols(xs, ys1, ys2, **kw): + kw = self._parse_cycle(n, **kw) + if stack: + y1 = y1 + y0 # avoid in-place modification + y2 = y2 + y0 + y0 = y0 + y2 - y1 # irrelevant that we added y0 to both + if negpos: + obj = self._call_negpos(name, x, y1, y2, colorkey='colors', **kw) + else: + obj = self._call_native(name, x, y1, y2, **kw) + for y in (y1, y2): + self._inbounds_xylim(extents, x, y, vert=vert) + if y.size == 1: # add sticky edges if bounds are scalar + sides.append(y) + objs.append(obj) + + # Draw guide and add sticky edges + self._fix_sticky_edges(objs, 'y' if vert else 'x', *sides) + self._update_guide(objs, **guide_kw) + return ( + objs[0] if len(objs) == 1 + else cbook.silent_list('LineCollection', objs) + ) + + # WARNING: breaking change from native 'ymin' and 'ymax' + @inputs._preprocess_or_redirect('x', 'y1', 'y2', ('c', 'color', 'colors')) + @docstring._snippet_manager + def vlines(self, *args, **kwargs): + """ + %(plot.vlines)s + """ + kwargs = _parse_vert(default_vert=True, **kwargs) + return self._apply_lines(*args, **kwargs) + + # WARNING: breaking change from native 'xmin' and 'xmax' + @inputs._preprocess_or_redirect('y', 'x1', 'x2', ('c', 'color', 'colors')) + @docstring._snippet_manager + def hlines(self, *args, **kwargs): + """ + %(plot.hlines)s + """ + kwargs = _parse_vert(default_vert=False, **kwargs) + return self._apply_lines(*args, **kwargs) + + def _parse_markersize( + self, s, *, smin=None, smax=None, area_size=True, absolute_size=None, **kwargs + ): + """ + Scale the marker sizes with optional keyword args. + """ + if s is not None: + s = inputs._to_numpy_array(s) + if absolute_size is None: + absolute_size = s.size == 1 or _inside_seaborn_call() + if not absolute_size or smin is not None or smax is not None: + smin = _not_none(smin, 1) + smax = _not_none(smax, rc['lines.markersize'] ** (1, 2)[area_size]) + dmin, dmax = inputs._safe_range(s) # data value range + if dmin is not None and dmax is not None and dmin != dmax: + s = smin + (smax - smin) * (s - dmin) / (dmax - dmin) + s = s ** (2, 1)[area_size] + return s, kwargs + + def _apply_scatter(self, xs, ys, ss, cc, *, vert=True, **kwargs): + """ + Apply scatter or scatterx markers. + """ + # Manual property cycling. Converts Line2D keywords used in property + # cycle to PathCollection keywords that can be passed to scatter. + # NOTE: Matplotlib uses the property cycler in _get_patches_for_fill for + # scatter() plots. It only ever inherits color from that. We instead use + # _get_lines to help overarching goal of unifying plot() and scatter(). + cycle_manually = { + 'alpha': 'alpha', 'color': 'c', + 'markerfacecolor': 'c', 'markeredgecolor': 'edgecolors', + 'marker': 'marker', 'markersize': 's', 'markeredgewidth': 'linewidths', + 'linestyle': 'linestyles', 'linewidth': 'linewidths', + } + + # Iterate over the columns + # NOTE: Use 'inbounds' for both cmap and axes 'inbounds' restriction + kw = kwargs.copy() + inbounds = kw.pop('inbounds', None) + kw.update(_pop_props(kw, 'collection')) + kw, extents = self._inbounds_extent(inbounds=inbounds, **kw) + xs, ys, kw = self._parse_1d_args(xs, ys, vert=vert, autoreverse=False, **kw) + ys, kw = inputs._dist_reduce(ys, **kw) + ss, kw = self._parse_markersize(ss, **kw) # parse 's' + infer_rgb = True + if cc is not None and not isinstance(cc, str): + test = np.atleast_1d(cc) # for testing only + if ( + any(_.ndim == 2 and _.shape[1] in (3, 4) for _ in (xs, ys)) + and test.ndim == 2 and test.shape[1] in (3, 4) + ): + infer_rgb = False + cc, kw = self._parse_color( + xs, ys, cc, inbounds=inbounds, apply_cycle=False, infer_rgb=infer_rgb, **kw + ) + guide_kw = _pop_params(kw, self._update_guide) + objs = [] + for _, n, x, y, s, c, kw in self._iter_arg_cols(xs, ys, ss, cc, **kw): + kw['s'], kw['c'] = s, c # make _parse_cycle() detect these + kw = self._parse_cycle(n, cycle_manually=cycle_manually, **kw) + *eb, kw = self._add_error_bars(x, y, vert=vert, default_barstds=True, **kw) + *es, kw = self._add_error_shading(x, y, vert=vert, color_key='c', **kw) + if not vert: + x, y = y, x + obj = self._call_native('scatter', x, y, **kw) + self._inbounds_xylim(extents, x, y) + objs.append((*eb, *es, obj) if eb or es else obj) + + self._update_guide(objs, queue_colorbar=False, **guide_kw) + return ( + objs[0] if len(objs) == 1 + else cbook.silent_list('PathCollection', objs) + ) + + # NOTE: Matplotlib internally applies scatter 'c' arguments as the + # 'facecolors' argument to PathCollection. So perfectly reasonable to + # point both 'color' and 'facecolor' arguments to the 'c' keyword here. + @inputs._preprocess_or_redirect( + 'x', + 'y', + _get_aliases('collection', 'sizes'), + _get_aliases('collection', 'colors', 'facecolors'), + keywords=_get_aliases('collection', 'linewidths', 'edgecolors') + ) + @docstring._concatenate_inherited + @docstring._snippet_manager + def scatter(self, *args, **kwargs): + """ + %(plot.scatter)s + """ + kwargs = _parse_vert(default_vert=True, **kwargs) + return self._apply_scatter(*args, **kwargs) + + @inputs._preprocess_or_redirect( + 'y', + 'x', + _get_aliases('collection', 'sizes'), + _get_aliases('collection', 'colors', 'facecolors'), + keywords=_get_aliases('collection', 'linewidths', 'edgecolors') + ) + @docstring._snippet_manager + def scatterx(self, *args, **kwargs): + """ + %(plot.scatterx)s + """ + kwargs = _parse_vert(default_vert=False, **kwargs) + return self._apply_scatter(*args, **kwargs) + + def _apply_fill( + self, xs, ys1, ys2, where, *, + vert=True, negpos=None, stack=None, stacked=None, **kwargs + ): + """ + Apply area shading. + """ + # Parse input arguments + kw = kwargs.copy() + kw.update(_pop_props(kw, 'patch')) + kw, extents = self._inbounds_extent(**kw) + name = 'fill_between' if vert else 'fill_betweenx' + stack = _not_none(stack=stack, stacked=stacked) + xs, ys1, ys2, kw = self._parse_1d_args(xs, ys1, ys2, vert=vert, **kw) + edgefix_kw = _pop_params(kw, self._fix_patch_edges) + + # Draw patches with default edge width zero + y0 = 0 + objs, xsides, ysides = [], [], [] + guide_kw = _pop_params(kw, self._update_guide) + for _, n, x, y1, y2, w, kw in self._iter_arg_cols(xs, ys1, ys2, where, **kw): + kw = self._parse_cycle(n, **kw) + if stack: + y1 = y1 + y0 # avoid in-place modification + y2 = y2 + y0 + y0 = y0 + y2 - y1 # irrelevant that we added y0 to both + if negpos: # NOTE: if user passes 'where' will issue a warning + obj = self._call_negpos(name, x, y1, y2, where=w, use_where=True, **kw) + else: + obj = self._call_native(name, x, y1, y2, where=w, **kw) + self._fix_patch_edges(obj, **edgefix_kw, **kw) + xsides.append(x) + for y in (y1, y2): + self._inbounds_xylim(extents, x, y, vert=vert) + if y.size == 1: # add sticky edges if bounds are scalar + ysides.append(y) + objs.append(obj) + + # Draw guide and add sticky edges + self._update_guide(objs, **guide_kw) + for axis, sides in zip('xy' if vert else 'yx', (xsides, ysides)): + self._fix_sticky_edges(objs, axis, *sides) + return ( + objs[0] if len(objs) == 1 + else cbook.silent_list('PolyCollection', objs) + ) + + @docstring._snippet_manager + def area(self, *args, **kwargs): + """ + %(plot.fill_between)s + """ + return self.fill_between(*args, **kwargs) + + @docstring._snippet_manager + def areax(self, *args, **kwargs): + """ + %(plot.fill_betweenx)s + """ + return self.fill_betweenx(*args, **kwargs) + + @inputs._preprocess_or_redirect('x', 'y1', 'y2', 'where') + @docstring._concatenate_inherited + @docstring._snippet_manager + def fill_between(self, *args, **kwargs): + """ + %(plot.fill_between)s + """ + kwargs = _parse_vert(default_vert=True, **kwargs) + return self._apply_fill(*args, **kwargs) + + @inputs._preprocess_or_redirect('y', 'x1', 'x2', 'where') + @docstring._concatenate_inherited + @docstring._snippet_manager + def fill_betweenx(self, *args, **kwargs): + """ + %(plot.fill_betweenx)s + """ + # NOTE: The 'horizontal' orientation will be inferred by downstream + # wrappers using the function name. + kwargs = _parse_vert(default_vert=False, **kwargs) + return self._apply_fill(*args, **kwargs) + + @staticmethod + def _convert_bar_width(x, width=1): + """ + Convert bar plot widths from relative to coordinate spacing. Relative + widths are much more convenient for users. + """ + # WARNING: This will fail for non-numeric non-datetime64 singleton + # datatypes but this is good enough for vast majority of cases. + x_test = inputs._to_numpy_array(x) + if len(x_test) >= 2: + x_step = x_test[1:] - x_test[:-1] + x_step = np.concatenate((x_step, x_step[-1:])) + elif x_test.dtype == np.datetime64: + x_step = np.timedelta64(1, 'D') + else: + x_step = np.array(0.5) + if np.issubdtype(x_test.dtype, np.datetime64): + # Avoid integer timedelta truncation + x_step = x_step.astype('timedelta64[ns]') + return width * x_step + + def _apply_bar( + self, xs, hs, ws, bs, *, absolute_width=None, + stack=None, stacked=None, negpos=False, orientation='vertical', **kwargs + ): + """ + Apply bar or barh command. Support default "minima" at zero. + """ + # Parse args + kw = kwargs.copy() + kw, extents = self._inbounds_extent(**kw) + name = 'barh' if orientation == 'horizontal' else 'bar' + stack = _not_none(stack=stack, stacked=stacked) + xs, hs, kw = self._parse_1d_args(xs, hs, orientation=orientation, **kw) + edgefix_kw = _pop_params(kw, self._fix_patch_edges) + if absolute_width is None: + absolute_width = _inside_seaborn_call() + + # Call func after converting bar width + b0 = 0 + objs = [] + kw.update(_pop_props(kw, 'patch')) + hs, kw = inputs._dist_reduce(hs, **kw) + guide_kw = _pop_params(kw, self._update_guide) + for i, n, x, h, w, b, kw in self._iter_arg_cols(xs, hs, ws, bs, **kw): + kw = self._parse_cycle(n, **kw) + # Adjust x or y coordinates for grouped and stacked bars + w = _not_none(w, np.array([0.8])) # same as mpl but in *relative* units + b = _not_none(b, np.array([0.0])) # same as mpl + if not absolute_width: + w = self._convert_bar_width(x, w) + if stack: + b = b + b0 + b0 = b0 + h + else: # instead "group" the bars (this is no-op if we have 1 column) + w = w / n # rescaled + o = 0.5 * (n - 1) # center coordinate + x = x + w * (i - o) # += may cause integer/float casting issue + # Draw simple bars + *eb, kw = self._add_error_bars(x, b + h, default_barstds=True, orientation=orientation, **kw) # noqa: E501 + if negpos: + obj = self._call_negpos(name, x, h, w, b, use_zero=True, **kw) + else: + obj = self._call_native(name, x, h, w, b, **kw) + self._fix_patch_edges(obj, **edgefix_kw, **kw) + for y in (b, b + h): + self._inbounds_xylim(extents, x, y, orientation=orientation) + objs.append((*eb, obj) if eb else obj) + + self._update_guide(objs, **guide_kw) + return ( + objs[0] if len(objs) == 1 + else cbook.silent_list('BarContainer', objs) + ) + + @inputs._preprocess_or_redirect('x', 'height', 'width', 'bottom') + @docstring._concatenate_inherited + @docstring._snippet_manager + def bar(self, *args, **kwargs): + """ + %(plot.bar)s + """ + kwargs = _parse_vert(default_orientation='vertical', **kwargs) + return self._apply_bar(*args, **kwargs) + + # WARNING: Swap 'height' and 'width' here so that they are always relative + # to the 'tall' axis. This lets people always pass 'width' as keyword + @inputs._preprocess_or_redirect('y', 'height', 'width', 'left') + @docstring._concatenate_inherited + @docstring._snippet_manager + def barh(self, *args, **kwargs): + """ + %(plot.barh)s + """ + kwargs = _parse_vert(default_orientation='horizontal', **kwargs) + return self._apply_bar(*args, **kwargs) + + # WARNING: 'labels' and 'colors' no longer passed through `data` (seems like + # extremely niche usage... `data` variables should be data-like) + @inputs._preprocess_or_redirect('x', 'explode') + @docstring._concatenate_inherited + @docstring._snippet_manager + def pie(self, x, explode, *, labelpad=None, labeldistance=None, **kwargs): + """ + %(plot.pie)s + """ + kw = kwargs.copy() + pad = _not_none(labeldistance=labeldistance, labelpad=labelpad, default=1.15) + wedge_kw = kw.pop('wedgeprops', None) or {} + wedge_kw.update(_pop_props(kw, 'patch')) + edgefix_kw = _pop_params(kw, self._fix_patch_edges) + _, x, kw = self._parse_1d_args( + x, autox=False, autoy=False, autoreverse=False, **kw + ) + kw = self._parse_cycle(x.size, **kw) + objs = self._call_native( + 'pie', x, explode, labeldistance=pad, wedgeprops=wedge_kw, **kw + ) + objs = tuple(cbook.silent_list(type(seq[0]).__name__, seq) for seq in objs) + self._fix_patch_edges(objs[0], **edgefix_kw, **wedge_kw) + return objs + + @staticmethod + def _parse_box_violin(fillcolor, fillalpha, edgecolor, **kw): + """ + Parse common boxplot and violinplot arguments. + """ + if isinstance(fillcolor, list): + warnings._warn_proplot( + 'Passing lists to fillcolor was deprecated in v0.9. Please use ' + f'the property cycler with e.g. cycle={fillcolor!r} instead.' + ) + kw['cycle'] = _not_none(cycle=kw.get('cycle', None), fillcolor=fillcolor) + fillcolor = None + if isinstance(fillalpha, list): + warnings._warn_proplot( + 'Passing lists to fillalpha was removed in v0.9. Please specify ' + 'different opacities using the property cycle colors instead.' + ) + fillalpha = fillalpha[0] # too complicated to try to apply this + if isinstance(edgecolor, list): + warnings._warn_proplot( + 'Passing lists of edgecolors was removed in v0.9. Please call the ' + 'plotting command multiple times with different edge colors instead.' + ) + edgecolor = edgecolor[0] + return fillcolor, fillalpha, edgecolor, kw + + def _apply_boxplot( + self, x, y, *, mean=None, means=None, vert=True, + fill=None, filled=None, marker=None, markersize=None, **kwargs + ): + """ + Apply the box plot. + """ + # Global and fill properties + kw = kwargs.copy() + kw.update(_pop_props(kw, 'patch')) + fill = _not_none(fill=fill, filled=filled) + means = _not_none(mean=mean, means=means, showmeans=kw.get('showmeans')) + linewidth = kw.pop('linewidth', rc['patch.linewidth']) + edgecolor = kw.pop('edgecolor', 'black') + fillcolor = kw.pop('facecolor', None) + fillalpha = kw.pop('alpha', None) + fillcolor, fillalpha, edgecolor, kw = self._parse_box_violin( + fillcolor, fillalpha, edgecolor, **kw + ) + if fill is None: + fill = fillcolor is not None or fillalpha is not None + fill = fill or kw.get('cycle') is not None + + # Parse non-color properties + # NOTE: Output dict keys are plural but we use singular for keyword args + props = {} + for key in ('boxes', 'whiskers', 'caps', 'fliers', 'medians', 'means'): + prefix = key.rstrip('es') # singular form + props[key] = iprops = _pop_props(kw, 'line', prefix=prefix) + iprops.setdefault('color', edgecolor) + iprops.setdefault('linewidth', linewidth) + iprops.setdefault('markeredgecolor', edgecolor) + + # Parse color properties + x, y, kw = self._parse_1d_args( + x, y, autoy=False, autoguide=False, vert=vert, **kw + ) + kw = self._parse_cycle(x.size, **kw) # possibly apply cycle + if fill and fillcolor is None: + parser = self._get_patches_for_fill + fillcolor = [parser.get_next_color() for _ in range(x.size)] + else: + fillcolor = [fillcolor] * x.size + + # Plot boxes + kw.setdefault('positions', x) + if means: + kw['showmeans'] = kw['meanline'] = True + y = inputs._dist_clean(y) + artists = self._call_native('boxplot', y, vert=vert, **kw) + artists = artists or {} # necessary? + artists = { + key: cbook.silent_list(type(objs[0]).__name__, objs) if objs else objs + for key, objs in artists.items() + } + + # Modify artist settings + for key, aprops in props.items(): + if key not in artists: # possible if not rendered + continue + objs = artists[key] + for i, obj in enumerate(objs): + # Update lines used for boxplot components + # TODO: Test this thoroughly! + iprops = { + key: ( + value[i // 2 if key in ('caps', 'whiskers') else i] + if isinstance(value, (list, np.ndarray)) + else value + ) + for key, value in aprops.items() + } + obj.update(iprops) + # "Filled" boxplot by adding patch beneath line path + if key == 'boxes' and ( + fillcolor[i] is not None or fillalpha is not None + ): + patch = mpatches.PathPatch( + obj.get_path(), + linewidth=0.0, + facecolor=fillcolor[i], + alpha=fillalpha, + ) + self.add_artist(patch) + # Outlier markers + if key == 'fliers': + if marker is not None: + obj.set_marker(marker) + if markersize is not None: + obj.set_markersize(markersize) + + return artists + + @docstring._snippet_manager + def box(self, *args, **kwargs): + """ + %(plot.boxplot)s + """ + return self.boxplot(*args, **kwargs) + + @docstring._snippet_manager + def boxh(self, *args, **kwargs): + """ + %(plot.boxploth)s + """ + return self.boxploth(*args, **kwargs) + + @inputs._preprocess_or_redirect('positions', 'y') + @docstring._concatenate_inherited + @docstring._snippet_manager + def boxplot(self, *args, **kwargs): + """ + %(plot.boxplot)s + """ + kwargs = _parse_vert(default_vert=True, **kwargs) + return self._apply_boxplot(*args, **kwargs) + + @inputs._preprocess_or_redirect('positions', 'x') + @docstring._snippet_manager + def boxploth(self, *args, **kwargs): + """ + %(plot.boxploth)s + """ + kwargs = _parse_vert(default_vert=False, **kwargs) + return self._apply_boxplot(*args, **kwargs) + + def _apply_violinplot( + self, x, y, vert=True, mean=None, means=None, median=None, medians=None, + showmeans=None, showmedians=None, showextrema=None, **kwargs + ): + """ + Apply the violinplot. + """ + # Parse keyword args + kw = kwargs.copy() + kw.update(_pop_props(kw, 'patch')) + kw.setdefault('capsize', 0) # caps are redundant for violin plots + means = _not_none(mean=mean, means=means, showmeans=showmeans) + medians = _not_none(median=median, medians=medians, showmedians=showmedians) + if showextrema: + kw['default_barpctiles'] = True + if not means and not medians: + medians = _not_none(medians, True) + linewidth = kw.pop('linewidth', None) + edgecolor = kw.pop('edgecolor', 'black') + fillcolor = kw.pop('facecolor', None) + fillalpha = kw.pop('alpha', None) + fillcolor, fillalpha, edgecolor, kw = self._parse_box_violin( + fillcolor, fillalpha, edgecolor, **kw + ) + + # Parse color properties + x, y, kw = self._parse_1d_args( + x, y, autoy=False, autoguide=False, vert=vert, **kw + ) + kw = self._parse_cycle(x.size, **kw) + if fillcolor is None: + parser = self._get_patches_for_fill + fillcolor = [parser.get_next_color() for _ in range(x.size)] + else: + fillcolor = [fillcolor] * x.size + + # Plot violins + y, kw = inputs._dist_reduce(y, means=means, medians=medians, **kw) + *eb, kw = self._add_error_bars(x, y, vert=vert, default_boxstds=True, default_marker=True, **kw) # noqa: E501 + kw.pop('labels', None) # already applied in _parse_1d_args + kw.setdefault('positions', x) # coordinates passed as keyword + y = _not_none(kw.pop('distribution'), y) # i.e. was reduced + y = inputs._dist_clean(y) + artists = self._call_native( + 'violinplot', y, vert=vert, + showmeans=False, showmedians=False, showextrema=False, **kw + ) + + # Modify body settings + artists = artists or {} # necessary? + bodies = artists.pop('bodies', ()) # should be no other entries + if bodies: + bodies = cbook.silent_list(type(bodies[0]).__name__, bodies) + for i, body in enumerate(bodies): + body.set_alpha(1.0) # change default to 1.0 + if fillcolor[i] is not None: + body.set_facecolor(fillcolor[i]) + if fillalpha is not None: + body.set_alpha(fillalpha[i]) + if edgecolor is not None: + body.set_edgecolor(edgecolor) + if linewidth is not None: + body.set_linewidths(linewidth) + return (bodies, *eb) if eb else bodies + + @docstring._snippet_manager + def violin(self, *args, **kwargs): + """ + %(plot.violinplot)s + """ + # WARNING: This disables use of 'violin' by users but + # probably very few people use this anyway. + if getattr(self, '_internal_call', None): + return super().violin(*args, **kwargs) + else: + return self.violinplot(*args, **kwargs) + + @docstring._snippet_manager + def violinh(self, *args, **kwargs): + """ + %(plot.violinploth)s + """ + return self.violinploth(*args, **kwargs) + + @inputs._preprocess_or_redirect('positions', 'y') + @docstring._concatenate_inherited + @docstring._snippet_manager + def violinplot(self, *args, **kwargs): + """ + %(plot.violinplot)s + """ + kwargs = _parse_vert(default_vert=True, **kwargs) + return self._apply_violinplot(*args, **kwargs) + + @inputs._preprocess_or_redirect('positions', 'x') + @docstring._snippet_manager + def violinploth(self, *args, **kwargs): + """ + %(plot.violinploth)s + """ + kwargs = _parse_vert(default_vert=False, **kwargs) + return self._apply_violinplot(*args, **kwargs) + + def _apply_hist( + self, xs, bins, *, + width=None, rwidth=None, stack=None, stacked=None, fill=None, filled=None, + histtype=None, orientation='vertical', **kwargs + ): + """ + Apply the histogram. + """ + # NOTE: While Axes.bar() adds labels to the container Axes.hist() only + # adds them to the first elements in the container for each column + # of the input data. Make sure that legend() will read both containers + # and individual items inside those containers. + _, xs, kw = self._parse_1d_args( + xs, autoreverse=False, orientation=orientation, **kwargs + ) + fill = _not_none(fill=fill, filled=filled) + stack = _not_none(stack=stack, stacked=stacked) + if fill is not None: + histtype = _not_none(histtype, 'stepfilled' if fill else 'step') + if stack is not None: + histtype = _not_none(histtype, 'barstacked' if stack else 'bar') + kw['bins'] = bins + kw['label'] = kw.pop('labels', None) # multiple labels are natively supported + kw['rwidth'] = _not_none(width=width, rwidth=rwidth) # latter is native + kw['histtype'] = histtype = _not_none(histtype, 'bar') + kw.update(_pop_props(kw, 'patch')) + edgefix_kw = _pop_params(kw, self._fix_patch_edges) + guide_kw = _pop_params(kw, self._update_guide) + n = xs.shape[1] if xs.ndim > 1 else 1 + kw = self._parse_cycle(n, **kw) + obj = self._call_native('hist', xs, orientation=orientation, **kw) + if histtype.startswith('bar'): + self._fix_patch_edges(obj[2], **edgefix_kw, **kw) + # Revert to mpl < 3.3 behavior where silent_list was always returned for + # non-bar-type histograms. Because consistency. + res = obj[2] + if type(res) is list: # 'step' histtype plots + res = cbook.silent_list('Polygon', res) + obj = (*obj[:2], res) + else: + for i, sub in enumerate(res): + if type(sub) is list: + res[i] = cbook.silent_list('Polygon', sub) + self._update_guide(res, **guide_kw) + return obj + + @inputs._preprocess_or_redirect('x', 'bins', keywords='weights') + @docstring._concatenate_inherited + @docstring._snippet_manager + def hist(self, *args, **kwargs): + """ + %(plot.hist)s + """ + kwargs = _parse_vert(default_orientation='vertical', **kwargs) + return self._apply_hist(*args, **kwargs) + + @inputs._preprocess_or_redirect('y', 'bins', keywords='weights') + @docstring._snippet_manager + def histh(self, *args, **kwargs): + """ + %(plot.histh)s + """ + kwargs = _parse_vert(default_orientation='horizontal', **kwargs) + return self._apply_hist(*args, **kwargs) + + @inputs._preprocess_or_redirect('x', 'y', 'bins', keywords='weights') + @docstring._concatenate_inherited + @docstring._snippet_manager + def hist2d(self, x, y, bins, **kwargs): + """ + %(plot.hist2d)s + """ + # Rely on the pcolormesh() override for this. + if bins is not None: + kwargs['bins'] = bins + return super().hist2d(x, y, autoreverse=False, default_discrete=False, **kwargs) + + # WARNING: breaking change from native 'C' + @inputs._preprocess_or_redirect('x', 'y', 'weights') + @docstring._concatenate_inherited + @docstring._snippet_manager + def hexbin(self, x, y, weights, **kwargs): + """ + %(plot.hexbin)s + """ + # WARNING: Cannot use automatic level generation here until counts are + # estimated. Inside _parse_level_vals if no manual levels were provided then + # _parse_level_num is skipped and args like levels=10 or locator=5 are ignored + kw = kwargs.copy() + x, y, kw = self._parse_1d_args( + x, y, autoreverse=False, autovalues=True, **kw + ) + kw.update(_pop_props(kw, 'collection')) # takes LineCollection props + kw = self._parse_cmap(x, y, y, skip_autolev=True, default_discrete=False, **kw) + norm = kw.get('norm', None) + if norm is not None and not isinstance(norm, pcolors.DiscreteNorm): + norm.vmin = norm.vmax = None # remove nonsense values + labels_kw = _pop_params(kw, self._add_auto_labels) + guide_kw = _pop_params(kw, self._update_guide) + m = self._call_native('hexbin', x, y, weights, **kw) + self._add_auto_labels(m, **labels_kw) + self._update_guide(m, queue_colorbar=False, **guide_kw) + return m + + @inputs._preprocess_or_redirect('x', 'y', 'z') + @docstring._concatenate_inherited + @docstring._snippet_manager + def contour(self, x, y, z, **kwargs): + """ + %(plot.contour)s + """ + x, y, z, kw = self._parse_2d_args(x, y, z, **kwargs) + kw.update(_pop_props(kw, 'collection')) + kw = self._parse_cmap( + x, y, z, min_levels=1, plot_lines=True, plot_contours=True, **kw + ) + labels_kw = _pop_params(kw, self._add_auto_labels) + guide_kw = _pop_params(kw, self._update_guide) + label = kw.pop('label', None) + m = self._call_native('contour', x, y, z, **kw) + m._legend_label = label + self._add_auto_labels(m, **labels_kw) + self._update_guide(m, queue_colorbar=False, **guide_kw) + return m + + @inputs._preprocess_or_redirect('x', 'y', 'z') + @docstring._concatenate_inherited + @docstring._snippet_manager + def contourf(self, x, y, z, **kwargs): + """ + %(plot.contourf)s + """ + x, y, z, kw = self._parse_2d_args(x, y, z, **kwargs) + kw.update(_pop_props(kw, 'collection')) + kw = self._parse_cmap(x, y, z, plot_contours=True, **kw) + contour_kw = _pop_kwargs(kw, 'edgecolors', 'linewidths', 'linestyles') + edgefix_kw = _pop_params(kw, self._fix_patch_edges) + labels_kw = _pop_params(kw, self._add_auto_labels) + guide_kw = _pop_params(kw, self._update_guide) + label = kw.pop('label', None) + m = cm = self._call_native('contourf', x, y, z, **kw) + m._legend_label = label + self._fix_patch_edges(m, **edgefix_kw, **contour_kw) # no-op if not contour_kw + if contour_kw or labels_kw: + cm = self._fix_contour_edges('contour', x, y, z, **kw, **contour_kw) + self._add_auto_labels(m, cm, **labels_kw) + self._update_guide(m, queue_colorbar=False, **guide_kw) + return m + + @inputs._preprocess_or_redirect('x', 'y', 'z') + @docstring._concatenate_inherited + @docstring._snippet_manager + def pcolor(self, x, y, z, **kwargs): + """ + %(plot.pcolor)s + """ + x, y, z, kw = self._parse_2d_args(x, y, z, edges=True, **kwargs) + kw.update(_pop_props(kw, 'collection')) + kw = self._parse_cmap(x, y, z, to_centers=True, **kw) + edgefix_kw = _pop_params(kw, self._fix_patch_edges) + labels_kw = _pop_params(kw, self._add_auto_labels) + guide_kw = _pop_params(kw, self._update_guide) + with self._keep_grid_bools(): + m = self._call_native('pcolor', x, y, z, **kw) + self._fix_patch_edges(m, **edgefix_kw, **kw) + self._add_auto_labels(m, **labels_kw) + self._update_guide(m, queue_colorbar=False, **guide_kw) + return m + + @inputs._preprocess_or_redirect('x', 'y', 'z') + @docstring._concatenate_inherited + @docstring._snippet_manager + def pcolormesh(self, x, y, z, **kwargs): + """ + %(plot.pcolormesh)s + """ + x, y, z, kw = self._parse_2d_args(x, y, z, edges=True, **kwargs) + kw.update(_pop_props(kw, 'collection')) + kw = self._parse_cmap(x, y, z, to_centers=True, **kw) + edgefix_kw = _pop_params(kw, self._fix_patch_edges) + labels_kw = _pop_params(kw, self._add_auto_labels) + guide_kw = _pop_params(kw, self._update_guide) + with self._keep_grid_bools(): + m = self._call_native('pcolormesh', x, y, z, **kw) + self._fix_patch_edges(m, **edgefix_kw, **kw) + self._add_auto_labels(m, **labels_kw) + self._update_guide(m, queue_colorbar=False, **guide_kw) + return m + + @inputs._preprocess_or_redirect('x', 'y', 'z') + @docstring._concatenate_inherited + @docstring._snippet_manager + def pcolorfast(self, x, y, z, **kwargs): + """ + %(plot.pcolorfast)s + """ + x, y, z, kw = self._parse_2d_args(x, y, z, edges=True, **kwargs) + kw.update(_pop_props(kw, 'collection')) + kw = self._parse_cmap(x, y, z, to_centers=True, **kw) + edgefix_kw = _pop_params(kw, self._fix_patch_edges) + labels_kw = _pop_params(kw, self._add_auto_labels) + guide_kw = _pop_params(kw, self._update_guide) + with self._keep_grid_bools(): + m = self._call_native('pcolorfast', x, y, z, **kw) + if not isinstance(m, mimage.AxesImage): # NOTE: PcolorImage is derivative + self._fix_patch_edges(m, **edgefix_kw, **kw) + self._add_auto_labels(m, **labels_kw) + elif edgefix_kw or labels_kw: + kw = {**edgefix_kw, **labels_kw} + warnings._warn_proplot( + f'Ignoring unused keyword argument(s): {kw}. These only work with ' + 'QuadMesh, not AxesImage. Consider using pcolor() or pcolormesh().' + ) + self._update_guide(m, queue_colorbar=False, **guide_kw) + return m + + @docstring._snippet_manager + def heatmap(self, *args, aspect=None, **kwargs): + """ + %(plot.heatmap)s + """ + obj = self.pcolormesh(*args, default_discrete=False, **kwargs) + aspect = _not_none(aspect, rc['image.aspect']) + if self._name != 'cartesian': + warnings._warn_proplot( + 'The heatmap() command is meant for CartesianAxes ' + 'only. Please use pcolor() or pcolormesh() instead.' + ) + return obj + coords = getattr(obj, '_coordinates', None) + xlocator = ylocator = None + if coords is not None: + coords = 0.5 * (coords[1:, ...] + coords[:-1, ...]) + coords = 0.5 * (coords[:, 1:, :] + coords[:, :-1, :]) + xlocator, ylocator = coords[0, :, 0], coords[:, 0, 1] + kw = {'aspect': aspect, 'xgrid': False, 'ygrid': False} + if xlocator is not None and self.xaxis.isDefault_majloc: + kw['xlocator'] = xlocator + if ylocator is not None and self.yaxis.isDefault_majloc: + kw['ylocator'] = ylocator + if self.xaxis.isDefault_minloc: + kw['xtickminor'] = False + if self.yaxis.isDefault_minloc: + kw['ytickminor'] = False + self.format(**kw) + return obj + + @inputs._preprocess_or_redirect('x', 'y', 'u', 'v', ('c', 'color', 'colors')) + @docstring._concatenate_inherited + @docstring._snippet_manager + def barbs(self, x, y, u, v, c, **kwargs): + """ + %(plot.barbs)s + """ + x, y, u, v, kw = self._parse_2d_args(x, y, u, v, allow1d=True, autoguide=False, **kwargs) # noqa: E501 + kw.update(_pop_props(kw, 'line')) # applied to barbs + c, kw = self._parse_color(x, y, c, **kw) + if mcolors.is_color_like(c): + kw['barbcolor'], c = c, None + a = [x, y, u, v] + if c is not None: + a.append(c) + kw.pop('colorbar_kw', None) # added by _parse_cmap + m = self._call_native('barbs', *a, **kw) + return m + + @inputs._preprocess_or_redirect('x', 'y', 'u', 'v', ('c', 'color', 'colors')) + @docstring._concatenate_inherited + @docstring._snippet_manager + def quiver(self, x, y, u, v, c, **kwargs): + """ + %(plot.quiver)s + """ + x, y, u, v, kw = self._parse_2d_args(x, y, u, v, allow1d=True, autoguide=False, **kwargs) # noqa: E501 + kw.update(_pop_props(kw, 'line')) # applied to arrow outline + c, kw = self._parse_color(x, y, c, **kw) + color = None + if mcolors.is_color_like(c): + color, c = c, None + if color is not None: + kw['color'] = color + a = [x, y, u, v] + if c is not None: + a.append(c) + kw.pop('colorbar_kw', None) # added by _parse_cmap + m = self._call_native('quiver', *a, **kw) + return m + + @docstring._snippet_manager + def stream(self, *args, **kwargs): + """ + %(plot.stream)s + """ + return self.streamplot(*args, **kwargs) + + # WARNING: breaking change from native streamplot() fifth positional arg 'density' + @inputs._preprocess_or_redirect( + 'x', 'y', 'u', 'v', ('c', 'color', 'colors'), keywords='start_points' + ) + @docstring._concatenate_inherited + @docstring._snippet_manager + def streamplot(self, x, y, u, v, c, **kwargs): + """ + %(plot.stream)s + """ + x, y, u, v, kw = self._parse_2d_args(x, y, u, v, **kwargs) + kw.update(_pop_props(kw, 'line')) # applied to lines + c, kw = self._parse_color(x, y, c, **kw) + if c is None: # throws an error if color not provided + c = pcolors.to_hex(self._get_lines.get_next_color()) + kw['color'] = c # always pass this + guide_kw = _pop_params(kw, self._update_guide) + label = kw.pop('label', None) + m = self._call_native('streamplot', x, y, u, v, **kw) + m.lines.set_label(label) # the collection label + self._update_guide(m.lines, queue_colorbar=False, **guide_kw) # use lines + return m + + @inputs._preprocess_or_redirect('x', 'y', 'z') + @docstring._concatenate_inherited + @docstring._snippet_manager + def tricontour(self, x, y, z, **kwargs): + """ + %(plot.tricontour)s + """ + kw = kwargs.copy() + if x is None or y is None or z is None: + raise ValueError('Three input arguments are required.') + kw.update(_pop_props(kw, 'collection')) + kw = self._parse_cmap( + x, y, z, min_levels=1, plot_lines=True, plot_contours=True, **kw + ) + labels_kw = _pop_params(kw, self._add_auto_labels) + guide_kw = _pop_params(kw, self._update_guide) + label = kw.pop('label', None) + m = self._call_native('tricontour', x, y, z, **kw) + m._legend_label = label + self._add_auto_labels(m, **labels_kw) + self._update_guide(m, queue_colorbar=False, **guide_kw) + return m + + @inputs._preprocess_or_redirect('x', 'y', 'z') + @docstring._concatenate_inherited + @docstring._snippet_manager + def tricontourf(self, x, y, z, **kwargs): + """ + %(plot.tricontourf)s + """ + kw = kwargs.copy() + if x is None or y is None or z is None: + raise ValueError('Three input arguments are required.') + kw.update(_pop_props(kw, 'collection')) + contour_kw = _pop_kwargs(kw, 'edgecolors', 'linewidths', 'linestyles') + kw = self._parse_cmap(x, y, z, plot_contours=True, **kw) + edgefix_kw = _pop_params(kw, self._fix_patch_edges) + labels_kw = _pop_params(kw, self._add_auto_labels) + guide_kw = _pop_params(kw, self._update_guide) + label = kw.pop('label', None) + m = cm = self._call_native('tricontourf', x, y, z, **kw) + m._legend_label = label + self._fix_patch_edges(m, **edgefix_kw, **contour_kw) # no-op if not contour_kw + if contour_kw or labels_kw: + cm = self._fix_contour_edges('tricontour', x, y, z, **kw, **contour_kw) + self._add_auto_labels(m, cm, **labels_kw) + self._update_guide(m, queue_colorbar=False, **guide_kw) + return m + + @inputs._preprocess_or_redirect('x', 'y', 'z') + @docstring._concatenate_inherited + @docstring._snippet_manager + def tripcolor(self, x, y, z, **kwargs): + """ + %(plot.tripcolor)s + """ + kw = kwargs.copy() + if x is None or y is None or z is None: + raise ValueError('Three input arguments are required.') + kw.update(_pop_props(kw, 'collection')) + kw = self._parse_cmap(x, y, z, **kw) + edgefix_kw = _pop_params(kw, self._fix_patch_edges) + labels_kw = _pop_params(kw, self._add_auto_labels) + guide_kw = _pop_params(kw, self._update_guide) + with self._keep_grid_bools(): + m = self._call_native('tripcolor', x, y, z, **kw) + self._fix_patch_edges(m, **edgefix_kw, **kw) + self._add_auto_labels(m, **labels_kw) + self._update_guide(m, queue_colorbar=False, **guide_kw) + return m + + # WARNING: breaking change from native 'X' + @inputs._preprocess_or_redirect('z') + @docstring._concatenate_inherited + @docstring._snippet_manager + def imshow(self, z, **kwargs): + """ + %(plot.imshow)s + """ + kw = kwargs.copy() + kw = self._parse_cmap(z, default_discrete=False, **kw) + guide_kw = _pop_params(kw, self._update_guide) + m = self._call_native('imshow', z, **kw) + self._update_guide(m, queue_colorbar=False, **guide_kw) + return m + + # WARNING: breaking change from native 'Z' + @inputs._preprocess_or_redirect('z') + @docstring._concatenate_inherited + @docstring._snippet_manager + def matshow(self, z, **kwargs): + """ + %(plot.matshow)s + """ + # Rely on imshow() override for this. + return super().matshow(z, **kwargs) + + # WARNING: breaking change from native 'Z' + @inputs._preprocess_or_redirect('z') + @docstring._concatenate_inherited + @docstring._snippet_manager + def spy(self, z, **kwargs): + """ + %(plot.spy)s + """ + kw = kwargs.copy() + kw.update(_pop_props(kw, 'line')) # takes valid Line2D properties + default_cmap = pcolors.DiscreteColormap(['w', 'k'], '_no_name') + kw = self._parse_cmap(z, default_cmap=default_cmap, **kw) + guide_kw = _pop_params(kw, self._update_guide) + m = self._call_native('spy', z, **kw) + self._update_guide(m, queue_colorbar=False, **guide_kw) + return m + + def _iter_arg_pairs(self, *args): + """ + Iterate over ``[x1,] y1, [fmt1,] [x2,] y2, [fmt2,] ...`` input. + """ + # NOTE: This is copied from _process_plot_var_args.__call__ to avoid relying + # on private API. We emulate this input style with successive plot() calls. + args = list(args) + while args: # this permits empty input + x, y, *args = args + if args and isinstance(args[0], str): # format string detected! + fmt, *args = args + elif isinstance(y, str): # omits some of matplotlib's rigor but whatevs + x, y, fmt = None, x, y + else: + fmt = None + yield x, y, fmt + + def _iter_arg_cols(self, *args, label=None, labels=None, values=None, **kwargs): + """ + Iterate over columns of positional arguments. + """ + # Handle cycle args and label lists + # NOTE: Arrays here should have had metadata stripped by _parse_1d_args + # but could still be pint quantities that get processed by axis converter. + is_array = lambda data: hasattr(data, 'ndim') and hasattr(data, 'shape') # noqa: E731, E501 + n = max(1 if not is_array(a) or a.ndim < 2 else a.shape[-1] for a in args) + labels = _not_none(label=label, values=values, labels=labels) + if not np.iterable(labels) or isinstance(labels, str): + labels = n * [labels] + if len(labels) != n: + raise ValueError(f'Array has {n} columns but got {len(labels)} labels.') + if labels is not None: + labels = [ + str(_not_none(label, '')) + for label in inputs._to_numpy_array(labels) + ] + else: + labels = n * [None] + + # Yield successive columns + for i in range(n): + kw = kwargs.copy() + kw['label'] = labels[i] or None + a = tuple(a if not is_array(a) or a.ndim < 2 else a[..., i] for a in args) + yield (i, n, *a, kw) + + # Related parsing functions for warnings + _level_parsers = (_parse_level_vals, _parse_level_num, _parse_level_lim) + + # Rename the shorthands + boxes = warnings._rename_objs('0.8.0', boxes=box) + violins = warnings._rename_objs('0.8.0', violins=violin) diff --git a/proplot/axes/polar.py b/proplot/axes/polar.py new file mode 100644 index 000000000..9a6bd2bec --- /dev/null +++ b/proplot/axes/polar.py @@ -0,0 +1,332 @@ +#!/usr/bin/env python3 +""" +Polar axes using azimuth and radius instead of *x* and *y*. +""" +import inspect + +import matplotlib.projections.polar as mpolar +import numpy as np + +from .. import constructor +from .. import ticker as pticker +from ..config import rc +from ..internals import ic # noqa: F401 +from ..internals import _not_none, _pop_rc, docstring +from . import plot, shared + +__all__ = ['PolarAxes'] + + +# Format docstring +_format_docstring = """ +r0 : float, default: 0 + The radial origin. +theta0 : {'N', 'NW', 'W', 'SW', 'S', 'SE', 'E', 'NE'}, optional + The zero azimuth location. +thetadir : {1, -1, 'anticlockwise', 'counterclockwise', 'clockwise'}, optional + The positive azimuth direction. Clockwise corresponds to + ``-1`` and anticlockwise corresponds to ``1``. +thetamin, thetamax : float, optional + The lower and upper azimuthal bounds in degrees. If + ``thetamax != thetamin + 360``, this produces a sector plot. +thetalim : 2-tuple of float or None, optional + Specifies `thetamin` and `thetamax` at once. +rmin, rmax : float, optional + The inner and outer radial limits. If ``r0 != rmin``, this + produces an annular plot. +rlim : 2-tuple of float or None, optional + Specifies `rmin` and `rmax` at once. +rborder : bool, optional + Whether to draw the polar axes border. Visibility of the "inner" + radial spine and "start" and "end" azimuthal spines is controlled + automatically by matplotlib. +thetagrid, rgrid, grid : bool, optional + Whether to draw major gridlines for the azimuthal and radial axis. + Use the keyword `grid` to toggle both. +thetagridminor, rgridminor, gridminor : bool, optional + Whether to draw minor gridlines for the azimuthal and radial axis. + Use the keyword `gridminor` to toggle both. +thetagridcolor, rgridcolor, gridcolor : color-spec, optional + Color for the major and minor azimuthal and radial gridlines. + Use the keyword `gridcolor` to set both at once. +thetalocator, rlocator : locator-spec, optional + Used to determine the azimuthal and radial gridline positions. + Passed to the `~proplot.constructor.Locator` constructor. Can be + float, list of float, string, or `matplotlib.ticker.Locator` instance. +thetalines, rlines + Aliases for `thetalocator`, `rlocator`. +thetalocator_kw, rlocator_kw : dict-like, optional + The azimuthal and radial locator settings. Passed to + `~proplot.constructor.Locator`. +thetaminorlocator, rminorlocator : optional + As for `thetalocator`, `rlocator`, but for the minor gridlines. +thetaminorticks, rminorticks : optional + Aliases for `thetaminorlocator`, `rminorlocator`. +thetaminorlocator_kw, rminorlocator_kw + As for `thetalocator_kw`, `rlocator_kw`, but for the minor locator. +rlabelpos : float, optional + The azimuth at which radial coordinates are labeled. +thetaformatter, rformatter : formatter-spec, optional + Used to determine the azimuthal and radial label format. + Passed to the `~proplot.constructor.Formatter` constructor. + Can be string, list of string, or `matplotlib.ticker.Formatter` + instance. Use ``[]``, ``'null'``, or ``'none'`` for no labels. +thetalabels, rlabels : optional + Aliases for `thetaformatter`, `rformatter`. +thetaformatter_kw, rformatter_kw : dict-like, optional + The azimuthal and radial label formatter settings. Passed to + `~proplot.constructor.Formatter`. +color : color-spec, default: :rc:`meta.color` + Color for the axes edge. Propagates to `labelcolor` unless specified + otherwise (similar to `proplot.axes.CartesianAxes.format`). +labelcolor, gridlabelcolor : color-spec, default: `color` or :rc:`grid.labelcolor` + Color for the gridline labels. +labelpad, gridlabelpad : unit-spec, default: :rc:`grid.labelpad` + The padding between the axes edge and the radial and azimuthal labels. + %(units.pt)s +labelsize, gridlabelsize : unit-spec or str, default: :rc:`grid.labelsize` + Font size for the gridline labels. + %(units.pt)s +labelweight, gridlabelweight : str, default: :rc:`grid.labelweight` + Font weight for the gridline labels. +""" +docstring._snippet_manager['polar.format'] = _format_docstring + + +class PolarAxes(shared._SharedAxes, plot.PlotAxes, mpolar.PolarAxes): + """ + Axes subclass for plotting in polar coordinates. Adds the `~PolarAxes.format` + method and overrides several existing methods. + + Important + --------- + This axes subclass can be used by passing ``proj='polar'`` + to axes-creation commands like `~proplot.figure.Figure.add_axes`, + `~proplot.figure.Figure.add_subplot`, and `~proplot.figure.Figure.subplots`. + """ + _name = 'polar' + + @docstring._snippet_manager + def __init__(self, *args, **kwargs): + """ + Parameters + ---------- + *args + Passed to `matplotlib.axes.Axes`. + %(polar.format)s + + Other parameters + ---------------- + %(axes.format)s + %(rc.init)s + + See also + -------- + PolarAxes.format + proplot.axes.Axes + proplot.axes.PlotAxes + matplotlib.projections.PolarAxes + proplot.figure.Figure.subplot + proplot.figure.Figure.add_subplot + """ + # Set tick length to zero so azimuthal labels are not too offset + # Change default radial axis formatter but keep default theta one + super().__init__(*args, **kwargs) + self.yaxis.set_major_formatter(pticker.AutoFormatter()) + self.yaxis.isDefault_majfmt = True + for axis in (self.xaxis, self.yaxis): + axis.set_tick_params(which='both', size=0) + + def _update_formatter(self, x, *, formatter=None, formatter_kw=None): + """ + Update the gridline label formatter. + """ + # Tick formatter and toggling + axis = getattr(self, x + 'axis') + formatter_kw = formatter_kw or {} + if formatter is not None: + formatter = constructor.Formatter(formatter, **formatter_kw) # noqa: E501 + axis.set_major_formatter(formatter) + + def _update_limits(self, x, *, min_=None, max_=None, lim=None): + """ + Update the limits. + """ + # Try to use public API where possible + r = 'theta' if x == 'x' else 'r' + min_, max_ = self._min_max_lim(r, min_, max_, lim) + if min_ is not None: + getattr(self, f'set_{r}min')(min_) + if max_ is not None: + getattr(self, f'set_{r}max')(max_) + + def _update_locators( + self, x, *, + locator=None, locator_kw=None, minorlocator=None, minorlocator_kw=None, + ): + """ + Update the gridline locator. + """ + # TODO: Add minor tick 'toggling' as with cartesian axes? + # NOTE: Must convert theta locator input to radians, then back to deg. + r = 'theta' if x == 'x' else 'r' + axis = getattr(self, x + 'axis') + min_ = getattr(self, f'get_{r}min')() + max_ = getattr(self, f'get_{r}max')() + for i, (loc, loc_kw) in enumerate( + zip((locator, minorlocator), (locator_kw, minorlocator_kw)) + ): + if loc is None: + continue + # Get locator + loc_kw = loc_kw or {} + loc = constructor.Locator(loc, **loc_kw) + # Sanitize values + array = loc.tick_values(min_, max_) + array = array[(array >= min_) & (array <= max_)] + if x == 'x': + array = np.deg2rad(array) + if np.isclose(array[-1], min_ + 2 * np.pi): # exclusive if 360 deg + array = array[:-1] + # Assign fixed location + loc = constructor.Locator(array) # convert to FixedLocator + if i == 0: + axis.set_major_locator(loc) + else: + axis.set_minor_locator(loc) + + @docstring._snippet_manager + def format( + self, *, r0=None, theta0=None, thetadir=None, + thetamin=None, thetamax=None, thetalim=None, + rmin=None, rmax=None, rlim=None, + thetagrid=None, rgrid=None, + thetagridminor=None, rgridminor=None, + thetagridcolor=None, rgridcolor=None, + rlabelpos=None, rscale=None, rborder=None, + thetalocator=None, rlocator=None, thetalines=None, rlines=None, + thetalocator_kw=None, rlocator_kw=None, + thetaminorlocator=None, rminorlocator=None, thetaminorlines=None, rminorlines=None, # noqa: E501 + thetaminorlocator_kw=None, rminorlocator_kw=None, + thetaformatter=None, rformatter=None, thetalabels=None, rlabels=None, + thetaformatter_kw=None, rformatter_kw=None, + labelpad=None, labelsize=None, labelcolor=None, labelweight=None, + **kwargs + ): + """ + Modify axes limits, radial and azimuthal gridlines, and more. Note that + all of the ``theta`` arguments are specified in degrees, not radians. + + Parameters + ---------- + %(polar.format)s + + Other parameters + ---------------- + %(axes.format)s + %(figure.format)s + %(rc.format)s + + See also + -------- + proplot.axes.Axes.format + proplot.config.Configurator.context + """ + # NOTE: Here we capture 'label.pad' rc argument normally used for + # x and y axis labels as shorthand for 'tick.labelpad'. + rc_kw, rc_mode = _pop_rc(kwargs) + labelcolor = _not_none(labelcolor, kwargs.get('color', None)) + with rc.context(rc_kw, mode=rc_mode): + # Not mutable default args + thetalocator_kw = thetalocator_kw or {} + thetaminorlocator_kw = thetaminorlocator_kw or {} + thetaformatter_kw = thetaformatter_kw or {} + rlocator_kw = rlocator_kw or {} + rminorlocator_kw = rminorlocator_kw or {} + rformatter_kw = rformatter_kw or {} + + # Flexible input + thetalocator = _not_none(thetalines=thetalines, thetalocator=thetalocator) + thetaformatter = _not_none(thetalabels=thetalabels, thetaformatter=thetaformatter) # noqa: E501 + thetaminorlocator = _not_none(thetaminorlines=thetaminorlines, thetaminorlocator=thetaminorlocator) # noqa: E501 + rlocator = _not_none(rlines=rlines, rlocator=rlocator) + rformatter = _not_none(rlabels=rlabels, rformatter=rformatter) + rminorlocator = _not_none(rminorlines=rminorlines, rminorlocator=rminorlocator) # noqa: E501 + + # Special radius settings + if r0 is not None: + self.set_rorigin(r0) + if rlabelpos is not None: + self.set_rlabel_position(rlabelpos) + if rscale is not None: + self.set_rscale(rscale) + if rborder is not None: + self.spines['polar'].set_visible(bool(rborder)) + + # Special azimuth settings + if theta0 is not None: + self.set_theta_zero_location(theta0) + if thetadir is not None: + self.set_theta_direction(thetadir) + + # Loop over axes + for ( + x, + min_, + max_, + lim, + grid, + gridminor, + gridcolor, + locator, + locator_kw, + formatter, + formatter_kw, + minorlocator, + minorlocator_kw, + ) in zip( + ('x', 'y'), + (thetamin, rmin), + (thetamax, rmax), + (thetalim, rlim), + (thetagrid, rgrid), + (thetagridminor, rgridminor), + (thetagridcolor, rgridcolor), + (thetalocator, rlocator), + (thetalocator_kw, rlocator_kw), + (thetaformatter, rformatter), + (thetaformatter_kw, rformatter_kw), + (thetaminorlocator, rminorlocator), + (thetaminorlocator_kw, rminorlocator_kw), + ): + # Axis limits + self._update_limits(x, min_=min_, max_=max_, lim=lim) + + # Axis tick settings + # NOTE: Here use 'grid.labelpad' instead of 'tick.labelpad'. Default + # offset for grid labels is larger than for tick labels. + self._update_ticks( + x, grid=grid, gridminor=gridminor, gridcolor=gridcolor, + gridpad=True, labelpad=labelpad, labelcolor=labelcolor, + labelsize=labelsize, labelweight=labelweight, + ) + + # Axis locator + self._update_locators( + x, locator=locator, locator_kw=locator_kw, + minorlocator=minorlocator, minorlocator_kw=minorlocator_kw + ) + + # Axis formatter + self._update_formatter( + x, formatter=formatter, formatter_kw=formatter_kw + ) + + # Parent format method + super().format(rc_kw=rc_kw, rc_mode=rc_mode, **kwargs) + + +# Apply signature obfuscation after storing previous signature +# NOTE: This is needed for __init__ +PolarAxes._format_signatures[PolarAxes] = inspect.signature(PolarAxes.format) +PolarAxes.format = docstring._obfuscate_kwargs(PolarAxes.format) diff --git a/proplot/axes/shared.py b/proplot/axes/shared.py new file mode 100644 index 000000000..2e15ffc11 --- /dev/null +++ b/proplot/axes/shared.py @@ -0,0 +1,167 @@ +#!/usr/bin/env python +""" +An axes used to jointly format Cartesian and polar axes. +""" +# NOTE: We could define these in base.py but idea is projection-specific formatters +# should never be defined on the base class. Might add to this class later anyway. +import numpy as np + +from ..config import rc +from ..internals import ic # noqa: F401 +from ..internals import _pop_kwargs +from ..utils import _fontsize_to_pt, _not_none, units + + +class _SharedAxes(object): + """ + Mix-in class with methods shared between `~proplot.axes.CartesianAxes` + and `~proplot.axes.PolarAxes`. + """ + @staticmethod + def _min_max_lim(key, min_=None, max_=None, lim=None): + """ + Translate and standardize minimum, maximum, and limit keyword arguments. + """ + if lim is None: + lim = (None, None) + if not np.iterable(lim) or not len(lim) == 2: + raise ValueError(f'Invalid {key}{lim!r}. Must be 2-tuple of values.') + min_ = _not_none(**{f'{key}min': min_, f'{key}lim_0': lim[0]}) + max_ = _not_none(**{f'{key}max': max_, f'{key}lim_1': lim[1]}) + return min_, max_ + + def _update_background( + self, x=None, tickwidth=None, tickwidthratio=None, **kwargs + ): + """ + Update the background patch and spines. + """ + # Update the background patch + kw_face, kw_edge = rc._get_background_props(**kwargs) + self.patch.update(kw_face) + if x is None: + opts = self.spines + elif x == 'x': + opts = ('bottom', 'top', 'inner', 'polar') + else: + opts = ('left', 'right', 'start', 'end') + for opt in opts: + self.spines.get(opt, {}).update(kw_edge) + + # Update the tick colors + axis = 'both' if x is None else x + x = _not_none(x, 'x') + obj = getattr(self, x + 'axis') + edgecolor = kw_edge.get('edgecolor', None) + if edgecolor is not None: + self.tick_params(axis=axis, which='both', color=edgecolor) + + # Update the tick widths + # NOTE: Only use 'linewidth' if it was explicitly passed. Do not + # include 'linewidth' inferred from rc['axes.linewidth'] setting. + kwmajor = getattr(obj, '_major_tick_kw', {}) # graceful fallback if API changes + kwminor = getattr(obj, '_minor_tick_kw', {}) + if 'linewidth' in kwargs: + tickwidth = _not_none(tickwidth, kwargs['linewidth']) + tickwidth = _not_none(tickwidth, rc.find('tick.width', context=True)) + tickwidthratio = _not_none(tickwidthratio, rc.find('tick.widthratio', context=True)) # noqa: E501 + tickwidth_prev = kwmajor.get('width', rc[x + 'tick.major.width']) + if tickwidth_prev == 0: + tickwidthratio_prev = rc['tick.widthratio'] # no other way of knowing + else: + tickwidthratio_prev = kwminor.get('width', rc[x + 'tick.minor.width']) / tickwidth_prev # noqa: E501 + for which in ('major', 'minor'): + kwticks = {} + if tickwidth is not None or tickwidthratio is not None: + tickwidth = _not_none(tickwidth, tickwidth_prev) + kwticks['width'] = tickwidth = units(tickwidth, 'pt') + if tickwidth == 0: # avoid unnecessary padding + kwticks['size'] = 0 + elif which == 'minor': + tickwidthratio = _not_none(tickwidthratio, tickwidthratio_prev) + kwticks['width'] *= tickwidthratio + self.tick_params(axis=axis, which=which, **kwticks) + + def _update_ticks( + self, x, *, grid=None, gridminor=None, gridpad=None, gridcolor=None, + ticklen=None, ticklenratio=None, tickdir=None, tickcolor=None, + labeldir=None, labelpad=None, labelcolor=None, labelsize=None, labelweight=None, + ): + """ + Update the gridlines and labels. Set `gridpad` to ``True`` to use grid padding. + """ + # Filter out text properties + axis = 'both' if x is None else x + kwtext = rc._get_ticklabel_props(axis) + kwtext_extra = _pop_kwargs(kwtext, 'weight', 'family') + kwtext = {'label' + key: value for key, value in kwtext.items()} + if labelcolor is not None: + kwtext['labelcolor'] = labelcolor + if labelsize is not None: + kwtext['labelsize'] = labelsize + if labelweight is not None: + kwtext_extra['weight'] = labelweight + + # Apply tick settings with tick_params when possible + x = _not_none(x, 'x') + obj = getattr(self, x + 'axis') + kwmajor = getattr(obj, '_major_tick_kw', {}) # graceful fallback if API changes + kwminor = getattr(obj, '_minor_tick_kw', {}) + ticklen_prev = kwmajor.get('size', rc[x + 'tick.major.size']) + if ticklen_prev == 0: + ticklenratio_prev = rc['tick.lenratio'] # no other way of knowing + else: + ticklenratio_prev = kwminor.get('size', rc[x + 'tick.minor.size']) / ticklen_prev # noqa: E501 + for b, which in zip((grid, gridminor), ('major', 'minor')): + # Tick properties + # NOTE: Must make 'tickcolor' overwrite 'labelcolor' or else 'color' + # passed to __init__ will not apply correctly. Annoying but unavoidable + kwticks = rc._get_tickline_props(axis, which=which) + if labelpad is not None: + kwticks['pad'] = labelpad + if tickcolor is not None: + kwticks['color'] = tickcolor + if ticklen is not None or ticklenratio is not None: + ticklen = _not_none(ticklen, ticklen_prev) + kwticks['size'] = ticklen = units(ticklen, 'pt') + if ticklen > 0 and which == 'minor': + ticklenratio = _not_none(ticklenratio, ticklenratio_prev) + kwticks['size'] *= ticklenratio + if gridpad: # use grid.labelpad instead of tick.labelpad + kwticks.pop('pad', None) + pad = rc.find('grid.labelpad', context=True) + if pad is not None: + kwticks['pad'] = units(pad, 'pt') + + # Tick direction properties + # NOTE: These have no x and y-specific versions but apply here anyway + if labeldir == 'in': # put tick labels inside the plot + tickdir = 'in' + kwticks.setdefault( + 'pad', + - rc[f'{axis}tick.major.size'] + - _not_none(labelpad, rc[f'{axis}tick.major.pad']) + - _fontsize_to_pt(rc[f'{axis}tick.labelsize']) + ) + if tickdir is not None: + kwticks['direction'] = tickdir + + # Gridline properties + # NOTE: Internally ax.grid() passes gridOn to ax.tick_params() but this + # is undocumented and might have weird side effects. Just use ax.grid() + b = rc._get_gridline_bool(b, axis=axis, which=which) + if b is not None: + self.grid(b, axis=axis, which=which) + kwlines = rc._get_gridline_props(which=which) + if 'axisbelow' in kwlines: + self.set_axisbelow(kwlines.pop('axisbelow')) + if gridcolor is not None: + kwlines['grid_color'] = gridcolor + + # Apply tick and gridline properties + self.tick_params(axis=axis, which=which, **kwticks, **kwlines, **kwtext) + + # Apply settings that can't be controlled with tick_params + if kwtext_extra: + for lab in obj.get_ticklabels(): + lab.update(kwtext_extra) diff --git a/proplot/axes/three.py b/proplot/axes/three.py new file mode 100644 index 000000000..6ab45148a --- /dev/null +++ b/proplot/axes/three.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +""" +The "3D" axes class. +""" +from . import base, shared + +try: + from mpl_toolkits.mplot3d import Axes3D +except ImportError: + Axes3D = object + + +class ThreeAxes(shared._SharedAxes, base.Axes, Axes3D): + """ + Simple mix-in of `proplot.axes.Axes` with `~mpl_toolkits.mplot3d.axes3d.Axes3D`. + + Important + --------- + Note that this subclass does *not* implement the `~proplot.axes.PlotAxes` + plotting overrides. This axes subclass can be used by passing ``proj='3d'`` or + ``proj='three'`` to axes-creation commands like `~proplot.figure.Figure.add_axes`, + `~proplot.figure.Figure.add_subplot`, and `~proplot.figure.Figure.subplots`. + """ + # TODO: Figure out a way to have internal Axes3D calls to plotting commands + # access the overrides rather than the originals? May be impossible. + _name = 'three' + _name_aliases = ('3d',) + + def __init__(self, *args, **kwargs): + import mpl_toolkits.mplot3d # noqa: F401 verify package is available + kwargs.setdefault('alpha', 0.0) + super().__init__(*args, **kwargs) diff --git a/proplot/axistools.py b/proplot/axistools.py deleted file mode 100644 index d2a05582a..000000000 --- a/proplot/axistools.py +++ /dev/null @@ -1,1515 +0,0 @@ -#!/usr/bin/env python3 -""" -Various axis `~matplotlib.ticker.Formatter` and `~matplotlib.scale.ScaleBase` -classes. Includes constructor functions so that these classes can be selected -with a shorthand syntax. -""" -import re -from .utils import _warn_proplot, _notNone -from .rctools import rc -from numbers import Number -from fractions import Fraction -import copy -import numpy as np -import numpy.ma as ma -import matplotlib.dates as mdates -import matplotlib.projections.polar as mpolar -import matplotlib.ticker as mticker -import matplotlib.scale as mscale -import matplotlib.transforms as mtransforms -try: # use this for debugging instead of print()! - from icecream import ic -except ImportError: # graceful fallback if IceCream isn't installed - ic = lambda *a: None if not a else (a[0] if len(a) == 1 else a) # noqa - -__all__ = [ - 'formatters', 'locators', 'scales', - 'Formatter', 'Locator', 'Scale', - 'AutoFormatter', 'CutoffScale', 'ExpScale', - 'FracFormatter', 'FuncScale', - 'InverseScale', - 'LinearScale', - 'LogitScale', - 'LogScale', - 'MercatorLatitudeScale', 'PowerScale', 'SimpleFormatter', - 'SineLatitudeScale', - 'SymmetricalLogScale', -] - -MAX_DIGITS = 32 # do not draw 1000 digits when LogScale limits include zero! -SCALE_PRESETS = { - 'quadratic': ('power', 2,), - 'cubic': ('power', 3,), - 'quartic': ('power', 4,), - 'height': ('exp', np.e, -1 / 7, 1013.25, True), - 'pressure': ('exp', np.e, -1 / 7, 1013.25, False), - 'db': ('exp', 10, 1, 0.1, True), - 'idb': ('exp', 10, 1, 0.1, False), - 'np': ('exp', np.e, 1, 1, True), - 'inp': ('exp', np.e, 1, 1, False), -} - - -def Locator(locator, *args, **kwargs): - """ - Return a `~matplotlib.ticker.Locator` instance. This function is used to - interpret the `xlocator`, `xlocator_kw`, `ylocator`, `ylocator_kw`, `xminorlocator`, - `xminorlocator_kw`, `yminorlocator`, and `yminorlocator_kw` arguments when - passed to `~proplot.axes.XYAxes.format`, and the `locator`, `locator_kw` - `minorlocator`, and `minorlocator_kw` arguments when passed to colorbar - methods wrapped by `~proplot.wrappers.colorbar_wrapper`. - - Parameters - ---------- - locator : `~matplotlib.ticker.Locator`, str, float, or list of float - If `~matplotlib.ticker.Locator`, the object is returned. - - If number, specifies the *multiple* used to define tick separation. - Returns a `~matplotlib.ticker.MultipleLocator` instance. - - If list of numbers, these points are ticked. Returns a - `~matplotlib.ticker.FixedLocator` instance. - - If string, a dictionary lookup is performed (see below table). - - ====================== ================================================= ================================================================================================ - Key Class Description - ====================== ================================================= ================================================================================================ - ``'null'``, ``'none'`` `~matplotlib.ticker.NullLocator` No ticks - ``'auto'`` `~matplotlib.ticker.AutoLocator` Major ticks at sensible locations - ``'minor'`` `~matplotlib.ticker.AutoMinorLocator` Minor ticks at sensible locations - ``'date'`` `~matplotlib.dates.AutoDateLocator` Default tick locations for datetime axes - ``'fixed'`` `~matplotlib.ticker.FixedLocator` Ticks at these exact locations - ``'index'`` `~matplotlib.ticker.IndexLocator` Ticks on the non-negative integers - ``'linear'`` `~matplotlib.ticker.LinearLocator` Exactly ``N`` ticks encompassing the axis limits, spaced as ``numpy.linspace(lo, hi, N)`` - ``'log'`` `~matplotlib.ticker.LogLocator` Ticks for log-scale axes - ``'logminor'`` `~matplotlib.ticker.LogLocator` preset Ticks for log-scale axes on the 1st through 9th multiples of each power of the base - ``'logit'`` `~matplotlib.ticker.LogitLocator` Ticks for logit-scale axes - ``'logitminor'`` `~matplotlib.ticker.LogitLocator` preset Ticks for logit-scale axes with ``minor=True`` passed to `~matplotlib.ticker.LogitLocator` - ``'maxn'`` `~matplotlib.ticker.MaxNLocator` No more than ``N`` ticks at sensible locations - ``'multiple'`` `~matplotlib.ticker.MultipleLocator` Ticks every ``N`` step away from zero - ``'symlog'`` `~matplotlib.ticker.SymmetricalLogLocator` Ticks for symmetrical log-scale axes - ``'symlogminor'`` `~matplotlib.ticker.SymmetricalLogLocator` preset Ticks for symmetrical log-scale axes on the 1st through 9th multiples of each power of the base - ``'theta'`` `~matplotlib.projections.polar.ThetaLocator` Like the base locator but default locations are every `numpy.pi`/8 radians - ``'year'`` `~matplotlib.dates.YearLocator` Ticks every ``N`` years - ``'month'`` `~matplotlib.dates.MonthLocator` Ticks every ``N`` months - ``'weekday'`` `~matplotlib.dates.WeekdayLocator` Ticks every ``N`` weekdays - ``'day'`` `~matplotlib.dates.DayLocator` Ticks every ``N`` days - ``'hour'`` `~matplotlib.dates.HourLocator` Ticks every ``N`` hours - ``'minute'`` `~matplotlib.dates.MinuteLocator` Ticks every ``N`` minutes - ``'second'`` `~matplotlib.dates.SecondLocator` Ticks every ``N`` seconds - ``'microsecond'`` `~matplotlib.dates.MicrosecondLocator` Ticks every ``N`` microseconds - ====================== ================================================= ================================================================================================ - - *args, **kwargs - Passed to the `~matplotlib.ticker.Locator` class. - - Returns - ------- - `~matplotlib.ticker.Locator` - A `~matplotlib.ticker.Locator` instance. - """ # noqa - if isinstance(locator, mticker.Locator): - return locator - # Pull out extra args - if np.iterable(locator) and not isinstance(locator, str) and not all( - isinstance(num, Number) for num in locator): - locator, args = locator[0], (*locator[1:], *args) - # Get the locator - if isinstance(locator, str): # dictionary lookup - # Shorthands and defaults - if locator in ('logminor', 'logitminor', 'symlogminor'): - locator, _ = locator.split('minor') - if locator == 'logit': - kwargs.setdefault('minor', True) - else: - kwargs.setdefault('subs', np.arange(1, 10)) - elif locator == 'index': - args = args or (1,) - if len(args) == 1: - args = (*args, 0) - # Lookup - if locator not in locators: - raise ValueError( - f'Unknown locator {locator!r}. Options are ' - + ', '.join(map(repr, locators.keys())) + '.' - ) - locator = locators[locator](*args, **kwargs) - elif isinstance(locator, Number): # scalar variable - locator = mticker.MultipleLocator(locator, *args, **kwargs) - elif np.iterable(locator): - locator = mticker.FixedLocator(np.sort(locator), *args, **kwargs) - else: - raise ValueError(f'Invalid locator {locator!r}.') - return locator - - -def Formatter(formatter, *args, date=False, index=False, **kwargs): - """ - Return a `~matplotlib.ticker.Formatter` instance. This function is used to - interpret the `xformatter`, `xformatter_kw`, `yformatter`, and - `yformatter_kw` arguments when passed to - `~proplot.axes.XYAxes.format`, and the `formatter` - and `formatter_kw` arguments when passed to colorbar methods wrapped by - `~proplot.wrappers.colorbar_wrapper`. - - Parameters - ---------- - formatter : `~matplotlib.ticker.Formatter`, str, list of str, or function - If `~matplotlib.ticker.Formatter`, the object is returned. - - If list of strings, ticks are labeled with these strings. Returns a - `~matplotlib.ticker.FixedFormatter` instance when `index` is ``False`` - and an `~matplotlib.ticker.IndexFormatter` instance when `index` is - ``True``. - - If function, labels will be generated using this function. Returns a - `~matplotlib.ticker.FuncFormatter` instance. - - If string, there are 4 possibilities: - - 1. If string contains ``'%'`` and `date` is ``False``, ticks will be - formatted using the C-notation ``string % number`` method. See - `this page \ -`__ - for a review. - 2. If string contains ``'%'`` and `date` is ``True``, datetime - ``string % number`` formatting is used. See - `this page \ -`__ - for a review. - 3. If string contains ``{x}`` or ``{x:...}``, ticks will be - formatted by calling ``string.format(x=number)``. - 4. In all other cases, a dictionary lookup is performed - (see below table). - - ====================== ============================================== =================================================================================================================================== - Key Class Description - ====================== ============================================== =================================================================================================================================== - ``'null'``, ``'none'`` `~matplotlib.ticker.NullFormatter` No tick labels - ``'auto'`` `AutoFormatter` New default tick labels for axes - ``'simple'`` `SimpleFormatter` New default tick labels for e.g. contour labels - ``'frac'`` `FracFormatter` Rational fractions - ``'date'`` `~matplotlib.dates.AutoDateFormatter` Default tick labels for datetime axes - ``'concise'`` `~matplotlib.dates.ConciseDateFormatter` More concise date labels introduced in `matplotlib 3.1 `__ - ``'datestr'`` `~matplotlib.dates.DateFormatter` Date formatting with C-style ``string % format`` notation - ``'eng'`` `~matplotlib.ticker.EngFormatter` Engineering notation - ``'fixed'`` `~matplotlib.ticker.FixedFormatter` List of strings - ``'formatstr'`` `~matplotlib.ticker.FormatStrFormatter` From C-style ``string % format`` notation - ``'func'`` `~matplotlib.ticker.FuncFormatter` Use an arbitrary function - ``'index'`` `~matplotlib.ticker.IndexFormatter` List of strings corresponding to non-negative integer positions along the axis - ``'log'``, ``'sci'`` `~matplotlib.ticker.LogFormatterSciNotation` For log-scale axes with scientific notation - ``'logit'`` `~matplotlib.ticker.LogitFormatter` For logistic-scale axes - ``'math'`` `~matplotlib.ticker.LogFormatterMathtext` For log-scale axes with math text - ``'percent'`` `~matplotlib.ticker.PercentFormatter` Trailing percent sign - ``'scalar'`` `~matplotlib.ticker.ScalarFormatter` Old default tick labels for axes - ``'strmethod'`` `~matplotlib.ticker.StrMethodFormatter` From the ``string.format`` method - ``'theta'`` `~matplotlib.projections.polar.ThetaFormatter` Formats radians as degrees, with a degree symbol - ``'e'`` `FracFormatter` preset Fractions of *e* - ``'pi'`` `FracFormatter` preset Fractions of :math:`\\pi` - ``'deg'`` `AutoFormatter` preset Trailing degree symbol - ``'deglat'`` `AutoFormatter` preset Trailing degree symbol and cardinal "SN" indicator - ``'deglon'`` `AutoFormatter` preset Trailing degree symbol and cardinal "WE" indicator - ``'lat'`` `AutoFormatter` preset Cardinal "SN" indicator - ``'lon'`` `AutoFormatter` preset Cardinal "WE" indicator - ====================== ============================================== =================================================================================================================================== - - date : bool, optional - Toggles the behavior when `formatter` contains a ``'%'`` sign (see - above). - index : bool, optional - Controls the behavior when `formatter` is a list of strings (see - above). - *args, **kwargs - Passed to the `~matplotlib.ticker.Formatter` class. - - Returns - ------- - `~matplotlib.ticker.Formatter` - A `~matplotlib.ticker.Formatter` instance. - """ # noqa - if isinstance(formatter, mticker.Formatter): # formatter object - return formatter - # Pull out extra args - if np.iterable(formatter) and not isinstance(formatter, str) and not all( - isinstance(item, str) for item in formatter - ): - formatter, args = formatter[0], (*formatter[1:], *args) - # Get the formatter - if isinstance(formatter, str): # assumption is list of strings - # Format strings - if re.search(r'{x?(:.+)?}', formatter): - formatter = mticker.StrMethodFormatter( - formatter, *args, **kwargs) # new-style .format() form - elif '%' in formatter: - if date: - formatter = mdates.DateFormatter( - formatter, *args, **kwargs) # %-style, dates - else: - formatter = mticker.FormatStrFormatter( - formatter, *args, **kwargs) # %-style, numbers - else: - # Fraction shorthands - if formatter in ('pi', 'e'): - if formatter == 'pi': - symbol, number = r'$\pi$', np.pi - else: - symbol, number = '$e$', np.e - kwargs.setdefault('symbol', symbol) - kwargs.setdefault('number', number) - formatter = 'frac' - # Cartographic shorthands - if formatter in ('deg', 'deglon', 'deglat', 'lon', 'lat'): - negpos, suffix = None, None - if 'deg' in formatter: - suffix = '\N{DEGREE SIGN}' - if 'lat' in formatter: - negpos = 'SN' - if 'lon' in formatter: - negpos = 'WE' - kwargs.setdefault('suffix', suffix) - kwargs.setdefault('negpos', negpos) - formatter = 'auto' - # Lookup - if formatter not in formatters: - raise ValueError( - f'Unknown formatter {formatter!r}. Options are ' - + ', '.join(map(repr, formatters.keys())) + '.' - ) - formatter = formatters[formatter](*args, **kwargs) - elif callable(formatter): - formatter = mticker.FuncFormatter(formatter, *args, **kwargs) - elif np.iterable(formatter): # list of strings on the major ticks - if index: - formatter = mticker.IndexFormatter(formatter) - else: - formatter = mticker.FixedFormatter(formatter) - else: - raise ValueError(f'Invalid formatter {formatter!r}.') - return formatter - - -def Scale(scale, *args, **kwargs): - """ - Return a `~matplotlib.scale.ScaleBase` instance. This function is used to - interpret the `xscale`, `xscale_kw`, `yscale`, and `yscale_kw` arguments - when passed to `~proplot.axes.CartesianAxes.format`. - - Parameters - ---------- - scale : `~matplotlib.scale.ScaleBase`, str, (str, ...), or class - If `~matplotlib.scale.ScaleBase`, the object is returned. - - If string, this is the registered scale name or scale "preset" (see - below table). - - If list or tuple and the first element is a string, the subsequent - items are passed to the scale class as positional arguments. For - example, ``ax.format(xscale=('power', 2))`` applies the ``'quadratic'`` - scale to the *x* axis. - - ================= ======================= ======================================================================= - Key Class Description - ================= ======================= ======================================================================= - ``'linear'`` `LinearScale` Linear - ``'log'`` `LogScale` Logarithmic - ``'symlog'`` `SymmetricalLogScale` Logarithmic beyond finite space around zero - ``'logit'`` `LogitScale` Logistic - ``'inverse'`` `InverseScale` Inverse - ``'function'`` `FuncScale` Scale from arbitrary forward and backwards functions - ``'sine'`` `SineLatitudeScale` Sine function (in degrees) - ``'mercator'`` `MercatorLatitudeScale` Mercator latitude function (in degrees) - ``'exp'`` `ExpScale` Arbitrary exponential function - ``'power'`` `PowerScale` Arbitrary power function - ``'cutoff'`` `CutoffScale` Arbitrary piecewise linear transformations - ``'quadratic'`` `PowerScale` (preset) Quadratic function - ``'cubic'`` `PowerScale` (preset) Cubic function - ``'quartic'`` `PowerScale` (preset) Cubic function - ``'db'`` `ExpScale` (preset) Ratio expressed as `decibels `__ - ``'np'`` `ExpScale` (preset) Ratio expressed as `nepers `__ - ``'idb'`` `ExpScale` (preset) `Decibels `__ expressed as ratio - ``'inp'`` `ExpScale` (preset) `Nepers `__ expressed as ratio - ``'pressure'`` `ExpScale` (preset) Height (in km) expressed linear in pressure - ``'height'`` `ExpScale` (preset) Pressure (in hPa) expressed linear in height - ================= ======================= ======================================================================= - - *args, **kwargs - Passed to the `~matplotlib.scale.ScaleBase` class. - - Returns - ------- - `~matplotlib.scale.ScaleBase` - The scale instance. - """ # noqa - # NOTE: Why not try to interpret FuncScale arguments, like when lists - # of numbers are passed to Locator? Because FuncScale *itself* accepts - # ScaleBase classes as arguments... but constructor functions cannot - # do anything but return the class instance upon receiving one. - if isinstance(scale, mscale.ScaleBase): - return scale - # Pull out extra args - if np.iterable(scale) and not isinstance(scale, str): - scale, args = scale[0], (*scale[1:], *args) - if not isinstance(scale, str): - raise ValueError(f'Invalid scale name {scale!r}. Must be string.') - # Get scale preset - if scale in SCALE_PRESETS: - if args or kwargs: - _warn_proplot( - f'Scale {scale!r} is a scale *preset*. Ignoring positional ' - 'argument(s): {args} and keyword argument(s): {kwargs}. ' - ) - scale, *args = SCALE_PRESETS[scale] - # Get scale - scale = scale.lower() - if scale in scales: - scale = scales[scale] - else: - raise ValueError( - f'Unknown scale or preset {scale!r}. Options are ' - + ', '.join(map(repr, list(scales) + list(SCALE_PRESETS))) + '.' - ) - return scale(*args, **kwargs) - - -def _sanitize(string, zerotrim=False): - """ - Sanitize tick label strings. - """ - if zerotrim and '.' in string: - string = string.rstrip('0').rstrip('.') - string = string.replace('-', '\N{MINUS SIGN}') - if string == '\N{MINUS SIGN}0': - string = '0' - return string - - -class AutoFormatter(mticker.ScalarFormatter): - """ - The new default formatter, a simple wrapper around - `~matplotlib.ticker.ScalarFormatter`. Differs from - `~matplotlib.ticker.ScalarFormatter` in the following ways: - - 1. Trims trailing zeros if any exist. - 2. Allows user to specify *range* within which major tick marks - are labelled. - 3. Allows user to add arbitrary prefix or suffix to every - tick label string. - """ - def __init__( - self, *args, - zerotrim=None, tickrange=None, - prefix=None, suffix=None, negpos=None, **kwargs - ): - """ - Parameters - ---------- - zerotrim : bool, optional - Whether to trim trailing zeros. - Default is :rc:`axes.formatter.zerotrim`. - tickrange : (float, float), optional - Range within which major tick marks are labelled. - prefix, suffix : str, optional - Prefix and suffix for all strings. - negpos : str, optional - Length-2 string indicating the suffix for "negative" and "positive" - numbers, meant to replace the minus sign. This is useful for - indicating cardinal geographic coordinates. - *args, **kwargs - Passed to `~matplotlib.ticker.ScalarFormatter`. - - Warning - ------- - The matplotlib `~matplotlib.ticker.ScalarFormatter` determines the - number of significant digits based on the axis limits, and therefore - may *truncate* digits while formatting ticks on highly non-linear - axis scales like `~proplot.axistools.LogScale`. We try to correct - this behavior with a patch. - """ - tickrange = tickrange or (-np.inf, np.inf) - super().__init__(*args, **kwargs) - zerotrim = _notNone(zerotrim, rc['axes.formatter.zerotrim']) - self._zerotrim = zerotrim - self._tickrange = tickrange - self._prefix = prefix or '' - self._suffix = suffix or '' - self._negpos = negpos or '' - - def __call__(self, x, pos=None): - """ - Convert number to a string. - - Parameters - ---------- - x : float - The value. - pos : float, optional - The position. - """ - # Tick range limitation - eps = abs(x) / 1000 - tickrange = self._tickrange - if (x + eps) < tickrange[0] or (x - eps) > tickrange[1]: - return '' # avoid some ticks - # Negative positive handling - if not self._negpos or x == 0: - tail = '' - elif x > 0: - tail = self._negpos[1] - else: - x *= -1 - tail = self._negpos[0] - # Format the string - string = super().__call__(x, pos) - string = _sanitize(string, zerotrim=self._zerotrim) - if string == '0' and x != 0: - string = ( - '{:.%df}' % min(abs(np.log10(abs(x))) // 1, MAX_DIGITS) - ).format(x) - string = _sanitize(string, zerotrim=self._zerotrim) - # Prefix and suffix - sign = '' - if string and string[0] == '\N{MINUS SIGN}': - sign, string = string[0], string[1:] - return sign + self._prefix + string + self._suffix + tail - - -def SimpleFormatter(precision=6, zerotrim=True): - """ - Return a `~matplotlib.ticker.FuncFormatter` instance that replicates the - `zerotrim` feature from `AutoFormatter`. This is more suitable for - arbitrary number formatting not necessarily associated with any - `~matplotlib.axis.Axis` instance, e.g. labeling contours. - - Parameters - ---------- - precision : int, optional - The maximum number of digits after the decimal point. - zerotrim : bool, optional - Whether to trim trailing zeros. - Default is :rc:`axes.formatter.zerotrim`. - """ - zerotrim = _notNone(zerotrim, rc['axes.formatter.zerotrim']) - - def f(x, pos): - string = ('{:.%df}' % precision).format(x) - if zerotrim and '.' in string: - string = string.rstrip('0').rstrip('.') - if string == '-0' or string == '\N{MINUS SIGN}0': - string = '0' - return string.replace('-', '\N{MINUS SIGN}') - return mticker.FuncFormatter(f) - - -def FracFormatter(symbol='', number=1): - r""" - Return a `~matplotlib.ticker.FuncFormatter` that formats numbers as - fractions or multiples of some arbitrary value. - This is powered by the builtin `~fractions.Fraction` class - and the `~fractions.Fraction.limit_denominator` method. - - Parameters - ---------- - symbol : str - The symbol, e.g. ``r'$\pi$'``. Default is ``''``. - number : float - The value, e.g. `numpy.pi`. Default is ``1``. - """ - def f(x, pos): # must accept location argument - frac = Fraction(x / number).limit_denominator() - if x == 0: - string = '0' - elif frac.denominator == 1: # denominator is one - if frac.numerator == 1 and symbol: - string = f'{symbol:s}' - elif frac.numerator == -1 and symbol: - string = f'-{symbol:s}' - else: - string = f'{frac.numerator:d}{symbol:s}' - else: - if frac.numerator == 1 and symbol: # numerator is +/-1 - string = f'{symbol:s}/{frac.denominator:d}' - elif frac.numerator == -1 and symbol: - string = f'-{symbol:s}/{frac.denominator:d}' - else: # and again make sure we use unicode minus! - string = f'{frac.numerator:d}{symbol:s}/{frac.denominator:d}' - return string.replace('-', '\N{MINUS SIGN}') - return mticker.FuncFormatter(f) - - -def _scale_factory(scale, axis, *args, **kwargs): # noqa: U100 - """ - If `scale` is a `~matplotlib.scale.ScaleBase` instance, do nothing. If - it is a registered scale name, look up and instantiate that scale. - """ - if isinstance(scale, mscale.ScaleBase): - if args or kwargs: - _warn_proplot(f'Ignoring args {args} and keyword args {kwargs}.') - return scale # do nothing - else: - scale = scale.lower() - if scale not in scales: - raise ValueError( - f'Unknown scale {scale!r}. Options are ' - + ', '.join(map(repr, scales.keys())) + '.' - ) - return scales[scale](*args, **kwargs) - - -def _parse_logscale_args(kwargs, *keys): - """ - Parse arguments for `LogScale` and `SymmetricalLogScale` that - inexplicably require ``x`` and ``y`` suffixes by default. - """ - for key in keys: - value = _notNone( # issues warning when multiple args passed! - kwargs.pop(key, None), - kwargs.pop(key + 'x', None), - kwargs.pop(key + 'y', None), - None, names=(key, key + 'x', key + 'y'), - ) - if key == 'linthresh' and value is None: - # NOTE: If linthresh is *exactly* on a power of the base, can - # end up with additional log-locator step inside the threshold, - # e.g. major ticks on -10, -1, -0.1, 0.1, 1, 10 for linthresh of - # 1. Adding slight offset to *desired* linthresh prevents this. - value = 1 + 1e-10 - if key == 'subs' and value is None: - value = np.arange(1, 10) - if value is not None: # dummy axis_name is 'x' - kwargs[key + 'x'] = value - return kwargs - - -class _ScaleBase(object): - """ - Mixin scale class that standardizes the - `~matplotlib.scale.ScaleBase.set_default_locators_and_formatters` - and `~matplotlib.scale.ScaleBase.get_transform` methods. - Also overrides `__init__` so you no longer have to instantiate scales - with an `~matplotlib.axis.Axis` instance. - """ - def __init__(self, *args, **kwargs): - # Pass a dummy axis to the superclass - axis = type('Axis', (object,), {'axis_name': 'x'})() - super().__init__(axis, *args, **kwargs) - self._default_smart_bounds = None - self._default_major_locator = None - self._default_minor_locator = None - self._default_major_formatter = None - self._default_minor_formatter = None - - def set_default_locators_and_formatters(self, axis, only_if_default=False): - """ - Apply all locators and formatters defined as attributes on - initialization and define defaults for all scales. - - Parameters - ---------- - axis : `~matplotlib.axis.Axis` - The axis. - only_if_default : bool, optional - Whether to refrain from updating the locators and formatters if - the axis is currently using non-default versions. Useful if we - want to avoid overwriting user customization when the scale - is changed. - """ - # Apply isDefault because matplotlib does this in axis._set_scale - # but sometimes we need to bypass this method! Minor locator can be - # "non default" even when user has not changed it, due to "turning - # minor ticks" on and off, so set as 'default' if AutoMinorLocator. - if self._default_smart_bounds is not None: - axis.set_smart_bounds(self._default_smart_bounds) - if not only_if_default or axis.isDefault_majloc: - axis.set_major_locator( - self._default_major_locator or Locator('auto') - ) - axis.isDefault_majloc = True - if not only_if_default or axis.isDefault_majfmt: - axis.set_major_formatter( - self._default_major_formatter or Formatter('auto') - ) - axis.isDefault_majfmt = True - if not only_if_default or axis.isDefault_minloc: - name = axis.axis_name if axis.axis_name in 'xy' else 'x' - axis.set_minor_locator( - self._default_minor_locator or Locator( - 'minor' if rc[name + 'tick.minor.visible'] else 'null' - ) - ) - axis.isDefault_minloc = True - if not only_if_default or axis.isDefault_minfmt: - axis.set_minor_formatter( - self._default_minor_formatter or Formatter('null') - ) - axis.isDefault_minfmt = True - - def get_transform(self): - """ - Return the scale transform. - """ - return self._transform - - -class LinearScale(_ScaleBase, mscale.LinearScale): - """ - As with `~matplotlib.scale.LinearScale` but with `AutoFormatter` as the - default major formatter. - """ - #: The registered scale name - name = 'linear' - - def __init__(self, **kwargs): - """ - """ - super().__init__(**kwargs) - self._transform = mtransforms.IdentityTransform() - - -class LogitScale(_ScaleBase, mscale.LogitScale): - """ - As with `~matplotlib.scale.LogitScale` but with `AutoFormatter` as the - default major formatter. - """ - #: The registered scale name - name = 'logit' - - def __init__(self, **kwargs): - """ - Parameters - ---------- - nonpos : {'mask', 'clip'} - Values outside of (0, 1) can be masked as invalid, or clipped to a - number very close to 0 or 1. - """ - super().__init__(**kwargs) - # self._default_major_formatter = Formatter('logit') - self._default_major_locator = Locator('logit') - self._default_minor_locator = Locator('logit', minor=True) - - -class LogScale(_ScaleBase, mscale.LogScale): - """ - As with `~matplotlib.scale.LogScale` but with `AutoFormatter` as the - default major formatter. Also, "``x``" and "``y``" versions of each - keyword argument are no longer required. - """ - #: The registered scale name - name = 'log' - - def __init__(self, **kwargs): - """ - Parameters - ---------- - base : float, optional - The base of the logarithm. Default is ``10``. - nonpos : {'mask', 'clip'}, optional - Non-positive values in *x* or *y* can be masked as - invalid, or clipped to a very small positive number. - subs : list of int, optional - Default *minor* tick locations are on these multiples of each power - of the base. For example, ``subs=(1,2,5)`` draws ticks on 1, 2, 5, - 10, 20, 50, etc. The default is ``subs=numpy.arange(1, 10)``. - basex, basey, nonposx, nonposy, subsx, subsy - Aliases for the above keywords. These used to be conditional - on the *name* of the axis. - """ - kwargs = _parse_logscale_args(kwargs, 'base', 'nonpos', 'subs') - super().__init__(**kwargs) - # self._default_major_formatter = Formatter('log') - self._default_major_locator = Locator('log', base=self.base) - self._default_minor_locator = Locator( - 'log', base=self.base, subs=self.subs - ) - - -class SymmetricalLogScale(_ScaleBase, mscale.SymmetricalLogScale): - """ - As with `~matplotlib.scale.SymmetricLogScale`. `AutoFormatter` is the new - default major formatter. Also, "``x``" and "``y``" versions of each - keyword argument are no longer required. - """ - #: The registered scale name - name = 'symlog' - - def __init__(self, **kwargs): - """ - Parameters - ---------- - base : float, optional - The base of the logarithm. Default is ``10``. - linthresh : float, optional - Defines the range ``(-linthresh, linthresh)``, within which the - plot is linear. This avoids having the plot go to infinity around - zero. Defaults to 2. - linscale : float, optional - This allows the linear range ``(-linthresh, linthresh)`` to be - stretched relative to the logarithmic range. Its value is the - number of decades to use for each half of the linear range. For - example, when `linscale` is ``1`` (the default), the space used - for the positive and negative halves of the linear range will be - equal to one decade in the logarithmic range. - subs : sequence of int, optional - Default *minor* tick locations are on these multiples of each power - of the base. For example, ``subs=(1, 2, 5)`` draws ticks on 1, 2, - 5, 10, 20, 50, 100, 200, 500, etc. The default is - ``subs=numpy.arange(1, 10)``. - basex, basey, linthreshx, linthreshy, linscalex, linscaley, \ -subsx, subsy - Aliases for the above keywords. These used to be conditional - on the *name* of the axis. - """ - # Note the symlog locator gets base and linthresh from the transform - kwargs = _parse_logscale_args( - kwargs, 'base', 'linthresh', 'linscale', 'subs') - super().__init__(**kwargs) - # self._default_major_formatter = Formatter('symlog')) - self._default_major_locator = Locator( - 'symlog', transform=self.get_transform() - ) - self._default_minor_locator = Locator( - 'symlog', transform=self.get_transform(), subs=self.subs - ) - - -class FuncScale(_ScaleBase, mscale.ScaleBase): - """ - An axis scale comprised of arbitrary forward and inverse transformations. - """ - #: The registered scale name - name = 'function' - - def __init__( - self, arg, invert=False, parent_scale=None, - major_locator=None, minor_locator=None, - major_formatter=None, minor_formatter=None, - smart_bounds=None, - ): - """ - Parameters - ---------- - arg : function, (function, function), or \ -`~matplotlib.scale.ScaleBase` - The transform used to translate units from the parent axis to - the secondary axis. Input can be as follows: - - * A single function that accepts a number and returns some - transformation of that number. If you do not provide the - inverse, the function must be - `linear `__ or \ -`involutory `__. - For example, to convert Kelvin to Celsius, use - ``ax.dual%(x)s(lambda x: x - 273.15)``. To convert kilometers - to meters, use ``ax.dual%(x)s(lambda x: x*1e3)``. - * A 2-tuple of such functions. The second function must be the - *inverse* of the first. For example, to apply the square, use - ``ax.dual%(x)s((lambda x: x**2, lambda x: x**0.5))``. - Again, if the first function is linear or involutory, you do - not need to provide the second! - * A `~matplotlib.scale.ScaleBase` instance, e.g. a scale returned - by the `~proplot.axistools.Scale` constructor function. The - forward transformation, inverse transformation, and default axis - locators and formatters are borrowed from the resulting scale - class. For example, to apply the inverse, use - ``ax.dual%(x)s(plot.Scale('inverse'))``. - To apply the base-10 exponential function, use - ``ax.dual%(x)s(plot.Scale('exp', 10))``. - - invert : bool, optional - If ``True``, the forward and inverse functions are *swapped*. - Used when drawing dual axes. - parent_scale : `~matplotlib.scale.ScaleBase` - The axis scale of the "parent" axis. Its forward transform is - applied to the `FuncTransform`. Used when drawing dual axes. - major_locator, minor_locator : `~matplotlib.ticker.Locator`, optional - The default major and minor locator. By default these are - borrowed from `transform`. If `transform` is not an axis scale, - they are the same as `~matplotlib.scale.LinearScale`. - major_formatter, minor_formatter : `~matplotlib.ticker.Formatter`, \ -optional - The default major and minor formatter. By default these are - borrowed from `transform`. If `transform` is not an axis scale, - they are the same as `~matplotlib.scale.LinearScale`. - smart_bounds : bool, optional - Whether "smart bounds" are enabled by default. If not ``None``, - this is passed to `~matplotlib.axis.Axis.set_smart_bounds` when - `~matplotlib.scale.ScaleBase.set_default_locators_and_formatters` - is called. By default these are borrowed from `transform`. - """ - # NOTE: We permit *arbitrary* parent axis scales. If the parent is - # non-linear, we use *its* default locators and formatters. Assumption - # is this is a log scale and the child is maybe some multiple or offset - # of that scale. If the parent axis scale is linear, use the funcscale - # defaults, which can inherit defaults. - super().__init__() - if callable(arg): - forward = inverse = arg - elif np.iterable(arg) and len(arg) == 2 and all(map(callable, arg)): - forward, inverse = arg - elif isinstance(arg, mscale.ScaleBase): - trans = arg.get_transform() - forward = trans.transform - inverse = trans.inverted().transform - else: - raise ValueError( - 'Input should be a function, 2-tuple of forward and ' - 'and inverse functions, or a matplotlib.scale.ScaleBase ' - f'instance, not {arg!r}.' - ) - - # Create the FuncTransform or composite transform used for this class - # May need to invert functions for dualx() and dualy() - if invert: - forward, inverse = inverse, forward - functransform = FuncTransform(forward, inverse) - - # Manage the "parent" axis scale - # NOTE: Makes sense to use the "inverse" function here because this is - # a transformation from some *other* axis to this one, not vice versa. - if isinstance(parent_scale, mscale.ScaleBase): - if isinstance(parent_scale, mscale.SymmetricalLogScale): - kwargs = { - key: getattr(parent_scale, key) - for key in ('base', 'linthresh', 'linscale', 'subs') - } - kwargs['linthresh'] = inverse(kwargs['linthresh']) - parent_scale = SymmetricalLogScale(**kwargs) - elif isinstance(parent_scale, CutoffScale): - args = list(parent_scale.args) # copy - for i in range(0, len(args), 2): - args[i] = inverse(args[i]) - parent_scale = CutoffScale(*args) - functransform = parent_scale.get_transform() + functransform - elif parent_scale is not None: - raise ValueError( - f'parent_scale {parent_scale!r} must be a ScaleBase instance, ' - f'not {type(parent_scale)!r}.' - ) - - # Transform and default stuff - self.functions = (forward, inverse) - self._transform = functransform - self._default_smart_bounds = smart_bounds - self._default_major_locator = major_locator - self._default_minor_locator = minor_locator - self._default_major_formatter = major_formatter - self._default_minor_formatter = minor_formatter - - # Try to borrow locators and formatters - # WARNING: Using the same locator on multiple axes can evidently - # have unintended side effects! Matplotlib bug. So we make copies. - for scale in (arg, parent_scale): - if not isinstance(scale, _ScaleBase): - continue - if isinstance(scale, mscale.LinearScale): - continue - for key in ( - 'smart_bounds', 'major_locator', 'minor_locator', - 'major_formatter', 'minor_formatter' - ): - key = '_default_' + key - attr = getattr(scale, key) - if getattr(self, key) is None and attr is not None: - setattr(self, key, copy.copy(attr)) - - -class FuncTransform(mtransforms.Transform): - input_dims = 1 - output_dims = 1 - is_separable = True - has_inverse = True - - def __init__(self, forward, inverse): - super().__init__() - if callable(forward) and callable(inverse): - self._forward = forward - self._inverse = inverse - else: - raise ValueError('arguments to FuncTransform must be functions') - - def inverted(self): - return FuncTransform(self._inverse, self._forward) - - def transform_non_affine(self, values): - return self._forward(values) - - -class PowerScale(_ScaleBase, mscale.ScaleBase): - r""" - "Power scale" that performs the transformation - - .. math:: - - x^{c} - - """ - #: The registered scale name - name = 'power' - - def __init__(self, power=1, inverse=False, *, minpos=1e-300): - """ - Parameters - ---------- - power : float, optional - The power :math:`c` to which :math:`x` is raised. - inverse : bool, optional - If ``True``, the "forward" direction performs - the inverse operation :math:`x^{1/c}`. - minpos : float, optional - The minimum permissible value, used to truncate negative values. - """ - super().__init__() - if not inverse: - self._transform = PowerTransform(power, minpos) - else: - self._transform = InvertedPowerTransform(power, minpos) - - def limit_range_for_scale(self, vmin, vmax, minpos): - """ - Return the range *vmin* and *vmax* limited to positive numbers. - """ - return max(vmin, minpos), max(vmax, minpos) - - -class PowerTransform(mtransforms.Transform): - input_dims = 1 - output_dims = 1 - has_inverse = True - is_separable = True - - def __init__(self, power, minpos): - super().__init__() - self.minpos = minpos - self._power = power - - def inverted(self): - return InvertedPowerTransform(self._power, self.minpos) - - def transform_non_affine(self, a): - aa = np.array(a) - aa[aa <= self.minpos] = self.minpos # necessary - return np.power(np.array(a), self._power) - - -class InvertedPowerTransform(mtransforms.Transform): - input_dims = 1 - output_dims = 1 - has_inverse = True - is_separable = True - - def __init__(self, power, minpos): - super().__init__() - self.minpos = minpos - self._power = power - - def inverted(self): - return PowerTransform(self._power, self.minpos) - - def transform_non_affine(self, a): - aa = np.array(a) - aa[aa <= self.minpos] = self.minpos # necessary - return np.power(np.array(a), 1 / self._power) - - -class ExpScale(_ScaleBase, mscale.ScaleBase): - r""" - "Exponential scale" that performs either of two transformations. When - `inverse` is ``False`` (the default), performs the transformation - - .. math:: - - Ca^{bx} - - where the constants :math:`a`, :math:`b`, and :math:`C` are set by the - input (see below). When `inverse` is ``True``, this performs the inverse - transformation - - .. math:: - - (\log_a(x) - \log_a(C))/b - - which in appearence is equivalent to `LogScale` since it is just a linear - transformation of the logarithm. - """ - #: The registered scale name - name = 'exp' - - def __init__( - self, a=np.e, b=1, c=1, inverse=False, minpos=1e-300, - ): - """ - Parameters - ---------- - a : float, optional - The base of the exponential, i.e. the :math:`a` in :math:`Ca^{bx}`. - b : float, optional - The scale for the exponent, i.e. the :math:`b` in :math:`Ca^{bx}`. - c : float, optional - The coefficient of the exponential, i.e. the :math:`C` - in :math:`Ca^{bx}`. - minpos : float, optional - The minimum permissible value, used to truncate negative values. - inverse : bool, optional - If ``True``, the "forward" direction performs the inverse - operation. - """ - super().__init__() - if not inverse: - self._transform = ExpTransform(a, b, c, minpos) - else: - self._transform = InvertedExpTransform(a, b, c, minpos) - - def limit_range_for_scale(self, vmin, vmax, minpos): - """ - Return the range *vmin* and *vmax* limited to positive numbers. - """ - return max(vmin, minpos), max(vmax, minpos) - - -class ExpTransform(mtransforms.Transform): - input_dims = 1 - output_dims = 1 - has_inverse = True - is_separable = True - - def __init__(self, a, b, c, minpos): - super().__init__() - self.minpos = minpos - self._a = a - self._b = b - self._c = c - - def inverted(self): - return InvertedExpTransform(self._a, self._b, self._c, self.minpos) - - def transform_non_affine(self, a): - return self._c * np.power(self._a, self._b * np.array(a)) - - -class InvertedExpTransform(mtransforms.Transform): - input_dims = 1 - output_dims = 1 - has_inverse = True - is_separable = True - - def __init__(self, a, b, c, minpos): - super().__init__() - self.minpos = minpos - self._a = a - self._b = b - self._c = c - - def inverted(self): - return ExpTransform(self._a, self._b, self._c, self.minpos) - - def transform_non_affine(self, a): - aa = np.array(a) - aa[aa <= self.minpos] = self.minpos # necessary - return np.log(aa / self._c) / (self._b * np.log(self._a)) - - -class MercatorLatitudeScale(_ScaleBase, mscale.ScaleBase): - """ - Axis scale that transforms coordinates as with latitude in the `Mercator \ -projection `__. - Adapted from `this matplotlib example \ -`__. - """r"""The scale function is as follows: - - .. math:: - - y = \\ln(\\tan(\\pi x/180) + \\sec(\\pi x/180)) - - The inverse scale function is as follows: - - .. math:: - - x = 180\\arctan(\\sinh(y))/\\pi - - """ - #: The registered scale name - name = 'mercator' - - def __init__(self, thresh=85.0): - """ - Parameters - ---------- - thresh : float, optional - Threshold between 0 and 90, used to constrain axis limits between - ``-thresh`` and ``+thresh``. - """ - super().__init__() - if thresh >= 90.0: - raise ValueError('Threshold "thresh" must be <=90.') - self._thresh = thresh - self._transform = MercatorLatitudeTransform(thresh) - self._default_major_formatter = Formatter('deg') - self._default_smart_bounds = True - - def limit_range_for_scale(self, vmin, vmax, minpos): # noqa: U100 - """ - Return the range *vmin* and *vmax* limited to within +/-90 degrees - (exclusive). - """ - return max(vmin, -self._thresh), min(vmax, self._thresh) - - -class MercatorLatitudeTransform(mtransforms.Transform): - input_dims = 1 - output_dims = 1 - is_separable = True - has_inverse = True - - def __init__(self, thresh): - super().__init__() - self._thresh = thresh - - def inverted(self): - return InvertedMercatorLatitudeTransform(self._thresh) - - def transform_non_affine(self, a): - # With safeguards - # TODO: Can improve this? - a = np.deg2rad(a) # convert to radians - m = ma.masked_where((a < -self._thresh) | (a > self._thresh), a) - if m.mask.any(): - return ma.log(np.abs(ma.tan(m) + 1 / ma.cos(m))) - else: - return np.log(np.abs(np.tan(a) + 1 / np.cos(a))) - - -class InvertedMercatorLatitudeTransform(mtransforms.Transform): - input_dims = 1 - output_dims = 1 - is_separable = True - has_inverse = True - - def __init__(self, thresh): - super().__init__() - self._thresh = thresh - - def inverted(self): - return MercatorLatitudeTransform(self._thresh) - - def transform_non_affine(self, a): - # m = ma.masked_where((a < -self._thresh) | (a > self._thresh), a) - # always assume in first/fourth quadrant, i.e. go from -pi/2 to pi/2 - return np.rad2deg(np.arctan2(1, np.sinh(a))) - - -class SineLatitudeScale(_ScaleBase, mscale.ScaleBase): - r""" - Axis scale that is linear in the *sine* of *x*. The axis limits are - constrained to fall between ``-90`` and ``+90`` degrees. The scale - function is as follows: - - .. math:: - - y = \sin(\pi x/180) - - The inverse scale function is as follows: - - .. math:: - - x = 180\arcsin(y)/\pi - """ - #: The registered scale name - name = 'sine' - - def __init__(self): - super().__init__() - self._transform = SineLatitudeTransform() - self._default_major_formatter = Formatter('deg') - self._default_smart_bounds = True - - def limit_range_for_scale(self, vmin, vmax, minpos): # noqa: U100 - """ - Return the range *vmin* and *vmax* limited to within +/-90 degrees - (inclusive). - """ - return max(vmin, -90), min(vmax, 90) - - -class SineLatitudeTransform(mtransforms.Transform): - input_dims = 1 - output_dims = 1 - is_separable = True - has_inverse = True - - def __init__(self): - super().__init__() - - def inverted(self): - return InvertedSineLatitudeTransform() - - def transform_non_affine(self, a): - # With safeguards - # TODO: Can improve this? - with np.errstate(invalid='ignore'): # NaNs will always be False - m = (a >= -90) & (a <= 90) - if not m.all(): - aa = ma.masked_where(~m, a) - return ma.sin(np.deg2rad(aa)) - else: - return np.sin(np.deg2rad(a)) - - -class InvertedSineLatitudeTransform(mtransforms.Transform): - input_dims = 1 - output_dims = 1 - is_separable = True - has_inverse = True - - def __init__(self): - super().__init__() - - def inverted(self): - return SineLatitudeTransform() - - def transform_non_affine(self, a): - # Clipping, instead of setting invalid - # NOTE: Using ma.arcsin below caused super weird errors, dun do that - aa = a.copy() - return np.rad2deg(np.arcsin(aa)) - - -class CutoffScale(_ScaleBase, mscale.ScaleBase): - """ - Axis scale composed of arbitrary piecewise linear transformations. - The axis can undergo discrete jumps, "accelerations", or "decelerations" - between successive thresholds. Adapted from - `this stackoverflow post `__. - """ - #: The registered scale name - name = 'cutoff' - - def __init__(self, *args): - """ - Parameters - ---------- - *args : (thresh_1, scale_1, ..., thresh_N, [scale_N]), optional - Sequence of "thresholds" and "scales". If the final scale is - omitted (i.e. you passed an odd number of arguments) it is set - to ``1``. Each ``scale_i`` in the sequence can be interpreted - as follows: - - * If ``scale_i < 1``, the axis is decelerated from ``thresh_i`` to - ``thresh_i+1``. For ``scale_N``, the axis is decelerated - everywhere above ``thresh_N``. - * If ``scale_i > 1``, the axis is accelerated from ``thresh_i`` to - ``thresh_i+1``. For ``scale_N``, the axis is accelerated - everywhere above ``thresh_N``. - * If ``scale_i == numpy.inf``, the axis *discretely jumps* from - ``thresh_i`` to ``thresh_i+1``. The final scale ``scale_N`` - *cannot* be ``numpy.inf``. - - Example - ------- - - >>> import proplot as plot - ... import numpy as np - ... scale = plot.CutoffScale(10, 0.5) # move slower above 10 - ... scale = plot.CutoffScale(10, 2, 20) # zoom out between 10 and 20 - ... scale = plot.CutoffScale(10, np.inf, 20) # jump from 10 to 20 - - """ - super().__init__() - args = list(args) - if len(args) % 2 == 1: - args.append(1) - self.args = args - self.threshs = args[::2] - self.scales = args[1::2] - self._transform = CutoffTransform(self.threshs, self.scales) - - -class CutoffTransform(mtransforms.Transform): - input_dims = 1 - output_dims = 1 - has_inverse = True - is_separable = True - - def __init__(self, threshs, scales, zero_dists=None): - # The zero_dists array is used to fill in distances where scales and - # threshold steps are zero. Used for inverting discrete transorms. - super().__init__() - dists = np.diff(threshs) - scales = np.asarray(scales) - threshs = np.asarray(threshs) - if len(scales) != len(threshs): - raise ValueError(f'Got {len(threshs)} but {len(scales)} scales.') - if any(scales < 0): - raise ValueError('Scales must be non negative.') - if scales[-1] in (0, np.inf): - raise ValueError('Final scale must be finite.') - if any(dists < 0): - raise ValueError('Thresholds must be monotonically increasing.') - if any((dists == 0) | (scales == 0)) and ( - any((dists == 0) != (scales == 0)) or zero_dists is None): - raise ValueError( - 'Got zero scales and distances in different places or ' - 'zero_dists is None.' - ) - self._scales = scales - self._threshs = threshs - with np.errstate(divide='ignore', invalid='ignore'): - dists = np.concatenate((threshs[:1], dists / scales[:-1])) - if zero_dists is not None: - dists[scales[:-1] == 0] = zero_dists - self._dists = dists - - def inverted(self): - # Use same algorithm for inversion! - threshs = np.cumsum(self._dists) # thresholds in transformed space - with np.errstate(divide='ignore', invalid='ignore'): - scales = 1 / self._scales # new scales are inverse - zero_dists = np.diff(self._threshs)[scales[:-1] == 0] - return CutoffTransform(threshs, scales, zero_dists=zero_dists) - - def transform_non_affine(self, a): - # Cannot do list comprehension because this method sometimes - # received non-1d arrays - dists = self._dists - scales = self._scales - threshs = self._threshs - aa = np.array(a) # copy - with np.errstate(divide='ignore', invalid='ignore'): - for i, ai in np.ndenumerate(a): - j = np.searchsorted(threshs, ai) - if j > 0: - aa[i] = ( - dists[:j].sum() + (ai - threshs[j - 1]) / scales[j - 1] - ) - return aa - - -class InverseScale(_ScaleBase, mscale.ScaleBase): - r""" - Axis scale that is linear in the *inverse* of *x*. The forward and inverse - scale functions are as follows: - - .. math:: - - y = x^{-1} - - """ - #: The registered scale name - name = 'inverse' - - def __init__(self): - super().__init__() - self._transform = InverseTransform() - # self._default_major_formatter = Formatter('log') - self._default_major_locator = Locator('log', base=10) - self._default_minor_locator = Locator( - 'log', base=10, subs=np.arange(1, 10) - ) - self._default_smart_bounds = True - - def limit_range_for_scale(self, vmin, vmax, minpos): - """ - Return the range *vmin* and *vmax* limited to positive numbers. - """ - # Unlike log-scale, we can't just warp the space between - # the axis limits -- have to actually change axis limits. Also this - # scale will invert and swap the limits you provide. Weird! - return max(vmin, minpos), max(vmax, minpos) - - -class InverseTransform(mtransforms.Transform): - # Create transform object - input_dims = 1 - output_dims = 1 - is_separable = True - has_inverse = True - - def __init__(self): - super().__init__() - - def inverted(self): - return InverseTransform() - - def transform_non_affine(self, a): - a = np.array(a) - # f = np.abs(a) <= self.minpos # attempt for negative-friendly - # aa[f] = np.sign(a[f])*self.minpos - with np.errstate(divide='ignore', invalid='ignore'): - return 1.0 / a - - -#: The registered scale names and their associated -#: `~matplotlib.scale.ScaleBase` classes. See `Scale` for a table. -scales = mscale._scale_mapping - -#: Mapping of strings to `~matplotlib.ticker.Locator` classes. See -#: `Locator` for a table.""" -locators = { - 'none': mticker.NullLocator, - 'null': mticker.NullLocator, - 'auto': mticker.AutoLocator, - 'log': mticker.LogLocator, - 'maxn': mticker.MaxNLocator, - 'linear': mticker.LinearLocator, - 'multiple': mticker.MultipleLocator, - 'fixed': mticker.FixedLocator, - 'index': mticker.IndexLocator, - 'symlog': mticker.SymmetricalLogLocator, - 'logit': mticker.LogitLocator, - 'minor': mticker.AutoMinorLocator, - 'date': mdates.AutoDateLocator, - 'microsecond': mdates.MicrosecondLocator, - 'second': mdates.SecondLocator, - 'minute': mdates.MinuteLocator, - 'hour': mdates.HourLocator, - 'day': mdates.DayLocator, - 'weekday': mdates.WeekdayLocator, - 'month': mdates.MonthLocator, - 'year': mdates.YearLocator, -} -if hasattr(mpolar, 'ThetaLocator'): - locators['theta'] = mpolar.ThetaLocator - -#: Mapping of strings to `~matplotlib.ticker.Formatter` classes. See -#: `Formatter` for a table. -formatters = { # note default LogFormatter uses ugly e+00 notation - 'auto': AutoFormatter, - 'frac': FracFormatter, - 'simple': SimpleFormatter, - 'date': mdates.AutoDateFormatter, - 'datestr': mdates.DateFormatter, - 'scalar': mticker.ScalarFormatter, - 'none': mticker.NullFormatter, - 'null': mticker.NullFormatter, - 'func': mticker.FuncFormatter, - 'strmethod': mticker.StrMethodFormatter, - 'formatstr': mticker.FormatStrFormatter, - 'log': mticker.LogFormatterSciNotation, - 'sci': mticker.LogFormatterSciNotation, - 'math': mticker.LogFormatterMathtext, - 'logit': mticker.LogitFormatter, - 'eng': mticker.EngFormatter, - 'percent': mticker.PercentFormatter, - 'index': mticker.IndexFormatter, -} -if hasattr(mdates, 'ConciseDateFormatter'): - formatters['concise'] = mdates.ConciseDateFormatter -if hasattr(mpolar, 'ThetaFormatter'): - formatters['theta'] = mpolar.ThetaFormatter - -# Monkey patch. Force scale_factory to accept ScaleBase instances, so that -# set_xscale and set_yscale can accept scales returned by the Scale constructor -if mscale.scale_factory is not _scale_factory: - mscale.scale_factory = _scale_factory - -# Custom scales and overrides -mscale.register_scale(CutoffScale) -mscale.register_scale(ExpScale) -mscale.register_scale(LogScale) -mscale.register_scale(LinearScale) -mscale.register_scale(LogitScale) -mscale.register_scale(FuncScale) -mscale.register_scale(PowerScale) -mscale.register_scale(SymmetricalLogScale) -mscale.register_scale(InverseScale) -mscale.register_scale(SineLatitudeScale) -mscale.register_scale(MercatorLatitudeScale) diff --git a/proplot/cmaps/Blue0.xml b/proplot/cmaps/Blue0.xml deleted file mode 100644 index 6bd0025d9..000000000 --- a/proplot/cmaps/Blue0.xml +++ /dev/null @@ -1 +0,0 @@ -
    \ No newline at end of file diff --git a/proplot/cmaps/Blue1_r.xml b/proplot/cmaps/Blues1_r.xml similarity index 100% rename from proplot/cmaps/Blue1_r.xml rename to proplot/cmaps/Blues1_r.xml diff --git a/proplot/cmaps/Blue2.xml b/proplot/cmaps/Blues2.xml similarity index 100% rename from proplot/cmaps/Blue2.xml rename to proplot/cmaps/Blues2.xml diff --git a/proplot/cmaps/Blue3.xml b/proplot/cmaps/Blues3.xml similarity index 100% rename from proplot/cmaps/Blue3.xml rename to proplot/cmaps/Blues3.xml diff --git a/proplot/cmaps/Blue4_r.xml b/proplot/cmaps/Blues4_r.xml similarity index 100% rename from proplot/cmaps/Blue4_r.xml rename to proplot/cmaps/Blues4_r.xml diff --git a/proplot/cmaps/Blue5.xml b/proplot/cmaps/Blues5.xml similarity index 100% rename from proplot/cmaps/Blue5.xml rename to proplot/cmaps/Blues5.xml diff --git a/proplot/cmaps/Blue6.xml b/proplot/cmaps/Blues6.xml similarity index 100% rename from proplot/cmaps/Blue6.xml rename to proplot/cmaps/Blues6.xml diff --git a/proplot/cmaps/Blue7.xml b/proplot/cmaps/Blues7.xml similarity index 100% rename from proplot/cmaps/Blue7.xml rename to proplot/cmaps/Blues7.xml diff --git a/proplot/cmaps/Blue8.xml b/proplot/cmaps/Blues8.xml similarity index 100% rename from proplot/cmaps/Blue8.xml rename to proplot/cmaps/Blues8.xml diff --git a/proplot/cmaps/Blue9.xml b/proplot/cmaps/Blues9.xml similarity index 98% rename from proplot/cmaps/Blue9.xml rename to proplot/cmaps/Blues9.xml index 946e7db87..cddc43881 100644 --- a/proplot/cmaps/Blue9.xml +++ b/proplot/cmaps/Blues9.xml @@ -1 +1 @@ -
    \ No newline at end of file +
    diff --git a/proplot/cmaps/Brown2.xml b/proplot/cmaps/Browns1.xml similarity index 100% rename from proplot/cmaps/Brown2.xml rename to proplot/cmaps/Browns1.xml diff --git a/proplot/cmaps/Brown1.xml b/proplot/cmaps/Browns2.xml similarity index 100% rename from proplot/cmaps/Brown1.xml rename to proplot/cmaps/Browns2.xml diff --git a/proplot/cmaps/Brown3.xml b/proplot/cmaps/Browns3.xml similarity index 100% rename from proplot/cmaps/Brown3.xml rename to proplot/cmaps/Browns3.xml diff --git a/proplot/cmaps/Brown4.xml b/proplot/cmaps/Browns4.xml similarity index 100% rename from proplot/cmaps/Brown4.xml rename to proplot/cmaps/Browns4.xml diff --git a/proplot/cmaps/Brown5.xml b/proplot/cmaps/Browns5.xml similarity index 100% rename from proplot/cmaps/Brown5.xml rename to proplot/cmaps/Browns5.xml diff --git a/proplot/cmaps/Brown6.xml b/proplot/cmaps/Browns6.xml similarity index 100% rename from proplot/cmaps/Brown6.xml rename to proplot/cmaps/Browns6.xml diff --git a/proplot/cmaps/Brown7.xml b/proplot/cmaps/Browns7.xml similarity index 100% rename from proplot/cmaps/Brown7.xml rename to proplot/cmaps/Browns7.xml diff --git a/proplot/cmaps/Brown8.xml b/proplot/cmaps/Browns8.xml similarity index 100% rename from proplot/cmaps/Brown8.xml rename to proplot/cmaps/Browns8.xml diff --git a/proplot/cmaps/Brown9.xml b/proplot/cmaps/Browns9.xml similarity index 100% rename from proplot/cmaps/Brown9.xml rename to proplot/cmaps/Browns9.xml diff --git a/proplot/cmaps/Crest.rgb b/proplot/cmaps/Crest.rgb new file mode 100644 index 000000000..f7455f102 --- /dev/null +++ b/proplot/cmaps/Crest.rgb @@ -0,0 +1,256 @@ +0.6468274, 0.80289262, 0.56592265 +0.64233318, 0.80081141, 0.56639461 +0.63791969, 0.7987162, 0.56674976 +0.6335316, 0.79661833, 0.56706128 +0.62915226, 0.7945212, 0.56735066 +0.62477862, 0.79242543, 0.56762143 +0.62042003, 0.79032918, 0.56786129 +0.61606327, 0.78823508, 0.56808666 +0.61171322, 0.78614216, 0.56829092 +0.60736933, 0.78405055, 0.56847436 +0.60302658, 0.78196121, 0.56864272 +0.59868708, 0.77987374, 0.56879289 +0.59435366, 0.77778758, 0.56892099 +0.59001953, 0.77570403, 0.56903477 +0.58568753, 0.77362254, 0.56913028 +0.58135593, 0.77154342, 0.56920908 +0.57702623, 0.76946638, 0.56926895 +0.57269165, 0.76739266, 0.5693172 +0.56835934, 0.76532092, 0.56934507 +0.56402533, 0.76325185, 0.56935664 +0.55968429, 0.76118643, 0.56935732 +0.55534159, 0.75912361, 0.56934052 +0.55099572, 0.75706366, 0.56930743 +0.54664626, 0.75500662, 0.56925799 +0.54228969, 0.75295306, 0.56919546 +0.53792417, 0.75090328, 0.56912118 +0.53355172, 0.74885687, 0.5690324 +0.52917169, 0.74681387, 0.56892926 +0.52478243, 0.74477453, 0.56881287 +0.52038338, 0.74273888, 0.56868323 +0.5159739, 0.74070697, 0.56854039 +0.51155269, 0.73867895, 0.56838507 +0.50711872, 0.73665492, 0.56821764 +0.50267118, 0.73463494, 0.56803826 +0.49822926, 0.73261388, 0.56785146 +0.49381422, 0.73058524, 0.56767484 +0.48942421, 0.72854938, 0.56751036 +0.48505993, 0.72650623, 0.56735752 +0.48072207, 0.72445575, 0.56721583 +0.4764113, 0.72239788, 0.56708475 +0.47212827, 0.72033258, 0.56696376 +0.46787361, 0.71825983, 0.56685231 +0.46364792, 0.71617961, 0.56674986 +0.45945271, 0.71409167, 0.56665625 +0.45528878, 0.71199595, 0.56657103 +0.45115557, 0.70989276, 0.5664931 +0.44705356, 0.70778212, 0.56642189 +0.44298321, 0.70566406, 0.56635683 +0.43894492, 0.70353863, 0.56629734 +0.43493911, 0.70140588, 0.56624286 +0.43096612, 0.69926587, 0.5661928 +0.42702625, 0.69711868, 0.56614659 +0.42311977, 0.69496438, 0.56610368 +0.41924689, 0.69280308, 0.56606355 +0.41540778, 0.69063486, 0.56602564 +0.41160259, 0.68845984, 0.56598944 +0.40783143, 0.68627814, 0.56595436 +0.40409434, 0.68408988, 0.56591994 +0.40039134, 0.68189518, 0.56588564 +0.39672238, 0.6796942, 0.56585103 +0.39308781, 0.67748696, 0.56581581 +0.38949137, 0.67527276, 0.56578084 +0.38592889, 0.67305266, 0.56574422 +0.38240013, 0.67082685, 0.56570561 +0.37890483, 0.66859548, 0.56566462 +0.37544276, 0.66635871, 0.56562081 +0.37201365, 0.66411673, 0.56557372 +0.36861709, 0.6618697, 0.5655231 +0.36525264, 0.65961782, 0.56546873 +0.36191986, 0.65736125, 0.56541032 +0.35861935, 0.65509998, 0.56534768 +0.35535621, 0.65283302, 0.56528211 +0.35212361, 0.65056188, 0.56521171 +0.34892097, 0.64828676, 0.56513633 +0.34574785, 0.64600783, 0.56505539 +0.34260357, 0.64372528, 0.5649689 +0.33948744, 0.64143931, 0.56487679 +0.33639887, 0.6391501, 0.56477869 +0.33334501, 0.63685626, 0.56467661 +0.33031952, 0.63455911, 0.564569 +0.3273199, 0.63225924, 0.56445488 +0.32434526, 0.62995682, 0.56433457 +0.32139487, 0.62765201, 0.56420795 +0.31846807, 0.62534504, 0.56407446 +0.3155731, 0.62303426, 0.56393695 +0.31270304, 0.62072111, 0.56379321 +0.30985436, 0.61840624, 0.56364307 +0.30702635, 0.61608984, 0.56348606 +0.30421803, 0.61377205, 0.56332267 +0.30143611, 0.61145167, 0.56315419 +0.29867863, 0.60912907, 0.56298054 +0.29593872, 0.60680554, 0.56280022 +0.29321538, 0.60448121, 0.56261376 +0.2905079, 0.60215628, 0.56242036 +0.28782827, 0.5998285, 0.56222366 +0.28516521, 0.59749996, 0.56202093 +0.28251558, 0.59517119, 0.56181204 +0.27987847, 0.59284232, 0.56159709 +0.27726216, 0.59051189, 0.56137785 +0.27466434, 0.58818027, 0.56115433 +0.2720767, 0.58584893, 0.56092486 +0.26949829, 0.58351797, 0.56068983 +0.26693801, 0.58118582, 0.56045121 +0.26439366, 0.57885288, 0.56020858 +0.26185616, 0.57652063, 0.55996077 +0.25932459, 0.57418919, 0.55970795 +0.25681303, 0.57185614, 0.55945297 +0.25431024, 0.56952337, 0.55919385 +0.25180492, 0.56719255, 0.5589305 +0.24929311, 0.56486397, 0.5586654 +0.24678356, 0.56253666, 0.55839491 +0.24426587, 0.56021153, 0.55812473 +0.24174022, 0.55788852, 0.55785448 +0.23921167, 0.55556705, 0.55758211 +0.23668315, 0.55324675, 0.55730676 +0.23414742, 0.55092825, 0.55703167 +0.23160473, 0.54861143, 0.5567573 +0.22905996, 0.54629572, 0.55648168 +0.22651648, 0.54398082, 0.5562029 +0.22396709, 0.54166721, 0.55592542 +0.22141221, 0.53935481, 0.55564885 +0.21885269, 0.53704347, 0.55537294 +0.21629986, 0.53473208, 0.55509319 +0.21374297, 0.53242154, 0.5548144 +0.21118255, 0.53011166, 0.55453708 +0.2086192, 0.52780237, 0.55426067 +0.20605624, 0.52549322, 0.55398479 +0.20350004, 0.5231837, 0.55370601 +0.20094292, 0.52087429, 0.55342884 +0.19838567, 0.51856489, 0.55315283 +0.19582911, 0.51625531, 0.55287818 +0.19327413, 0.51394542, 0.55260469 +0.19072933, 0.51163448, 0.5523289 +0.18819045, 0.50932268, 0.55205372 +0.18565609, 0.50701014, 0.55177937 +0.18312739, 0.50469666, 0.55150597 +0.18060561, 0.50238204, 0.55123374 +0.178092, 0.50006616, 0.55096224 +0.17558808, 0.49774882, 0.55069118 +0.17310341, 0.49542924, 0.5504176 +0.17063111, 0.49310789, 0.55014445 +0.1681728, 0.49078458, 0.54987159 +0.1657302, 0.48845913, 0.54959882 +0.16330517, 0.48613135, 0.54932605 +0.16089963, 0.48380104, 0.54905306 +0.15851561, 0.48146803, 0.54877953 +0.15615526, 0.47913212, 0.54850526 +0.15382083, 0.47679313, 0.54822991 +0.15151471, 0.47445087, 0.54795318 +0.14924112, 0.47210502, 0.54767411 +0.1470032, 0.46975537, 0.54739226 +0.14480101, 0.46740187, 0.54710832 +0.14263736, 0.46504434, 0.54682188 +0.14051521, 0.46268258, 0.54653253 +0.13843761, 0.46031639, 0.54623985 +0.13640774, 0.45794558, 0.5459434 +0.13442887, 0.45556994, 0.54564272 +0.1325044, 0.45318928, 0.54533736 +0.13063777, 0.4508034, 0.54502674 +0.12883252, 0.44841211, 0.5447104 +0.12709242, 0.44601517, 0.54438795 +0.1254209, 0.44361244, 0.54405855 +0.12382162, 0.44120373, 0.54372156 +0.12229818, 0.43878887, 0.54337634 +0.12085453, 0.4363676, 0.54302253 +0.11949938, 0.43393955, 0.54265715 +0.11823166, 0.43150478, 0.54228104 +0.11705496, 0.42906306, 0.54189388 +0.115972, 0.42661431, 0.54149449 +0.11498598, 0.42415835, 0.54108222 +0.11409965, 0.42169502, 0.54065622 +0.11331533, 0.41922424, 0.5402155 +0.11263542, 0.41674582, 0.53975931 +0.1120615, 0.4142597, 0.53928656 +0.11159738, 0.41176567, 0.53879549 +0.11125248, 0.40926325, 0.53828203 +0.11101698, 0.40675289, 0.53774864 +0.11089152, 0.40423445, 0.53719455 +0.11085121, 0.4017095, 0.53662425 +0.11087217, 0.39917938, 0.53604354 +0.11095515, 0.39664394, 0.53545166 +0.11110676, 0.39410282, 0.53484509 +0.11131735, 0.39155635, 0.53422678 +0.11158595, 0.38900446, 0.53359634 +0.11191139, 0.38644711, 0.5329534 +0.11229224, 0.38388426, 0.53229748 +0.11273683, 0.38131546, 0.53162393 +0.11323438, 0.37874109, 0.53093619 +0.11378271, 0.37616112, 0.53023413 +0.11437992, 0.37357557, 0.52951727 +0.11502681, 0.37098429, 0.52878396 +0.11572661, 0.36838709, 0.52803124 +0.11646936, 0.36578429, 0.52726234 +0.11725299, 0.3631759, 0.52647685 +0.1180755, 0.36056193, 0.52567436 +0.1189438, 0.35794203, 0.5248497 +0.11984752, 0.35531657, 0.52400649 +0.1207833, 0.35268564, 0.52314492 +0.12174895, 0.35004927, 0.52226461 +0.12274959, 0.34740723, 0.52136104 +0.12377809, 0.34475975, 0.52043639 +0.12482961, 0.34210702, 0.51949179 +0.125902, 0.33944908, 0.51852688 +0.12699998, 0.33678574, 0.51753708 +0.12811691, 0.33411727, 0.51652464 +0.12924811, 0.33144384, 0.51549084 +0.13039157, 0.32876552, 0.51443538 +0.13155228, 0.32608217, 0.51335321 +0.13272282, 0.32339407, 0.51224759 +0.13389954, 0.32070138, 0.51111946 +0.13508064, 0.31800419, 0.50996862 +0.13627149, 0.31530238, 0.50878942 +0.13746376, 0.31259627, 0.50758645 +0.13865499, 0.30988598, 0.50636017 +0.13984364, 0.30717161, 0.50511042 +0.14103515, 0.30445309, 0.50383119 +0.14222093, 0.30173071, 0.50252813 +0.14339946, 0.2990046, 0.50120127 +0.14456941, 0.29627483, 0.49985054 +0.14573579, 0.29354139, 0.49847009 +0.14689091, 0.29080452, 0.49706566 +0.1480336, 0.28806432, 0.49563732 +0.1491628, 0.28532086, 0.49418508 +0.15028228, 0.28257418, 0.49270402 +0.15138673, 0.27982444, 0.49119848 +0.15247457, 0.27707172, 0.48966925 +0.15354487, 0.2743161, 0.48811641 +0.15459955, 0.27155765, 0.4865371 +0.15563716, 0.26879642, 0.4849321 +0.1566572, 0.26603191, 0.48330429 +0.15765823, 0.26326032, 0.48167456 +0.15862147, 0.26048295, 0.48005785 +0.15954301, 0.25770084, 0.47845341 +0.16043267, 0.25491144, 0.4768626 +0.16129262, 0.25211406, 0.4752857 +0.1621119, 0.24931169, 0.47372076 +0.16290577, 0.24649998, 0.47217025 +0.16366819, 0.24368054, 0.47063302 +0.1644021, 0.24085237, 0.46910949 +0.16510882, 0.2380149, 0.46759982 +0.16579015, 0.23516739, 0.46610429 +0.1664433, 0.2323105, 0.46462219 +0.16707586, 0.22944155, 0.46315508 +0.16768475, 0.22656122, 0.46170223 +0.16826815, 0.22366984, 0.46026308 +0.16883174, 0.22076514, 0.45883891 +0.16937589, 0.21784655, 0.45742976 +0.16990129, 0.21491339, 0.45603578 +0.1704074, 0.21196535, 0.45465677 +0.17089473, 0.20900176, 0.4532928 +0.17136819, 0.20602012, 0.45194524 +0.17182683, 0.20302012, 0.45061386 +0.17227059, 0.20000106, 0.44929865 +0.17270583, 0.19695949, 0.44800165 +0.17313804, 0.19389201, 0.44672488 +0.17363177, 0.19076859, 0.44549087 diff --git a/proplot/cmaps/Flare.rgb b/proplot/cmaps/Flare.rgb new file mode 100644 index 000000000..6a5d19261 --- /dev/null +++ b/proplot/cmaps/Flare.rgb @@ -0,0 +1,256 @@ +0.92907237, 0.68878959, 0.50411509 +0.92891402, 0.68494686, 0.50173994 +0.92864754, 0.68116207, 0.4993754 +0.92836112, 0.67738527, 0.49701572 +0.9280599, 0.67361354, 0.49466044 +0.92775569, 0.66983999, 0.49230866 +0.9274375, 0.66607098, 0.48996097 +0.927111, 0.66230315, 0.48761688 +0.92677996, 0.6585342, 0.485276 +0.92644317, 0.65476476, 0.48293832 +0.92609759, 0.65099658, 0.48060392 +0.925747, 0.64722729, 0.47827244 +0.92539502, 0.64345456, 0.47594352 +0.92503106, 0.6396848, 0.47361782 +0.92466877, 0.6359095, 0.47129427 +0.92429828, 0.63213463, 0.46897349 +0.92392172, 0.62835879, 0.46665526 +0.92354597, 0.62457749, 0.46433898 +0.9231622, 0.6207962, 0.46202524 +0.92277222, 0.61701365, 0.45971384 +0.92237978, 0.61322733, 0.45740444 +0.92198615, 0.60943622, 0.45509686 +0.92158735, 0.60564276, 0.45279137 +0.92118373, 0.60184659, 0.45048789 +0.92077582, 0.59804722, 0.44818634 +0.92036413, 0.59424414, 0.44588663 +0.91994924, 0.5904368, 0.44358868 +0.91952943, 0.58662619, 0.4412926 +0.91910675, 0.58281075, 0.43899817 +0.91868096, 0.57899046, 0.4367054 +0.91825103, 0.57516584, 0.43441436 +0.91781857, 0.57133556, 0.43212486 +0.9173814, 0.56750099, 0.4298371 +0.91694139, 0.56366058, 0.42755089 +0.91649756, 0.55981483, 0.42526631 +0.91604942, 0.55596387, 0.42298339 +0.9155979, 0.55210684, 0.42070204 +0.9151409, 0.54824485, 0.4184247 +0.91466138, 0.54438817, 0.41617858 +0.91416896, 0.54052962, 0.41396347 +0.91366559, 0.53666778, 0.41177769 +0.91315173, 0.53280208, 0.40962196 +0.91262605, 0.52893336, 0.40749715 +0.91208866, 0.52506133, 0.40540404 +0.91153952, 0.52118582, 0.40334346 +0.91097732, 0.51730767, 0.4013163 +0.910403, 0.51342591, 0.39932342 +0.90981494, 0.50954168, 0.39736571 +0.90921368, 0.5056543, 0.39544411 +0.90859797, 0.50176463, 0.39355952 +0.90796841, 0.49787195, 0.39171297 +0.90732341, 0.4939774, 0.38990532 +0.90666382, 0.49008006, 0.38813773 +0.90598815, 0.486181, 0.38641107 +0.90529624, 0.48228017, 0.38472641 +0.90458808, 0.47837738, 0.38308489 +0.90386248, 0.47447348, 0.38148746 +0.90311921, 0.4705685, 0.37993524 +0.90235809, 0.46666239, 0.37842943 +0.90157824, 0.46275577, 0.37697105 +0.90077904, 0.45884905, 0.37556121 +0.89995995, 0.45494253, 0.37420106 +0.89912041, 0.4510366, 0.37289175 +0.8982602, 0.44713126, 0.37163458 +0.89737819, 0.44322747, 0.37043052 +0.89647387, 0.43932557, 0.36928078 +0.89554477, 0.43542759, 0.36818855 +0.89458871, 0.4315354, 0.36715654 +0.89360794, 0.42764714, 0.36618273 +0.89260152, 0.42376366, 0.36526813 +0.8915687, 0.41988565, 0.36441384 +0.89050882, 0.41601371, 0.36362102 +0.8894159, 0.41215334, 0.36289639 +0.888292, 0.40830288, 0.36223756 +0.88713784, 0.40446193, 0.36164328 +0.88595253, 0.40063149, 0.36111438 +0.88473115, 0.39681635, 0.3606566 +0.88347246, 0.39301805, 0.36027074 +0.88217931, 0.38923439, 0.35995244 +0.880851, 0.38546632, 0.35970244 +0.87947728, 0.38172422, 0.35953127 +0.87806542, 0.37800172, 0.35942941 +0.87661509, 0.37429964, 0.35939659 +0.87511668, 0.37062819, 0.35944178 +0.87357554, 0.36698279, 0.35955811 +0.87199254, 0.3633634, 0.35974223 +0.87035691, 0.35978174, 0.36000516 +0.86867647, 0.35623087, 0.36033559 +0.86694949, 0.35271349, 0.36073358 +0.86516775, 0.34923921, 0.36120624 +0.86333996, 0.34580008, 0.36174113 +0.86145909, 0.3424046, 0.36234402 +0.85952586, 0.33905327, 0.36301129 +0.85754536, 0.33574168, 0.36373567 +0.855514, 0.33247568, 0.36451271 +0.85344392, 0.32924217, 0.36533344 +0.8513284, 0.32604977, 0.36620106 +0.84916723, 0.32289973, 0.36711424 +0.84696243, 0.31979068, 0.36806976 +0.84470627, 0.31673295, 0.36907066 +0.84240761, 0.31371695, 0.37010969 +0.84005337, 0.31075974, 0.37119284 +0.83765537, 0.30784814, 0.3723105 +0.83520234, 0.30499724, 0.37346726 +0.83270291, 0.30219766, 0.37465552 +0.83014895, 0.29946081, 0.37587769 +0.82754694, 0.29677989, 0.37712733 +0.82489111, 0.29416352, 0.37840532 +0.82218644, 0.29160665, 0.37970606 +0.81942908, 0.28911553, 0.38102921 +0.81662276, 0.28668665, 0.38236999 +0.81376555, 0.28432371, 0.383727 +0.81085964, 0.28202508, 0.38509649 +0.8079055, 0.27979128, 0.38647583 +0.80490309, 0.27762348, 0.3878626 +0.80185613, 0.2755178, 0.38925253 +0.79876118, 0.27347974, 0.39064559 +0.79562644, 0.27149928, 0.39203532 +0.79244362, 0.2695883, 0.39342447 +0.78922456, 0.26773176, 0.3948046 +0.78596161, 0.26594053, 0.39617873 +0.7826624, 0.26420493, 0.39754146 +0.77932717, 0.26252522, 0.39889102 +0.77595363, 0.2609049, 0.4002279 +0.77254999, 0.25933319, 0.40154704 +0.76911107, 0.25781758, 0.40284959 +0.76564158, 0.25635173, 0.40413341 +0.76214598, 0.25492998, 0.40539471 +0.75861834, 0.25356035, 0.40663694 +0.75506533, 0.25223402, 0.40785559 +0.75148963, 0.2509473, 0.40904966 +0.74788835, 0.24970413, 0.41022028 +0.74426345, 0.24850191, 0.41136599 +0.74061927, 0.24733457, 0.41248516 +0.73695678, 0.24620072, 0.41357737 +0.73327278, 0.24510469, 0.41464364 +0.72957096, 0.24404127, 0.4156828 +0.72585394, 0.24300672, 0.41669383 +0.7221226, 0.24199971, 0.41767651 +0.71837612, 0.24102046, 0.41863486 +0.71463236, 0.24004289, 0.41956983 +0.7108932, 0.23906316, 0.42048681 +0.70715842, 0.23808142, 0.42138647 +0.70342811, 0.2370976, 0.42226844 +0.69970218, 0.23611179, 0.42313282 +0.69598055, 0.2351247, 0.42397678 +0.69226314, 0.23413578, 0.42480327 +0.68854988, 0.23314511, 0.42561234 +0.68484064, 0.23215279, 0.42640419 +0.68113541, 0.23115942, 0.42717615 +0.67743412, 0.23016472, 0.42792989 +0.67373662, 0.22916861, 0.42866642 +0.67004287, 0.22817117, 0.42938576 +0.66635279, 0.22717328, 0.43008427 +0.66266621, 0.22617435, 0.43076552 +0.65898313, 0.22517434, 0.43142956 +0.65530349, 0.22417381, 0.43207427 +0.65162696, 0.22317307, 0.4327001 +0.64795375, 0.22217149, 0.43330852 +0.64428351, 0.22116972, 0.43389854 +0.64061624, 0.22016818, 0.43446845 +0.63695183, 0.21916625, 0.43502123 +0.63329016, 0.21816454, 0.43555493 +0.62963102, 0.2171635, 0.43606881 +0.62597451, 0.21616235, 0.43656529 +0.62232019, 0.21516239, 0.43704153 +0.61866821, 0.21416307, 0.43749868 +0.61501835, 0.21316435, 0.43793808 +0.61137029, 0.21216761, 0.4383556 +0.60772426, 0.2111715, 0.43875552 +0.60407977, 0.21017746, 0.43913439 +0.60043678, 0.20918503, 0.43949412 +0.59679524, 0.20819447, 0.43983393 +0.59315487, 0.20720639, 0.44015254 +0.58951566, 0.20622027, 0.44045213 +0.58587715, 0.20523751, 0.44072926 +0.5822395, 0.20425693, 0.44098758 +0.57860222, 0.20328034, 0.44122241 +0.57496549, 0.20230637, 0.44143805 +0.57132875, 0.20133689, 0.4416298 +0.56769215, 0.20037071, 0.44180142 +0.5640552, 0.19940936, 0.44194923 +0.56041794, 0.19845221, 0.44207535 +0.55678004, 0.1975, 0.44217824 +0.55314129, 0.19655316, 0.44225723 +0.54950166, 0.19561118, 0.44231412 +0.54585987, 0.19467771, 0.44234111 +0.54221157, 0.19375869, 0.44233698 +0.5385549, 0.19285696, 0.44229959 +0.5348913, 0.19197036, 0.44222958 +0.53122177, 0.1910974, 0.44212735 +0.52754464, 0.19024042, 0.44199159 +0.52386353, 0.18939409, 0.44182449 +0.52017476, 0.18856368, 0.44162345 +0.51648277, 0.18774266, 0.44139128 +0.51278481, 0.18693492, 0.44112605 +0.50908361, 0.18613639, 0.4408295 +0.50537784, 0.18534893, 0.44050064 +0.50166912, 0.18457008, 0.44014054 +0.49795686, 0.18380056, 0.43974881 +0.49424218, 0.18303865, 0.43932623 +0.49052472, 0.18228477, 0.43887255 +0.48680565, 0.1815371, 0.43838867 +0.48308419, 0.18079663, 0.43787408 +0.47936222, 0.18006056, 0.43733022 +0.47563799, 0.17933127, 0.43675585 +0.47191466, 0.17860416, 0.43615337 +0.46818879, 0.17788392, 0.43552047 +0.46446454, 0.17716458, 0.43486036 +0.46073893, 0.17645017, 0.43417097 +0.45701462, 0.17573691, 0.43345429 +0.45329097, 0.17502549, 0.43271025 +0.44956744, 0.17431649, 0.4319386 +0.44584668, 0.17360625, 0.43114133 +0.44212538, 0.17289906, 0.43031642 +0.43840678, 0.17219041, 0.42946642 +0.43469046, 0.17148074, 0.42859124 +0.4309749, 0.17077192, 0.42769008 +0.42726297, 0.17006003, 0.42676519 +0.42355299, 0.16934709, 0.42581586 +0.41984535, 0.16863258, 0.42484219 +0.41614149, 0.16791429, 0.42384614 +0.41244029, 0.16719372, 0.42282661 +0.40874177, 0.16647061, 0.42178429 +0.40504765, 0.16574261, 0.42072062 +0.401357, 0.16501079, 0.41963528 +0.397669, 0.16427607, 0.418528 +0.39398585, 0.16353554, 0.41740053 +0.39030735, 0.16278924, 0.41625344 +0.3866314, 0.16203977, 0.41508517 +0.38295904, 0.16128519, 0.41389849 +0.37928736, 0.16052483, 0.41270599 +0.37562649, 0.15974704, 0.41151182 +0.37197803, 0.15895049, 0.41031532 +0.36833779, 0.15813871, 0.40911916 +0.36470944, 0.15730861, 0.40792149 +0.36109117, 0.15646169, 0.40672362 +0.35748213, 0.15559861, 0.40552633 +0.353885, 0.15471714, 0.40432831 +0.35029682, 0.15381967, 0.4031316 +0.34671861, 0.1529053, 0.40193587 +0.34315191, 0.15197275, 0.40074049 +0.33959331, 0.15102466, 0.3995478 +0.33604378, 0.15006017, 0.39835754 +0.33250529, 0.14907766, 0.39716879 +0.32897621, 0.14807831, 0.39598285 +0.3254559, 0.14706248, 0.39480044 +0.32194567, 0.14602909, 0.39362106 +0.31844477, 0.14497857, 0.39244549 +0.31494974, 0.14391333, 0.39127626 +0.31146605, 0.14282918, 0.39011024 +0.30798857, 0.1417297, 0.38895105 +0.30451661, 0.14061515, 0.38779953 +0.30105136, 0.13948445, 0.38665531 +0.2975886, 0.1383403, 0.38552159 +0.29408557, 0.13721193, 0.38442775 diff --git a/proplot/cmaps/Green1_r.xml b/proplot/cmaps/Greens1_r.xml similarity index 100% rename from proplot/cmaps/Green1_r.xml rename to proplot/cmaps/Greens1_r.xml diff --git a/proplot/cmaps/Green2.xml b/proplot/cmaps/Greens2.xml similarity index 100% rename from proplot/cmaps/Green2.xml rename to proplot/cmaps/Greens2.xml diff --git a/proplot/cmaps/Green3_r.xml b/proplot/cmaps/Greens3_r.xml similarity index 100% rename from proplot/cmaps/Green3_r.xml rename to proplot/cmaps/Greens3_r.xml diff --git a/proplot/cmaps/Green4.xml b/proplot/cmaps/Greens4.xml similarity index 100% rename from proplot/cmaps/Green4.xml rename to proplot/cmaps/Greens4.xml diff --git a/proplot/cmaps/Green5.xml b/proplot/cmaps/Greens5.xml similarity index 100% rename from proplot/cmaps/Green5.xml rename to proplot/cmaps/Greens5.xml diff --git a/proplot/cmaps/Green6_r.xml b/proplot/cmaps/Greens6_r.xml similarity index 100% rename from proplot/cmaps/Green6_r.xml rename to proplot/cmaps/Greens6_r.xml diff --git a/proplot/cmaps/Green7.xml b/proplot/cmaps/Greens7.xml similarity index 100% rename from proplot/cmaps/Green7.xml rename to proplot/cmaps/Greens7.xml diff --git a/proplot/cmaps/Green8.xml b/proplot/cmaps/Greens8.xml similarity index 100% rename from proplot/cmaps/Green8.xml rename to proplot/cmaps/Greens8.xml diff --git a/proplot/cmaps/IceFire.rgb b/proplot/cmaps/IceFire.rgb index 302d6d2b0..9e27602a7 100644 --- a/proplot/cmaps/IceFire.rgb +++ b/proplot/cmaps/IceFire.rgb @@ -1,26 +1,26 @@ 0.73936227, 0.90443867, 0.85757238 0.72888063, 0.89639109, 0.85488394 -0.71834255, 0.88842162, 0.8521605 -0.70773866, 0.88052939, 0.849422 +0.71834255, 0.88842162, 0.8521605 +0.70773866, 0.88052939, 0.849422 0.69706215, 0.87271313, 0.84668315 0.68629021, 0.86497329, 0.84398721 0.67543654, 0.85730617, 0.84130969 0.66448539, 0.84971123, 0.83868005 0.65342679, 0.84218728, 0.83611512 0.64231804, 0.83471867, 0.83358584 -0.63117745, 0.827294 , 0.83113431 +0.63117745, 0.827294, 0.83113431 0.62000484, 0.81991069, 0.82876741 0.60879435, 0.81256797, 0.82648905 0.59754118, 0.80526458, 0.82430414 0.58624247, 0.79799884, 0.82221573 -0.57489525, 0.7907688 , 0.82022901 +0.57489525, 0.7907688, 0.82022901 0.56349779, 0.78357215, 0.81834861 0.55204294, 0.77640827, 0.81657563 0.54052516, 0.76927562, 0.81491462 0.52894085, 0.76217215, 0.81336913 0.51728854, 0.75509528, 0.81194156 0.50555676, 0.74804469, 0.81063503 -0.49373871, 0.7410187 , 0.80945242 +0.49373871, 0.7410187, 0.80945242 0.48183174, 0.73401449, 0.80839675 0.46982587, 0.72703075, 0.80747097 0.45770893, 0.72006648, 0.80667756 @@ -28,33 +28,33 @@ 0.43318643, 0.70617126, 0.80549278 0.42110294, 0.69916972, 0.80506683 0.40925101, 0.69211059, 0.80473246 -0.3976693 , 0.68498786, 0.80448272 +0.3976693, 0.68498786, 0.80448272 0.38632002, 0.67781125, 0.80431024 0.37523981, 0.67057537, 0.80420832 0.36442578, 0.66328229, 0.80417474 0.35385939, 0.65593699, 0.80420591 -0.34358916, 0.64853177, 0.8043 +0.34358916, 0.64853177, 0.8043 0.33355526, 0.64107876, 0.80445484 0.32383062, 0.63356578, 0.80467091 -0.31434372, 0.62600624, 0.8049475 -0.30516161, 0.618389 , 0.80528692 +0.31434372, 0.62600624, 0.8049475 +0.30516161, 0.618389, 0.80528692 0.29623491, 0.61072284, 0.80569021 0.28759072, 0.60300319, 0.80616055 0.27923924, 0.59522877, 0.80669803 -0.27114651, 0.5874047 , 0.80730545 +0.27114651, 0.5874047, 0.80730545 0.26337153, 0.57952055, 0.80799113 0.25588696, 0.57157984, 0.80875922 -0.248686 , 0.56358255, 0.80961366 +0.248686, 0.56358255, 0.80961366 0.24180668, 0.55552289, 0.81055123 -0.23526251, 0.54739477, 0.8115939 +0.23526251, 0.54739477, 0.8115939 0.22921445, 0.53918506, 0.81267292 -0.22397687, 0.53086094, 0.8137141 +0.22397687, 0.53086094, 0.8137141 0.21977058, 0.52241482, 0.81457651 0.21658989, 0.51384321, 0.81528511 0.21452772, 0.50514155, 0.81577278 0.21372783, 0.49630865, 0.81589566 0.21409503, 0.48734861, 0.81566163 -0.2157176 , 0.47827123, 0.81487615 +0.2157176, 0.47827123, 0.81487615 0.21842857, 0.46909168, 0.81351614 0.22211705, 0.45983212, 0.81146983 0.22665681, 0.45052233, 0.80860217 @@ -64,43 +64,43 @@ 0.24865068, 0.41341842, 0.78869164 0.25423116, 0.40433127, 0.78155831 0.25950239, 0.39535521, 0.77376848 -0.2644736 , 0.38651212, 0.76524809 +0.2644736, 0.38651212, 0.76524809 0.26901584, 0.37779582, 0.75621942 -0.27318141, 0.36922056, 0.746605 -0.27690355, 0.3607736 , 0.73659374 +0.27318141, 0.36922056, 0.746605 +0.27690355, 0.3607736, 0.73659374 0.28023585, 0.35244234, 0.72622103 0.28306009, 0.34438449, 0.71500731 0.28535896, 0.33660243, 0.70303975 0.28708711, 0.32912157, 0.69034504 0.28816354, 0.32200604, 0.67684067 0.28862749, 0.31519824, 0.66278813 -0.28847904, 0.30869064, 0.6482815 +0.28847904, 0.30869064, 0.6482815 0.28770912, 0.30250126, 0.63331265 0.28640325, 0.29655509, 0.61811374 0.28458943, 0.29082155, 0.60280913 0.28233561, 0.28527482, 0.58742866 -0.27967038, 0.2798938 , 0.57204225 +0.27967038, 0.2798938, 0.57204225 0.27665361, 0.27465357, 0.55667809 -0.27332564, 0.2695165 , 0.54145387 +0.27332564, 0.2695165, 0.54145387 0.26973851, 0.26447054, 0.52634916 -0.2659204 , 0.25949691, 0.511417 +0.2659204, 0.25949691, 0.511417 0.26190145, 0.25458123, 0.49668768 -0.2577151 , 0.24971691, 0.48214874 +0.2577151, 0.24971691, 0.48214874 0.25337618, 0.24490494, 0.46778758 0.24890842, 0.24013332, 0.45363816 -0.24433654, 0.23539226, 0.4397245 -0.23967922, 0.23067729, 0.4260591 +0.24433654, 0.23539226, 0.4397245 +0.23967922, 0.23067729, 0.4260591 0.23495608, 0.22598894, 0.41262952 0.23018113, 0.22132414, 0.39945577 0.22534609, 0.21670847, 0.38645794 0.22048761, 0.21211723, 0.37372555 -0.2156198 , 0.20755389, 0.36125301 +0.2156198, 0.20755389, 0.36125301 0.21074637, 0.20302717, 0.34903192 0.20586893, 0.19855368, 0.33701661 0.20101757, 0.19411573, 0.32529173 0.19619947, 0.18972425, 0.31383846 0.19140726, 0.18540157, 0.30260777 -0.1866769 , 0.1811332 , 0.29166583 +0.1866769, 0.1811332, 0.29166583 0.18201285, 0.17694992, 0.28088776 0.17745228, 0.17282141, 0.27044211 0.17300684, 0.16876921, 0.26024893 @@ -117,22 +117,22 @@ 0.13405762, 0.13188822, 0.16820842 0.13165767, 0.12950667, 0.16183275 0.12948748, 0.12733187, 0.15580631 -0.12755435, 0.1253723 , 0.15014098 -0.12586516, 0.12363617, 0.1448459 +0.12755435, 0.1253723, 0.15014098 +0.12586516, 0.12363617, 0.1448459 0.12442647, 0.12213143, 0.13992571 0.12324241, 0.12086419, 0.13539995 0.12232067, 0.11984278, 0.13124644 0.12166209, 0.11907077, 0.12749671 0.12126982, 0.11855309, 0.12415079 -0.12114244, 0.11829179, 0.1212385 +0.12114244, 0.11829179, 0.1212385 0.12127766, 0.11828837, 0.11878534 -0.12284806, 0.1179729 , 0.11772022 +0.12284806, 0.1179729, 0.11772022 0.12619498, 0.11721796, 0.11770203 -0.129968 , 0.11663788, 0.11792377 +0.129968, 0.11663788, 0.11792377 0.13410011, 0.11625146, 0.11839138 0.13855459, 0.11606618, 0.11910584 -0.14333775, 0.11607038, 0.1200606 -0.148417 , 0.11626929, 0.12125453 +0.14333775, 0.11607038, 0.1200606 +0.148417, 0.11626929, 0.12125453 0.15377389, 0.11666192, 0.12268364 0.15941427, 0.11723486, 0.12433911 0.16533376, 0.11797856, 0.12621303 @@ -140,7 +140,7 @@ 0.17797765, 0.11994436, 0.13058435 0.18468769, 0.12114722, 0.13306426 0.19165663, 0.12247737, 0.13572616 -0.19884415, 0.12394381, 0.1385669 +0.19884415, 0.12394381, 0.1385669 0.20627181, 0.12551883, 0.14157124 0.21394877, 0.12718055, 0.14472604 0.22184572, 0.12893119, 0.14802579 @@ -150,37 +150,37 @@ 0.25546457, 0.13661751, 0.16248722 0.26433628, 0.13865956, 0.16637301 0.27341345, 0.14070412, 0.17034221 -0.28264773, 0.14277192, 0.1743957 +0.28264773, 0.14277192, 0.1743957 0.29202272, 0.14486161, 0.17852793 -0.30159648, 0.14691224, 0.1827169 +0.30159648, 0.14691224, 0.1827169 0.31129002, 0.14897583, 0.18695213 0.32111555, 0.15103351, 0.19119629 -0.33107961, 0.1530674 , 0.19543758 -0.34119892, 0.15504762, 0.1996803 +0.33107961, 0.1530674, 0.19543758 +0.34119892, 0.15504762, 0.1996803 0.35142388, 0.15701131, 0.20389086 -0.36178937, 0.1589124 , 0.20807639 +0.36178937, 0.1589124, 0.20807639 0.37229381, 0.16073993, 0.21223189 -0.38288348, 0.16254006, 0.2163249 +0.38288348, 0.16254006, 0.2163249 0.39359592, 0.16426336, 0.22036577 0.40444332, 0.16588767, 0.22434027 -0.41537995, 0.16745325, 0.2282297 +0.41537995, 0.16745325, 0.2282297 0.42640867, 0.16894939, 0.23202755 0.43754706, 0.17034847, 0.23572899 -0.44878564, 0.1716535 , 0.23932344 -0.4601126 , 0.17287365, 0.24278607 +0.44878564, 0.1716535, 0.23932344 +0.4601126, 0.17287365, 0.24278607 0.47151732, 0.17401641, 0.24610337 -0.48300689, 0.17506676, 0.2492737 +0.48300689, 0.17506676, 0.2492737 0.49458302, 0.17601892, 0.25227688 -0.50623876, 0.17687777, 0.255096 -0.5179623 , 0.17765528, 0.2577162 -0.52975234, 0.17835232, 0.2601134 +0.50623876, 0.17687777, 0.255096 +0.5179623, 0.17765528, 0.2577162 +0.52975234, 0.17835232, 0.2601134 0.54159776, 0.17898292, 0.26226847 0.55348804, 0.17956232, 0.26416003 0.56541729, 0.18010175, 0.26575971 -0.57736669, 0.180631 , 0.26704888 +0.57736669, 0.180631, 0.26704888 0.58932081, 0.18117827, 0.26800409 0.60127582, 0.18175888, 0.26858488 -0.61319563, 0.1824336 , 0.2687872 +0.61319563, 0.1824336, 0.2687872 0.62506376, 0.18324015, 0.26858301 0.63681202, 0.18430173, 0.26795276 0.64842603, 0.18565472, 0.26689463 @@ -194,7 +194,7 @@ 0.73407638, 0.21145051, 0.24710661 0.74396983, 0.21631913, 0.24380715 0.75361506, 0.22163653, 0.24043996 -0.7630579 , 0.22731637, 0.23700095 +0.7630579, 0.22731637, 0.23700095 0.77222228, 0.23346231, 0.23356628 0.78115441, 0.23998404, 0.23013825 0.78979746, 0.24694858, 0.22678822 @@ -203,54 +203,54 @@ 0.81417437, 0.27001406, 0.21744645 0.82177364, 0.27837336, 0.21468316 0.82915955, 0.28696963, 0.21210766 -0.83628628, 0.2958499 , 0.20977813 +0.83628628, 0.2958499, 0.20977813 0.84322168, 0.30491136, 0.20766435 -0.84995458, 0.31415945, 0.2057863 +0.84995458, 0.31415945, 0.2057863 0.85648867, 0.32358058, 0.20415327 0.86286243, 0.33312058, 0.20274969 0.86908321, 0.34276705, 0.20157271 -0.87512876, 0.3525416 , 0.20064949 +0.87512876, 0.3525416, 0.20064949 0.88100349, 0.36243385, 0.19999078 -0.8866469 , 0.37249496, 0.1997976 +0.8866469, 0.37249496, 0.1997976 0.89203964, 0.38273475, 0.20013431 0.89713496, 0.39318156, 0.20121514 0.90195099, 0.40380687, 0.20301555 0.90648379, 0.41460191, 0.20558847 -0.9106967 , 0.42557857, 0.20918529 +0.9106967, 0.42557857, 0.20918529 0.91463791, 0.43668557, 0.21367954 0.91830723, 0.44790913, 0.21916352 0.92171507, 0.45922856, 0.22568002 -0.92491786, 0.4705936 , 0.23308207 +0.92491786, 0.4705936, 0.23308207 0.92790792, 0.48200153, 0.24145932 0.93073701, 0.49341219, 0.25065486 -0.93343918, 0.5048017 , 0.26056148 +0.93343918, 0.5048017, 0.26056148 0.93602064, 0.51616486, 0.27118485 0.93850535, 0.52748892, 0.28242464 0.94092933, 0.53875462, 0.29416042 -0.94330011, 0.5499628 , 0.30634189 +0.94330011, 0.5499628, 0.30634189 0.94563159, 0.56110987, 0.31891624 0.94792955, 0.57219822, 0.33184256 -0.95020929, 0.5832232 , 0.34508419 +0.95020929, 0.5832232, 0.34508419 0.95247324, 0.59419035, 0.35859866 0.95471709, 0.60510869, 0.37236035 0.95698411, 0.61595766, 0.38629631 0.95923863, 0.62676473, 0.40043317 -0.9615041 , 0.6375203 , 0.41474106 +0.9615041, 0.6375203, 0.41474106 0.96371553, 0.64826619, 0.42928335 0.96591497, 0.65899621, 0.44380444 0.96809871, 0.66971662, 0.45830232 -0.9702495 , 0.6804394 , 0.47280492 -0.9723881 , 0.69115622, 0.48729272 +0.9702495, 0.6804394, 0.47280492 +0.9723881, 0.69115622, 0.48729272 0.97450723, 0.70187358, 0.50178034 -0.9766108 , 0.712592 , 0.51626837 +0.9766108, 0.712592, 0.51626837 0.97871716, 0.72330511, 0.53074053 0.98082222, 0.73401769, 0.54520694 -0.9829001 , 0.74474445, 0.5597019 +0.9829001, 0.74474445, 0.5597019 0.98497466, 0.75547635, 0.57420239 0.98705581, 0.76621129, 0.58870185 0.98913325, 0.77695637, 0.60321626 0.99119918, 0.78771716, 0.61775821 -0.9932672 , 0.79848979, 0.63231691 +0.9932672, 0.79848979, 0.63231691 0.99535958, 0.80926704, 0.64687278 0.99740544, 0.82008078, 0.66150571 -0.9992197 , 0.83100723, 0.6764127 +0.9992197, 0.83100723, 0.6764127 diff --git a/proplot/cmaps/Mako.rgb b/proplot/cmaps/Mako.rgb index 533b0009a..95fd7701e 100644 --- a/proplot/cmaps/Mako.rgb +++ b/proplot/cmaps/Mako.rgb @@ -3,22 +3,22 @@ 0.05356262, 0.01950702, 0.03018802 0.05774337, 0.02205989, 0.03545515 0.06188095, 0.02474764, 0.04115287 -0.06598247, 0.0275665 , 0.04691409 +0.06598247, 0.0275665, 0.04691409 0.07005374, 0.03051278, 0.05264306 0.07409947, 0.03358324, 0.05834631 0.07812339, 0.03677446, 0.06403249 -0.08212852, 0.0400833 , 0.06970862 +0.08212852, 0.0400833, 0.06970862 0.08611731, 0.04339148, 0.07538208 0.09009161, 0.04664706, 0.08105568 0.09405308, 0.04985685, 0.08673591 0.09800301, 0.05302279, 0.09242646 0.10194255, 0.05614641, 0.09813162 -0.10587261, 0.05922941, 0.103854 -0.1097942 , 0.06227277, 0.10959847 +0.10587261, 0.05922941, 0.103854 +0.1097942, 0.06227277, 0.10959847 0.11370826, 0.06527747, 0.11536893 0.11761516, 0.06824548, 0.12116393 0.12151575, 0.07117741, 0.12698763 -0.12541095, 0.07407363, 0.1328442 +0.12541095, 0.07407363, 0.1328442 0.12930083, 0.07693611, 0.13873064 0.13317849, 0.07976988, 0.14465095 0.13701138, 0.08259683, 0.15060265 @@ -33,11 +33,11 @@ 0.16914226, 0.10805832, 0.20589698 0.17243586, 0.11091443, 0.21221911 0.17566717, 0.11378321, 0.21857219 -0.17884322, 0.11666074, 0.2249565 +0.17884322, 0.11666074, 0.2249565 0.18195582, 0.11955283, 0.23136943 0.18501213, 0.12245547, 0.23781116 0.18800459, 0.12537395, 0.24427914 -0.19093944, 0.1283047 , 0.25077369 +0.19093944, 0.1283047, 0.25077369 0.19381092, 0.13125179, 0.25729255 0.19662307, 0.13421303, 0.26383543 0.19937337, 0.13719028, 0.27040111 @@ -49,24 +49,24 @@ 0.21458611, 0.15539445, 0.31023563 0.21690827, 0.15848519, 0.31694355 0.21916481, 0.16159489, 0.32366939 -0.2213631 , 0.16471913, 0.33041431 -0.22349947, 0.1678599 , 0.33717781 -0.2255714 , 0.1710185 , 0.34395925 +0.2213631, 0.16471913, 0.33041431 +0.22349947, 0.1678599, 0.33717781 +0.2255714, 0.1710185, 0.34395925 0.22758415, 0.17419169, 0.35075983 0.22953569, 0.17738041, 0.35757941 -0.23142077, 0.18058733, 0.3644173 -0.2332454 , 0.18380872, 0.37127514 -0.2350092 , 0.18704459, 0.3781528 -0.23670785, 0.190297 , 0.38504973 +0.23142077, 0.18058733, 0.3644173 +0.2332454, 0.18380872, 0.37127514 +0.2350092, 0.18704459, 0.3781528 +0.23670785, 0.190297, 0.38504973 0.23834119, 0.19356547, 0.39196711 0.23991189, 0.19684817, 0.39890581 -0.24141903, 0.20014508, 0.4058667 -0.24286214, 0.20345642, 0.4128484 +0.24141903, 0.20014508, 0.4058667 +0.24286214, 0.20345642, 0.4128484 0.24423453, 0.20678459, 0.41985299 0.24554109, 0.21012669, 0.42688124 -0.2467815 , 0.21348266, 0.43393244 -0.24795393, 0.21685249, 0.4410088 -0.24905614, 0.22023618, 0.448113 +0.2467815, 0.21348266, 0.43393244 +0.24795393, 0.21685249, 0.4410088 +0.24905614, 0.22023618, 0.448113 0.25007383, 0.22365053, 0.45519562 0.25098926, 0.22710664, 0.46223892 0.25179696, 0.23060342, 0.46925447 @@ -74,13 +74,13 @@ 0.25307401, 0.23772973, 0.48316271 0.25353152, 0.24136961, 0.49001976 0.25386167, 0.24506548, 0.49679407 -0.25406082, 0.2488164 , 0.50348932 +0.25406082, 0.2488164, 0.50348932 0.25412435, 0.25262843, 0.51007843 0.25404842, 0.25650743, 0.51653282 0.25383134, 0.26044852, 0.52286845 -0.2534705 , 0.26446165, 0.52903422 -0.25296722, 0.2685428 , 0.53503572 -0.2523226 , 0.27269346, 0.54085315 +0.2534705, 0.26446165, 0.52903422 +0.25296722, 0.2685428, 0.53503572 +0.2523226, 0.27269346, 0.54085315 0.25153974, 0.27691629, 0.54645752 0.25062402, 0.28120467, 0.55185939 0.24958205, 0.28556371, 0.55701246 @@ -90,21 +90,21 @@ 0.24436202, 0.30357852, 0.57519929 0.24285591, 0.30819938, 0.57913247 0.24129828, 0.31286235, 0.58278615 -0.23970131, 0.3175495 , 0.5862272 +0.23970131, 0.3175495, 0.5862272 0.23807973, 0.32226344, 0.58941872 0.23644557, 0.32699241, 0.59240198 -0.2348113 , 0.33173196, 0.59518282 +0.2348113, 0.33173196, 0.59518282 0.23318874, 0.33648036, 0.59775543 -0.2315855 , 0.34122763, 0.60016456 +0.2315855, 0.34122763, 0.60016456 0.23001121, 0.34597357, 0.60240251 -0.2284748 , 0.35071512, 0.6044784 +0.2284748, 0.35071512, 0.6044784 0.22698081, 0.35544612, 0.60642528 0.22553305, 0.36016515, 0.60825252 0.22413977, 0.36487341, 0.60994938 0.22280246, 0.36956728, 0.61154118 0.22152555, 0.37424409, 0.61304472 0.22030752, 0.37890437, 0.61446646 -0.2191538 , 0.38354668, 0.61581561 +0.2191538, 0.38354668, 0.61581561 0.21806257, 0.38817169, 0.61709794 0.21703799, 0.39277882, 0.61831922 0.21607792, 0.39736958, 0.61948028 @@ -114,7 +114,7 @@ 0.21288172, 0.41555771, 0.62373011 0.21223835, 0.42006355, 0.62471794 0.21165312, 0.42455441, 0.62568371 -0.21112526, 0.42903064, 0.6266318 +0.21112526, 0.42903064, 0.6266318 0.21065161, 0.43349321, 0.62756504 0.21023306, 0.43794288, 0.62848279 0.20985996, 0.44238227, 0.62938329 @@ -129,7 +129,7 @@ 0.20692679, 0.48201774, 0.63812656 0.20663156, 0.48640018, 0.63914367 0.20634336, 0.49078002, 0.64016638 -0.20606303, 0.49515755, 0.6411939 +0.20606303, 0.49515755, 0.6411939 0.20578999, 0.49953341, 0.64222457 0.20552612, 0.50390766, 0.64325811 0.20527189, 0.50828072, 0.64429331 @@ -141,15 +141,15 @@ 0.20401238, 0.53450825, 0.65048638 0.20385896, 0.53887991, 0.65150606 0.20372653, 0.54325208, 0.65251978 -0.20361709, 0.5476249 , 0.6535266 +0.20361709, 0.5476249, 0.6535266 0.20353258, 0.55199854, 0.65452542 -0.20347472, 0.55637318, 0.655515 +0.20347472, 0.55637318, 0.655515 0.20344718, 0.56074869, 0.65649508 0.20345161, 0.56512531, 0.65746419 0.20349089, 0.56950304, 0.65842151 0.20356842, 0.57388184, 0.65936642 0.20368663, 0.57826181, 0.66029768 -0.20384884, 0.58264293, 0.6612145 +0.20384884, 0.58264293, 0.6612145 0.20405904, 0.58702506, 0.66211645 0.20431921, 0.59140842, 0.66300179 0.20463464, 0.59579264, 0.66387079 @@ -160,19 +160,19 @@ 0.20721003, 0.61772167, 0.66792838 0.20795035, 0.62210845, 0.66867802 0.20877302, 0.62649546, 0.66940555 -0.20968223, 0.63088252, 0.6701105 +0.20968223, 0.63088252, 0.6701105 0.21068163, 0.63526951, 0.67079211 0.21177544, 0.63965621, 0.67145005 0.21298582, 0.64404072, 0.67208182 0.21430361, 0.64842404, 0.67268861 0.21572716, 0.65280655, 0.67326978 -0.21726052, 0.65718791, 0.6738255 +0.21726052, 0.65718791, 0.6738255 0.21890636, 0.66156803, 0.67435491 -0.220668 , 0.66594665, 0.67485792 +0.220668, 0.66594665, 0.67485792 0.22255447, 0.67032297, 0.67533374 0.22458372, 0.67469531, 0.67578061 0.22673713, 0.67906542, 0.67620044 -0.22901625, 0.6834332 , 0.67659251 +0.22901625, 0.6834332, 0.67659251 0.23142316, 0.68779836, 0.67695703 0.23395924, 0.69216072, 0.67729378 0.23663857, 0.69651881, 0.67760151 @@ -189,68 +189,68 @@ 0.27397488, 0.74429484, 0.67917096 0.27822463, 0.74860229, 0.67914468 0.28264201, 0.75290034, 0.67907959 -0.2873016 , 0.75717817, 0.67899164 +0.2873016, 0.75717817, 0.67899164 0.29215894, 0.76144162, 0.67886578 0.29729823, 0.76567816, 0.67871894 0.30268199, 0.76989232, 0.67853896 0.30835665, 0.77407636, 0.67833512 0.31435139, 0.77822478, 0.67811118 -0.3206671 , 0.78233575, 0.67786729 +0.3206671, 0.78233575, 0.67786729 0.32733158, 0.78640315, 0.67761027 0.33437168, 0.79042043, 0.67734882 0.34182112, 0.79437948, 0.67709394 0.34968889, 0.79827511, 0.67685638 0.35799244, 0.80210037, 0.67664969 0.36675371, 0.80584651, 0.67649539 -0.3759816 , 0.80950627, 0.67641393 +0.3759816, 0.80950627, 0.67641393 0.38566792, 0.81307432, 0.67642947 0.39579804, 0.81654592, 0.67656899 0.40634556, 0.81991799, 0.67686215 0.41730243, 0.82318339, 0.67735255 -0.4285828 , 0.82635051, 0.6780564 +0.4285828, 0.82635051, 0.6780564 0.44012728, 0.82942353, 0.67900049 0.45189421, 0.83240398, 0.68021733 -0.46378379, 0.83530763, 0.6817062 +0.46378379, 0.83530763, 0.6817062 0.47573199, 0.83814472, 0.68347352 0.48769865, 0.84092197, 0.68552698 0.49962354, 0.84365379, 0.68783929 -0.5114027 , 0.8463718 , 0.69029789 +0.5114027, 0.8463718, 0.69029789 0.52301693, 0.84908401, 0.69288545 0.53447549, 0.85179048, 0.69561066 -0.54578602, 0.8544913 , 0.69848331 +0.54578602, 0.8544913, 0.69848331 0.55695565, 0.85718723, 0.70150427 0.56798832, 0.85987893, 0.70468261 0.57888639, 0.86256715, 0.70802931 -0.5896541 , 0.8652532 , 0.71154204 +0.5896541, 0.8652532, 0.71154204 0.60028928, 0.86793835, 0.71523675 0.61079441, 0.87062438, 0.71910895 0.62116633, 0.87331311, 0.72317003 0.63140509, 0.87600675, 0.72741689 0.64150735, 0.87870746, 0.73185717 -0.65147219, 0.8814179 , 0.73648495 -0.66129632, 0.8841403 , 0.74130658 +0.65147219, 0.8814179, 0.73648495 +0.66129632, 0.8841403, 0.74130658 0.67097934, 0.88687758, 0.74631123 0.68051833, 0.88963189, 0.75150483 0.68991419, 0.89240612, 0.75687187 0.69916533, 0.89520211, 0.76241714 0.70827373, 0.89802257, 0.76812286 0.71723995, 0.90086891, 0.77399039 -0.72606665, 0.90374337, 0.7800041 +0.72606665, 0.90374337, 0.7800041 0.73475675, 0.90664718, 0.78615802 0.74331358, 0.90958151, 0.79244474 0.75174143, 0.91254787, 0.79884925 0.76004473, 0.91554656, 0.80536823 0.76827704, 0.91856549, 0.81196513 -0.77647029, 0.921603 , 0.81855729 +0.77647029, 0.921603, 0.81855729 0.78462009, 0.92466151, 0.82514119 0.79273542, 0.92773848, 0.83172131 -0.8008109 , 0.93083672, 0.83829355 +0.8008109, 0.93083672, 0.83829355 0.80885107, 0.93395528, 0.84485982 -0.81685878, 0.9370938 , 0.85142101 -0.82483206, 0.94025378, 0.8579751 +0.81685878, 0.9370938, 0.85142101 +0.82483206, 0.94025378, 0.8579751 0.83277661, 0.94343371, 0.86452477 0.84069127, 0.94663473, 0.87106853 -0.84857662, 0.9498573 , 0.8776059 -0.8564431 , 0.95309792, 0.88414253 +0.84857662, 0.9498573, 0.8776059 +0.8564431, 0.95309792, 0.88414253 0.86429066, 0.95635719, 0.89067759 0.87218969, 0.95960708, 0.89725384 diff --git a/proplot/cmaps/GrayCycle.txt b/proplot/cmaps/MonoCycle.txt similarity index 100% rename from proplot/cmaps/GrayCycle.txt rename to proplot/cmaps/MonoCycle.txt diff --git a/proplot/cmaps/Orange5.xml b/proplot/cmaps/Oranges1.xml similarity index 100% rename from proplot/cmaps/Orange5.xml rename to proplot/cmaps/Oranges1.xml diff --git a/proplot/cmaps/Orange4.xml b/proplot/cmaps/Oranges2.xml similarity index 100% rename from proplot/cmaps/Orange4.xml rename to proplot/cmaps/Oranges2.xml diff --git a/proplot/cmaps/Orange6.xml b/proplot/cmaps/Oranges3.xml similarity index 100% rename from proplot/cmaps/Orange6.xml rename to proplot/cmaps/Oranges3.xml diff --git a/proplot/cmaps/Orange7.xml b/proplot/cmaps/Oranges4.xml similarity index 100% rename from proplot/cmaps/Orange7.xml rename to proplot/cmaps/Oranges4.xml diff --git a/proplot/cmaps/RedPurple8_r.xml b/proplot/cmaps/Purples1_r.xml similarity index 100% rename from proplot/cmaps/RedPurple8_r.xml rename to proplot/cmaps/Purples1_r.xml diff --git a/proplot/cmaps/RedPurple6.xml b/proplot/cmaps/Purples2.xml similarity index 100% rename from proplot/cmaps/RedPurple6.xml rename to proplot/cmaps/Purples2.xml diff --git a/proplot/cmaps/RedPurple7.xml b/proplot/cmaps/Purples3.xml similarity index 100% rename from proplot/cmaps/RedPurple7.xml rename to proplot/cmaps/Purples3.xml diff --git a/proplot/cmaps/RedPurple1.xml b/proplot/cmaps/Reds1.xml similarity index 100% rename from proplot/cmaps/RedPurple1.xml rename to proplot/cmaps/Reds1.xml diff --git a/proplot/cmaps/RedPurple2.xml b/proplot/cmaps/Reds2.xml similarity index 100% rename from proplot/cmaps/RedPurple2.xml rename to proplot/cmaps/Reds2.xml diff --git a/proplot/cmaps/RedPurple3.xml b/proplot/cmaps/Reds3.xml similarity index 100% rename from proplot/cmaps/RedPurple3.xml rename to proplot/cmaps/Reds3.xml diff --git a/proplot/cmaps/RedPurple4.xml b/proplot/cmaps/Reds4.xml similarity index 100% rename from proplot/cmaps/RedPurple4.xml rename to proplot/cmaps/Reds4.xml diff --git a/proplot/cmaps/RedPurple5.xml b/proplot/cmaps/Reds5.xml similarity index 100% rename from proplot/cmaps/RedPurple5.xml rename to proplot/cmaps/Reds5.xml diff --git a/proplot/cmaps/Rocket.rgb b/proplot/cmaps/Rocket.rgb index 2faff12d8..1deb31eb5 100644 --- a/proplot/cmaps/Rocket.rgb +++ b/proplot/cmaps/Rocket.rgb @@ -1,6 +1,6 @@ 0.01060815, 0.01808215, 0.10018654 0.01428972, 0.02048237, 0.10374486 -0.01831941, 0.0229766 , 0.10738511 +0.01831941, 0.0229766, 0.10738511 0.02275049, 0.02554464, 0.11108639 0.02759119, 0.02818316, 0.11483751 0.03285175, 0.03088792, 0.11863035 @@ -8,27 +8,27 @@ 0.04447016, 0.03648425, 0.12631831 0.05032105, 0.03936808, 0.13020508 0.05611171, 0.04224835, 0.13411624 -0.0618531 , 0.04504866, 0.13804929 +0.0618531, 0.04504866, 0.13804929 0.06755457, 0.04778179, 0.14200206 -0.0732236 , 0.05045047, 0.14597263 -0.0788708 , 0.05305461, 0.14995981 +0.0732236, 0.05045047, 0.14597263 +0.0788708, 0.05305461, 0.14995981 0.08450105, 0.05559631, 0.15396203 0.09011319, 0.05808059, 0.15797687 0.09572396, 0.06050127, 0.16200507 0.10132312, 0.06286782, 0.16604287 0.10692823, 0.06517224, 0.17009175 -0.1125315 , 0.06742194, 0.17414848 +0.1125315, 0.06742194, 0.17414848 0.11813947, 0.06961499, 0.17821272 0.12375803, 0.07174938, 0.18228425 0.12938228, 0.07383015, 0.18636053 0.13501631, 0.07585609, 0.19044109 -0.14066867, 0.0778224 , 0.19452676 -0.14633406, 0.07973393, 0.1986151 +0.14066867, 0.0778224, 0.19452676 +0.14633406, 0.07973393, 0.1986151 0.15201338, 0.08159108, 0.20270523 0.15770877, 0.08339312, 0.20679668 -0.16342174, 0.0851396 , 0.21088893 +0.16342174, 0.0851396, 0.21088893 0.16915387, 0.08682996, 0.21498104 -0.17489524, 0.08848235, 0.2190294 +0.17489524, 0.08848235, 0.2190294 0.18065495, 0.09009031, 0.22303512 0.18643324, 0.09165431, 0.22699705 0.19223028, 0.09317479, 0.23091409 @@ -36,34 +36,34 @@ 0.20388117, 0.09608689, 0.23860907 0.20973515, 0.09747934, 0.24238489 0.21560818, 0.09882993, 0.24611154 -0.22150014, 0.10013944, 0.2497868 +0.22150014, 0.10013944, 0.2497868 0.22741085, 0.10140876, 0.25340813 0.23334047, 0.10263737, 0.25697736 -0.23928891, 0.10382562, 0.2604936 +0.23928891, 0.10382562, 0.2604936 0.24525608, 0.10497384, 0.26395596 0.25124182, 0.10608236, 0.26736359 0.25724602, 0.10715148, 0.27071569 -0.26326851, 0.1081815 , 0.27401148 -0.26930915, 0.1091727 , 0.2772502 +0.26326851, 0.1081815, 0.27401148 +0.26930915, 0.1091727, 0.2772502 0.27536766, 0.11012568, 0.28043021 -0.28144375, 0.11104133, 0.2835489 -0.2875374 , 0.11191896, 0.28660853 -0.29364846, 0.11275876, 0.2896085 +0.28144375, 0.11104133, 0.2835489 +0.2875374, 0.11191896, 0.28660853 +0.29364846, 0.11275876, 0.2896085 0.29977678, 0.11356089, 0.29254823 0.30592213, 0.11432553, 0.29542718 0.31208435, 0.11505284, 0.29824485 -0.31826327, 0.1157429 , 0.30100076 +0.31826327, 0.1157429, 0.30100076 0.32445869, 0.11639585, 0.30369448 0.33067031, 0.11701189, 0.30632563 -0.33689808, 0.11759095, 0.3088938 +0.33689808, 0.11759095, 0.3088938 0.34314168, 0.11813362, 0.31139721 -0.34940101, 0.11863987, 0.3138355 -0.355676 , 0.11910909, 0.31620996 -0.36196644, 0.1195413 , 0.31852037 +0.34940101, 0.11863987, 0.3138355 +0.355676, 0.11910909, 0.31620996 +0.36196644, 0.1195413, 0.31852037 0.36827206, 0.11993653, 0.32076656 0.37459292, 0.12029443, 0.32294825 0.38092887, 0.12061482, 0.32506528 -0.38727975, 0.12089756, 0.3271175 +0.38727975, 0.12089756, 0.3271175 0.39364518, 0.12114272, 0.32910494 0.40002537, 0.12134964, 0.33102734 0.40642019, 0.12151801, 0.33288464 @@ -83,16 +83,16 @@ 0.49742847, 0.11955087, 0.35206533 0.50403286, 0.11907081, 0.35295152 0.51065109, 0.11853959, 0.35377385 -0.51728314, 0.1179558 , 0.35453252 +0.51728314, 0.1179558, 0.35453252 0.52392883, 0.11731817, 0.35522789 0.53058853, 0.11662445, 0.35585982 0.53726173, 0.11587369, 0.35642903 0.54394898, 0.11506307, 0.35693521 -0.5506426 , 0.11420757, 0.35737863 +0.5506426, 0.11420757, 0.35737863 0.55734473, 0.11330456, 0.35775059 0.56405586, 0.11235265, 0.35804813 0.57077365, 0.11135597, 0.35827146 -0.5774991 , 0.11031233, 0.35841679 +0.5774991, 0.11031233, 0.35841679 0.58422945, 0.10922707, 0.35848469 0.59096382, 0.10810205, 0.35847347 0.59770215, 0.10693774, 0.35838029 @@ -102,20 +102,20 @@ 0.62466162, 0.10197244, 0.35716891 0.63139686, 0.10067417, 0.35664819 0.63812122, 0.09938212, 0.35603757 -0.64483795, 0.0980891 , 0.35533555 +0.64483795, 0.0980891, 0.35533555 0.65154562, 0.09680192, 0.35454107 -0.65824241, 0.09552918, 0.3536529 -0.66492652, 0.09428017, 0.3526697 +0.65824241, 0.09552918, 0.3536529 +0.66492652, 0.09428017, 0.3526697 0.67159578, 0.09306598, 0.35159077 -0.67824099, 0.09192342, 0.3504148 -0.684863 , 0.09085633, 0.34914061 -0.69146268, 0.0898675 , 0.34776864 -0.69803757, 0.08897226, 0.3462986 -0.70457834, 0.0882129 , 0.34473046 -0.71108138, 0.08761223, 0.3430635 -0.7175507 , 0.08716212, 0.34129974 +0.67824099, 0.09192342, 0.3504148 +0.684863, 0.09085633, 0.34914061 +0.69146268, 0.0898675, 0.34776864 +0.69803757, 0.08897226, 0.3462986 +0.70457834, 0.0882129, 0.34473046 +0.71108138, 0.08761223, 0.3430635 +0.7175507, 0.08716212, 0.34129974 0.72398193, 0.08688725, 0.33943958 -0.73035829, 0.0868623 , 0.33748452 +0.73035829, 0.0868623, 0.33748452 0.73669146, 0.08704683, 0.33543669 0.74297501, 0.08747196, 0.33329799 0.74919318, 0.08820542, 0.33107204 @@ -125,35 +125,35 @@ 0.77344838, 0.09405684, 0.32136808 0.77932641, 0.09634794, 0.31876642 0.78513609, 0.09892473, 0.31610488 -0.79085854, 0.10184672, 0.313391 -0.7965014 , 0.10506637, 0.31063031 -0.80205987, 0.10858333, 0.30783 +0.79085854, 0.10184672, 0.313391 +0.7965014, 0.10506637, 0.31063031 +0.80205987, 0.10858333, 0.30783 0.80752799, 0.11239964, 0.30499738 0.81291606, 0.11645784, 0.30213802 0.81820481, 0.12080606, 0.29926105 -0.82341472, 0.12535343, 0.2963705 +0.82341472, 0.12535343, 0.2963705 0.82852822, 0.13014118, 0.29347474 0.83355779, 0.13511035, 0.29057852 -0.83850183, 0.14025098, 0.2876878 +0.83850183, 0.14025098, 0.2876878 0.84335441, 0.14556683, 0.28480819 -0.84813096, 0.15099892, 0.281943 +0.84813096, 0.15099892, 0.281943 0.85281737, 0.15657772, 0.27909826 -0.85742602, 0.1622583 , 0.27627462 +0.85742602, 0.1622583, 0.27627462 0.86196552, 0.16801239, 0.27346473 0.86641628, 0.17387796, 0.27070818 0.87079129, 0.17982114, 0.26797378 0.87507281, 0.18587368, 0.26529697 0.87925878, 0.19203259, 0.26268136 -0.8833417 , 0.19830556, 0.26014181 +0.8833417, 0.19830556, 0.26014181 0.88731387, 0.20469941, 0.25769539 -0.89116859, 0.21121788, 0.2553592 +0.89116859, 0.21121788, 0.2553592 0.89490337, 0.21785614, 0.25314362 -0.8985026 , 0.22463251, 0.25108745 +0.8985026, 0.22463251, 0.25108745 0.90197527, 0.23152063, 0.24918223 0.90530097, 0.23854541, 0.24748098 0.90848638, 0.24568473, 0.24598324 -0.911533 , 0.25292623, 0.24470258 -0.9144225 , 0.26028902, 0.24369359 +0.911533, 0.25292623, 0.24470258 +0.9144225, 0.26028902, 0.24369359 0.91717106, 0.26773821, 0.24294137 0.91978131, 0.27526191, 0.24245973 0.92223947, 0.28287251, 0.24229568 @@ -161,24 +161,24 @@ 0.92676657, 0.29823282, 0.24285536 0.92882964, 0.30598085, 0.24362274 0.93078135, 0.31373977, 0.24468803 -0.93262051, 0.3215093 , 0.24606461 +0.93262051, 0.3215093, 0.24606461 0.93435067, 0.32928362, 0.24775328 0.93599076, 0.33703942, 0.24972157 0.93752831, 0.34479177, 0.25199928 0.93899289, 0.35250734, 0.25452808 0.94036561, 0.36020899, 0.25734661 -0.94167588, 0.36786594, 0.2603949 +0.94167588, 0.36786594, 0.2603949 0.94291042, 0.37549479, 0.26369821 -0.94408513, 0.3830811 , 0.26722004 +0.94408513, 0.3830811, 0.26722004 0.94520419, 0.39062329, 0.27094924 0.94625977, 0.39813168, 0.27489742 -0.94727016, 0.4055909 , 0.27902322 +0.94727016, 0.4055909, 0.27902322 0.94823505, 0.41300424, 0.28332283 0.94914549, 0.42038251, 0.28780969 0.95001704, 0.42771398, 0.29244728 0.95085121, 0.43500005, 0.29722817 0.95165009, 0.44224144, 0.30214494 -0.9524044 , 0.44944853, 0.3072105 +0.9524044, 0.44944853, 0.3072105 0.95312556, 0.45661389, 0.31239776 0.95381595, 0.46373781, 0.31769923 0.95447591, 0.47082238, 0.32310953 @@ -186,38 +186,38 @@ 0.95569679, 0.48489115, 0.33421404 0.95626788, 0.49187351, 0.33985601 0.95681685, 0.49882008, 0.34555431 -0.9573439 , 0.50573243, 0.35130912 +0.9573439, 0.50573243, 0.35130912 0.95784842, 0.51261283, 0.35711942 0.95833051, 0.51946267, 0.36298589 0.95879054, 0.52628305, 0.36890904 -0.95922872, 0.53307513, 0.3748895 +0.95922872, 0.53307513, 0.3748895 0.95964538, 0.53983991, 0.38092784 -0.96004345, 0.54657593, 0.3870292 +0.96004345, 0.54657593, 0.3870292 0.96042097, 0.55328624, 0.39319057 0.96077819, 0.55997184, 0.39941173 -0.9611152 , 0.5666337 , 0.40569343 +0.9611152, 0.5666337, 0.40569343 0.96143273, 0.57327231, 0.41203603 0.96173392, 0.57988594, 0.41844491 0.96201757, 0.58647675, 0.42491751 0.96228344, 0.59304598, 0.43145271 -0.96253168, 0.5995944 , 0.43805131 +0.96253168, 0.5995944, 0.43805131 0.96276513, 0.60612062, 0.44471698 -0.96298491, 0.6126247 , 0.45145074 +0.96298491, 0.6126247, 0.45145074 0.96318967, 0.61910879, 0.45824902 -0.96337949, 0.6255736 , 0.46511271 +0.96337949, 0.6255736, 0.46511271 0.96355923, 0.63201624, 0.47204746 0.96372785, 0.63843852, 0.47905028 -0.96388426, 0.64484214, 0.4861196 -0.96403203, 0.65122535, 0.4932578 +0.96388426, 0.64484214, 0.4861196 +0.96403203, 0.65122535, 0.4932578 0.96417332, 0.65758729, 0.50046894 -0.9643063 , 0.66393045, 0.5077467 +0.9643063, 0.66393045, 0.5077467 0.96443322, 0.67025402, 0.51509334 0.96455845, 0.67655564, 0.52251447 0.96467922, 0.68283846, 0.53000231 0.96479861, 0.68910113, 0.53756026 -0.96492035, 0.69534192, 0.5451917 -0.96504223, 0.7015636 , 0.5528892 -0.96516917, 0.70776351, 0.5606593 +0.96492035, 0.69534192, 0.5451917 +0.96504223, 0.7015636, 0.5528892 +0.96516917, 0.70776351, 0.5606593 0.96530224, 0.71394212, 0.56849894 0.96544032, 0.72010124, 0.57640375 0.96559206, 0.72623592, 0.58438387 @@ -231,10 +231,10 @@ 0.96739773, 0.77451297, 0.65057302 0.96773482, 0.78044149, 0.65912731 0.96810471, 0.78634563, 0.66773889 -0.96850919, 0.79222565, 0.6764046 +0.96850919, 0.79222565, 0.6764046 0.96893132, 0.79809112, 0.68512266 0.96935926, 0.80395415, 0.69383201 -0.9698028 , 0.80981139, 0.70252255 +0.9698028, 0.80981139, 0.70252255 0.97025511, 0.81566605, 0.71120296 0.97071849, 0.82151775, 0.71987163 0.97120159, 0.82736371, 0.72851999 @@ -246,11 +246,11 @@ 0.97441222, 0.86238689, 0.78015619 0.97501782, 0.86821321, 0.78871034 0.97564391, 0.87403763, 0.79725261 -0.97628674, 0.87986189, 0.8057883 +0.97628674, 0.87986189, 0.8057883 0.97696114, 0.88568129, 0.81430324 0.97765722, 0.89149971, 0.82280948 0.97837585, 0.89731727, 0.83130786 0.97912374, 0.90313207, 0.83979337 -0.979891 , 0.90894778, 0.84827858 +0.979891, 0.90894778, 0.84827858 0.98067764, 0.91476465, 0.85676611 0.98137749, 0.92061729, 0.86536915 diff --git a/proplot/cmaps/Vlag.rgb b/proplot/cmaps/Vlag.rgb index a4dc0f9b0..2f57cfe2a 100644 --- a/proplot/cmaps/Vlag.rgb +++ b/proplot/cmaps/Vlag.rgb @@ -1,124 +1,124 @@ 0.13850039, 0.41331206, 0.74052025 0.15077609, 0.41762684, 0.73970427 -0.16235219, 0.4219191 , 0.7389667 -0.1733322 , 0.42619024, 0.73832537 +0.16235219, 0.4219191, 0.7389667 +0.1733322, 0.42619024, 0.73832537 0.18382538, 0.43044226, 0.73776764 -0.19394034, 0.4346772 , 0.73725867 +0.19394034, 0.4346772, 0.73725867 0.20367115, 0.43889576, 0.73685314 0.21313625, 0.44310003, 0.73648045 0.22231173, 0.44729079, 0.73619681 0.23125148, 0.45146945, 0.73597803 -0.23998101, 0.45563715, 0.7358223 +0.23998101, 0.45563715, 0.7358223 0.24853358, 0.45979489, 0.73571524 -0.25691416, 0.4639437 , 0.73566943 +0.25691416, 0.4639437, 0.73566943 0.26513894, 0.46808455, 0.73568319 0.27322194, 0.47221835, 0.73575497 0.28117543, 0.47634598, 0.73588332 0.28901021, 0.48046826, 0.73606686 -0.2967358 , 0.48458597, 0.73630433 +0.2967358, 0.48458597, 0.73630433 0.30436071, 0.48869986, 0.73659451 -0.3118955 , 0.49281055, 0.73693255 +0.3118955, 0.49281055, 0.73693255 0.31935389, 0.49691847, 0.73730851 -0.32672701, 0.5010247 , 0.73774013 +0.32672701, 0.5010247, 0.73774013 0.33402607, 0.50512971, 0.73821941 0.34125337, 0.50923419, 0.73874905 0.34840921, 0.51333892, 0.73933402 0.35551826, 0.51744353, 0.73994642 -0.3625676 , 0.52154929, 0.74060763 +0.3625676, 0.52154929, 0.74060763 0.36956356, 0.52565656, 0.74131327 0.37649902, 0.52976642, 0.74207698 0.38340273, 0.53387791, 0.74286286 -0.39025859, 0.53799253, 0.7436962 -0.39706821, 0.54211081, 0.744578 +0.39025859, 0.53799253, 0.7436962 +0.39706821, 0.54211081, 0.744578 0.40384046, 0.54623277, 0.74549872 0.41058241, 0.55035849, 0.74645094 0.41728385, 0.55448919, 0.74745174 0.42395178, 0.55862494, 0.74849357 -0.4305964 , 0.56276546, 0.74956387 -0.4372044 , 0.56691228, 0.75068412 -0.4437909 , 0.57106468, 0.75183427 -0.45035117, 0.5752235 , 0.75302312 +0.4305964, 0.56276546, 0.74956387 +0.4372044, 0.56691228, 0.75068412 +0.4437909, 0.57106468, 0.75183427 +0.45035117, 0.5752235, 0.75302312 0.45687824, 0.57938983, 0.75426297 0.46339713, 0.58356191, 0.75551816 0.46988778, 0.58774195, 0.75682037 0.47635605, 0.59192986, 0.75816245 -0.48281101, 0.5961252 , 0.75953212 -0.4892374 , 0.60032986, 0.76095418 +0.48281101, 0.5961252, 0.75953212 +0.4892374, 0.60032986, 0.76095418 0.49566225, 0.60454154, 0.76238852 0.50206137, 0.60876307, 0.76387371 0.50845128, 0.61299312, 0.76538551 -0.5148258 , 0.61723272, 0.76693475 +0.5148258, 0.61723272, 0.76693475 0.52118385, 0.62148236, 0.76852436 0.52753571, 0.62574126, 0.77013939 0.53386831, 0.63001125, 0.77180152 -0.54020159, 0.63429038, 0.7734803 +0.54020159, 0.63429038, 0.7734803 0.54651272, 0.63858165, 0.77521306 0.55282975, 0.64288207, 0.77695608 0.55912585, 0.64719519, 0.77875327 0.56542599, 0.65151828, 0.78056551 0.57170924, 0.65585426, 0.78242747 -0.57799572, 0.6602009 , 0.78430751 +0.57799572, 0.6602009, 0.78430751 0.58426817, 0.66456073, 0.78623458 -0.590544 , 0.66893178, 0.78818117 +0.590544, 0.66893178, 0.78818117 0.59680758, 0.67331643, 0.79017369 0.60307553, 0.67771273, 0.79218572 0.60934065, 0.68212194, 0.79422987 -0.61559495, 0.68654548, 0.7963202 +0.61559495, 0.68654548, 0.7963202 0.62185554, 0.69098125, 0.79842918 0.62810662, 0.69543176, 0.80058381 0.63436425, 0.69989499, 0.80275812 0.64061445, 0.70437326, 0.80497621 -0.6468706 , 0.70886488, 0.80721641 -0.65312213, 0.7133717 , 0.80949719 +0.6468706, 0.70886488, 0.80721641 +0.65312213, 0.7133717, 0.80949719 0.65937818, 0.71789261, 0.81180392 0.66563334, 0.72242871, 0.81414642 0.67189155, 0.72697967, 0.81651872 0.67815314, 0.73154569, 0.81892097 0.68441395, 0.73612771, 0.82136094 0.69068321, 0.74072452, 0.82382353 -0.69694776, 0.7453385 , 0.82633199 -0.70322431, 0.74996721, 0.8288583 +0.69694776, 0.7453385, 0.82633199 +0.70322431, 0.74996721, 0.8288583 0.70949595, 0.75461368, 0.83143221 -0.7157774 , 0.75927574, 0.83402904 +0.7157774, 0.75927574, 0.83402904 0.72206299, 0.76395461, 0.83665922 -0.72835227, 0.76865061, 0.8393242 -0.73465238, 0.7733628 , 0.84201224 +0.72835227, 0.76865061, 0.8393242 +0.73465238, 0.7733628, 0.84201224 0.74094862, 0.77809393, 0.84474951 0.74725683, 0.78284158, 0.84750915 0.75357103, 0.78760701, 0.85030217 0.75988961, 0.79239077, 0.85313207 0.76621987, 0.79719185, 0.85598668 -0.77255045, 0.8020125 , 0.85888658 +0.77255045, 0.8020125, 0.85888658 0.77889241, 0.80685102, 0.86181298 0.78524572, 0.81170768, 0.86476656 0.79159841, 0.81658489, 0.86776906 -0.79796459, 0.82148036, 0.8707962 +0.79796459, 0.82148036, 0.8707962 0.80434168, 0.82639479, 0.87385315 -0.8107221 , 0.83132983, 0.87695392 -0.81711301, 0.8362844 , 0.88008641 +0.8107221, 0.83132983, 0.87695392 +0.81711301, 0.8362844, 0.88008641 0.82351479, 0.84125863, 0.88325045 0.82992772, 0.84625263, 0.88644594 -0.83634359, 0.85126806, 0.8896878 +0.83634359, 0.85126806, 0.8896878 0.84277295, 0.85630293, 0.89295721 0.84921192, 0.86135782, 0.89626076 -0.85566206, 0.866432 , 0.89959467 +0.85566206, 0.866432, 0.89959467 0.86211514, 0.87152627, 0.90297183 0.86857483, 0.87663856, 0.90638248 0.87504231, 0.88176648, 0.90981938 0.88151194, 0.88690782, 0.91328493 0.88797938, 0.89205857, 0.91677544 -0.89443865, 0.89721298, 0.9202854 +0.89443865, 0.89721298, 0.9202854 0.90088204, 0.90236294, 0.92380601 0.90729768, 0.90749778, 0.92732797 0.91367037, 0.91260329, 0.93083814 0.91998105, 0.91766106, 0.93431861 0.92620596, 0.92264789, 0.93774647 -0.93231683, 0.9275351 , 0.94109192 -0.93827772, 0.9322888 , 0.94432312 +0.93231683, 0.9275351, 0.94109192 +0.93827772, 0.9322888, 0.94432312 0.94404755, 0.93686925, 0.94740137 0.94958284, 0.94123072, 0.95027696 -0.95482682, 0.9453245 , 0.95291103 -0.9597248 , 0.94909728, 0.95525103 +0.95482682, 0.9453245, 0.95291103 +0.9597248, 0.94909728, 0.95525103 0.96422552, 0.95249273, 0.95723271 0.96826161, 0.95545812, 0.95882188 0.97178458, 0.95793984, 0.95995705 @@ -126,21 +126,21 @@ 0.97708604, 0.96127366, 0.96071853 0.97877855, 0.96205832, 0.96030095 0.97978484, 0.96222949, 0.95935496 -0.9805997 , 0.96155216, 0.95813083 +0.9805997, 0.96155216, 0.95813083 0.98152619, 0.95993719, 0.95639322 -0.9819726 , 0.95766608, 0.95399269 -0.98191855, 0.9547873 , 0.95098107 +0.9819726, 0.95766608, 0.95399269 +0.98191855, 0.9547873, 0.95098107 0.98138514, 0.95134771, 0.94740644 0.98040845, 0.94739906, 0.94332125 0.97902107, 0.94300131, 0.93878672 0.97729348, 0.93820409, 0.93385135 -0.9752533 , 0.933073 , 0.92858252 +0.9752533, 0.933073, 0.92858252 0.97297834, 0.92765261, 0.92302309 0.97049104, 0.92200317, 0.91723505 0.96784372, 0.91616744, 0.91126063 0.96507281, 0.91018664, 0.90514124 0.96222034, 0.90409203, 0.89890756 -0.9593079 , 0.89791478, 0.89259122 +0.9593079, 0.89791478, 0.89259122 0.95635626, 0.89167908, 0.88621654 0.95338303, 0.88540373, 0.87980238 0.95040174, 0.87910333, 0.87336339 @@ -149,67 +149,67 @@ 0.94150476, 0.86014606, 0.85399191 0.93857394, 0.85382798, 0.84753642 0.93566206, 0.84751766, 0.84108935 -0.93277194, 0.8412164 , 0.83465197 +0.93277194, 0.8412164, 0.83465197 0.92990106, 0.83492672, 0.82822708 0.92704736, 0.82865028, 0.82181656 0.92422703, 0.82238092, 0.81541333 0.92142581, 0.81612448, 0.80902415 0.91864501, 0.80988032, 0.80264838 0.91587578, 0.80365187, 0.79629001 -0.9131367 , 0.79743115, 0.78994 +0.9131367, 0.79743115, 0.78994 0.91041602, 0.79122265, 0.78360361 0.90771071, 0.78502727, 0.77728196 -0.90501581, 0.77884674, 0.7709771 +0.90501581, 0.77884674, 0.7709771 0.90235365, 0.77267117, 0.76467793 -0.8997019 , 0.76650962, 0.75839484 -0.89705346, 0.76036481, 0.752131 +0.8997019, 0.76650962, 0.75839484 +0.89705346, 0.76036481, 0.752131 0.89444021, 0.75422253, 0.74587047 0.89183355, 0.74809474, 0.73962689 0.88923216, 0.74198168, 0.73340061 0.88665892, 0.73587283, 0.72717995 0.88408839, 0.72977904, 0.72097718 0.88153537, 0.72369332, 0.71478461 -0.87899389, 0.7176179 , 0.70860487 -0.87645157, 0.71155805, 0.7024439 -0.8739399 , 0.70549893, 0.6962854 -0.87142626, 0.6994551 , 0.69014561 -0.8689268 , 0.69341868, 0.68401597 -0.86643562, 0.687392 , 0.67789917 +0.87899389, 0.7176179, 0.70860487 +0.87645157, 0.71155805, 0.7024439 +0.8739399, 0.70549893, 0.6962854 +0.87142626, 0.6994551, 0.69014561 +0.8689268, 0.69341868, 0.68401597 +0.86643562, 0.687392, 0.67789917 0.86394434, 0.68137863, 0.67179927 -0.86147586, 0.67536728, 0.665704 -0.85899928, 0.66937226, 0.6596292 -0.85654668, 0.66337773, 0.6535577 +0.86147586, 0.67536728, 0.665704 +0.85899928, 0.66937226, 0.6596292 +0.85654668, 0.66337773, 0.6535577 0.85408818, 0.65739772, 0.64750494 0.85164413, 0.65142189, 0.64145983 -0.84920091, 0.6454565 , 0.63542932 -0.84676427, 0.63949827, 0.62941 +0.84920091, 0.6454565, 0.63542932 +0.84676427, 0.63949827, 0.62941 0.84433231, 0.63354773, 0.62340261 0.84190106, 0.62760645, 0.61740899 0.83947935, 0.62166951, 0.61142404 -0.8370538 , 0.61574332, 0.60545478 +0.8370538, 0.61574332, 0.60545478 0.83463975, 0.60981951, 0.59949247 -0.83221877, 0.60390724, 0.593547 +0.83221877, 0.60390724, 0.593547 0.82980985, 0.59799607, 0.58760751 0.82740268, 0.59209095, 0.58167944 -0.82498638, 0.5861973 , 0.57576866 -0.82258181, 0.5803034 , 0.56986307 +0.82498638, 0.5861973, 0.57576866 +0.82258181, 0.5803034, 0.56986307 0.82016611, 0.57442123, 0.56397539 0.81776305, 0.56853725, 0.55809173 0.81534551, 0.56266602, 0.55222741 -0.81294293, 0.55679056, 0.5463651 +0.81294293, 0.55679056, 0.5463651 0.81052113, 0.55092973, 0.54052443 0.80811509, 0.54506305, 0.53468464 0.80568952, 0.53921036, 0.52886622 0.80327506, 0.53335335, 0.52305077 0.80084727, 0.52750583, 0.51725256 -0.79842217, 0.5216578 , 0.51146173 +0.79842217, 0.5216578, 0.51146173 0.79599382, 0.51581223, 0.50568155 0.79355781, 0.50997127, 0.49991444 0.79112596, 0.50412707, 0.49415289 0.78867442, 0.49829386, 0.48841129 -0.7862306 , 0.49245398, 0.48267247 -0.7837687 , 0.48662309, 0.47695216 -0.78130809, 0.4807883 , 0.47123805 +0.7862306, 0.49245398, 0.48267247 +0.7837687, 0.48662309, 0.47695216 +0.78130809, 0.4807883, 0.47123805 0.77884467, 0.47495151, 0.46553236 0.77636283, 0.46912235, 0.45984473 0.77388383, 0.46328617, 0.45416141 @@ -220,12 +220,12 @@ 0.76133542, 0.43410655, 0.42592523 0.75880631, 0.42825801, 0.42030488 0.75624913, 0.42241905, 0.41470727 -0.7536919 , 0.41656866, 0.40911347 +0.7536919, 0.41656866, 0.40911347 0.75112748, 0.41071104, 0.40352792 -0.74854331, 0.40485474, 0.3979589 +0.74854331, 0.40485474, 0.3979589 0.74594723, 0.39899309, 0.39240088 0.74334332, 0.39312199, 0.38685075 -0.74073277, 0.38723941, 0.3813074 +0.74073277, 0.38723941, 0.3813074 0.73809409, 0.38136133, 0.37578553 0.73544692, 0.37547129, 0.37027123 0.73278943, 0.36956954, 0.36476549 @@ -240,17 +240,17 @@ 0.70816772, 0.31591904, 0.31572637 0.70534784, 0.30987734, 0.31033414 0.70250944, 0.30381489, 0.30495353 -0.69965211, 0.2977301 , 0.2995846 -0.6967754 , 0.29162126, 0.29422741 +0.69965211, 0.2977301, 0.2995846 +0.6967754, 0.29162126, 0.29422741 0.69388446, 0.28548074, 0.28887769 -0.69097561, 0.2793096 , 0.28353795 +0.69097561, 0.2793096, 0.28353795 0.68803513, 0.27311993, 0.27821876 -0.6850794 , 0.26689144, 0.27290694 -0.682108 , 0.26062114, 0.26760246 -0.67911013, 0.2543177 , 0.26231367 +0.6850794, 0.26689144, 0.27290694 +0.682108, 0.26062114, 0.26760246 +0.67911013, 0.2543177, 0.26231367 0.67609393, 0.24796818, 0.25703372 0.67305921, 0.24156846, 0.25176238 0.67000176, 0.23511902, 0.24650278 0.66693423, 0.22859879, 0.24124404 -0.6638441 , 0.22201742, 0.2359961 +0.6638441, 0.22201742, 0.2359961 0.66080672, 0.21526712, 0.23069468 diff --git a/proplot/cmaps/Orange1.xml b/proplot/cmaps/Yellows1.xml similarity index 100% rename from proplot/cmaps/Orange1.xml rename to proplot/cmaps/Yellows1.xml diff --git a/proplot/cmaps/Orange2.xml b/proplot/cmaps/Yellows2.xml similarity index 100% rename from proplot/cmaps/Orange2.xml rename to proplot/cmaps/Yellows2.xml diff --git a/proplot/cmaps/Orange3.xml b/proplot/cmaps/Yellows3.xml similarity index 100% rename from proplot/cmaps/Orange3.xml rename to proplot/cmaps/Yellows3.xml diff --git a/proplot/cmaps/Orange8.xml b/proplot/cmaps/Yellows4.xml similarity index 100% rename from proplot/cmaps/Orange8.xml rename to proplot/cmaps/Yellows4.xml diff --git a/proplot/cmaps/bam.txt b/proplot/cmaps/bam.txt new file mode 100644 index 000000000..d25300560 --- /dev/null +++ b/proplot/cmaps/bam.txt @@ -0,0 +1,256 @@ +0.396221 0.008120 0.296046 +0.405071 0.018136 0.304731 +0.413908 0.028663 0.313417 +0.422702 0.040100 0.322082 +0.431480 0.051043 0.330719 +0.440186 0.061170 0.339349 +0.448852 0.070776 0.347916 +0.457446 0.079825 0.356448 +0.465986 0.088578 0.364920 +0.474476 0.096961 0.373353 +0.482885 0.105159 0.381732 +0.491248 0.113219 0.390047 +0.499512 0.120980 0.398320 +0.507735 0.128684 0.406522 +0.515865 0.136227 0.414658 +0.523942 0.143720 0.422726 +0.531939 0.151068 0.430745 +0.539852 0.158372 0.438676 +0.547699 0.165535 0.446549 +0.555458 0.172692 0.454351 +0.563154 0.179773 0.462068 +0.570749 0.186802 0.469724 +0.578269 0.193793 0.477285 +0.585715 0.200686 0.484772 +0.593064 0.207599 0.492191 +0.600322 0.214443 0.499515 +0.607503 0.221263 0.506769 +0.614575 0.228042 0.513944 +0.621566 0.234793 0.521019 +0.628471 0.241495 0.528013 +0.635265 0.248193 0.534924 +0.641970 0.254854 0.541739 +0.648573 0.261477 0.548485 +0.655073 0.268085 0.555119 +0.661482 0.274690 0.561680 +0.667785 0.281274 0.568142 +0.673982 0.287850 0.574519 +0.680079 0.294422 0.580821 +0.686090 0.300996 0.587036 +0.692006 0.307579 0.593184 +0.697824 0.314190 0.599256 +0.703570 0.320832 0.605260 +0.709238 0.327489 0.611221 +0.714827 0.334225 0.617127 +0.720359 0.341004 0.622996 +0.725835 0.347863 0.628833 +0.731247 0.354808 0.634644 +0.736614 0.361830 0.640431 +0.741931 0.368954 0.646198 +0.747201 0.376180 0.651956 +0.752415 0.383507 0.657697 +0.757585 0.390940 0.663420 +0.762697 0.398474 0.669124 +0.767757 0.406121 0.674811 +0.772762 0.413861 0.680476 +0.777711 0.421699 0.686131 +0.782590 0.429651 0.691758 +0.787410 0.437698 0.697355 +0.792161 0.445812 0.702933 +0.796843 0.454037 0.708479 +0.801459 0.462331 0.713988 +0.806005 0.470716 0.719473 +0.810475 0.479167 0.724922 +0.814878 0.487694 0.730333 +0.819205 0.496265 0.735705 +0.823465 0.504919 0.741043 +0.827650 0.513622 0.746349 +0.831770 0.522364 0.751618 +0.835811 0.531163 0.756839 +0.839791 0.539993 0.762024 +0.843693 0.548872 0.767172 +0.847537 0.557765 0.772284 +0.851315 0.566693 0.777361 +0.855038 0.575640 0.782391 +0.858692 0.584597 0.787386 +0.862297 0.593569 0.792343 +0.865836 0.602547 0.797261 +0.869326 0.611531 0.802145 +0.872763 0.620515 0.806996 +0.876148 0.629479 0.811803 +0.879485 0.638442 0.816577 +0.882775 0.647388 0.821310 +0.886025 0.656316 0.826006 +0.889223 0.665206 0.830667 +0.892381 0.674072 0.835284 +0.895493 0.682901 0.839864 +0.898559 0.691686 0.844395 +0.901586 0.700422 0.848884 +0.904569 0.709106 0.853321 +0.907510 0.717722 0.857714 +0.910403 0.726269 0.862057 +0.913249 0.734744 0.866335 +0.916044 0.743137 0.870554 +0.918796 0.751441 0.874715 +0.921500 0.759641 0.878806 +0.924149 0.767750 0.882826 +0.926743 0.775755 0.886778 +0.929283 0.783650 0.890643 +0.931766 0.791422 0.894433 +0.934188 0.799080 0.898128 +0.936556 0.806609 0.901737 +0.938849 0.814002 0.905253 +0.941077 0.821269 0.908662 +0.943239 0.828393 0.911968 +0.945326 0.835374 0.915162 +0.947335 0.842209 0.918244 +0.949262 0.848885 0.921199 +0.951110 0.855405 0.924030 +0.952868 0.861758 0.926721 +0.954533 0.867942 0.929273 +0.956105 0.873944 0.931677 +0.957573 0.879765 0.933924 +0.958939 0.885401 0.936016 +0.960196 0.890837 0.937931 +0.961338 0.896075 0.939674 +0.962365 0.901100 0.941231 +0.963273 0.905916 0.942607 +0.964053 0.910508 0.943781 +0.964708 0.914872 0.944761 +0.965233 0.919005 0.945533 +0.965625 0.922910 0.946096 +0.965883 0.926568 0.946450 +0.966004 0.929988 0.946591 +0.965987 0.933158 0.946515 +0.965833 0.936091 0.946222 +0.965541 0.938770 0.945714 +0.965110 0.941205 0.944990 +0.964541 0.943403 0.944041 +0.963832 0.945364 0.942874 +0.962982 0.947096 0.941475 +0.961977 0.948607 0.939848 +0.960826 0.949910 0.937971 +0.959505 0.951014 0.935838 +0.958017 0.951915 0.933427 +0.956346 0.952631 0.930741 +0.954487 0.953159 0.927747 +0.952432 0.953495 0.924451 +0.950180 0.953643 0.920838 +0.947725 0.953600 0.916912 +0.945064 0.953366 0.912654 +0.942194 0.952937 0.908076 +0.939113 0.952313 0.903169 +0.935826 0.951501 0.897934 +0.932323 0.950494 0.892381 +0.928614 0.949292 0.886503 +0.924695 0.947902 0.880298 +0.920564 0.946317 0.873781 +0.916225 0.944549 0.866944 +0.911675 0.942591 0.859800 +0.906913 0.940444 0.852344 +0.901935 0.938111 0.844574 +0.896749 0.935596 0.836502 +0.891343 0.932894 0.828128 +0.885727 0.930017 0.819456 +0.879889 0.926953 0.810494 +0.873838 0.923715 0.801243 +0.867568 0.920288 0.791707 +0.861075 0.916687 0.781896 +0.854364 0.912903 0.771808 +0.847429 0.908938 0.761455 +0.840281 0.904797 0.750858 +0.832904 0.900475 0.740012 +0.825314 0.895977 0.728931 +0.817511 0.891301 0.717646 +0.809502 0.886455 0.706155 +0.801283 0.881434 0.694478 +0.792872 0.876243 0.682651 +0.784272 0.870890 0.670686 +0.775481 0.865376 0.658589 +0.766526 0.859709 0.646405 +0.757409 0.853884 0.634150 +0.748144 0.847918 0.621829 +0.738738 0.841816 0.609500 +0.729203 0.835576 0.597164 +0.719562 0.829215 0.584846 +0.709818 0.822730 0.572576 +0.699987 0.816140 0.560369 +0.690091 0.809446 0.548257 +0.680134 0.802656 0.536247 +0.670152 0.795787 0.524370 +0.660127 0.788849 0.512641 +0.650098 0.781842 0.501084 +0.640071 0.774774 0.489693 +0.630054 0.767666 0.478509 +0.620072 0.760525 0.467529 +0.610116 0.753348 0.456756 +0.600220 0.746157 0.446208 +0.590378 0.738957 0.435899 +0.580611 0.731744 0.425808 +0.570913 0.724539 0.415960 +0.561303 0.717347 0.406352 +0.551785 0.710162 0.396963 +0.542354 0.703003 0.387823 +0.533021 0.695871 0.378914 +0.523790 0.688770 0.370223 +0.514665 0.681694 0.361769 +0.505631 0.674670 0.353512 +0.496719 0.667690 0.345472 +0.487925 0.660742 0.337637 +0.479221 0.653854 0.330000 +0.470630 0.647015 0.322546 +0.462142 0.640226 0.315260 +0.453764 0.633490 0.308149 +0.445490 0.626806 0.301204 +0.437336 0.620190 0.294420 +0.429267 0.613614 0.287785 +0.421300 0.607118 0.281292 +0.413451 0.600663 0.274939 +0.405696 0.594277 0.268721 +0.398033 0.587944 0.262603 +0.390461 0.581678 0.256604 +0.382998 0.575476 0.250720 +0.375613 0.569319 0.244926 +0.368332 0.563236 0.239258 +0.361120 0.557201 0.233647 +0.354001 0.551210 0.228166 +0.346969 0.545275 0.222728 +0.339994 0.539377 0.217359 +0.333085 0.533519 0.212065 +0.326204 0.527697 0.206814 +0.319400 0.521887 0.201563 +0.312601 0.516093 0.196416 +0.305861 0.510308 0.191225 +0.299097 0.504520 0.186094 +0.292330 0.498703 0.180910 +0.285551 0.492853 0.175743 +0.278784 0.486993 0.170584 +0.271947 0.481080 0.165356 +0.265112 0.475148 0.160123 +0.258231 0.469156 0.154884 +0.251336 0.463102 0.149612 +0.244348 0.457022 0.144259 +0.237381 0.450891 0.138859 +0.230295 0.444712 0.133443 +0.223223 0.438468 0.127969 +0.216083 0.432194 0.122390 +0.208861 0.425844 0.116835 +0.201589 0.419466 0.111210 +0.194315 0.413033 0.105465 +0.186913 0.406543 0.099681 +0.179463 0.400004 0.093826 +0.171937 0.393422 0.087904 +0.164360 0.386787 0.081888 +0.156663 0.380117 0.075659 +0.148841 0.373390 0.069479 +0.140972 0.366627 0.063040 +0.132915 0.359804 0.056434 +0.124705 0.352944 0.049637 +0.116381 0.346024 0.042550 +0.107779 0.339092 0.035407 +0.098981 0.332097 0.028358 +0.089900 0.325051 0.021963 +0.080424 0.317996 0.016000 +0.070529 0.310912 0.010244 +0.060124 0.303780 0.005140 +0.049098 0.296663 0.000000 diff --git a/proplot/cmaps/bamO.txt b/proplot/cmaps/bamO.txt new file mode 100644 index 000000000..8284a0e80 --- /dev/null +++ b/proplot/cmaps/bamO.txt @@ -0,0 +1,256 @@ +0.309457 0.186351 0.263735 +0.314190 0.186088 0.268799 +0.319432 0.186148 0.274282 +0.325147 0.186551 0.280197 +0.331347 0.187352 0.286518 +0.338005 0.188507 0.293233 +0.345086 0.190024 0.300292 +0.352539 0.191933 0.307655 +0.360321 0.194241 0.315302 +0.368411 0.196871 0.323164 +0.376730 0.199805 0.331181 +0.385222 0.203117 0.339369 +0.393875 0.206732 0.347622 +0.402626 0.210573 0.355935 +0.411441 0.214681 0.364258 +0.420286 0.219033 0.372586 +0.429134 0.223572 0.380903 +0.437964 0.228309 0.389157 +0.446738 0.233164 0.397348 +0.455461 0.238202 0.405482 +0.464119 0.243348 0.413527 +0.472679 0.248640 0.421480 +0.481156 0.253994 0.429358 +0.489540 0.259448 0.437141 +0.497841 0.264947 0.444823 +0.506004 0.270557 0.452410 +0.514091 0.276200 0.459886 +0.522042 0.281881 0.467281 +0.529905 0.287597 0.474571 +0.537651 0.293365 0.481760 +0.545277 0.299176 0.488851 +0.552791 0.304992 0.495845 +0.560199 0.310841 0.502746 +0.567489 0.316690 0.509557 +0.574660 0.322565 0.516263 +0.581731 0.328438 0.522891 +0.588687 0.334334 0.529423 +0.595544 0.340226 0.535855 +0.602293 0.346118 0.542217 +0.608951 0.352036 0.548501 +0.615498 0.357980 0.554700 +0.621966 0.363917 0.560834 +0.628369 0.369891 0.566920 +0.634692 0.375904 0.572950 +0.640951 0.381949 0.578936 +0.647171 0.388053 0.584897 +0.653341 0.394218 0.590830 +0.659489 0.400467 0.596762 +0.665608 0.406776 0.602679 +0.671704 0.413186 0.608608 +0.677792 0.419683 0.614521 +0.683851 0.426271 0.620464 +0.689908 0.432978 0.626396 +0.695947 0.439783 0.632345 +0.701958 0.446693 0.638296 +0.707959 0.453708 0.644247 +0.713931 0.460832 0.650207 +0.719877 0.468066 0.656163 +0.725788 0.475416 0.662108 +0.731652 0.482851 0.668038 +0.737474 0.490405 0.673946 +0.743249 0.498060 0.679830 +0.748955 0.505788 0.685695 +0.754607 0.513643 0.691513 +0.760174 0.521567 0.697287 +0.765668 0.529594 0.703014 +0.771068 0.537694 0.708676 +0.776369 0.545865 0.714257 +0.781564 0.554099 0.719769 +0.786644 0.562400 0.725186 +0.791589 0.570729 0.730493 +0.796402 0.579113 0.735686 +0.801073 0.587505 0.740754 +0.805585 0.595916 0.745683 +0.809934 0.604323 0.750461 +0.814105 0.612691 0.755072 +0.818092 0.621032 0.759494 +0.821892 0.629302 0.763733 +0.825481 0.637498 0.767765 +0.828871 0.645585 0.771589 +0.832042 0.653555 0.775175 +0.834992 0.661385 0.778533 +0.837725 0.669049 0.781650 +0.840232 0.676532 0.784516 +0.842516 0.683811 0.787127 +0.844575 0.690881 0.789484 +0.846414 0.697729 0.791584 +0.848043 0.704340 0.793438 +0.849467 0.710707 0.795034 +0.850679 0.716823 0.796387 +0.851704 0.722675 0.797505 +0.852545 0.728280 0.798393 +0.853199 0.733631 0.799060 +0.853688 0.738726 0.799513 +0.854021 0.743569 0.799761 +0.854204 0.748167 0.799816 +0.854245 0.752516 0.799688 +0.854154 0.756640 0.799388 +0.853939 0.760534 0.798923 +0.853609 0.764202 0.798306 +0.853176 0.767656 0.797549 +0.852646 0.770911 0.796660 +0.852019 0.773955 0.795651 +0.851302 0.776821 0.794530 +0.850515 0.779490 0.793304 +0.849658 0.781993 0.791974 +0.848729 0.784323 0.790560 +0.847737 0.786491 0.789068 +0.846690 0.788501 0.787492 +0.845591 0.790366 0.785849 +0.844440 0.792095 0.784137 +0.843235 0.793698 0.782352 +0.841984 0.795166 0.780500 +0.840675 0.796520 0.778580 +0.839311 0.797767 0.776588 +0.837888 0.798912 0.774506 +0.836394 0.799954 0.772342 +0.834828 0.800895 0.770075 +0.833175 0.801744 0.767693 +0.831433 0.802498 0.765192 +0.829579 0.803158 0.762536 +0.827601 0.803719 0.759724 +0.825493 0.804175 0.756737 +0.823236 0.804516 0.753547 +0.820811 0.804733 0.750140 +0.818203 0.804815 0.746495 +0.815400 0.804750 0.742593 +0.812374 0.804519 0.738402 +0.809118 0.804104 0.733915 +0.805596 0.803483 0.729101 +0.801807 0.802638 0.723944 +0.797724 0.801546 0.718424 +0.793335 0.800185 0.712521 +0.788609 0.798521 0.706228 +0.783540 0.796535 0.699512 +0.778115 0.794213 0.692391 +0.772325 0.791515 0.684832 +0.766170 0.788440 0.676869 +0.759644 0.784968 0.668493 +0.752768 0.781087 0.659733 +0.745556 0.776805 0.650619 +0.738026 0.772114 0.641182 +0.730218 0.767033 0.631484 +0.722150 0.761582 0.621553 +0.713884 0.755784 0.611465 +0.705448 0.749656 0.601256 +0.696889 0.743254 0.590995 +0.688251 0.736583 0.580733 +0.679563 0.729701 0.570512 +0.670885 0.722623 0.560391 +0.662229 0.715405 0.550391 +0.653620 0.708054 0.540550 +0.645096 0.700613 0.530895 +0.636681 0.693104 0.521440 +0.628372 0.685545 0.512209 +0.620190 0.677960 0.503201 +0.612134 0.670367 0.494446 +0.604236 0.662773 0.485909 +0.596470 0.655190 0.477623 +0.588849 0.647642 0.469583 +0.581389 0.640134 0.461762 +0.574072 0.632660 0.454181 +0.566912 0.625236 0.446824 +0.559891 0.617867 0.439693 +0.553007 0.610561 0.432781 +0.546284 0.603315 0.426069 +0.539679 0.596130 0.419580 +0.533226 0.589010 0.413282 +0.526914 0.581968 0.407172 +0.520717 0.574994 0.401260 +0.514663 0.568095 0.395525 +0.508732 0.561267 0.389953 +0.502908 0.554517 0.384566 +0.497226 0.547836 0.379344 +0.491659 0.541225 0.374286 +0.486187 0.534698 0.369365 +0.480833 0.528236 0.364595 +0.475599 0.521837 0.359972 +0.470450 0.515513 0.355492 +0.465388 0.509258 0.351122 +0.460424 0.503042 0.346875 +0.455552 0.496890 0.342745 +0.450746 0.490791 0.338706 +0.445999 0.484700 0.334785 +0.441325 0.478657 0.330917 +0.436703 0.472631 0.327155 +0.432131 0.466622 0.323474 +0.427591 0.460610 0.319862 +0.423078 0.454627 0.316310 +0.418617 0.448629 0.312805 +0.414194 0.442636 0.309395 +0.409806 0.436642 0.306024 +0.405446 0.430653 0.302685 +0.401128 0.424671 0.299449 +0.396828 0.418692 0.296246 +0.392591 0.412747 0.293104 +0.388398 0.406798 0.290030 +0.384244 0.400893 0.287014 +0.380149 0.394994 0.284074 +0.376111 0.389146 0.281188 +0.372115 0.383336 0.278389 +0.368203 0.377566 0.275657 +0.364344 0.371847 0.272969 +0.360555 0.366209 0.270382 +0.356867 0.360604 0.267825 +0.353231 0.355114 0.265390 +0.349687 0.349667 0.263026 +0.346217 0.344303 0.260727 +0.342866 0.339056 0.258511 +0.339591 0.333887 0.256373 +0.336394 0.328820 0.254342 +0.333326 0.323846 0.252370 +0.330337 0.318997 0.250471 +0.327452 0.314252 0.248696 +0.324674 0.309640 0.246965 +0.322009 0.305123 0.245320 +0.319444 0.300716 0.243769 +0.316977 0.296450 0.242309 +0.314604 0.292283 0.240914 +0.312334 0.288227 0.239613 +0.310193 0.284282 0.238366 +0.308103 0.280435 0.237234 +0.306142 0.276712 0.236133 +0.304230 0.273054 0.235121 +0.302414 0.269494 0.234152 +0.300692 0.265998 0.233248 +0.299059 0.262568 0.232447 +0.297484 0.259220 0.231704 +0.295982 0.255894 0.231002 +0.294554 0.252650 0.230358 +0.293206 0.249426 0.229791 +0.291941 0.246247 0.229311 +0.290731 0.243100 0.228903 +0.289612 0.239988 0.228559 +0.288564 0.236926 0.228286 +0.287595 0.233831 0.228094 +0.286723 0.230801 0.227991 +0.285950 0.227796 0.227986 +0.285287 0.224780 0.228086 +0.284740 0.221841 0.228300 +0.284320 0.218938 0.228635 +0.284039 0.216081 0.229092 +0.283909 0.213228 0.229688 +0.283947 0.210480 0.230472 +0.284167 0.207793 0.231448 +0.284590 0.205176 0.232578 +0.285235 0.202653 0.233925 +0.286126 0.200228 0.235529 +0.287291 0.197952 0.237369 +0.288761 0.195806 0.239455 +0.290526 0.193826 0.241856 +0.292654 0.191986 0.244552 +0.295152 0.190366 0.247637 +0.298053 0.188977 0.251062 +0.301386 0.187848 0.254882 +0.305190 0.186937 0.259096 diff --git a/proplot/cmaps/batlowK.txt b/proplot/cmaps/batlowK.txt new file mode 100644 index 000000000..4445c4382 --- /dev/null +++ b/proplot/cmaps/batlowK.txt @@ -0,0 +1,256 @@ +0.010753 0.014697 0.019692 +0.014705 0.022477 0.030110 +0.018513 0.030323 0.040780 +0.022378 0.038435 0.050426 +0.026306 0.046036 0.059125 +0.030304 0.053084 0.066960 +0.034413 0.059722 0.074236 +0.038727 0.065759 0.081005 +0.042807 0.071572 0.087422 +0.046912 0.076977 0.093391 +0.050665 0.082318 0.099291 +0.054034 0.087386 0.105264 +0.057094 0.092252 0.111406 +0.059840 0.096955 0.117574 +0.061906 0.101764 0.123839 +0.063619 0.106781 0.130240 +0.065295 0.111812 0.136574 +0.067040 0.116915 0.143036 +0.068875 0.122095 0.149526 +0.070634 0.127396 0.156034 +0.072509 0.132771 0.162552 +0.074477 0.138204 0.169098 +0.076456 0.143668 0.175595 +0.078557 0.149202 0.182112 +0.080790 0.154778 0.188638 +0.083060 0.160393 0.195092 +0.085423 0.166078 0.201483 +0.087972 0.171786 0.207853 +0.090567 0.177545 0.214133 +0.093232 0.183260 0.220332 +0.096073 0.189035 0.226414 +0.099048 0.194810 0.232379 +0.102120 0.200531 0.238216 +0.105300 0.206323 0.243914 +0.108613 0.212039 0.249479 +0.112086 0.217721 0.254898 +0.115617 0.223380 0.260106 +0.119265 0.228989 0.265133 +0.122989 0.234553 0.269996 +0.126857 0.240022 0.274620 +0.130849 0.245439 0.279068 +0.134881 0.250798 0.283276 +0.138951 0.256048 0.287251 +0.143145 0.261228 0.291027 +0.147380 0.266306 0.294544 +0.151645 0.271268 0.297840 +0.156004 0.276147 0.300894 +0.160318 0.280867 0.303715 +0.164731 0.285506 0.306317 +0.169166 0.290031 0.308659 +0.173556 0.294428 0.310775 +0.178017 0.298716 0.312636 +0.182400 0.302864 0.314300 +0.186842 0.306916 0.315751 +0.191253 0.310846 0.316965 +0.195664 0.314629 0.317978 +0.200042 0.318327 0.318792 +0.204460 0.321901 0.319412 +0.208804 0.325363 0.319848 +0.213139 0.328745 0.320104 +0.217489 0.332010 0.320186 +0.221792 0.335193 0.320103 +0.226115 0.338255 0.319861 +0.230374 0.341265 0.319470 +0.234681 0.344173 0.318940 +0.238923 0.347038 0.318279 +0.243176 0.349812 0.317491 +0.247449 0.352526 0.316589 +0.251710 0.355198 0.315573 +0.255942 0.357801 0.314434 +0.260210 0.360342 0.313216 +0.264469 0.362862 0.311879 +0.268751 0.365341 0.310498 +0.273021 0.367780 0.309008 +0.277316 0.370193 0.307415 +0.281616 0.372579 0.305795 +0.285925 0.374946 0.304064 +0.290267 0.377293 0.302282 +0.294627 0.379616 0.300452 +0.299021 0.381926 0.298565 +0.303407 0.384228 0.296633 +0.307831 0.386515 0.294629 +0.312276 0.388813 0.292599 +0.316769 0.391095 0.290521 +0.321267 0.393363 0.288414 +0.325789 0.395648 0.286257 +0.330360 0.397923 0.284087 +0.334965 0.400200 0.281891 +0.339585 0.402473 0.279660 +0.344221 0.404760 0.277399 +0.348924 0.407059 0.275117 +0.353644 0.409360 0.272813 +0.358412 0.411665 0.270515 +0.363216 0.413984 0.268161 +0.368065 0.416312 0.265832 +0.372949 0.418645 0.263483 +0.377884 0.420993 0.261105 +0.382865 0.423355 0.258737 +0.387883 0.425735 0.256346 +0.392957 0.428127 0.253979 +0.398087 0.430528 0.251604 +0.403260 0.432943 0.249209 +0.408481 0.435379 0.246826 +0.413762 0.437823 0.244440 +0.419098 0.440271 0.242092 +0.424487 0.442749 0.239736 +0.429935 0.445229 0.237417 +0.435446 0.447720 0.235090 +0.440997 0.450241 0.232761 +0.446626 0.452770 0.230476 +0.452317 0.455302 0.228247 +0.458050 0.457842 0.226025 +0.463863 0.460395 0.223816 +0.469736 0.462960 0.221669 +0.475666 0.465536 0.219575 +0.481657 0.468124 0.217516 +0.487724 0.470721 0.215509 +0.493842 0.473306 0.213560 +0.500027 0.475918 0.211700 +0.506285 0.478518 0.209872 +0.512611 0.481122 0.208142 +0.519008 0.483730 0.206514 +0.525458 0.486342 0.204928 +0.531980 0.488949 0.203465 +0.538571 0.491567 0.202080 +0.545217 0.494161 0.200816 +0.551934 0.496747 0.199677 +0.558700 0.499335 0.198667 +0.565538 0.501923 0.197790 +0.572444 0.504495 0.197061 +0.579398 0.507041 0.196466 +0.586401 0.509584 0.196003 +0.593461 0.512106 0.195704 +0.600565 0.514621 0.195578 +0.607724 0.517110 0.195628 +0.614908 0.519570 0.195861 +0.622139 0.522008 0.196288 +0.629405 0.524435 0.196891 +0.636706 0.526835 0.197664 +0.644015 0.529201 0.198646 +0.651360 0.531533 0.199831 +0.658707 0.533834 0.201229 +0.666072 0.536117 0.202843 +0.673443 0.538372 0.204668 +0.680806 0.540579 0.206694 +0.688177 0.542766 0.208891 +0.695533 0.544912 0.211329 +0.702875 0.547030 0.213960 +0.710196 0.549120 0.216803 +0.717498 0.551171 0.219836 +0.724766 0.553188 0.223074 +0.732003 0.555184 0.226505 +0.739206 0.557160 0.230098 +0.746357 0.559088 0.233920 +0.753468 0.561000 0.237930 +0.760533 0.562896 0.242103 +0.767537 0.564753 0.246451 +0.774488 0.566607 0.250993 +0.781382 0.568429 0.255684 +0.788204 0.570240 0.260551 +0.794960 0.572046 0.265581 +0.801642 0.573833 0.270777 +0.808252 0.575621 0.276121 +0.814779 0.577391 0.281596 +0.821227 0.579171 0.287225 +0.827587 0.580942 0.293013 +0.833864 0.582717 0.298949 +0.840044 0.584505 0.305009 +0.846122 0.586299 0.311204 +0.852114 0.588095 0.317528 +0.857992 0.589913 0.323975 +0.863776 0.591754 0.330553 +0.869442 0.593602 0.337256 +0.874999 0.595471 0.344053 +0.880440 0.597368 0.350993 +0.885767 0.599286 0.358020 +0.890967 0.601223 0.365144 +0.896046 0.603196 0.372373 +0.900995 0.605190 0.379704 +0.905817 0.607224 0.387114 +0.910503 0.609277 0.394618 +0.915050 0.611361 0.402191 +0.919463 0.613469 0.409859 +0.923739 0.615617 0.417579 +0.927865 0.617794 0.425363 +0.931854 0.620007 0.433199 +0.935701 0.622232 0.441089 +0.939399 0.624502 0.449037 +0.942953 0.626789 0.457005 +0.946354 0.629107 0.465016 +0.949616 0.631451 0.473048 +0.952732 0.633819 0.481108 +0.955705 0.636207 0.489183 +0.958530 0.638611 0.497276 +0.961213 0.641035 0.505355 +0.963757 0.643480 0.513454 +0.966164 0.645940 0.521531 +0.968439 0.648414 0.529613 +0.970577 0.650903 0.537674 +0.972592 0.653396 0.545714 +0.974473 0.655909 0.553726 +0.976236 0.658417 0.561727 +0.977883 0.660933 0.569685 +0.979417 0.663465 0.577622 +0.980837 0.665986 0.585537 +0.982155 0.668518 0.593404 +0.983366 0.671047 0.601236 +0.984488 0.673577 0.609045 +0.985511 0.676107 0.616807 +0.986448 0.678636 0.624536 +0.987302 0.681160 0.632231 +0.988071 0.683691 0.639896 +0.988771 0.686219 0.647523 +0.989401 0.688747 0.655120 +0.989960 0.691262 0.662700 +0.990456 0.693784 0.670249 +0.990893 0.696312 0.677772 +0.991275 0.698827 0.685275 +0.991605 0.701350 0.692767 +0.991885 0.703875 0.700236 +0.992120 0.706404 0.707700 +0.992311 0.708933 0.715164 +0.992461 0.711464 0.722616 +0.992573 0.713995 0.730083 +0.992649 0.716542 0.737542 +0.992691 0.719087 0.745016 +0.992701 0.721638 0.752499 +0.992680 0.724205 0.760001 +0.992630 0.726777 0.767519 +0.992553 0.729361 0.775060 +0.992449 0.731956 0.782627 +0.992319 0.734560 0.790218 +0.992164 0.737174 0.797837 +0.991985 0.739807 0.805487 +0.991783 0.742445 0.813170 +0.991557 0.745094 0.820887 +0.991309 0.747765 0.828640 +0.991037 0.750442 0.836423 +0.990743 0.753129 0.844243 +0.990426 0.755839 0.852101 +0.990085 0.758559 0.859990 +0.989722 0.761287 0.867914 +0.989334 0.764029 0.875868 +0.988920 0.766782 0.883858 +0.988480 0.769547 0.891877 +0.988016 0.772325 0.899926 +0.987529 0.775110 0.908005 +0.987013 0.777910 0.916102 +0.986467 0.780712 0.924232 +0.985893 0.783527 0.932382 +0.985291 0.786351 0.940560 +0.984659 0.789179 0.948751 +0.983992 0.792010 0.956963 +0.983293 0.794852 0.965193 +0.982566 0.797698 0.973442 +0.981802 0.800553 0.981706 diff --git a/proplot/cmaps/batlowW.txt b/proplot/cmaps/batlowW.txt new file mode 100644 index 000000000..9d3b6a832 --- /dev/null +++ b/proplot/cmaps/batlowW.txt @@ -0,0 +1,256 @@ +0.004637 0.098343 0.349833 +0.008580 0.104559 0.350923 +0.012565 0.110825 0.351981 +0.016171 0.116932 0.353057 +0.019623 0.122982 0.354106 +0.022916 0.129014 0.355168 +0.026056 0.135014 0.356195 +0.029046 0.140931 0.357225 +0.031891 0.146753 0.358229 +0.034696 0.152562 0.359228 +0.037367 0.158357 0.360219 +0.039804 0.164072 0.361200 +0.042104 0.169711 0.362175 +0.044107 0.175274 0.363120 +0.045968 0.180761 0.364057 +0.047742 0.186205 0.364976 +0.049465 0.191514 0.365883 +0.050890 0.196766 0.366763 +0.052254 0.201845 0.367610 +0.053547 0.206876 0.368458 +0.054774 0.211752 0.369266 +0.055952 0.216510 0.370049 +0.057021 0.221141 0.370813 +0.057975 0.225648 0.371557 +0.059056 0.230019 0.372281 +0.060029 0.234335 0.372984 +0.060869 0.238500 0.373673 +0.061774 0.242593 0.374342 +0.062771 0.246598 0.374979 +0.063628 0.250519 0.375608 +0.064516 0.254395 0.376235 +0.065420 0.258168 0.376837 +0.066347 0.261923 0.377420 +0.067303 0.265626 0.377994 +0.068289 0.269301 0.378559 +0.069324 0.272923 0.379112 +0.070259 0.276546 0.379654 +0.071367 0.280126 0.380186 +0.072397 0.283712 0.380708 +0.073609 0.287275 0.381213 +0.074722 0.290850 0.381694 +0.075923 0.294401 0.382160 +0.077185 0.297955 0.382618 +0.078521 0.301501 0.383059 +0.079937 0.305058 0.383472 +0.081445 0.308597 0.383855 +0.082923 0.312102 0.384210 +0.084562 0.315645 0.384537 +0.086163 0.319146 0.384832 +0.087968 0.322650 0.385091 +0.089792 0.326111 0.385310 +0.091746 0.329599 0.385487 +0.093708 0.333047 0.385618 +0.095823 0.336461 0.385699 +0.098076 0.339885 0.385728 +0.100356 0.343272 0.385700 +0.102811 0.346633 0.385611 +0.105329 0.349979 0.385460 +0.107987 0.353292 0.385241 +0.110818 0.356590 0.384953 +0.113716 0.359846 0.384591 +0.116737 0.363076 0.384154 +0.119874 0.366291 0.383641 +0.123139 0.369454 0.383038 +0.126576 0.372589 0.382340 +0.130156 0.375698 0.381574 +0.133788 0.378770 0.380713 +0.137589 0.381805 0.379750 +0.141488 0.384800 0.378701 +0.145532 0.387762 0.377561 +0.149666 0.390693 0.376333 +0.153889 0.393575 0.374991 +0.158287 0.396424 0.373569 +0.162762 0.399230 0.372042 +0.167344 0.402003 0.370431 +0.172003 0.404743 0.368738 +0.176781 0.407449 0.366938 +0.181640 0.410124 0.365049 +0.186615 0.412757 0.363076 +0.191653 0.415346 0.361015 +0.196804 0.417914 0.358876 +0.201972 0.420449 0.356673 +0.207271 0.422956 0.354373 +0.212607 0.425450 0.351993 +0.218027 0.427911 0.349563 +0.223504 0.430345 0.347062 +0.229043 0.432764 0.344476 +0.234654 0.435164 0.341852 +0.240272 0.437545 0.339169 +0.245982 0.439903 0.336410 +0.251759 0.442260 0.333629 +0.257547 0.444599 0.330774 +0.263409 0.446918 0.327894 +0.269300 0.449242 0.324965 +0.275221 0.451550 0.322012 +0.281172 0.453849 0.319020 +0.287175 0.456145 0.316006 +0.293216 0.458438 0.312941 +0.299300 0.460720 0.309880 +0.305400 0.463000 0.306777 +0.311512 0.465284 0.303650 +0.317673 0.467571 0.300520 +0.323850 0.469853 0.297387 +0.330066 0.472127 0.294217 +0.336283 0.474399 0.291064 +0.342551 0.476670 0.287877 +0.348821 0.478945 0.284696 +0.355133 0.481216 0.281518 +0.361443 0.483493 0.278338 +0.367784 0.485773 0.275148 +0.374169 0.488065 0.271939 +0.380564 0.490346 0.268779 +0.386976 0.492625 0.265584 +0.393432 0.494929 0.262401 +0.399911 0.497226 0.259238 +0.406433 0.499522 0.256044 +0.412984 0.501846 0.252898 +0.419564 0.504170 0.249730 +0.426174 0.506490 0.246602 +0.432844 0.508841 0.243475 +0.439544 0.511184 0.240371 +0.446292 0.513556 0.237334 +0.453095 0.515924 0.234271 +0.459940 0.518331 0.231262 +0.466852 0.520735 0.228294 +0.473806 0.523168 0.225350 +0.480827 0.525610 0.222487 +0.487926 0.528073 0.219689 +0.495074 0.530561 0.216965 +0.502293 0.533067 0.214322 +0.509585 0.535591 0.211799 +0.516956 0.538156 0.209364 +0.524395 0.540725 0.207098 +0.531919 0.543327 0.204958 +0.539513 0.545957 0.202999 +0.547198 0.548608 0.201224 +0.554960 0.551277 0.199680 +0.562810 0.553974 0.198389 +0.570723 0.556699 0.197370 +0.578726 0.559444 0.196644 +0.586800 0.562218 0.196211 +0.594947 0.564990 0.196125 +0.603162 0.567801 0.196417 +0.611435 0.570616 0.197082 +0.619762 0.573454 0.198141 +0.628129 0.576301 0.199637 +0.636533 0.579142 0.201587 +0.644949 0.581983 0.204022 +0.653386 0.584833 0.206876 +0.661828 0.587663 0.210168 +0.670246 0.590490 0.213943 +0.678626 0.593304 0.218155 +0.686968 0.596087 0.222809 +0.695248 0.598847 0.227875 +0.703452 0.601573 0.233312 +0.711566 0.604272 0.239170 +0.719571 0.606922 0.245346 +0.727453 0.609526 0.251884 +0.735211 0.612081 0.258684 +0.742823 0.614585 0.265760 +0.750275 0.617046 0.273064 +0.757569 0.619447 0.280559 +0.764695 0.621775 0.288258 +0.771646 0.624066 0.296099 +0.778414 0.626281 0.304057 +0.785008 0.628449 0.312105 +0.791414 0.630553 0.320265 +0.797642 0.632594 0.328440 +0.803694 0.634586 0.336654 +0.809572 0.636521 0.344891 +0.815275 0.638397 0.353127 +0.820812 0.640231 0.361344 +0.826193 0.642012 0.369540 +0.831423 0.643746 0.377704 +0.836498 0.645442 0.385816 +0.841438 0.647106 0.393892 +0.846241 0.648727 0.401904 +0.850921 0.650319 0.409880 +0.855491 0.651886 0.417778 +0.859944 0.653422 0.425620 +0.864294 0.654944 0.433400 +0.868550 0.656464 0.441129 +0.872709 0.657965 0.448814 +0.876785 0.659459 0.456431 +0.880792 0.660959 0.464021 +0.884727 0.662478 0.471563 +0.888597 0.664004 0.479070 +0.892407 0.665555 0.486558 +0.896161 0.667138 0.494037 +0.899867 0.668767 0.501512 +0.903528 0.670447 0.508991 +0.907149 0.672180 0.516481 +0.910733 0.673988 0.524014 +0.914285 0.675882 0.531586 +0.917806 0.677876 0.539214 +0.921295 0.679968 0.546917 +0.924761 0.682193 0.554701 +0.928203 0.684556 0.562590 +0.931623 0.687077 0.570573 +0.935018 0.689766 0.578695 +0.938388 0.692640 0.586948 +0.941729 0.695707 0.595351 +0.945043 0.698986 0.603915 +0.948316 0.702501 0.612626 +0.951554 0.706258 0.621515 +0.954746 0.710260 0.630581 +0.957886 0.714519 0.639804 +0.960963 0.719053 0.649193 +0.963967 0.723851 0.658728 +0.966899 0.728920 0.668419 +0.969737 0.734257 0.678227 +0.972482 0.739853 0.688143 +0.975108 0.745684 0.698135 +0.977623 0.751759 0.708191 +0.980008 0.758039 0.718265 +0.982258 0.764508 0.728333 +0.984363 0.771146 0.738362 +0.986319 0.777917 0.748321 +0.988123 0.784799 0.758171 +0.989780 0.791754 0.767880 +0.991281 0.798766 0.777438 +0.992638 0.805795 0.786803 +0.993846 0.812822 0.795954 +0.994914 0.819823 0.804891 +0.995854 0.826783 0.813590 +0.996670 0.833675 0.822058 +0.997373 0.840486 0.830275 +0.997972 0.847203 0.838249 +0.998474 0.853825 0.845983 +0.998889 0.860347 0.853484 +0.999224 0.866751 0.860762 +0.999489 0.873048 0.867823 +0.999692 0.879227 0.874668 +0.999842 0.885298 0.881323 +0.999948 0.891255 0.887786 +1.000000 0.897109 0.894081 +1.000000 0.902855 0.900211 +1.000000 0.908502 0.906194 +1.000000 0.914057 0.912034 +1.000000 0.919515 0.917749 +1.000000 0.924887 0.923339 +1.000000 0.930178 0.928808 +1.000000 0.935378 0.934173 +1.000000 0.940497 0.939436 +1.000000 0.945530 0.944595 +1.000000 0.950480 0.949652 +1.000000 0.955350 0.954618 +1.000000 0.960136 0.959490 +1.000000 0.964839 0.964271 +1.000000 0.969468 0.968970 +1.000000 0.974019 0.973585 +1.000000 0.978494 0.978119 +1.000000 0.982906 0.982588 +1.000000 0.987256 0.986989 +1.000000 0.991546 0.991332 +1.000000 0.995792 0.995630 diff --git a/proplot/cmaps/bukavu.txt b/proplot/cmaps/bukavu.txt new file mode 100644 index 000000000..475049ea9 --- /dev/null +++ b/proplot/cmaps/bukavu.txt @@ -0,0 +1,256 @@ +0.100212 0.200031 0.200060 +0.101191 0.203272 0.207592 +0.102143 0.206539 0.215142 +0.103092 0.209783 0.222796 +0.103985 0.213077 0.230487 +0.104850 0.216454 0.238291 +0.105813 0.219832 0.246192 +0.106760 0.223269 0.254224 +0.107644 0.226757 0.262353 +0.108569 0.230280 0.270634 +0.109598 0.233891 0.279034 +0.110555 0.237584 0.287565 +0.111547 0.241301 0.296277 +0.112568 0.245083 0.305146 +0.113600 0.248979 0.314176 +0.114599 0.252929 0.323400 +0.115734 0.256969 0.332836 +0.116832 0.261101 0.342479 +0.117992 0.265341 0.352366 +0.119158 0.269718 0.362542 +0.120346 0.274202 0.373019 +0.121586 0.278850 0.383845 +0.122882 0.283639 0.395030 +0.124236 0.288607 0.406609 +0.125661 0.293741 0.418586 +0.127051 0.299083 0.430983 +0.128553 0.304588 0.443756 +0.130118 0.310301 0.456863 +0.131660 0.316185 0.470253 +0.133278 0.322251 0.483789 +0.134970 0.328505 0.497373 +0.136682 0.334957 0.510815 +0.138534 0.341564 0.523987 +0.140459 0.348357 0.536745 +0.142490 0.355321 0.548980 +0.144640 0.362403 0.560654 +0.146848 0.369604 0.571790 +0.149180 0.376901 0.582435 +0.151539 0.384231 0.592690 +0.153947 0.391601 0.602616 +0.156413 0.398956 0.612318 +0.158840 0.406316 0.621860 +0.161284 0.413636 0.631298 +0.163712 0.420921 0.640640 +0.166093 0.428185 0.649921 +0.168507 0.435409 0.659140 +0.170899 0.442592 0.668325 +0.173277 0.449741 0.677466 +0.175644 0.456863 0.686565 +0.178065 0.463979 0.695632 +0.180422 0.471073 0.704648 +0.182859 0.478161 0.713600 +0.185425 0.485257 0.722435 +0.188104 0.492374 0.731088 +0.190987 0.499506 0.739448 +0.194285 0.506669 0.747347 +0.197982 0.513858 0.754635 +0.202274 0.521048 0.761138 +0.207225 0.528251 0.766736 +0.212780 0.535444 0.771369 +0.218964 0.542634 0.775033 +0.225640 0.549788 0.777836 +0.232684 0.556913 0.779901 +0.239996 0.563996 0.781409 +0.247484 0.571025 0.782510 +0.255043 0.578015 0.783346 +0.262599 0.584970 0.784026 +0.270167 0.591867 0.784609 +0.277685 0.598710 0.785142 +0.285153 0.605508 0.785651 +0.292600 0.612263 0.786152 +0.299999 0.618983 0.786650 +0.307345 0.625642 0.787141 +0.314656 0.632271 0.787628 +0.321937 0.638860 0.788116 +0.329190 0.645413 0.788605 +0.336382 0.651946 0.789089 +0.343567 0.658439 0.789569 +0.350732 0.664908 0.790046 +0.357860 0.671358 0.790523 +0.364955 0.677788 0.790999 +0.372036 0.684183 0.791473 +0.379106 0.690567 0.791945 +0.386142 0.696933 0.792417 +0.393169 0.703279 0.792888 +0.400176 0.709604 0.793358 +0.407155 0.715911 0.793826 +0.414119 0.722188 0.794290 +0.421057 0.728460 0.794752 +0.427995 0.734717 0.795214 +0.434921 0.740950 0.795680 +0.441861 0.747188 0.796155 +0.448844 0.753416 0.796651 +0.455922 0.759657 0.797187 +0.463180 0.765932 0.797793 +0.470755 0.772248 0.798515 +0.478756 0.778641 0.799408 +0.487370 0.785141 0.800529 +0.496724 0.791765 0.801946 +0.506968 0.798543 0.803713 +0.518143 0.805473 0.805851 +0.530218 0.812557 0.808363 +0.543111 0.819768 0.811211 +0.556666 0.827086 0.814340 +0.570715 0.834467 0.817689 +0.585107 0.841883 0.821192 +0.599679 0.849307 0.824785 +0.614327 0.856715 0.828432 +0.628998 0.864106 0.832098 +0.643623 0.871454 0.835758 +0.658195 0.878761 0.839411 +0.672685 0.886031 0.843041 +0.687080 0.893247 0.846647 +0.701389 0.900424 0.850237 +0.715620 0.907557 0.853797 +0.729753 0.914639 0.857340 +0.743801 0.921679 0.860861 +0.757765 0.928671 0.864361 +0.771646 0.935628 0.867837 +0.785445 0.942543 0.871291 +0.799166 0.949410 0.874724 +0.812805 0.956246 0.878142 +0.826380 0.963041 0.881540 +0.839886 0.969802 0.884919 +0.853317 0.976533 0.888284 +0.866697 0.983238 0.891633 +0.880020 0.989920 0.894972 +0.893294 0.996576 0.898292 +0.003238 0.252045 0.149354 +0.007019 0.256307 0.145763 +0.010799 0.260687 0.142058 +0.014794 0.265160 0.138304 +0.018683 0.269781 0.134456 +0.022712 0.274507 0.130529 +0.026950 0.279416 0.126478 +0.031482 0.284449 0.122349 +0.036599 0.289678 0.118255 +0.042054 0.295059 0.114007 +0.047635 0.300623 0.109822 +0.053619 0.306379 0.105514 +0.060099 0.312256 0.101269 +0.066906 0.318335 0.097065 +0.074310 0.324537 0.092956 +0.082337 0.330875 0.089057 +0.090972 0.337335 0.085272 +0.100262 0.343849 0.081885 +0.110317 0.350438 0.078683 +0.120944 0.357022 0.076034 +0.132315 0.363561 0.073986 +0.144252 0.370041 0.072442 +0.156732 0.376420 0.071703 +0.169691 0.382616 0.071689 +0.182997 0.388631 0.072425 +0.196674 0.394401 0.074055 +0.210507 0.399912 0.076332 +0.224465 0.405144 0.079395 +0.238494 0.410080 0.083096 +0.252488 0.414691 0.087378 +0.266362 0.419003 0.092025 +0.280061 0.423005 0.096959 +0.293551 0.426733 0.102245 +0.306807 0.430180 0.107676 +0.319777 0.433368 0.113262 +0.332457 0.436338 0.118843 +0.344814 0.439083 0.124409 +0.356885 0.441663 0.130047 +0.368642 0.444069 0.135530 +0.380100 0.446322 0.141034 +0.391284 0.448464 0.146382 +0.402174 0.450492 0.151674 +0.412839 0.452436 0.156891 +0.423228 0.454289 0.162020 +0.433405 0.456071 0.167034 +0.443377 0.457801 0.171937 +0.453142 0.459488 0.176782 +0.462719 0.461123 0.181540 +0.472152 0.462721 0.186262 +0.481412 0.464317 0.190881 +0.490552 0.465857 0.195464 +0.499549 0.467411 0.199986 +0.508467 0.468951 0.204530 +0.517277 0.470491 0.209003 +0.526008 0.472050 0.213491 +0.534678 0.473620 0.218014 +0.543306 0.475259 0.222568 +0.551904 0.476935 0.227193 +0.560481 0.478701 0.231925 +0.569058 0.480571 0.236788 +0.577652 0.482582 0.241812 +0.586281 0.484751 0.247059 +0.594937 0.487140 0.252583 +0.603644 0.489753 0.258407 +0.612383 0.492658 0.264604 +0.621172 0.495891 0.271214 +0.629985 0.499486 0.278276 +0.638807 0.503485 0.285804 +0.647608 0.507925 0.293868 +0.656364 0.512800 0.302451 +0.665013 0.518153 0.311569 +0.673537 0.523965 0.321206 +0.681870 0.530237 0.331312 +0.689988 0.536951 0.341891 +0.697835 0.544056 0.352859 +0.705394 0.551536 0.364166 +0.712628 0.559334 0.375775 +0.719546 0.567414 0.387613 +0.726124 0.575723 0.399636 +0.732383 0.584209 0.411808 +0.738327 0.592854 0.424069 +0.743995 0.601596 0.436406 +0.749400 0.610428 0.448784 +0.754593 0.619320 0.461182 +0.759575 0.628240 0.473598 +0.764407 0.637180 0.486042 +0.769098 0.646127 0.498511 +0.773686 0.655073 0.510983 +0.778197 0.664020 0.523503 +0.782643 0.672946 0.536052 +0.787052 0.681840 0.548666 +0.791426 0.690717 0.561324 +0.795790 0.699560 0.574057 +0.800153 0.708366 0.586871 +0.804511 0.717119 0.599769 +0.808874 0.725805 0.612736 +0.813236 0.734416 0.625787 +0.817606 0.742940 0.638902 +0.821977 0.751357 0.652066 +0.826332 0.759638 0.665252 +0.830673 0.767783 0.678442 +0.834985 0.775766 0.691599 +0.839261 0.783567 0.704691 +0.843484 0.791163 0.717687 +0.847652 0.798548 0.730537 +0.851753 0.805703 0.743222 +0.855778 0.812628 0.755699 +0.859717 0.819315 0.767947 +0.863572 0.825769 0.779955 +0.867330 0.831996 0.791702 +0.871005 0.837996 0.803186 +0.874595 0.843788 0.814415 +0.878108 0.849404 0.825404 +0.881552 0.854851 0.836182 +0.884938 0.860163 0.846777 +0.888283 0.865366 0.857243 +0.891602 0.870499 0.867627 +0.894916 0.875597 0.877980 +0.898229 0.880679 0.888357 +0.901570 0.885786 0.898808 +0.904946 0.890933 0.909378 +0.908364 0.896151 0.920105 +0.911830 0.901447 0.931020 +0.915349 0.906839 0.942140 +0.918924 0.912328 0.953474 +0.922553 0.917920 0.965016 +0.926219 0.923605 0.976769 +0.929921 0.929373 0.988709 diff --git a/proplot/cmaps/fes.txt b/proplot/cmaps/fes.txt new file mode 100644 index 000000000..7cf5da2d7 --- /dev/null +++ b/proplot/cmaps/fes.txt @@ -0,0 +1,256 @@ +0.049747 0.049747 0.049747 +0.060384 0.060385 0.060384 +0.069772 0.069773 0.069773 +0.078116 0.078117 0.078117 +0.085807 0.085810 0.085809 +0.092920 0.092923 0.092922 +0.099846 0.099848 0.099847 +0.106800 0.106801 0.106801 +0.113681 0.113682 0.113682 +0.120546 0.120547 0.120547 +0.127458 0.127458 0.127458 +0.134389 0.134389 0.134389 +0.141276 0.141276 0.141276 +0.148126 0.148126 0.148126 +0.154994 0.154994 0.154994 +0.161878 0.161878 0.161878 +0.168702 0.168702 0.168702 +0.175509 0.175509 0.175509 +0.182309 0.182309 0.182309 +0.189121 0.189121 0.189121 +0.195892 0.195892 0.195892 +0.202633 0.202633 0.202633 +0.209369 0.209369 0.209369 +0.216100 0.216100 0.216100 +0.222776 0.222776 0.222776 +0.229416 0.229416 0.229416 +0.236073 0.236073 0.236073 +0.242663 0.242663 0.242663 +0.249255 0.249255 0.249255 +0.255806 0.255806 0.255806 +0.262342 0.262342 0.262342 +0.268854 0.268854 0.268854 +0.275321 0.275321 0.275321 +0.281757 0.281757 0.281757 +0.288160 0.288160 0.288160 +0.294538 0.294538 0.294538 +0.300889 0.300889 0.300889 +0.307211 0.307211 0.307211 +0.313508 0.313508 0.313508 +0.319763 0.319763 0.319763 +0.325968 0.325968 0.325968 +0.332175 0.332175 0.332175 +0.338315 0.338315 0.338315 +0.344435 0.344435 0.344435 +0.350537 0.350537 0.350537 +0.356585 0.356585 0.356585 +0.362593 0.362593 0.362593 +0.368580 0.368580 0.368580 +0.374523 0.374523 0.374523 +0.380429 0.380429 0.380429 +0.386290 0.386290 0.386290 +0.392132 0.392132 0.392132 +0.397937 0.397937 0.397937 +0.403699 0.403699 0.403699 +0.409422 0.409422 0.409422 +0.415111 0.415111 0.415111 +0.420763 0.420763 0.420763 +0.426389 0.426389 0.426389 +0.431998 0.431998 0.431998 +0.437562 0.437562 0.437562 +0.443100 0.443100 0.443100 +0.448628 0.448628 0.448628 +0.454139 0.454139 0.454139 +0.459646 0.459646 0.459646 +0.465151 0.465151 0.465151 +0.470678 0.470678 0.470678 +0.476215 0.476215 0.476215 +0.481780 0.481780 0.481780 +0.487380 0.487380 0.487380 +0.493005 0.493005 0.493005 +0.498699 0.498699 0.498699 +0.504437 0.504437 0.504437 +0.510215 0.510215 0.510215 +0.516057 0.516057 0.516057 +0.521966 0.521966 0.521966 +0.527943 0.527943 0.527943 +0.533976 0.533976 0.533976 +0.540081 0.540081 0.540081 +0.546261 0.546261 0.546261 +0.552494 0.552494 0.552494 +0.558792 0.558792 0.558792 +0.565167 0.565167 0.565167 +0.571618 0.571618 0.571618 +0.578130 0.578130 0.578130 +0.584725 0.584725 0.584725 +0.591382 0.591382 0.591382 +0.598109 0.598109 0.598109 +0.604918 0.604918 0.604918 +0.611794 0.611794 0.611794 +0.618752 0.618752 0.618752 +0.625771 0.625771 0.625771 +0.632874 0.632874 0.632874 +0.640054 0.640054 0.640054 +0.647306 0.647306 0.647306 +0.654632 0.654632 0.654632 +0.662051 0.662051 0.662051 +0.669536 0.669536 0.669536 +0.677106 0.677106 0.677106 +0.684747 0.684747 0.684747 +0.692484 0.692484 0.692484 +0.700289 0.700289 0.700289 +0.708183 0.708183 0.708183 +0.716164 0.716164 0.716164 +0.724220 0.724220 0.724220 +0.732371 0.732371 0.732371 +0.740598 0.740598 0.740598 +0.748916 0.748916 0.748916 +0.757325 0.757325 0.757325 +0.765823 0.765823 0.765823 +0.774402 0.774402 0.774402 +0.783078 0.783078 0.783078 +0.791845 0.791845 0.791845 +0.800705 0.800705 0.800705 +0.809657 0.809657 0.809657 +0.818698 0.818698 0.818698 +0.827840 0.827840 0.827840 +0.837081 0.837081 0.837081 +0.846409 0.846409 0.846409 +0.855845 0.855845 0.855845 +0.865372 0.865372 0.865372 +0.875003 0.875003 0.875003 +0.884733 0.884733 0.884733 +0.894563 0.894563 0.894563 +0.904484 0.904484 0.904484 +0.914505 0.914505 0.914505 +0.924609 0.924609 0.924609 +0.934801 0.934801 0.934801 +0.945072 0.945072 0.945072 +0.008504 0.251455 0.150584 +0.020763 0.255549 0.147623 +0.033696 0.259727 0.144685 +0.047492 0.263977 0.141796 +0.060283 0.268282 0.138984 +0.072337 0.272675 0.136309 +0.084200 0.277146 0.133853 +0.095826 0.281652 0.131562 +0.107505 0.286180 0.129509 +0.119218 0.290743 0.127733 +0.131026 0.295279 0.126274 +0.142886 0.299785 0.125109 +0.154808 0.304209 0.124283 +0.166802 0.308560 0.123809 +0.178769 0.312776 0.123672 +0.190688 0.316886 0.123858 +0.202550 0.320854 0.124344 +0.214319 0.324652 0.125107 +0.225947 0.328324 0.126116 +0.237408 0.331837 0.127259 +0.248675 0.335221 0.128625 +0.259751 0.338433 0.130154 +0.270639 0.341543 0.131712 +0.281300 0.344520 0.133371 +0.291800 0.347411 0.135095 +0.302077 0.350188 0.136822 +0.312189 0.352880 0.138601 +0.322153 0.355506 0.140369 +0.331942 0.358053 0.142144 +0.341591 0.360533 0.143910 +0.351115 0.362988 0.145685 +0.360506 0.365396 0.147404 +0.369822 0.367762 0.149120 +0.379051 0.370105 0.150827 +0.388209 0.372423 0.152514 +0.397309 0.374731 0.154209 +0.406376 0.377020 0.155919 +0.415400 0.379290 0.157562 +0.424412 0.381563 0.159240 +0.433411 0.383829 0.160917 +0.442431 0.386086 0.162594 +0.451446 0.388367 0.164275 +0.460478 0.390646 0.165918 +0.469568 0.392932 0.167627 +0.478669 0.395244 0.169360 +0.487835 0.397568 0.171061 +0.497037 0.399915 0.172819 +0.506302 0.402299 0.174642 +0.515634 0.404731 0.176467 +0.525043 0.407220 0.178437 +0.534518 0.409777 0.180428 +0.544067 0.412418 0.182567 +0.553688 0.415152 0.184891 +0.563393 0.418024 0.187387 +0.573154 0.421052 0.190091 +0.582965 0.424279 0.193110 +0.592835 0.427733 0.196486 +0.602709 0.431441 0.200204 +0.612580 0.435443 0.204466 +0.622413 0.439773 0.209188 +0.632165 0.444477 0.214506 +0.641780 0.449558 0.220458 +0.651216 0.455053 0.227021 +0.660405 0.460949 0.234261 +0.669309 0.467271 0.242155 +0.677865 0.473972 0.250697 +0.686027 0.481048 0.259864 +0.693765 0.488473 0.269590 +0.701065 0.496177 0.279813 +0.707907 0.504161 0.290477 +0.714300 0.512334 0.301535 +0.720267 0.520687 0.312924 +0.725831 0.529182 0.324564 +0.731018 0.537769 0.336420 +0.735881 0.546431 0.348448 +0.740466 0.555137 0.360584 +0.744803 0.563896 0.372839 +0.748940 0.572671 0.385162 +0.752913 0.581454 0.397553 +0.756761 0.590253 0.409988 +0.760506 0.599065 0.422437 +0.764169 0.607875 0.434943 +0.767774 0.616678 0.447445 +0.771347 0.625474 0.459981 +0.774878 0.634273 0.472535 +0.778400 0.643049 0.485095 +0.781912 0.651821 0.497693 +0.785417 0.660563 0.510285 +0.788929 0.669296 0.522915 +0.792446 0.677998 0.535562 +0.795980 0.686665 0.548263 +0.799544 0.695301 0.560987 +0.803122 0.703895 0.573772 +0.806743 0.712439 0.586614 +0.810388 0.720932 0.599523 +0.814079 0.729364 0.612487 +0.817808 0.737713 0.625525 +0.821582 0.745977 0.638617 +0.825384 0.754139 0.651756 +0.829229 0.762170 0.664908 +0.833087 0.770067 0.678072 +0.836970 0.777809 0.691191 +0.840855 0.785370 0.704253 +0.844735 0.792737 0.717217 +0.848596 0.799902 0.730032 +0.852435 0.806848 0.742676 +0.856225 0.813565 0.755116 +0.859973 0.820063 0.767320 +0.863665 0.826342 0.779284 +0.867288 0.832403 0.790988 +0.870854 0.838258 0.802431 +0.874358 0.843922 0.813620 +0.877803 0.849423 0.824574 +0.881194 0.854772 0.835320 +0.884543 0.860002 0.845888 +0.887857 0.865135 0.856333 +0.891160 0.870212 0.866696 +0.894463 0.875265 0.877033 +0.897775 0.880312 0.887403 +0.901115 0.885392 0.897852 +0.904494 0.890521 0.908424 +0.907927 0.895726 0.919151 +0.911405 0.901015 0.930076 +0.914944 0.906406 0.941204 +0.918538 0.911896 0.952554 +0.922190 0.917495 0.964118 +0.925881 0.923186 0.975892 +0.929608 0.928963 0.987857 diff --git a/proplot/cmaps/vanimo.txt b/proplot/cmaps/vanimo.txt new file mode 100644 index 000000000..a0f79c00a --- /dev/null +++ b/proplot/cmaps/vanimo.txt @@ -0,0 +1,256 @@ +1.000000 0.803458 0.992153 +0.993966 0.791971 0.983740 +0.987910 0.780517 0.975350 +0.981851 0.769103 0.966994 +0.975778 0.757742 0.958668 +0.969707 0.746427 0.950372 +0.963631 0.735173 0.942114 +0.957554 0.723972 0.933887 +0.951473 0.712836 0.925705 +0.945393 0.701767 0.917560 +0.939308 0.690768 0.909446 +0.933218 0.679840 0.901374 +0.927131 0.668991 0.893338 +0.921042 0.658211 0.885344 +0.914952 0.647507 0.877382 +0.908857 0.636888 0.869461 +0.902762 0.626341 0.861576 +0.896659 0.615882 0.853722 +0.890548 0.605505 0.845909 +0.884436 0.595222 0.838134 +0.878313 0.585030 0.830390 +0.872186 0.574915 0.822679 +0.866046 0.564900 0.814999 +0.859899 0.554987 0.807356 +0.853732 0.545168 0.799745 +0.847560 0.535441 0.792158 +0.841377 0.525828 0.784610 +0.835174 0.516304 0.777088 +0.828958 0.506899 0.769588 +0.822720 0.497608 0.762123 +0.816465 0.488415 0.754693 +0.810184 0.479340 0.747283 +0.803891 0.470383 0.739904 +0.797569 0.461543 0.732548 +0.791229 0.452831 0.725221 +0.784867 0.444242 0.717916 +0.778473 0.435779 0.710641 +0.772057 0.427450 0.703392 +0.765618 0.419249 0.696170 +0.759143 0.411185 0.688974 +0.752644 0.403267 0.681791 +0.746120 0.395486 0.674646 +0.739569 0.387834 0.667523 +0.732972 0.380340 0.660409 +0.726340 0.372969 0.653314 +0.719666 0.365747 0.646233 +0.712934 0.358639 0.639153 +0.706153 0.351664 0.632064 +0.699290 0.344807 0.624955 +0.692359 0.338042 0.617815 +0.685319 0.331373 0.610637 +0.678174 0.324794 0.603401 +0.670910 0.318296 0.596091 +0.663515 0.311843 0.588700 +0.655976 0.305490 0.581230 +0.648281 0.299167 0.573663 +0.640446 0.292887 0.565992 +0.632449 0.286670 0.558216 +0.624298 0.280512 0.550347 +0.615982 0.274422 0.542368 +0.607521 0.268384 0.534281 +0.598894 0.262404 0.526102 +0.590116 0.256479 0.517821 +0.581197 0.250628 0.509438 +0.572137 0.244835 0.500969 +0.562937 0.239137 0.492403 +0.553590 0.233483 0.483757 +0.544127 0.227949 0.475046 +0.534537 0.222455 0.466234 +0.524829 0.217056 0.457361 +0.515010 0.211741 0.448426 +0.505084 0.206510 0.439420 +0.495068 0.201312 0.430365 +0.484947 0.196279 0.421246 +0.474764 0.191275 0.412100 +0.464496 0.186395 0.402901 +0.454153 0.181572 0.393668 +0.443760 0.176876 0.384410 +0.433308 0.172253 0.375132 +0.422821 0.167733 0.365848 +0.412322 0.163322 0.356553 +0.401775 0.158973 0.347259 +0.391249 0.154715 0.337955 +0.380710 0.150576 0.328694 +0.370175 0.146512 0.319446 +0.359685 0.142579 0.310245 +0.349231 0.138721 0.301060 +0.338829 0.134989 0.291964 +0.328494 0.131330 0.282930 +0.318241 0.127777 0.273963 +0.308084 0.124311 0.265078 +0.298047 0.120969 0.256311 +0.288151 0.117775 0.247683 +0.278414 0.114625 0.239157 +0.268845 0.111687 0.230788 +0.259457 0.108775 0.222588 +0.250246 0.106051 0.214548 +0.241306 0.103413 0.206729 +0.232581 0.100857 0.199050 +0.224100 0.098494 0.191632 +0.215928 0.096182 0.184434 +0.207987 0.094098 0.177481 +0.200322 0.092102 0.170724 +0.192993 0.090210 0.164255 +0.185958 0.088461 0.157989 +0.179182 0.086861 0.151972 +0.172717 0.085310 0.146233 +0.166577 0.084017 0.140752 +0.160701 0.082745 0.135463 +0.155145 0.081683 0.130493 +0.149902 0.080653 0.125704 +0.144926 0.079780 0.121121 +0.140204 0.079037 0.116848 +0.135778 0.078426 0.112815 +0.131685 0.077944 0.108940 +0.127825 0.077586 0.105294 +0.124216 0.077332 0.101903 +0.120913 0.077161 0.098724 +0.117925 0.077088 0.095739 +0.115124 0.077124 0.092921 +0.112670 0.077278 0.090344 +0.110421 0.077557 0.087858 +0.108355 0.077968 0.085431 +0.106650 0.078516 0.083233 +0.105001 0.079207 0.081185 +0.103676 0.080048 0.079202 +0.102454 0.081036 0.077408 +0.101433 0.082173 0.075793 +0.100602 0.083343 0.074344 +0.099957 0.084733 0.073021 +0.099492 0.086174 0.071799 +0.099204 0.087868 0.070716 +0.099092 0.089631 0.069813 +0.099154 0.091582 0.069047 +0.099384 0.093597 0.068337 +0.099759 0.095871 0.067776 +0.100291 0.098368 0.067351 +0.100986 0.101005 0.067056 +0.101850 0.103903 0.066891 +0.102897 0.107015 0.066853 +0.104071 0.110313 0.066942 +0.105427 0.113799 0.067155 +0.107008 0.117503 0.067485 +0.108664 0.121419 0.067929 +0.110590 0.125611 0.068490 +0.112654 0.129982 0.069162 +0.114826 0.134530 0.069842 +0.117253 0.139234 0.070610 +0.119847 0.144220 0.071528 +0.122590 0.149366 0.072403 +0.125578 0.154671 0.073463 +0.128666 0.160153 0.074429 +0.131965 0.165838 0.075451 +0.135397 0.171688 0.076499 +0.138981 0.177714 0.077615 +0.142733 0.183822 0.078814 +0.146576 0.190103 0.080098 +0.150581 0.196536 0.081473 +0.154679 0.203040 0.082820 +0.158914 0.209679 0.084315 +0.163242 0.216440 0.085726 +0.167643 0.223261 0.087378 +0.172141 0.230146 0.088955 +0.176729 0.237170 0.090617 +0.181386 0.244184 0.092314 +0.186149 0.251320 0.094071 +0.190925 0.258459 0.095839 +0.195784 0.265666 0.097702 +0.200666 0.272896 0.099539 +0.205639 0.280155 0.101441 +0.210622 0.287439 0.103417 +0.215650 0.294748 0.105341 +0.220718 0.302066 0.107372 +0.225786 0.309422 0.109424 +0.230874 0.316746 0.111461 +0.235999 0.324075 0.113544 +0.241117 0.331405 0.115627 +0.246251 0.338744 0.117744 +0.251422 0.346052 0.119875 +0.256556 0.353368 0.122020 +0.261715 0.360650 0.124218 +0.266861 0.367934 0.126445 +0.272000 0.375189 0.128653 +0.277166 0.382423 0.130918 +0.282309 0.389635 0.133156 +0.287411 0.396816 0.135411 +0.292531 0.403983 0.137728 +0.297634 0.411110 0.139977 +0.302709 0.418200 0.142319 +0.307778 0.425274 0.144661 +0.312834 0.432309 0.146987 +0.317872 0.439291 0.149374 +0.322894 0.446251 0.151733 +0.327875 0.453179 0.154140 +0.332865 0.460062 0.156598 +0.337807 0.466927 0.159042 +0.342760 0.473743 0.161548 +0.347686 0.480544 0.164065 +0.352603 0.487332 0.166607 +0.357533 0.494097 0.169225 +0.362447 0.500861 0.171847 +0.367380 0.507636 0.174584 +0.372343 0.514432 0.177377 +0.377346 0.521251 0.180219 +0.382376 0.528122 0.183176 +0.387463 0.535047 0.186258 +0.392612 0.542036 0.189423 +0.397829 0.549105 0.192718 +0.403109 0.556237 0.196160 +0.408462 0.563476 0.199699 +0.413899 0.570782 0.203451 +0.419422 0.578187 0.207341 +0.425026 0.585699 0.211405 +0.430714 0.593292 0.215646 +0.436494 0.600980 0.220093 +0.442370 0.608780 0.224699 +0.448326 0.616665 0.229564 +0.454389 0.624650 0.234680 +0.460529 0.632737 0.239972 +0.466791 0.640920 0.245529 +0.473127 0.649213 0.251375 +0.479586 0.657599 0.257447 +0.486124 0.666078 0.263824 +0.492766 0.674662 0.270472 +0.499512 0.683346 0.277405 +0.506360 0.692132 0.284637 +0.513311 0.701007 0.292203 +0.520349 0.709983 0.300077 +0.527497 0.719052 0.308279 +0.534736 0.728215 0.316816 +0.542067 0.737471 0.325672 +0.549496 0.746819 0.334905 +0.557016 0.756252 0.344435 +0.564609 0.765774 0.354337 +0.572299 0.775372 0.364570 +0.580064 0.785059 0.375151 +0.587893 0.794816 0.386071 +0.595806 0.804651 0.397338 +0.603786 0.814553 0.408937 +0.611815 0.824526 0.420855 +0.619911 0.834567 0.433105 +0.628048 0.844666 0.445657 +0.636232 0.854824 0.458524 +0.644446 0.865034 0.471684 +0.652701 0.875305 0.485107 +0.660987 0.885624 0.498820 +0.669299 0.895992 0.512776 +0.677630 0.906409 0.526986 +0.685970 0.916874 0.541406 +0.694321 0.927380 0.556050 +0.702695 0.937942 0.570898 +0.711071 0.948545 0.585932 +0.719449 0.959198 0.601116 +0.727825 0.969893 0.616456 +0.736200 0.980634 0.631914 +0.744576 0.991413 0.647477 diff --git a/proplot/colors.py b/proplot/colors.py new file mode 100644 index 000000000..dfc39bd80 --- /dev/null +++ b/proplot/colors.py @@ -0,0 +1,3171 @@ +#!/usr/bin/env python3 +""" +Various colormap classes and colormap normalization classes. +""" +# NOTE: To avoid name conflicts between registered colormaps and colors, print +# set(pplt.colors._cmap_database) & set(pplt.colors._color_database) whenever +# you add new colormaps. v0.8 result is {'gray', 'marine', 'ocean', 'pink'} due +# to the MATLAB and GNUPlot colormaps. Want to minimize conflicts. +# NOTE: We feel that LinearSegmentedColormap should always be used for smooth color +# transitions while ListedColormap should always be used for qualitative color sets. +# Other sources use ListedColormap for dense "perceptually uniform" colormaps possibly +# seeking optimization. However testing reveals that initialization of even very +# dense 256-level colormaps is only 1.25ms vs. 0.25ms for a ListedColormap with the +# same data (+1ms). Also ListedColormap was designed for qualitative transitions +# because specifying N different from len(colors) will cyclically loop around the +# colors or truncate colors. So we translate the relevant ListedColormaps to +# LinearSegmentedColormaps for consistency. See :rc:`cmap.listedthresh` +import functools +import json +import os +import re +from collections.abc import MutableMapping +from numbers import Integral, Number +from xml.etree import ElementTree + +import matplotlib.cm as mcm +import matplotlib.colors as mcolors +import numpy as np +import numpy.ma as ma + +from .config import rc +from .internals import ic # noqa: F401 +from .internals import ( + _kwargs_to_args, + _not_none, + _pop_props, + docstring, + inputs, + warnings, +) +from .utils import set_alpha, to_hex, to_rgb, to_rgba, to_xyz, to_xyza + +__all__ = [ + 'DiscreteColormap', + 'ContinuousColormap', + 'PerceptualColormap', + 'DiscreteNorm', + 'DivergingNorm', + 'SegmentedNorm', + 'ColorDatabase', + 'ColormapDatabase', + 'ListedColormap', # deprecated + 'LinearSegmentedColormap', # deprecated + 'PerceptuallyUniformColormap', # deprecated + 'LinearSegmentedNorm', # deprecated +] + +# Default colormap properties +DEFAULT_NAME = '_no_name' +DEFAULT_SPACE = 'hsl' + +# Color regexes +# NOTE: We do not compile hex regex because config.py needs this surrounded by \A\Z +_regex_hex = r'#(?:[0-9a-fA-F]{3,4}){2}' # 6-8 digit hex +REGEX_HEX_MULTI = re.compile(_regex_hex) +REGEX_HEX_SINGLE = re.compile(rf'\A{_regex_hex}\Z') +REGEX_ADJUST = re.compile(r'\A(light|dark|medium|pale|charcoal)?\s*(gr[ea]y[0-9]?)?\Z') + +# Colormap constants +CMAPS_CYCLIC = tuple( # cyclic colormaps loaded from rgb files + key.lower() for key in ( + 'MonoCycle', + 'twilight', + 'Phase', + 'romaO', + 'brocO', + 'corkO', + 'vikO', + 'bamO', + ) +) +CMAPS_DIVERGING = { # mirrored dictionary mapping for reversed names + key.lower(): value.lower() + for key1, key2 in ( + ('BR', 'RB'), + ('NegPos', 'PosNeg'), + ('CoolWarm', 'WarmCool'), + ('ColdHot', 'HotCold'), + ('DryWet', 'WetDry'), + ('PiYG', 'GYPi'), + ('PRGn', 'GnRP'), + ('BrBG', 'GBBr'), + ('PuOr', 'OrPu'), + ('RdGy', 'GyRd'), + ('RdBu', 'BuRd'), + ('RdYlBu', 'BuYlRd'), + ('RdYlGn', 'GnYlRd'), + ) + for key, value in ((key1, key2), (key2, key1)) +} +for _cmap_diverging in ( # remaining diverging cmaps (see PlotAxes._parse_cmap) + 'Div', + 'Vlag', + 'Spectral', + 'Balance', + 'Delta', + 'Curl', + 'roma', + 'broc', + 'cork', + 'vik', + 'bam', + 'lisbon', + 'tofino', + 'berlin', + 'vanimo', +): + CMAPS_DIVERGING[_cmap_diverging.lower()] = _cmap_diverging.lower() +CMAPS_REMOVED = { + 'Blue0': '0.6.0', + 'Cool': '0.6.0', + 'Warm': '0.6.0', + 'Hot': '0.6.0', + 'Floral': '0.6.0', + 'Contrast': '0.6.0', + 'Sharp': '0.6.0', + 'Viz': '0.6.0', +} +CMAPS_RENAMED = { + 'GrayCycle': ('MonoCycle', '0.6.0'), + 'Blue1': ('Blues1', '0.7.0'), + 'Blue2': ('Blues2', '0.7.0'), + 'Blue3': ('Blues3', '0.7.0'), + 'Blue4': ('Blues4', '0.7.0'), + 'Blue5': ('Blues5', '0.7.0'), + 'Blue6': ('Blues6', '0.7.0'), + 'Blue7': ('Blues7', '0.7.0'), + 'Blue8': ('Blues8', '0.7.0'), + 'Blue9': ('Blues9', '0.7.0'), + 'Green1': ('Greens1', '0.7.0'), + 'Green2': ('Greens2', '0.7.0'), + 'Green3': ('Greens3', '0.7.0'), + 'Green4': ('Greens4', '0.7.0'), + 'Green5': ('Greens5', '0.7.0'), + 'Green6': ('Greens6', '0.7.0'), + 'Green7': ('Greens7', '0.7.0'), + 'Green8': ('Greens8', '0.7.0'), + 'Orange1': ('Yellows1', '0.7.0'), + 'Orange2': ('Yellows2', '0.7.0'), + 'Orange3': ('Yellows3', '0.7.0'), + 'Orange4': ('Oranges2', '0.7.0'), + 'Orange5': ('Oranges1', '0.7.0'), + 'Orange6': ('Oranges3', '0.7.0'), + 'Orange7': ('Oranges4', '0.7.0'), + 'Orange8': ('Yellows4', '0.7.0'), + 'Brown1': ('Browns1', '0.7.0'), + 'Brown2': ('Browns2', '0.7.0'), + 'Brown3': ('Browns3', '0.7.0'), + 'Brown4': ('Browns4', '0.7.0'), + 'Brown5': ('Browns5', '0.7.0'), + 'Brown6': ('Browns6', '0.7.0'), + 'Brown7': ('Browns7', '0.7.0'), + 'Brown8': ('Browns8', '0.7.0'), + 'Brown9': ('Browns9', '0.7.0'), + 'RedPurple1': ('Reds1', '0.7.0'), + 'RedPurple2': ('Reds2', '0.7.0'), + 'RedPurple3': ('Reds3', '0.7.0'), + 'RedPurple4': ('Reds4', '0.7.0'), + 'RedPurple5': ('Reds5', '0.7.0'), + 'RedPurple6': ('Purples1', '0.7.0'), + 'RedPurple7': ('Purples2', '0.7.0'), + 'RedPurple8': ('Purples3', '0.7.0'), +} + +# Color constants +COLORS_OPEN = {} # populated during register_colors +COLORS_XKCD = {} # populated during register_colors +COLORS_KEEP = ( + *( # always load these XKCD colors regardless of settings + 'charcoal', 'tomato', 'burgundy', 'maroon', 'burgundy', 'lavendar', + 'taupe', 'sand', 'stone', 'earth', 'sand brown', 'sienna', + 'terracotta', 'moss', 'crimson', 'mauve', 'rose', 'teal', 'forest', + 'grass', 'sage', 'pine', 'vermillion', 'russet', 'cerise', 'avocado', + 'wine', 'brick', 'umber', 'mahogany', 'puce', 'grape', 'blurple', + 'cranberry', 'sand', 'aqua', 'jade', 'coral', 'olive', 'magenta', + 'turquoise', 'sea blue', 'royal blue', 'slate blue', 'slate grey', + 'baby blue', 'salmon', 'beige', 'peach', 'mustard', 'lime', 'indigo', + 'cornflower', 'marine', 'cloudy blue', 'tangerine', 'scarlet', 'navy', + 'cool grey', 'warm grey', 'chocolate', 'raspberry', 'denim', + 'gunmetal', 'midnight', 'chartreuse', 'ivory', 'khaki', 'plum', + 'silver', 'tan', 'wheat', 'buff', 'bisque', 'cerulean', + ), + *( # common combinations + 'red orange', 'yellow orange', 'yellow green', + 'blue green', 'blue violet', 'red violet', + 'bright red', # backwards compatibility + ), + *( # common names + prefix + color + for color in ( + 'red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet', + 'brown', 'grey', 'gray', + ) + for prefix in ('', 'light ', 'dark ', 'medium ', 'pale ') + ) +) +COLORS_REMOVE = ( + # filter these out, let's try to be professional here... + 'shit', + 'poop', + 'poo', + 'pee', + 'piss', + 'puke', + 'vomit', + 'snot', + 'booger', + 'bile', + 'diarrhea', + 'icky', + 'sickly', +) +COLORS_REPLACE = ( + # prevent registering similar-sounding names + # these can all be combined + ('/', ' '), # convert [color1]/[color2] to compound (e.g. grey/blue to grey blue) + ("'s", 's'), # robin's egg + ('egg blue', 'egg'), # robin's egg blue + ('grey', 'gray'), # 'Murica + ('ochre', 'ocher'), # ... + ('forrest', 'forest'), # ... + ('ocre', 'ocher'), # correct spelling + ('kelley', 'kelly'), # ... + ('reddish', 'red'), # remove [color]ish where it modifies the spelling of color + ('purplish', 'purple'), # ... + ('pinkish', 'pink'), + ('yellowish', 'yellow'), + ('bluish', 'blue'), + ('greyish', 'grey'), + ('ish', ''), # these are all [color]ish ('ish' substring appears nowhere else) + ('bluey', 'blue'), # remove [color]y trailing y + ('greeny', 'green'), # ... + ('reddy', 'red'), + ('pinky', 'pink'), + ('purply', 'purple'), + ('purpley', 'purple'), + ('yellowy', 'yellow'), + ('orangey', 'orange'), + ('browny', 'brown'), + ('minty', 'mint'), # now remove [object]y trailing y + ('grassy', 'grass'), # ... + ('mossy', 'moss'), + ('dusky', 'dusk'), + ('rusty', 'rust'), + ('muddy', 'mud'), + ('sandy', 'sand'), + ('leafy', 'leaf'), + ('dusty', 'dust'), + ('dirty', 'dirt'), + ('peachy', 'peach'), + ('stormy', 'storm'), + ('cloudy', 'cloud'), + ('grayblue', 'gray blue'), # separate merge compounds + ('bluegray', 'gray blue'), # ... + ('lightblue', 'light blue'), + ('yellowgreen', 'yellow green'), + ('yelloworange', 'yellow orange'), +) + +# Simple snippets +_N_docstring = """ +N : int, default: :rc:`image.lut` + Number of points in the colormap lookup table. +""" +_alpha_docstring = """ +alpha : float, optional + The opacity for the entire colormap. This overrides + the input opacities. +""" +_cyclic_docstring = """ +cyclic : bool, optional + Whether the colormap is cyclic. If ``True``, this changes how the leftmost + and rightmost color levels are selected, and `extend` can only be + ``'neither'`` (a warning will be issued otherwise). +""" +_gamma_docstring = """ +gamma : float, optional + Set `gamma1` and `gamma2` to this identical value. +gamma1 : float, optional + If greater than 1, make low saturation colors more prominent. If + less than 1, make high saturation colors more prominent. Similar to + the `HCLWizard `_ option. +gamma2 : float, optional + If greater than 1, make high luminance colors more prominent. If + less than 1, make low luminance colors more prominent. Similar to + the `HCLWizard `_ option. +""" +_space_docstring = """ +space : {'hsl', 'hpl', 'hcl', 'hsv'}, optional + The hue, saturation, luminance-style colorspace to use for interpreting + the channels. See `this page `__ for + a full description. +""" +_name_docstring = """ +name : str, default: '_no_name' + The colormap name. This can also be passed as the first + positional string argument. +""" +_ratios_docstring = """ +ratios : sequence of float, optional + Relative extents of each color transition. Must have length + ``len(colors) - 1``. Larger numbers indicate a slower + transition, smaller numbers indicate a faster transition. +""" +docstring._snippet_manager['colors.N'] = _N_docstring +docstring._snippet_manager['colors.alpha'] = _alpha_docstring +docstring._snippet_manager['colors.cyclic'] = _cyclic_docstring +docstring._snippet_manager['colors.gamma'] = _gamma_docstring +docstring._snippet_manager['colors.space'] = _space_docstring +docstring._snippet_manager['colors.ratios'] = _ratios_docstring +docstring._snippet_manager['colors.name'] = _name_docstring + +# List classmethod snippets +_from_list_docstring = """ +colors : sequence of color-spec or tuple + If a sequence of RGB[A] tuples or color strings, the colormap + transitions evenly from ``colors[0]`` at the left-hand side + to ``colors[-1]`` at the right-hand side. + + If a sequence of (float, color-spec) tuples, the float values are the + coordinate of each transition and must range from 0 to 1. This + can be used to divide the colormap range unevenly. +%(colors.name)s +%(colors.ratios)s + For example, ``('red', 'blue', 'green')`` with ``ratios=(2, 1)`` + creates a colormap with the transition from red to blue taking + *twice as long* as the transition from blue to green. +""" +docstring._snippet_manager['colors.from_list'] = _from_list_docstring + + +def _clip_colors(colors, clip=True, gray=0.2, warn_invalid=False): + """ + Clip impossible colors rendered in an HSL-to-RGB colorspace + conversion. Used by `PerceptualColormap`. + + Parameters + ---------- + colors : sequence of 3-tuple + The RGB colors. + clip : bool, optional + If `clip` is ``True`` (the default), RGB channel values >1 + are clipped to 1. Otherwise, the color is masked out as gray. + gray : float, optional + The identical RGB channel values (gray color) to be used + if `clip` is ``True``. + """ + colors = np.asarray(colors) + under = colors < 0 + over = colors > 1 + if clip: + colors[under], colors[over] = 0, 1 + else: + colors[under | over] = gray + if warn_invalid: + msg = 'Clipped' if clip else 'Invalid' + for i, name in enumerate('rgb'): + if np.any(under[:, i]) or np.any(over[:, i]): + warnings._warn_proplot(f'{msg} {name!r} channel.') + return colors + + +def _color_channel(color, channel, space='hcl'): + """ + Get the hue, saturation, or luminance channel value from the input color. The + color name `color` can optionally be a string with the format ``'color+x'`` + or ``'color-x'``, where `x` is the offset from the channel value. + + Parameters + ---------- + color : color-spec + The color. Sanitized with `to_rgba`. + channel : optional + The HCL channel to be retrieved. + space : optional + The colorspace for the corresponding channel value. + + Returns + ------- + value : float + The channel value. + """ + # Interpret channel + if callable(color) or isinstance(color, Number): + return color + if channel == 'hue': + channel = 0 + elif channel in ('chroma', 'saturation'): + channel = 1 + elif channel == 'luminance': + channel = 2 + else: + raise ValueError(f'Unknown channel {channel!r}.') + # Interpret string or RGB tuple + offset = 0 + if isinstance(color, str): + m = re.search('([-+][0-9.]+)$', color) + if m: + offset = float(m.group(0)) + color = color[:m.start()] + return offset + to_xyz(color, space)[channel] + + +def _make_segment_data(values, coords=None, ratios=None): + """ + Return a segmentdata array or callable given the input colors + and coordinates. + + Parameters + ---------- + values : sequence of float + The channel values. + coords : sequence of float, optional + The segment coordinates. + ratios : sequence of float, optional + The relative length of each segment transition. + """ + # Allow callables + if callable(values): + return values + values = np.atleast_1d(values) + if len(values) == 1: + value = values[0] + return [(0, value, value), (1, value, value)] + + # Get coordinates + if not np.iterable(values): + raise TypeError('Colors must be iterable, got {values!r}.') + if coords is not None: + coords = np.atleast_1d(coords) + if ratios is not None: + warnings._warn_proplot( + f'Segment coordinates were provided, ignoring ' + f'ratios={ratios!r}.' + ) + if len(coords) != len(values) or coords[0] != 0 or coords[-1] != 1: + raise ValueError( + f'Coordinates must range from 0 to 1, got {coords!r}.' + ) + elif ratios is not None: + coords = np.atleast_1d(ratios) + if len(coords) != len(values) - 1: + raise ValueError( + f'Need {len(values) - 1} ratios for {len(values)} colors, ' + f'but got {len(coords)} ratios.' + ) + coords = np.concatenate(([0], np.cumsum(coords))) + coords = coords / np.max(coords) # normalize to 0-1 + else: + coords = np.linspace(0, 1, len(values)) + + # Build segmentdata array + array = [] + for c, value in zip(coords, values): + array.append((c, value, value)) + return array + + +def _make_lookup_table(N, data, gamma=1.0, inverse=False): + r""" + Generate lookup tables of HSL values given specified gradations. Similar to + `~matplotlib.colors.makeMappingArray` but permits *circular* hue gradations, + disables clipping of out-of-bounds values, and uses fancier "gamma" scaling. + + Parameters + ---------- + N : int + Number of points in the colormap lookup table. + data : array-like + Sequence of `(x, y_0, y_1)` tuples specifying channel jumps + (from `y_0` to `y_1`) and `x` coordinate of those jumps + (ranges between 0 and 1). See `~matplotlib.colors.LinearSegmentedColormap`. + gamma : float or sequence of float, optional + To obtain channel values between coordinates `x_i` and `x_{i+1}` + in rows `i` and `i+1` of `data` we use the formula: + + .. math:: + + y = y_{1,i} + w_i^{\gamma_i}*(y_{0,i+1} - y_{1,i}) + + where `\gamma_i` corresponds to `gamma` and the weight `w_i` ranges from + 0 to 1 between rows ``i`` and ``i+1``. If `gamma` is float, it applies + to every transition. Otherwise, its length must equal ``data.shape[0]-1``. + + This is similar to the `matplotlib.colors.makeMappingArray` `gamma` except + it controls the weighting for transitions *between* each segment data + coordinate rather than the coordinates themselves. This makes more sense + for `PerceptualColormap`\ s because they usually contain just a + handful of transitions representing chained segments. + inverse : bool, optional + If ``True``, `w_i^{\gamma_i}` is replaced with `1 - (1 - w_i)^{\gamma_i}` -- + that is, when `gamma` is greater than 1, this weights colors toward *higher* + channel values instead of lower channel values. + + This is implemented in case we want to apply *equal* "gamma scaling" + to different HSL channels in different directions. Usually, this + is done to weight low data values with higher luminance *and* lower + saturation, thereby emphasizing "extreme" data values. + """ + # Allow for *callable* instead of linearly interpolating between segments + gammas = np.atleast_1d(gamma) + if np.any(gammas < 0.01) or np.any(gammas > 10): + raise ValueError('Gamma can only be in range [0.01,10].') + if callable(data): + if len(gammas) > 1: + raise ValueError('Only one gamma allowed for functional segmentdata.') + x = np.linspace(0, 1, N)**gamma + lut = np.array(data(x), dtype=float) + return lut + + # Get array + data = np.array(data) + shape = data.shape + if len(shape) != 2 or shape[1] != 3: + raise ValueError('Mapping data must have shape N x 3.') + if len(gammas) != 1 and len(gammas) != shape[0] - 1: + raise ValueError(f'Expected {shape[0] - 1} gammas for {shape[0]} coords. Got {len(gamma)}.') # noqa: E501 + if len(gammas) == 1: + gammas = np.repeat(gammas, shape[:1]) + + # Get indices + x = data[:, 0] + y0 = data[:, 1] + y1 = data[:, 2] + if x[0] != 0.0 or x[-1] != 1.0: + raise ValueError('Data mapping points must start with x=0 and end with x=1.') + if np.any(np.diff(x) < 0): + raise ValueError('Data mapping points must have x in increasing order.') + x = x * (N - 1) + + # Get distances from the segmentdata entry to the *left* for each requested + # level, excluding ends at (0, 1), which must exactly match segmentdata ends. + # NOTE: numpy.searchsorted returns where xq[i] must be inserted so it is + # larger than x[ind[i]-1] but smaller than x[ind[i]]. + xq = (N - 1) * np.linspace(0, 1, N) + ind = np.searchsorted(x, xq)[1:-1] + offsets = (xq[1:-1] - x[ind - 1]) / (x[ind] - x[ind - 1]) + + # Scale distances in each segment by input gamma + # The ui are starting-points, the ci are counts from that point over which + # segment applies (i.e. where to apply the gamma), the relevant 'segment' + # is to the *left* of index returned by searchsorted + _, uind, cind = np.unique(ind, return_index=True, return_counts=True) + for ui, ci in zip(uind, cind): # length should be N-1 + gamma = gammas[ind[ui] - 1] # the relevant segment is *left* of this number + if gamma == 1: + continue + if ci == 0: # no lookup table coordinates fall inside this segment + reverse = False + else: # reverse if we are transitioning to *lower* channel value + reverse = (y0[ind[ui]] - y1[ind[ui] - 1]) < 0 + if inverse: # reverse if we are transitioning to *higher* channel value + reverse = not reverse + if reverse: + offsets[ui:ui + ci] = 1 - (1 - offsets[ui:ui + ci]) ** gamma + else: + offsets[ui:ui + ci] **= gamma + + # Perform successive linear interpolations rolled up into one equation + lut = np.zeros((N,), float) + lut[1:-1] = y1[ind - 1] + offsets * (y0[ind] - y1[ind - 1]) + lut[0] = y1[0] + lut[-1] = y0[-1] + return lut + + +def _load_colors(path, warn_on_failure=True): + """ + Read colors from the input file. + + Parameters + ---------- + warn_on_failure : bool, optional + If ``True``, issue a warning when loading fails instead of raising an error. + """ + # Warn or raise error (matches Colormap._from_file behavior) + if not os.path.isfile(path): + message = f'Failed to load color data file {path!r}. File not found.' + if warn_on_failure: + warnings._warn_proplot(message) + else: + raise FileNotFoundError(message) + + # Iterate through lines + loaded = {} + with open(path, 'r') as fh: + for count, line in enumerate(fh): + stripped = line.strip() + if not stripped or stripped[0] == '#': + continue + pair = tuple(item.strip().lower() for item in line.split(':')) + if len(pair) != 2 or not REGEX_HEX_SINGLE.match(pair[1]): + warnings._warn_proplot( + f'Illegal line #{count + 1} in color file {path!r}:\n' + f'{line!r}\n' + f'Lines must be formatted as "name: hexcolor".' + ) + continue + loaded[pair[0]] = pair[1] + + return loaded + + +def _standardize_colors(input, space, margin): + """ + Standardize the input colors. + + Parameters + ---------- + input : dict + The colors. + space : optional + The colorspace used to filter colors. + margin : optional + The proportional margin required for unique colors (e.g. 0.1 + is 36 hue units, 10 saturation units, 10 luminance units). + """ + output = {} + colors = [] + channels = [] + + # Always add these colors and ignore other colors that are too close + # We do this for colors with nice names or that proplot devs really like + for name in COLORS_KEEP: + color = input.pop(name, None) + if color is None: + continue + if 'grey' in name: + name = name.replace('grey', 'gray') + colors.append((name, color)) + channels.append(to_xyz(color, space=space)) + output[name] = color # required in case "kept" colors are close to each other + + # Translate remaining colors and remove bad names + # WARNING: Unique axis argument requires numpy version >=1.13 + for name, color in input.items(): + for sub, rep in COLORS_REPLACE: + if sub in name: + name = name.replace(sub, rep) + if any(sub in name for sub in COLORS_REMOVE): + continue # remove "unpofessional" names + if name in output: + continue # prioritize names that come first + colors.append((name, color)) # category name pair + channels.append(to_xyz(color, space=space)) + + # Get locations of "perceptually distinct" colors + channels = np.asarray(channels) + if not channels.size: + return output + channels = channels / np.array([360, 100, 100]) + channels = np.round(channels / margin).astype(np.int64) + _, idxs = np.unique(channels, return_index=True, axis=0) + + # Return only "distinct" colors + for idx in idxs: + name, color = colors[idx] + output[name] = color + return output + + +class _Colormap(object): + """ + Mixin class used to add some helper methods. + """ + def _get_data(self, ext, alpha=True): + """ + Return a string containing the colormap colors for saving. + + Parameters + ---------- + ext : {'hex', 'txt', 'rgb'} + The filename extension. + alpha : bool, optional + Whether to include an opacity column. + """ + # Get lookup table colors and filter out bad ones + if not self._isinit: + self._init() + colors = self._lut[:-3, :] + + # Get data string + if ext == 'hex': + data = ', '.join(mcolors.to_hex(color) for color in colors) + elif ext in ('txt', 'rgb'): + rgb = mcolors.to_rgba if alpha else mcolors.to_rgb + data = [rgb(color) for color in colors] + data = '\n'.join(' '.join(f'{num:0.6f}' for num in line) for line in data) + else: + raise ValueError( + f'Invalid extension {ext!r}. Options are: ' + "'hex', 'txt', 'rgb', 'rgba'." + ) + return data + + def _make_name(self, suffix=None): + """ + Generate a default colormap name. Do not append more than one + leading underscore or more than one identical suffix. + """ + name = self.name + name = name or '' + if name[:1] != '_': + name = '_' + name + suffix = suffix or 'copy' + suffix = '_' + suffix + if name[-len(suffix):] != suffix: + name = name + suffix + return name + + def _parse_path(self, path, ext=None, subfolder=None): + """ + Parse the user input path. + + Parameters + ---------- + path : path-like, optional + The file path. + ext : str + The default extension. + subfolder : str, optional + The subfolder. + """ + # Get the folder + folder = rc.user_folder(subfolder=subfolder) + if path is not None: + path = os.path.expanduser(path or '.') # interpret empty string as '.' + if os.path.isdir(path): + folder, path = path, None + # Get the filename + if path is None: + path = os.path.join(folder, self.name) + if not os.path.splitext(path)[1]: + path = path + '.' + ext # default file extension + return path + + @staticmethod + def _pop_args(*args, names=None, **kwargs): + """ + Pop the name as a first positional argument or keyword argument. + Supports matplotlib-style ``Colormap(name, data, N)`` input + algongside more intuitive ``Colormap(data, name, N)`` input. + """ + names = names or () + if isinstance(names, str): + names = (names,) + names = ('name', *names) + args, kwargs = _kwargs_to_args(names, *args, **kwargs) + if args[0] is not None and args[1] is None: + args[:2] = (None, args[0]) + if args[0] is None: + args[0] = DEFAULT_NAME + return (*args, kwargs) + + @classmethod + def _from_file(cls, path, warn_on_failure=False): + """ + Read generalized colormap and color cycle files. + """ + path = os.path.expanduser(path) + name, ext = os.path.splitext(os.path.basename(path)) + listed = issubclass(cls, mcolors.ListedColormap) + reversed = name[-2:] == '_r' + + # Warn if loading failed during `register_cmaps` or `register_cycles` + # but raise error if user tries to load a file. + def _warn_or_raise(descrip, error=RuntimeError): + prefix = f'Failed to load colormap or color cycle file {path!r}.' + if warn_on_failure: + warnings._warn_proplot(prefix + ' ' + descrip) + else: + raise error(prefix + ' ' + descrip) + if not os.path.isfile(path): + return _warn_or_raise('File not found.', FileNotFoundError) + + # Directly read segmentdata json file + # NOTE: This is special case! Immediately return name and cmap + ext = ext[1:] + if ext == 'json': + if listed: + return _warn_or_raise('Cannot load cycles from JSON files.') + try: + with open(path, 'r') as fh: + data = json.load(fh) + except json.JSONDecodeError: + return _warn_or_raise('JSON decoding error.', json.JSONDecodeError) + kw = {} + for key in ('cyclic', 'gamma', 'gamma1', 'gamma2', 'space'): + if key in data: + kw[key] = data.pop(key, None) + if 'red' in data: + cmap = ContinuousColormap(name, data) + else: + cmap = PerceptualColormap(name, data, **kw) + if reversed: + cmap = cmap.reversed(name[:-2]) + return cmap + + # Read .rgb and .rgba files + if ext in ('txt', 'rgb'): + # Load file + # NOTE: This appears to be biggest import time bottleneck! Increases + # time from 0.05s to 0.2s, with numpy loadtxt or with this regex thing. + delim = re.compile(r'[,\s]+') + data = [ + delim.split(line.strip()) + for line in open(path) + if line.strip() and line.strip()[0] != '#' + ] + try: + data = [[float(num) for num in line] for line in data] + except ValueError: + return _warn_or_raise( + 'Expected a table of comma or space-separated floats.' + ) + # Build x-coordinates and standardize shape + data = np.array(data) + if data.shape[1] not in (3, 4): + return _warn_or_raise( + f'Expected 3 or 4 columns of floats. Got {data.shape[1]} columns.' + ) + if ext[0] != 'x': # i.e. no x-coordinates specified explicitly + x = np.linspace(0, 1, data.shape[0]) + else: + x, data = data[:, 0], data[:, 1:] + + # Load XML files created with scivizcolor + # Adapted from script found here: + # https://sciviscolor.org/matlab-matplotlib-pv44/ + elif ext == 'xml': + try: + doc = ElementTree.parse(path) + except ElementTree.ParseError: + return _warn_or_raise('XML parsing error.', ElementTree.ParseError) + x, data = [], [] + for s in doc.getroot().findall('.//Point'): + # Verify keys + if any(key not in s.attrib for key in 'xrgb'): + return _warn_or_raise( + 'Missing an x, r, g, or b key inside one or more tags.' + ) + # Get data + color = [] + for key in 'rgbao': # o for opacity + if key not in s.attrib: + continue + color.append(float(s.attrib[key])) + x.append(float(s.attrib['x'])) + data.append(color) + # Convert to array + if not all( + len(data[0]) == len(color) and len(color) in (3, 4) for color in data + ): + return _warn_or_raise( + 'Unexpected channel number or mixed channels across tags.' + ) + + # Read hex strings + elif ext == 'hex': + # Read arbitrary format + string = open(path).read() # into single string + data = REGEX_HEX_MULTI.findall(string) + if len(data) < 2: + return _warn_or_raise( + 'Failed to find 6-digit or 8-digit HEX strings.' + ) + # Convert to array + x = np.linspace(0, 1, len(data)) + data = [to_rgb(color) for color in data] + + # Invalid extension + else: + return _warn_or_raise( + 'Unknown colormap file extension {ext!r}. Options are: ' + + ', '.join(map(repr, ('json', 'txt', 'rgb', 'hex'))) + + '.' + ) + + # Standardize and reverse if necessary to cmap + # TODO: Document the fact that filenames ending in _r return a reversed + # version of the colormap stored in that file. + x = np.array(x) + x = (x - x.min()) / (x.max() - x.min()) # ensure they span 0-1 + data = np.array(data) + if np.any(data > 2): # from 0-255 to 0-1 + data = data / 255 + if reversed: + name = name[:-2] + data = data[::-1, :] + x = 1 - x[::-1] + if listed: + return DiscreteColormap(data, name) + else: + data = [(x, color) for x, color in zip(x, data)] + return ContinuousColormap.from_list(name, data) + + +class ContinuousColormap(mcolors.LinearSegmentedColormap, _Colormap): + r""" + Replacement for `~matplotlib.colors.LinearSegmentedColormap`. + """ + def __str__(self): + return type(self).__name__ + f'(name={self.name!r})' + + def __repr__(self): + string = f" 'name': {self.name!r},\n" + if hasattr(self, '_space'): + string += f" 'space': {self._space!r},\n" + if hasattr(self, '_cyclic'): + string += f" 'cyclic': {self._cyclic!r},\n" + for key, data in self._segmentdata.items(): + if callable(data): + string += f' {key!r}: ,\n' + else: + stop = data[-1][1] + start = data[0][2] + string += f' {key!r}: [{start:.2f}, ..., {stop:.2f}],\n' + return type(self).__name__ + '({\n' + string + '})' + + @docstring._snippet_manager + def __init__(self, *args, gamma=1, alpha=None, cyclic=False, **kwargs): + """ + Parameters + ---------- + segmentdata : dict-like + Dictionary containing the keys ``'red'``, ``'green'``, ``'blue'``, and + (optionally) ``'alpha'``. The shorthands ``'r'``, ``'g'``, ``'b'``, + and ``'a'`` are also acceptable. The key values can be callable + functions that return channel values given a colormap index, or + 3-column arrays indicating the coordinates and channel transitions. See + `matplotlib.colors.LinearSegmentedColormap` for a detailed explanation. + %(colors.name)s + %(colors.N)s + gamma : float, optional + Gamma scaling used for the *x* coordinates. + %(colors.alpha)s + %(colors.cyclic)s + + Other parameters + ---------------- + **kwargs + Passed to `matplotlib.colors.LinearSegmentedColormap`. + + See also + -------- + DiscreteColormap + matplotlib.colors.LinearSegmentedColormap + proplot.constructor.Colormap + """ + # NOTE: Additional keyword args should raise matplotlib error + name, segmentdata, N, kwargs = self._pop_args( + *args, names=('segmentdata', 'N'), **kwargs + ) + if not isinstance(segmentdata, dict): + raise ValueError(f'Invalid segmentdata {segmentdata}. Must be a dict.') + N = _not_none(N, rc['image.lut']) + data = _pop_props(segmentdata, 'rgba', 'hsla') + if segmentdata: + raise ValueError(f'Invalid segmentdata keys {tuple(segmentdata)}.') + super().__init__(name, data, N=N, gamma=gamma, **kwargs) + self._cyclic = cyclic + if alpha is not None: + self.set_alpha(alpha) + + def append(self, *args, ratios=None, name=None, N=None, **kwargs): + """ + Return the concatenation of this colormap with the + input colormaps. + + Parameters + ---------- + *args + Instances of `ContinuousColormap`. + ratios : sequence of float, optional + Relative extent of each component colormap in the + merged colormap. Length must equal ``len(args) + 1``. + For example, ``cmap1.append(cmap2, ratios=(2, 1))`` generates + a colormap with the left two-thrids containing colors from + ``cmap1`` and the right one-third containing colors from ``cmap2``. + name : str, optional + The colormap name. Default is to merge each name with underscores and + prepend a leading underscore, for example ``_name1_name2``. + N : int, optional + The number of points in the colormap lookup table. Default is + to sum the length of each lookup table. + + Other parameters + ---------------- + **kwargs + Passed to `ContinuousColormap.copy` + or `PerceptualColormap.copy`. + + Returns + ------- + ContinuousColormap + The colormap. + + See also + -------- + DiscreteColormap.append + """ + # Parse input args + if not args: + return self + if not all(isinstance(cmap, mcolors.LinearSegmentedColormap) for cmap in args): + raise TypeError(f'Arguments {args!r} must be LinearSegmentedColormaps.') + + # PerceptualColormap --> ContinuousColormap conversions + cmaps = [self, *args] + spaces = {getattr(cmap, '_space', None) for cmap in cmaps} + to_continuous = len(spaces) > 1 # mixed colorspaces *or* mixed types + if to_continuous: + for i, cmap in enumerate(cmaps): + if isinstance(cmap, PerceptualColormap): + cmaps[i] = cmap.to_continuous() + + # Combine the segmentdata, and use the y1/y2 slots at merge points so + # we never interpolate between end colors of different colormaps + segmentdata = {} + if name is None: + name = '_' + '_'.join(cmap.name for cmap in cmaps) + if not np.iterable(ratios): + ratios = [1] * len(cmaps) + ratios = np.asarray(ratios) / np.sum(ratios) + x0 = np.append(0, np.cumsum(ratios)) # coordinates for edges + xw = x0[1:] - x0[:-1] # widths between edges + for key in cmaps[0]._segmentdata.keys(): # not self._segmentdata + # Callable segments + # WARNING: If just reference a global 'funcs' list from inside the + # 'data' function it can get overwritten in this loop. Must + # embed 'funcs' into the definition using a keyword argument. + datas = [cmap._segmentdata[key] for cmap in cmaps] + if all(map(callable, datas)): # expand range from x-to-w to 0-1 + def xyy(ix, funcs=datas): # noqa: E306 + ix = np.atleast_1d(ix) + kx = np.empty(ix.shape) + for j, jx in enumerate(ix.flat): + idx = max(np.searchsorted(x0, jx) - 1, 0) + kx.flat[j] = funcs[idx]((jx - x0[idx]) / xw[idx]) + return kx + + # Concatenate segment arrays and make the transition at the + # seam instant so we *never interpolate* between end colors + # of different maps. + elif not any(map(callable, datas)): + datas = [] + for x, w, cmap in zip(x0[:-1], xw, cmaps): + xyy = np.array(cmap._segmentdata[key]) + xyy[:, 0] = x + w * xyy[:, 0] + datas.append(xyy) + for i in range(len(datas) - 1): + datas[i][-1, 2] = datas[i + 1][0, 2] + datas[i + 1] = datas[i + 1][1:, :] + xyy = np.concatenate(datas, axis=0) + xyy[:, 0] = xyy[:, 0] / xyy[:, 0].max(axis=0) # fix fp errors + + else: + raise TypeError( + 'Cannot merge colormaps with mixed callable ' + 'and non-callable segment data.' + ) + segmentdata[key] = xyy + + # Handle gamma values + ikey = None + if key == 'saturation': + ikey = 'gamma1' + elif key == 'luminance': + ikey = 'gamma2' + if not ikey or ikey in kwargs: + continue + gamma = [] + callable_ = all(map(callable, datas)) + for cmap in cmaps: + igamma = getattr(cmap, '_' + ikey) + if not np.iterable(igamma): + if callable_: + igamma = (igamma,) + else: + igamma = (igamma,) * (len(cmap._segmentdata[key]) - 1) + gamma.extend(igamma) + if callable_: + if any(igamma != gamma[0] for igamma in gamma[1:]): + warnings._warn_proplot( + 'Cannot use multiple segment gammas when concatenating ' + f'callable segments. Using the first gamma of {gamma[0]}.' + ) + gamma = gamma[0] + kwargs[ikey] = gamma + + # Return copy or merge mixed types + if to_continuous and isinstance(self, PerceptualColormap): + return ContinuousColormap(name, segmentdata, N, **kwargs) + else: + return self.copy(name, segmentdata, N, **kwargs) + + def cut(self, cut=None, name=None, left=None, right=None, **kwargs): + """ + Return a version of the colormap with the center "cut out". + This is great for making the transition from "negative" to "positive" + in a diverging colormap more distinct. + + Parameters + ---------- + cut : float, optional + The proportion to cut from the center of the colormap. For example, + ``cut=0.1`` cuts the central 10%, or ``cut=-0.1`` fills the ctranl 10% + of the colormap with the current central color (usually white). + name : str, default: '_name_copy' + The new colormap name. + left, right : float, default: 0, 1 + The colormap indices for the "leftmost" and "rightmost" + colors. See `~ContinuousColormap.truncate` for details. + right : float, optional + The colormap index for the new "rightmost" color. Must fall between + + Other parameters + ---------------- + **kwargs + Passed to `ContinuousColormap.copy` or `PerceptualColormap.copy`. + + Returns + ------- + ContinuousColormap + The colormap. + + See also + -------- + ContinuousColormap.truncate + DiscreteColormap.truncate + """ + # Parse input args + left = max(_not_none(left, 0), 0) + right = min(_not_none(right, 1), 1) + cut = _not_none(cut, 0) + offset = 0.5 * cut + if offset < 0: # add extra 'white' later on + offset = 0 + elif offset == 0: + return self.truncate(left, right) + + # Decompose cut into two truncations followed by concatenation + if 0.5 - offset < left or 0.5 + offset > right: + raise ValueError(f'Invalid cut={cut} for left={left} and right={right}.') + if name is None: + name = self._make_name() + cmap_left = self.truncate(left, 0.5 - offset) + cmap_right = self.truncate(0.5 + offset, right) + + # Permit adding extra 'white' to colormap center + # NOTE: Rely on channel abbreviations to simplify code here + args = [] + if cut < 0: + ratio = 0.5 - 0.5 * abs(cut) # ratio for flanks on either side + space = getattr(self, '_space', None) or 'rgb' + xyza = to_xyza(self(0.5), space=space) + segmentdata = { + key: _make_segment_data(x) for key, x in zip(space + 'a', xyza) + } + args.append(type(self)(DEFAULT_NAME, segmentdata, self.N)) + kwargs.setdefault('ratios', (ratio, abs(cut), ratio)) + args.append(cmap_right) + + return cmap_left.append(*args, name=name, **kwargs) + + def reversed(self, name=None, **kwargs): + """ + Return a reversed copy of the colormap. + + Parameters + ---------- + name : str, default: '_name_r' + The new colormap name. + + Other parameters + ---------------- + **kwargs + Passed to `ContinuousColormap.copy` + or `PerceptualColormap.copy`. + + See also + -------- + matplotlib.colors.LinearSegmentedColormap.reversed + """ + # Reverse segments + segmentdata = { + key: ( + (lambda x, func=data: func(x)) + if callable(data) else + [(1.0 - x, y1, y0) for x, y0, y1 in reversed(data)] + ) + for key, data in self._segmentdata.items() + } + + # Reverse gammas + if name is None: + name = self._make_name(suffix='r') + for key in ('gamma1', 'gamma2'): + if key in kwargs: + continue + gamma = getattr(self, '_' + key, None) + if gamma is not None and np.iterable(gamma): + kwargs[key] = gamma[::-1] + + cmap = self.copy(name, segmentdata, **kwargs) + cmap._rgba_under, cmap._rgba_over = cmap._rgba_over, cmap._rgba_under + return cmap + + @docstring._snippet_manager + def save(self, path=None, alpha=True): + """ + Save the colormap data to a file. + + Parameters + ---------- + path : path-like, optional + The output filename. If not provided, the colormap is saved in the + ``cmaps`` subfolder in `~proplot.config.Configurator.user_folder` + under the filename ``name.json`` (where ``name`` is the colormap + name). Valid extensions are shown in the below table. + + %(rc.cmap_exts)s + + alpha : bool, optional + Whether to include an opacity column for ``.rgb`` + and ``.txt`` files. + + See also + -------- + DiscreteColormap.save + """ + # NOTE: We sanitize segmentdata before saving to json. Convert numpy float to + # builtin float, np.array to list of lists, and callable to list of lists. + # We tried encoding func.__code__ with base64 and marshal instead, but when + # cmap.append() embeds functions as keyword arguments, this seems to make it + # *impossible* to load back up the function with FunctionType (error message: + # arg 5 (closure) must be tuple). Instead use this brute force workaround. + filename = self._parse_path(path, ext='json', subfolder='cmaps') + _, ext = os.path.splitext(filename) + if ext[1:] != 'json': + # Save lookup table colors + data = self._get_data(ext[1:], alpha=alpha) + with open(filename, 'w') as fh: + fh.write(data) + else: + # Save segment data itself + data = {} + for key, value in self._segmentdata.items(): + if callable(value): + x = np.linspace(0, 1, rc['image.lut']) # just save the transitions + y = np.array([value(_) for _ in x]).squeeze() + value = np.vstack((x, y, y)).T + data[key] = np.asarray(value).astype(float).tolist() + keys = () + if isinstance(self, PerceptualColormap): + keys = ('cyclic', 'gamma1', 'gamma2', 'space') + elif isinstance(self, ContinuousColormap): + keys = ('cyclic', 'gamma') + for key in keys: # add all attrs to dictionary + data[key] = getattr(self, '_' + key) + with open(filename, 'w') as fh: + json.dump(data, fh, indent=4) + print(f'Saved colormap to {filename!r}.') + + def set_alpha(self, alpha, coords=None, ratios=None): + """ + Set the opacity for the entire colormap or set up an opacity gradation. + + Parameters + ---------- + alpha : float or sequence of float + If float, this is the opacity for the entire colormap. If sequence of + float, the colormap traverses these opacity values. + coords : sequence of float, optional + Colormap coordinates for the opacity values. The first and last + coordinates must be ``0`` and ``1``. If `alpha` is not scalar, the + default coordinates are ``np.linspace(0, 1, len(alpha))``. + ratios : sequence of float, optional + Relative extent of each opacity transition segment. Length should + equal ``len(alpha) + 1``. For example + ``cmap.set_alpha((1, 1, 0), ratios=(2, 1))`` creates a transtion from + 100 percent to 0 percent opacity in the right *third* of the colormap. + + See also + -------- + DiscreteColormap.set_alpha + """ + alpha = _make_segment_data(alpha, coords=coords, ratios=ratios) + self._segmentdata['alpha'] = alpha + self._isinit = False + + def set_cyclic(self, b): + """ + Set whether this colormap is "cyclic". See `ContinuousColormap` for details. + """ + self._cyclic = bool(b) + self._isinit = False + + def shifted(self, shift=180, name=None, **kwargs): + """ + Return a cyclicaly shifted version of the colormap. If the colormap + cyclic property is set to ``False`` a warning will be raised. + + Parameters + ---------- + shift : float, default: 180 + The number of degrees to shift, out of 360 degrees. + name : str, default: '_name_s' + The new colormap name. + + Other parameters + ---------------- + **kwargs + Passed to `ContinuousColormap.copy` or `PerceptualColormap.copy`. + + See also + -------- + DiscreteColormap.shifted + """ + shift = shift or 0 + shift %= 360 + shift /= 360 + if shift == 0: + return self + if name is None: + name = self._make_name(suffix='s') + if not self._cyclic: + warnings._warn_proplot( + f'Shifting non-cyclic colormap {self.name!r}. To suppress this ' + 'warning use cmap.set_cyclic(True) or Colormap(..., cyclic=True).' + ) + self._cyclic = True + ratios = (1 - shift, shift) + cmap_left = self.truncate(shift, 1) + cmap_right = self.truncate(0, shift) + return cmap_left.append(cmap_right, ratios=ratios, name=name, **kwargs) + + def truncate(self, left=None, right=None, name=None, **kwargs): + """ + Return a truncated version of the colormap. + + Parameters + ---------- + left : float, default: 0 + The colormap index for the new "leftmost" color. Must fall between ``0`` + and ``1``. For example, ``left=0.1`` cuts the leftmost 10%% of the colors. + right : float, default: 1 + The colormap index for the new "rightmost" color. Must fall between ``0`` + and ``1``. For example, ``right=0.9`` cuts the leftmost 10%% of the colors. + name : str, default: '_name_copy' + The new colormap name. + + Other parameters + ---------------- + **kwargs + Passed to `ContinuousColormap.copy` + or `PerceptualColormap.copy`. + + See also + -------- + DiscreteColormap.truncate + """ + # Bail out + left = max(_not_none(left, 0), 0) + right = min(_not_none(right, 1), 1) + if left == 0 and right == 1: + return self + if name is None: + name = self._make_name() + + # Resample the segmentdata arrays + segmentdata = {} + for key, data in self._segmentdata.items(): + # Callable array + # WARNING: If just reference a global 'xyy' callable from inside + # the lambda function it gets overwritten in the loop! Must embed + # the old callable in the new one as a default keyword arg. + if callable(data): + def xyy(x, func=data): + return func(left + x * (right - left)) + + # Slice + # l is the first point where x > 0 or x > left, should be >= 1 + # r is the last point where r < 1 or r < right + else: + xyy = np.asarray(data) + x = xyy[:, 0] + l = np.searchsorted(x, left) # first x value > left # noqa + r = np.searchsorted(x, right) - 1 # last x value < right + xc = xyy[l:r + 1, :].copy() + xl = xyy[l - 1, 1:] + (left - x[l - 1]) * ( + (xyy[l, 1:] - xyy[l - 1, 1:]) / (x[l] - x[l - 1]) + ) + xr = xyy[r, 1:] + (right - x[r]) * ( + (xyy[r + 1, 1:] - xyy[r, 1:]) / (x[r + 1] - x[r]) + ) + xyy = np.vstack(((left, *xl), xc, (right, *xr))) + xyy[:, 0] = (xyy[:, 0] - left) / (right - left) + + # Retain the corresponding gamma *segments* + segmentdata[key] = xyy + if key == 'saturation': + ikey = 'gamma1' + elif key == 'luminance': + ikey = 'gamma2' + else: + continue + if ikey in kwargs: + continue + gamma = getattr(self, '_' + ikey) + if np.iterable(gamma): + if callable(xyy): + if any(igamma != gamma[0] for igamma in gamma[1:]): + warnings._warn_proplot( + 'Cannot use multiple segment gammas when ' + 'truncating colormap. Using the first gamma ' + f'of {gamma[0]}.' + ) + gamma = gamma[0] + else: + igamma = gamma[l - 1:r + 1] + if len(igamma) == 0: # TODO: issue warning? + gamma = gamma[0] + else: + gamma = igamma + kwargs[ikey] = gamma + + return self.copy(name, segmentdata, **kwargs) + + def copy( + self, name=None, segmentdata=None, N=None, *, + alpha=None, gamma=None, cyclic=None + ): + """ + Return a new colormap with relevant properties copied from this one + if they were not provided as keyword arguments. + + Parameters + ---------- + name : str, default: '_name_copy' + The new colormap name. + segmentdata, N, alpha, gamma, cyclic : optional + See `ContinuousColormap`. If not provided, these are copied + from the current colormap. + + See also + -------- + DiscreteColormap.copy + PerceptualColormap.copy + """ + if name is None: + name = self._make_name() + if segmentdata is None: + segmentdata = self._segmentdata.copy() + if gamma is None: + gamma = self._gamma + if cyclic is None: + cyclic = self._cyclic + if N is None: + N = self.N + cmap = ContinuousColormap( + name, segmentdata, N, + alpha=alpha, gamma=gamma, cyclic=cyclic + ) + cmap._rgba_bad = self._rgba_bad + cmap._rgba_under = self._rgba_under + cmap._rgba_over = self._rgba_over + return cmap + + def to_discrete(self, samples=10, name=None, **kwargs): + """ + Convert the `ContinuousColormap` to a `DiscreteColormap` by drawing + samples from the colormap. + + Parameters + ---------- + samples : int or sequence of float, optional + If integer, draw samples at the colormap coordinates + ``np.linspace(0, 1, samples)``. If sequence of float, + draw samples at the specified points. + name : str, default: '_name_copy' + The new colormap name. + + Other parameters + ---------------- + **kwargs + Passed to `DiscreteColormap`. + + See also + -------- + PerceptualColormap.to_continuous + """ + if isinstance(samples, Integral): + samples = np.linspace(0, 1, samples) + elif not np.iterable(samples): + raise TypeError('Samples must be integer or iterable.') + samples = np.asarray(samples) + colors = self(samples) + if name is None: + name = self._make_name() + return DiscreteColormap(colors, name=name, **kwargs) + + @classmethod + @docstring._snippet_manager + def from_file(cls, path, *, warn_on_failure=False): + """ + Load colormap from a file. + + Parameters + ---------- + path : path-like + The file path. Valid file extensions are shown in the below table. + + %(rc.cmap_exts)s + + warn_on_failure : bool, optional + If ``True``, issue a warning when loading fails instead of + raising an error. + + See also + -------- + DiscreteColormap.from_file + """ + return cls._from_file(path, warn_on_failure=warn_on_failure) + + @classmethod + @docstring._snippet_manager + def from_list(cls, *args, **kwargs): + """ + Make a `ContinuousColormap` from a sequence of colors. + + Parameters + ---------- + %(colors.from_list)s + + Other parameters + ---------------- + **kwargs + Passed to `ContinuousColormap`. + + Returns + ------- + ContinuousColormap + The colormap. + + See also + -------- + matplotlib.colors.LinearSegmentedColormap.from_list + PerceptualColormap.from_list + """ + # Get coordinates + name, colors, ratios, kwargs = cls._pop_args( + *args, names=('colors', 'ratios'), **kwargs + ) + coords = None + if not np.iterable(colors): + raise TypeError('Colors must be iterable.') + if ( + np.iterable(colors[0]) + and len(colors[0]) == 2 + and not isinstance(colors[0], str) + ): + coords, colors = zip(*colors) + colors = [to_rgba(color) for color in colors] + + # Build segmentdata + keys = ('red', 'green', 'blue', 'alpha') + cdict = {} + for key, values in zip(keys, zip(*colors)): + cdict[key] = _make_segment_data(values, coords, ratios) + return cls(name, cdict, **kwargs) + + # Deprecated + to_listed = warnings._rename_objs( + '0.8.0', + to_listed=to_discrete + ) + concatenate, punched, truncated, updated = warnings._rename_objs( + '0.6.0', + concatenate=append, + punched=cut, + truncated=truncate, + updated=copy, + ) + + +class DiscreteColormap(mcolors.ListedColormap, _Colormap): + r""" + Replacement for `~matplotlib.colors.ListedColormap`. + """ + def __str__(self): + return f'DiscreteColormap(name={self.name!r})' + + def __repr__(self): + colors = [c if isinstance(c, str) else to_hex(c) for c in self.colors] + string = 'DiscreteColormap({\n' + string += f" 'name': {self.name!r},\n" + string += f" 'colors': {colors!r},\n" + string += '})' + return string + + def __init__(self, colors, name=None, N=None, alpha=None, **kwargs): + """ + Parameters + ---------- + colors : sequence of color-spec, optional + The colormap colors. + name : str, default: '_no_name' + The colormap name. + N : int, default: ``len(colors)`` + The number of levels. The color list is truncated or wrapped + to match this length. + alpha : float, optional + The opacity for the colormap colors. This overrides the + input color opacities. + + Other parameters + ---------------- + **kwargs + Passed to `~matplotlib.colors.ListedColormap`. + + See also + -------- + ContinuousColormap + matplotlib.colors.ListedColormap + proplot.constructor.Colormap + """ + # NOTE: This also improves 'monochrome' detection to test all items + # in the list. Otherwise ContourSet does not apply negative_linestyle + # to monochromatic colormaps generated by passing a 'colors' keyword. + # Also note that under the hood, just like proplot, ContourSet builds + # identical monochromatic ListedColormaps when it receives scalar colors. + N = _not_none(N, len(colors)) + name = _not_none(name, DEFAULT_NAME) + super().__init__(colors, name=name, N=N, **kwargs) + if alpha is not None: + self.set_alpha(alpha) + for i, color in enumerate(self.colors): + if isinstance(color, np.ndarray): + self.colors[i] = color.tolist() + if self.colors and all(self.colors[0] == color for color in self.colors): + self.monochrome = True # for contour negative dash style + + def append(self, *args, name=None, N=None, **kwargs): + """ + Append arbitrary colormaps onto this colormap. + + Parameters + ---------- + *args + Instances of `DiscreteColormap`. + name : str, optional + The new colormap name. Default is to merge each name with underscores and + prepend a leading underscore, for example ``_name1_name2``. + N : int, optional + The number of points in the colormap lookup table. Default is + the number of colors in the concatenated lists. + + Other parameters + ---------------- + **kwargs + Passed to `~DiscreteColormap.copy`. + + See also + -------- + ContinuousColormap.append + """ + if not args: + return self + if not all(isinstance(cmap, mcolors.ListedColormap) for cmap in args): + raise TypeError(f'Arguments {args!r} must be DiscreteColormap.') + cmaps = (self, *args) + if name is None: + name = '_' + '_'.join(cmap.name for cmap in cmaps) + colors = [color for cmap in cmaps for color in cmap.colors] + N = _not_none(N, len(colors)) + return self.copy(colors, name, N, **kwargs) + + @docstring._snippet_manager + def save(self, path=None, alpha=True): + """ + Save the colormap data to a file. + + Parameters + ---------- + path : path-like, optional + The output filename. If not provided, the colormap is saved in the + ``cycles`` subfolder in `~proplot.config.Configurator.user_folder` + under the filename ``name.hex`` (where ``name`` is the color cycle + name). Valid extensions are described in the below table. + + %(rc.cycle_exts)s + + alpha : bool, optional + Whether to include an opacity column for ``.rgb`` + and ``.txt`` files. + + See also + -------- + ContinuousColormap.save + """ + filename = self._parse_path(path, ext='hex', subfolder='cycles') + _, ext = os.path.splitext(filename) + data = self._get_data(ext[1:], alpha=alpha) + with open(filename, 'w') as fh: + fh.write(data) + print(f'Saved colormap to {filename!r}.') + + def set_alpha(self, alpha): + """ + Set the opacity for the entire colormap. + + Parameters + ---------- + alpha : float + The opacity. + + See also + -------- + ContinuousColormap.set_alpha + """ + self.colors = [set_alpha(color, alpha) for color in self.colors] + self._init() + + def reversed(self, name=None, **kwargs): + """ + Return a reversed version of the colormap. + + Parameters + ---------- + name : str, default: '_name_r' + The new colormap name. + + Other parameters + ---------------- + **kwargs + Passed to `DiscreteColormap.copy` + + See also + -------- + matplotlib.colors.ListedColormap.reversed + """ + if name is None: + name = self._make_name(suffix='r') + colors = self.colors[::-1] + cmap = self.copy(colors, name, **kwargs) + cmap._rgba_under, cmap._rgba_over = cmap._rgba_over, cmap._rgba_under + return cmap + + def shifted(self, shift=1, name=None): + """ + Return a cyclically shifted version of the colormap. + + Parameters + ---------- + shift : float, default: 1 + The number of list indices to shift. + name : str, eefault: '_name_s' + The new colormap name. + + See also + -------- + ContinuousColormap.shifted + """ + if not shift: + return self + if name is None: + name = self._make_name(suffix='s') + shift = shift % len(self.colors) + colors = list(self.colors) + colors = colors[shift:] + colors[:shift] + return self.copy(colors, name, len(colors)) + + def truncate(self, left=None, right=None, name=None): + """ + Return a truncated version of the colormap. + + Parameters + ---------- + left : float, default: None + The colormap index for the new "leftmost" color. Must fall between ``0`` + and ``self.N``. For example, ``left=2`` drops the first two colors. + right : float, default: None + The colormap index for the new "rightmost" color. Must fall between ``0`` + and ``self.N``. For example, ``right=4`` keeps the first four colors. + name : str, default: '_name_copy' + The new colormap name. + + See also + -------- + ContinuousColormap.truncate + """ + if left is None and right is None: + return self + if name is None: + name = self._make_name() + colors = self.colors[left:right] + return self.copy(colors, name, len(colors)) + + def copy(self, colors=None, name=None, N=None, *, alpha=None): + """ + Return a new colormap with relevant properties copied from this one + if they were not provided as keyword arguments. + + Parameters + ---------- + name : str, default: '_name_copy' + The new colormap name. + colors, N, alpha : optional + See `DiscreteColormap`. If not provided, + these are copied from the current colormap. + + See also + -------- + ContinuousColormap.copy + PerceptualColormap.copy + """ + if name is None: + name = self._make_name() + if colors is None: + colors = list(self.colors) # copy + if N is None: + N = self.N + cmap = DiscreteColormap(colors, name, N=N, alpha=alpha) + cmap._rgba_bad = self._rgba_bad + cmap._rgba_under = self._rgba_under + cmap._rgba_over = self._rgba_over + return cmap + + @classmethod + @docstring._snippet_manager + def from_file(cls, path, *, warn_on_failure=False): + """ + Load color cycle from a file. + + Parameters + ---------- + path : path-like + The file path. Valid file extensions are shown in the below table. + + %(rc.cycle_exts)s + + warn_on_failure : bool, optional + If ``True``, issue a warning when loading fails instead of + raising an error. + + See also + -------- + ContinuousColormap.from_file + """ + return cls._from_file(path, warn_on_failure=warn_on_failure) + + # Rename methods + concatenate, truncated, updated = warnings._rename_objs( + '0.6.0', + concatenate=append, + truncated=truncate, + updated=copy, + ) + + +class PerceptualColormap(ContinuousColormap): + """ + A `ContinuousColormap` with linear transitions across hue, saturation, + and luminance rather than red, blue, and green. + """ + @docstring._snippet_manager + def __init__( + self, *args, space=None, clip=True, gamma=None, gamma1=None, gamma2=None, + **kwargs + ): + """ + Parameters + ---------- + segmentdata : dict-like + Dictionary containing the keys ``'hue'``, ``'saturation'``, + ``'luminance'``, and (optionally) ``'alpha'``. The key ``'chroma'`` is + treated as a synonym for ``'saturation'``. The shorthands ``'h'``, + ``'s'``, ``'l'``, ``'a'``, and ``'c'`` are also acceptable. The key + values can be callable functions that return channel values given a + colormap index, or 3-column arrays indicating the coordinates and + channel transitions. See `~matplotlib.colors.LinearSegmentedColormap` + for a more detailed explanation. + %(colors.name)s + %(colors.N)s + %(colors.space)s + clip : bool, optional + Whether to "clip" impossible colors (i.e. truncate HCL colors with + RGB channels with values greater than 1) or mask them out as gray. + %(colors.gamma)s + %(colors.alpha)s + %(colors.cyclic)s + + Other parameters + ---------------- + **kwargs + Passed to `matploitlib.colors.LinearSegmentedColormap`. + + Example + ------- + The below example generates a `PerceptualColormap` from a + `segmentdata` dictionary that uses color names for the hue data, + instead of channel values between ``0`` and ``360``. + + >>> import proplot as pplt + >>> data = { + >>> 'h': [[0, 'red', 'red'], [1, 'blue', 'blue']], + >>> 's': [[0, 100, 100], [1, 100, 100]], + >>> 'l': [[0, 100, 100], [1, 20, 20]], + >>> } + >>> cmap = pplt.PerceptualColormap(data) + + See also + -------- + ContinuousColormap + proplot.constructor.Colormap + """ + # Checks + name, segmentdata, N, kwargs = self._pop_args( + *args, names=('segmentdata', 'N'), **kwargs + ) + data = _pop_props(segmentdata, 'hsla') + if segmentdata: + raise ValueError(f'Invalid segmentdata keys {tuple(segmentdata)}.') + space = _not_none(space, DEFAULT_SPACE).lower() + if space not in ('rgb', 'hsv', 'hpl', 'hsl', 'hcl'): + raise ValueError(f'Unknown colorspace {space!r}.') + # Convert color strings to channel values + for key, array in data.items(): + if callable(array): # permit callable + continue + for i, xyy in enumerate(array): + xyy = list(xyy) # make copy! + for j, y in enumerate(xyy[1:]): # modify the y values + xyy[j + 1] = _color_channel(y, key, space) + data[key][i] = xyy + # Initialize + super().__init__(name, data, gamma=1.0, N=N, **kwargs) + self._gamma1 = _not_none(gamma1, gamma, 1.0) + self._gamma2 = _not_none(gamma2, gamma, 1.0) + self._space = space + self._clip = clip + + def _init(self): + """ + As with `~matplotlib.colors.LinearSegmentedColormap`, but convert + each value in the lookup table from ``self._space`` to RGB. + """ + # First generate the lookup table + channels = ('hue', 'saturation', 'luminance') + inverses = (False, False, True) # weight low chroma, high luminance + gammas = (1.0, self._gamma1, self._gamma2) + self._lut_hsl = np.ones((self.N + 3, 4), float) # fill + for i, (channel, gamma, inverse) in enumerate(zip(channels, gammas, inverses)): + self._lut_hsl[:-3, i] = _make_lookup_table( + self.N, self._segmentdata[channel], gamma, inverse + ) + if 'alpha' in self._segmentdata: + self._lut_hsl[:-3, 3] = _make_lookup_table( + self.N, self._segmentdata['alpha'] + ) + self._lut_hsl[:-3, 0] %= 360 + + # Make hues circular, set extremes i.e. copy HSL values + self._lut = self._lut_hsl.copy() + self._set_extremes() # generally just used end values in segmentdata + self._isinit = True + + # Now convert values to RGB and clip colors + for i in range(self.N + 3): + self._lut[i, :3] = to_rgb(self._lut[i, :3], self._space) + self._lut[:, :3] = _clip_colors(self._lut[:, :3], self._clip) + + @docstring._snippet_manager + def set_gamma(self, gamma=None, gamma1=None, gamma2=None): + """ + Set the gamma value(s) for the luminance and saturation transitions. + + Parameters + ---------- + %(colors.gamma)s + """ + gamma1 = _not_none(gamma1, gamma) + gamma2 = _not_none(gamma2, gamma) + if gamma1 is not None: + self._gamma1 = gamma1 + if gamma2 is not None: + self._gamma2 = gamma2 + self._init() + + def copy( + self, name=None, segmentdata=None, N=None, *, + alpha=None, gamma=None, cyclic=None, + clip=None, gamma1=None, gamma2=None, space=None + ): + """ + Return a new colormap with relevant properties copied from this one + if they were not provided as keyword arguments. + + Parameters + ---------- + name : str, default: '_name_copy' + The new colormap name. + segmentdata, N, alpha, clip, cyclic, gamma, gamma1, gamma2, space : optional + See `PerceptualColormap`. If not provided, + these are copied from the current colormap. + + See also + -------- + DiscreteColormap.copy + ContinuousColormap.copy + """ + if name is None: + name = self._make_name() + if segmentdata is None: + segmentdata = self._segmentdata.copy() + if space is None: + space = self._space + if clip is None: + clip = self._clip + if gamma is not None: + gamma1 = gamma2 = gamma + if gamma1 is None: + gamma1 = self._gamma1 + if gamma2 is None: + gamma2 = self._gamma2 + if cyclic is None: + cyclic = self._cyclic + if N is None: + N = self.N + cmap = PerceptualColormap( + name, segmentdata, N, + alpha=alpha, clip=clip, cyclic=cyclic, + gamma1=gamma1, gamma2=gamma2, space=space + ) + cmap._rgba_bad = self._rgba_bad + cmap._rgba_under = self._rgba_under + cmap._rgba_over = self._rgba_over + return cmap + + def to_continuous(self, name=None, **kwargs): + """ + Convert the `PerceptualColormap` to a standard `ContinuousColormap`. + This is used to merge such colormaps. + + Parameters + ---------- + name : str, default: '_name_copy' + The new colormap name. + + Other parameters + ---------------- + **kwargs + Passed to `ContinuousColormap`. + + See also + -------- + ContinuousColormap.to_discrete + """ + if not self._isinit: + self._init() + if name is None: + name = self._make_name() + return ContinuousColormap.from_list(name, self._lut[:-3, :], **kwargs) + + @classmethod + @docstring._snippet_manager + @warnings._rename_kwargs('0.7.0', fade='saturation', shade='luminance') + def from_color(cls, *args, **kwargs): + """ + Return a simple monochromatic "sequential" colormap that blends from white + or near-white to the input color. + + Parameters + ---------- + color : color-spec + RGB tuple, hex string, or named color string. + %(colors.name)s + %(colors.space)s + l, s, a, c + Shorthands for `luminance`, `saturation`, `alpha`, and `chroma`. + luminance : float or color-spec, default: 100 + If float, this is the luminance channel strength on the left-hand + side of the colormap. If RGB[A] tuple, hex string, or named color + string, the luminance is inferred from the color. + saturation, alpha : float or color-spec, optional + As with `luminance`, except the default `saturation` and the default + `alpha` are the channel values taken from `color`. + chroma + Alias for `saturation`. + + Other parameters + ---------------- + **kwargs + Passed to `PerceptualColormap.from_hsl`. + + Returns + ------- + PerceptualColormap + The colormap. + + See also + -------- + PerceptualColormap.from_hsl + PerceptualColormap.from_list + """ + name, color, space, kwargs = cls._pop_args( + *args, names=('color', 'space'), **kwargs + ) + space = _not_none(space, DEFAULT_SPACE).lower() + props = _pop_props(kwargs, 'hsla') + if props.get('hue', None) is not None: + raise TypeError("from_color() got an unexpected keyword argument 'hue'") + hue, saturation, luminance, alpha = to_xyza(color, space) + alpha_fade = props.pop('alpha', 1) + luminance_fade = props.pop('luminance', 100) + saturation_fade = props.pop('saturation', saturation) + return cls.from_hsl( + name, hue=hue, space=space, + alpha=(alpha_fade, alpha), + saturation=(saturation_fade, saturation), + luminance=(luminance_fade, luminance), + **kwargs + ) + + @classmethod + @docstring._snippet_manager + def from_hsl(cls, *args, **kwargs): + """ + Make a `~PerceptualColormap` by specifying the hue, + saturation, and luminance transitions individually. + + Parameters + ---------- + %(colors.space)s + %(colors.name)s + %(colors.ratios)s + For example, ``luminance=(100, 50, 0)`` with ``ratios=(2, 1)`` results + in a colormap with the transition from luminance ``100`` to ``50`` taking + *twice as long* as the transition from luminance ``50`` to ``0``. + h, s, l, a, c + Shorthands for `hue`, `saturation`, `luminance`, `alpha`, and `chroma`. + hue : float or color-spec or sequence, default: 0 + Hue channel value or sequence of values. The shorthand keyword `h` is also + acceptable. Values can be any of the following. + + 1. Numbers, within the range 0 to 360 for hue and 0 to 100 for + saturation and luminance. + 2. Color string names or hex strings, in which case the channel + value for that color is looked up. + saturation : float or color-spec or sequence, default: 50 + As with `hue`, but for the saturation channel. + luminance : float or color-spec or sequence, default: ``(100, 20)`` + As with `hue`, but for the luminance channel. + alpha : float or color-spec or sequence, default: 1 + As with `hue`, but for the alpha (opacity) channel. + chroma + Alias for `saturation`. + + Other parameters + ---------------- + **kwargs + Passed to `PerceptualColormap`. + + Returns + ------- + PerceptualColormap + The colormap. + + See also + -------- + PerceptualColormap.from_color + PerceptualColormap.from_list + """ + name, space, ratios, kwargs = cls._pop_args( + *args, names=('space', 'ratios'), **kwargs + ) + cdict = {} + props = _pop_props(kwargs, 'hsla') + for key, default in ( + ('hue', 0), + ('saturation', 100), + ('luminance', (100, 20)), + ('alpha', 1), + ): + value = props.pop(key, default) + cdict[key] = _make_segment_data(value, ratios=ratios) + return cls(name, cdict, space=space, **kwargs) + + @classmethod + @docstring._snippet_manager + def from_list(cls, *args, adjust_grays=True, **kwargs): + """ + Make a `PerceptualColormap` from a sequence of colors. + + Parameters + ---------- + %(colors.from_list)s + adjust_grays : bool, optional + Whether to adjust the hues of grayscale colors (including ``'white'``, + ``'black'``, and the ``'grayN'`` open-color colors) to the hues of the + preceding and subsequent colors in the sequence. This facilitates the + construction of diverging colormaps with monochromatic segments using + e.g. ``PerceptualColormap.from_list(['blue', 'white', 'red'])``. + + Other parameters + ---------------- + **kwargs + Passed to `PerceptualColormap`. + + Returns + ------- + PerceptualColormap + The colormap. + + See also + -------- + matplotlib.colors.LinearSegmentedColormap.from_list + ContinuousColormap.from_list + PerceptualColormap.from_color + PerceptualColormap.from_hsl + """ + # Get coordinates + coords = None + space = kwargs.get('space', DEFAULT_SPACE).lower() + name, colors, ratios, kwargs = cls._pop_args( + *args, names=('colors', 'ratios'), **kwargs + ) + if not np.iterable(colors): + raise ValueError(f'Colors must be iterable, got colors={colors!r}') + if ( + np.iterable(colors[0]) and len(colors[0]) == 2 + and not isinstance(colors[0], str) + ): + coords, colors = zip(*colors) + + # Build segmentdata + keys = ('hue', 'saturation', 'luminance', 'alpha') + hslas = [to_xyza(color, space) for color in colors] + cdict = {} + for key, values in zip(keys, zip(*hslas)): + cdict[key] = _make_segment_data(values, coords, ratios) + + # Adjust grays + if adjust_grays: + hues = cdict['hue'] # segment data + for i, color in enumerate(colors): + rgb = to_rgb(color) + if isinstance(color, str) and REGEX_ADJUST.match(color): + pass + elif not np.allclose(np.array(rgb), rgb[0]): + continue + hues[i] = list(hues[i]) # enforce mutability + if i > 0: + hues[i][1] = hues[i - 1][2] + if i < len(hues) - 1: + hues[i][2] = hues[i + 1][1] + + return cls(name, cdict, **kwargs) + + # Deprecated + to_linear_segmented = warnings._rename_objs( + '0.8.0', + to_linear_segmented=to_continuous + ) + + +def _interpolate_scalar(x, x0, x1, y0, y1): + """ + Interpolate between two points. + """ + return y0 + (y1 - y0) * (x - x0) / (x1 - x0) + + +def _interpolate_extrapolate_vector(xq, x, y): + """ + Interpolate between two vectors. Similar to `numpy.interp` except this + does not truncate out-of-bounds values (i.e. this is reversible). + """ + # Follow example of _make_lookup_table for efficient, vectorized + # linear interpolation across multiple segments. + # * Normal test puts values at a[i] if a[i-1] < v <= a[i]; for + # left-most data, satisfy a[0] <= v <= a[1] + # * searchsorted gives where xq[i] must be inserted so it is larger + # than x[ind[i]-1] but smaller than x[ind[i]] + # yq = ma.masked_array(np.interp(xq, x, y), mask=ma.getmask(xq)) + x = np.asarray(x) + y = np.asarray(y) + xq = np.atleast_1d(xq) + idx = np.searchsorted(x, xq) + idx[idx == 0] = 1 # get normed value <0 + idx[idx == len(x)] = len(x) - 1 # get normed value >0 + distance = (xq - x[idx - 1]) / (x[idx] - x[idx - 1]) + yq = distance * (y[idx] - y[idx - 1]) + y[idx - 1] + yq = ma.masked_array(yq, mask=ma.getmask(xq)) + return yq + + +def _sanitize_levels(levels, minsize=2): + """ + Ensure the levels are monotonic. If they are descending, reverse them. + """ + # NOTE: Matplotlib does not support datetime colormap levels as of 3.5 + levels = inputs._to_numpy_array(levels) + if levels.ndim != 1 or levels.size < minsize: + raise ValueError(f'Levels {levels} must be a 1D array with size >= {minsize}.') + if isinstance(levels, ma.core.MaskedArray): + levels = levels.filled(np.nan) + if not inputs._is_numeric(levels) or not np.all(np.isfinite(levels)): + raise ValueError(f'Levels {levels} does not support non-numeric cmap levels.') + diffs = np.sign(np.diff(levels)) + if np.all(diffs == 1): + descending = False + elif np.all(diffs == -1): + descending = True + levels = levels[::-1] + else: + raise ValueError(f'Levels {levels} must be monotonic.') + return levels, descending + + +class DiscreteNorm(mcolors.BoundaryNorm): + """ + Meta-normalizer that discretizes the possible color values returned by + arbitrary continuous normalizers given a sequence of level boundaries. + """ + # See this post: https://stackoverflow.com/a/48614231/4970632 + # WARNING: Must be child of BoundaryNorm. Many methods in ColorBarBase + # test for class membership, crucially including _process_values(), which + # if it doesn't detect BoundaryNorm will try to use DiscreteNorm.inverse(). + @warnings._rename_kwargs( + '0.7.0', extend='unique', descending='DiscreteNorm(descending_levels)' + ) + def __init__( + self, levels, + norm=None, unique=None, step=None, clip=False, ticks=None, labels=None + ): + """ + Parameters + ---------- + levels : sequence of float + The level boundaries. Must be monotonically increasing or decreasing. + If the latter then `~DiscreteNorm.descending` is set to ``True`` and the + colorbar axis drawn with this normalizer will be reversed. + norm : `~matplotlib.colors.Normalize`, optional + The normalizer used to transform `levels` and data values passed to + `~DiscreteNorm.__call__` before discretization. The ``vmin`` and ``vmax`` + of the normalizer are set to the minimum and maximum values in `levels`. + unique : {'neither', 'both', 'min', 'max'}, optional + Which out-of-bounds regions should be assigned unique colormap colors. + Possible values are equivalent to the `extend` values. Internally, proplot + sets this depending on the user-input `extend`, whether the colormap is + cyclic, and whether `~matplotlib.colors.Colormap.set_under` + or `~matplotlib.colors.Colormap.set_over` were called for the colormap. + step : float, optional + The intensity of the transition to out-of-bounds colors as a fraction + of the adjacent step between in-bounds colors. Internally, proplot sets + this to ``0.5`` for cyclic colormaps and ``1`` for all other colormaps. + This only has an effect on lower colors when `unique` is ``'min'`` or + ``'both'``, and on upper colors when `unique` is ``'max'`` or ``'both'``. + clip : bool, optional + Whether to clip values falling outside of the level bins. This only + has an effect on lower colors when `unique` is ``'min'`` or ``'both'``, + and on upper colors when `unique` is ``'max'`` or ``'both'``. + + Other parameters + ---------------- + ticks : array-like, default: `levels` + Default tick values to use for colorbars drawn with this normalizer. This + is set to the level centers when `values` is passed to a plotting command. + labels : array-like, optional + Default tick labels to use for colorbars drawn with this normalizer. This + is set to values when drawing on-the-fly colorbars. + + Note + ---- + This normalizer makes sure that levels always span the full range of + colors in the colormap, whether `extend` is set to ``'min'``, ``'max'``, + ``'neither'``, or ``'both'``. In matplotlib, when `extend` is not ``'both'``, + the most intense colors are cut off (reserved for "out of bounds" data), + even though they are not being used. + + See also + -------- + proplot.constructor.Norm + proplot.colors.SegmentedNorm + proplot.ticker.DiscreteLocator + """ + # Parse input arguments + # NOTE: This must be a subclass BoundaryNorm, so ColorbarBase will + # detect it... even though we completely override it. + if step is None: + step = 1.0 + if unique is None: + unique = 'neither' + if not norm: + norm = mcolors.Normalize() + elif isinstance(norm, mcolors.BoundaryNorm): + raise ValueError('Normalizer cannot be instance of BoundaryNorm.') + elif not isinstance(norm, mcolors.Normalize): + raise ValueError('Normalizer must be instance of Normalize.') + uniques = ('min', 'max', 'both', 'neither') + if unique not in uniques: + raise ValueError( + f'Unknown unique setting {unique!r}. Options are: ' + + ', '.join(map(repr, uniques)) + + '.' + ) + + # Process level boundaries and centers + # NOTE: Currently there are no normalizers that reverse direction + # of levels. Tried that with SegmentedNorm but colorbar ticks fail. + # Instead user-reversed levels will always get passed here just as + # they are passed to SegmentedNorm inside plot.py + levels, descending = _sanitize_levels(levels) + vcenter = getattr(norm, 'vcenter', None) + vmin = norm.vmin = np.min(levels) + vmax = norm.vmax = np.max(levels) + bins, _ = _sanitize_levels(norm(levels)) + mids = np.zeros((levels.size + 1,)) + mids[1:-1] = 0.5 * (levels[1:] + levels[:-1]) + mids[0], mids[-1] = mids[1], mids[-2] + + # Adjust color coordinate for each bin + # For same out-of-bounds colors, looks like [0 - eps, 0, ..., 1, 1 + eps] + # For unique out-of-bounds colors, looks like [0 - eps, X, ..., 1 - X, 1 + eps] + # NOTE: Critical that we scale the bin centers in "physical space" and *then* + # translate to color coordinates so that nonlinearities in the normalization + # stay intact. If we scaled the bin centers in *normalized space* to have + # minimum 0 maximum 1, would mess up color distribution. However this is still + # not perfect... get asymmetric color intensity either side of central point. + # So we add special handling for diverging norms below to improve symmetry. + if unique in ('min', 'both'): + scale = levels[0] - levels[1] if len(levels) == 2 else mids[1] - mids[2] + mids[0] += step * scale + if unique in ('max', 'both'): + scale = levels[-1] - levels[-2] if len(levels) == 2 else mids[-2] - mids[-3] + mids[-1] += step * scale + mmin = np.min(mids) + mmax = np.max(mids) + if np.isclose(mmin, mmax): + mmin = mmin - (mmin or 1) * 1e-10 + mmax = mmax + (mmax or 1) * 1e-10 + if vcenter is None: # not diverging norm or centered segmented norm + mids = _interpolate_scalar(mids, mmin, mmax, vmin, vmax) + else: + mask1, mask2 = mids < vcenter, mids >= vcenter + mids[mask1] = _interpolate_scalar(mids[mask1], mmin, vcenter, vmin, vcenter) + mids[mask2] = _interpolate_scalar(mids[mask2], vcenter, mmax, vcenter, vmax) + + # Instance attributes + # NOTE: If clip is True, we clip values to the centers of the end bins + # rather than vmin/vmax to prevent out-of-bounds colors from getting an + # in-bounds bin color due to landing on a bin edge. + # NOTE: With unique='min' the minimimum in-bounds and out-of-bounds + # colors are the same so clip=True will have no effect. Same goes + # for unique='max' with maximum colors. + eps = 1e-10 + dest = norm(mids) + dest[0] -= eps # dest guaranteed to be numpy.float64 + dest[-1] += eps + self._ticks = _not_none(ticks, levels) + self._labels = labels + self._descending = descending + self._bmin = np.min(mids) + self._bmax = np.max(mids) + self._bins = bins + self._dest = dest + self._norm = norm + self.N = levels.size + self.boundaries = levels + mcolors.Normalize.__init__(self, vmin=vmin, vmax=vmax, clip=clip) + + # Add special clipping + # WARNING: For some reason must clip manually for LogNorm, or end + # up with unpredictable fill value, weird "out-of-bounds" colors + self._norm_clip = None + if isinstance(norm, mcolors.LogNorm): + self._norm_clip = (1e-249, None) + + def __call__(self, value, clip=None): + """ + Normalize data values to 0-1. + + Parameters + ---------- + value : numeric + The data to be normalized. + clip : bool, default: ``self.clip`` + Whether to clip values falling outside of the level bins. + """ + # Follow example of SegmentedNorm, but perform no interpolation, + # just use searchsorted to bin the data. + norm_clip = self._norm_clip + if norm_clip: # special extra clipping due to normalizer + value = np.clip(value, *norm_clip) + if clip is None: # builtin clipping + clip = self.clip + if clip: # note that np.clip can handle masked arrays + value = np.clip(value, self._bmin, self._bmax) + xq, is_scalar = self.process_value(value) + xq = self._norm(xq) + yq = self._dest[np.searchsorted(self._bins, xq)] + yq = ma.array(yq, mask=ma.getmask(xq)) + if is_scalar: + yq = np.atleast_1d(yq)[0] + if self.descending: + yq = 1 - yq + return yq + + def inverse(self, value): # noqa: U100 + """ + Raise an error. + + Raises + ------ + ValueError + Inversion after discretization is impossible. + """ + raise ValueError('DiscreteNorm is not invertible.') + + @property + def descending(self): + """ + Boolean indicating whether the levels are descending. + """ + return self._descending + + +class SegmentedNorm(mcolors.Normalize): + """ + Normalizer that scales data linearly with respect to the + interpolated index in an arbitrary monotonic level sequence. + """ + def __init__(self, levels, vcenter=None, vmin=None, vmax=None, clip=None, fair=True): # noqa: E501 + """ + Parameters + ---------- + levels : sequence of float + The level boundaries. Must be monotonically increasing or decreasing. + vcenter : float, default: None + The central colormap value. Default is to omit this. + vmin : float, optional + Ignored but included for consistency. Set to ``min(levels)``. + vmax : float, optional + Ignored but included for consistency. Set to ``max(levels)``. + clip : bool, optional + Whether to clip values falling outside of `vmin` and `vmax`. + fair : bool, optional + Whether to use fair scaling. See `DivergingNorm`. + + See also + -------- + proplot.constructor.Norm + proplot.colors.DiscreteNorm + + Note + ---- + The algorithm this normalizer uses to select normalized values + in-between level list indices is adapted from the algorithm + `~matplotlib.colors.LinearSegmentedColormap` uses to select channel + values in-between segment data points (hence the name `SegmentedNorm`). + + Example + ------- + In the below example, unevenly spaced levels are passed to + `~matplotlib.axes.Axes.contourf`, resulting in the automatic + application of `SegmentedNorm`. + + >>> import proplot as pplt + >>> import numpy as np + >>> levels = [1, 2, 5, 10, 20, 50, 100, 200, 500, 1000] + >>> data = 10 ** (3 * np.random.rand(10, 10)) + >>> fig, ax = pplt.subplots() + >>> ax.contourf(data, levels=levels) + """ + # WARNING: Tried using descending levels by adding 1 - yq to __call__() and + # inverse() but then tick labels fail. Instead just silently reverse here and + # the corresponding DiscreteLocator should enforce the descending axis. + levels, _ = _sanitize_levels(levels) + dest = np.linspace(0, 1, len(levels)) + vmin = np.min(levels) + vmax = np.max(levels) + if vcenter is not None: + center = _interpolate_extrapolate_vector(vcenter, levels, dest) + idxs, = np.where(np.isclose(vcenter, levels)) + if fair: + delta = center - 0.5 + delta = max(-(dest[0] - delta), dest[-1] - delta - 1) + dest = (dest - center) / (1 + 2 * delta) + 0.5 + elif idxs.size and idxs[0] > 0 and idxs[0] < len(levels) - 1: + dest1 = np.linspace(0, 0.5, idxs[0] + 1) + dest2 = np.linspace(0.5, 1, len(levels) - idxs[0]) + dest = np.append(dest1, dest2[1:]) + else: + raise ValueError(f'Center {vcenter} not in level list {levels}.') + super().__init__(vmin=vmin, vmax=vmax, clip=clip) + self.vcenter = vcenter # used for DiscreteNorm + self._x = self.boundaries = levels # 'boundaries' are used in PlotAxes + self._y = dest + + def __call__(self, value, clip=None): + """ + Normalize the data values to 0-1. Inverse of `~SegmentedNorm.inverse`. + + Parameters + ---------- + value : numeric + The data to be normalized. + clip : bool, default: ``self.clip`` + Whether to clip values falling outside of the minimum and maximum levels. + """ + if clip is None: # builtin clipping + clip = self.clip + if clip: # numpy.clip can handle masked arrays + value = np.clip(value, self.vmin, self.vmax) + xq, is_scalar = self.process_value(value) + yq = _interpolate_extrapolate_vector(xq, self._x, self._y) + if is_scalar: + yq = np.atleast_1d(yq)[0] + return yq + + def inverse(self, value): + """ + Inverse of `~SegmentedNorm.__call__`. + + Parameters + ---------- + value : numeric + The data to be un-normalized. + """ + yq, is_scalar = self.process_value(value) + xq = _interpolate_extrapolate_vector(yq, self._y, self._x) + if is_scalar: + xq = np.atleast_1d(xq)[0] + return xq + + +class DivergingNorm(mcolors.Normalize): + """ + Normalizer that ensures some central data value lies at the central + colormap color. The default central value is ``0``. + """ + def __str__(self): + return type(self).__name__ + f'(center={self.vcenter!r})' + + def __init__(self, vcenter=0, vmin=None, vmax=None, clip=None, fair=True): + """ + Parameters + ---------- + vcenter : float, default: 0 + The central data value. + vmin : float, optional + The minimum data value. + vmax : float, optional + The maximum data value. + clip : bool, optional + Whether to clip values falling outside of `vmin` and `vmax`. + fair : bool, optional + If ``True`` (default), the speeds of the color gradations on either side + of the center point are equal, but colormap colors may be omitted. If + ``False``, all colormap colors are included, but the color gradations on + one side may be faster than the other side. ``False`` should be used with + great care, as it may result in a misleading interpretation of your data. + + See also + -------- + proplot.constructor.Norm + """ + # NOTE: This post is an excellent summary of matplotlib's DivergingNorm history: + # https://github.com/matplotlib/matplotlib/issues/15336#issuecomment-535291287 + # NOTE: This is a stale PR that plans to implement the same features. + # https://github.com/matplotlib/matplotlib/pull/15333#issuecomment-537545430 + # Since proplot is starting without matplotlib's baggage we can just implement + # a diverging norm like they would prefer if they didn't have to worry about + # confusing users: single class, default "fair" scaling that can be turned off. + super().__init__(vmin, vmax, clip) + self.vcenter = vcenter + self.fair = fair + + def __call__(self, value, clip=None): + """ + Normalize the data values to 0-1. + + Parameters + ---------- + value : numeric + The data to be normalized. + clip : bool, default: ``self.clip`` + Whether to clip values falling outside of `vmin` and `vmax`. + """ + xq, is_scalar = self.process_value(value) + self.autoscale_None(xq) # sets self.vmin, self.vmax if None + if clip is None: # builtin clipping + clip = self.clip + if clip: # note that np.clip can handle masked arrays + value = np.clip(value, self.vmin, self.vmax) + if self.vmin > self.vmax: + raise ValueError('vmin must be less than or equal to vmax.') + elif self.vmin == self.vmax: + x = [self.vmin, self.vmax] + y = [0.0, 0.0] + elif self.vcenter >= self.vmax: + x = [self.vmin, self.vcenter] + y = [0.0, 0.5] + elif self.vcenter <= self.vmin: + x = [self.vcenter, self.vmax] + y = [0.5, 1.0] + elif self.fair: + offset = max(abs(self.vcenter - self.vmin), abs(self.vmax - self.vcenter)) + x = [self.vcenter - offset, self.vcenter + offset] + y = [0.0, 1.0] + else: + x = [self.vmin, self.vcenter, self.vmax] + y = [0.0, 0.5, 1.0] + yq = _interpolate_extrapolate_vector(xq, x, y) + if is_scalar: + yq = np.atleast_1d(yq)[0] + return yq + + def autoscale_None(self, z): + """ + Get vmin and vmax, and then clip at vcenter. + """ + super().autoscale_None(z) + if self.vmin > self.vcenter: + self.vmin = self.vcenter + if self.vmax < self.vcenter: + self.vmax = self.vcenter + + +def _init_color_database(): + """ + Initialize the subclassed database. + """ + database = mcolors._colors_full_map + if not isinstance(database, ColorDatabase): + database = mcolors._colors_full_map = ColorDatabase(database) + if hasattr(mcolors, 'colorConverter'): # suspect deprecation is coming soon + mcolors.colorConverter.cache = database.cache + mcolors.colorConverter.colors = database + return database + + +def _init_cmap_database(): + """ + Initialize the subclassed database. + """ + # WARNING: Skip over the matplotlib native duplicate entries + # with suffixes '_r' and '_shifted'. + attr = '_cmap_registry' if hasattr(mcm, '_cmap_registry') else 'cmap_d' + database = getattr(mcm, attr) + if mcm.get_cmap is not _get_cmap: + mcm.get_cmap = _get_cmap + if mcm.register_cmap is not _register_cmap: + mcm.register_cmap = _register_cmap + if not isinstance(database, ColormapDatabase): + database = { + key: value for key, value in database.items() + if key[-2:] != '_r' and key[-8:] != '_shifted' + } + database = ColormapDatabase(database) + setattr(mcm, attr, database) + return database + + +_mpl_register_cmap = mcm.register_cmap +@functools.wraps(_mpl_register_cmap) # noqa: E302 +def _register_cmap(*args, **kwargs): + """ + Monkey patch for `~matplotlib.cm.register_cmap`. Ignores warning + message when re-registering existing colormaps. This is unnecessary + and triggers 100 warnings when importing seaborn. + """ + with warnings.catch_warnings(): + warnings.simplefilter('ignore', UserWarning) + return _mpl_register_cmap(*args, **kwargs) + + +@functools.wraps(mcm.get_cmap) +def _get_cmap(name=None, lut=None): + """ + Monkey patch for `~matplotlib.cm.get_cmap`. Permits case-insensitive + search of monkey-patched colormap database. This was broken in v3.2.0 + because matplotlib now uses _check_in_list with cmap dictionary keys. + """ + if name is None: + name = rc['image.cmap'] + if isinstance(name, mcolors.Colormap): + return name + cmap = _cmap_database[name] + if lut is not None: + cmap = cmap._resample(lut) + return cmap + + +def _get_cmap_subtype(name, subtype): + """ + Get a colormap belonging to a particular class. If none are found then raise + a useful error message that omits colormaps from other classes. + """ + # NOTE: Right now this is just used in rc validation but could be used elsewhere + if subtype == 'discrete': + cls = DiscreteColormap + elif subtype == 'continuous': + cls = ContinuousColormap + elif subtype == 'perceptual': + cls = PerceptualColormap + else: + raise RuntimeError(f'Invalid subtype {subtype!r}.') + cmap = _cmap_database.get(name, None) + if not isinstance(cmap, cls): + names = sorted(k for k, v in _cmap_database.items() if isinstance(v, cls)) + raise ValueError( + f'Invalid {subtype} colormap name {name!r}. Options are: ' + + ', '.join(map(repr, names)) + + '.' + ) + return cmap + + +def _translate_cmap(cmap, lut=None, cyclic=None, listedthresh=None): + """ + Translate the input argument to a proplot colormap subclass. Auto-detect + cyclic colormaps based on names and re-apply default lookup table size. + """ + # Parse args + # WARNING: Apply default 'cyclic' property to native matplotlib colormaps + # based on known names. Maybe slightly dangerous but cleanest approach + lut = _not_none(lut, rc['image.lut']) + cyclic = _not_none(cyclic, cmap.name and cmap.name.lower() in CMAPS_CYCLIC) + listedthresh = _not_none(listedthresh, rc['cmap.listedthresh']) + + # Translate the colormap + # WARNING: Here we ignore 'N' in order to respect proplotrc lut sizes + # when initializing proplot. + bad = cmap._rgba_bad + under = cmap._rgba_under + over = cmap._rgba_over + name = cmap.name + if isinstance(cmap, (DiscreteColormap, ContinuousColormap)): + pass + elif isinstance(cmap, mcolors.LinearSegmentedColormap): + data = dict(cmap._segmentdata) + cmap = ContinuousColormap(name, data, N=lut, gamma=cmap._gamma, cyclic=cyclic) + elif isinstance(cmap, mcolors.ListedColormap): + colors = list(cmap.colors) + if len(colors) > listedthresh: # see notes at top of file + cmap = ContinuousColormap.from_list(name, colors, N=lut, cyclic=cyclic) + else: + cmap = DiscreteColormap(colors, name) + elif isinstance(cmap, mcolors.Colormap): # base class + pass + else: + raise ValueError( + f'Invalid colormap type {type(cmap).__name__!r}. ' + 'Must be instance of matplotlib.colors.Colormap.' + ) + + # Apply hidden settings + cmap._rgba_bad = bad + cmap._rgba_under = under + cmap._rgba_over = over + + return cmap + + +class _ColorCache(dict): + """ + Replacement for the native color cache. + """ + def __getitem__(self, key): + """ + Get the standard color, colormap color, or color cycle color. + """ + # NOTE: Matplotlib 'color' args are passed to to_rgba, which tries to read + # directly from cache and if that fails, sanitizes input, which raises + # error on receiving (colormap, idx) tuple. So we have to override cache. + return self._get_rgba(*key) + + def _get_rgba(self, arg, alpha): + """ + Try to get the color from the registered colormap or color cycle. + """ + key = (arg, alpha) + if isinstance(arg, str) or not np.iterable(arg) or len(arg) != 2: + return dict.__getitem__(self, key) + if not isinstance(arg[0], str) or not isinstance(arg[1], Number): + return dict.__getitem__(self, key) + # Try to get the colormap + try: + cmap = _cmap_database[arg[0]] + except (KeyError, TypeError): + return dict.__getitem__(self, key) + # Read the colormap value + if isinstance(cmap, DiscreteColormap): + if not 0 <= arg[1] < len(cmap.colors): + raise ValueError( + f'Color cycle sample for {arg[0]!r} cycle must be ' + f'between 0 and {len(cmap.colors) - 1}, got {arg[1]}.' + ) + rgba = cmap.colors[arg[1]] # draw from list of colors + else: + if not 0 <= arg[1] <= 1: + raise ValueError( + f'Colormap sample for {arg[0]!r} colormap must be ' + f'between 0 and 1, got {arg[1]}.' + ) + rgba = cmap(arg[1]) # get color selection + # Return the colormap value + rgba = to_rgba(rgba) + a = _not_none(alpha, rgba[3]) + return (*rgba[:3], a) + + +class ColorDatabase(MutableMapping, dict): + """ + Dictionary subclass used to replace the builtin matplotlib color database. + See `~ColorDatabase.__getitem__` for details. + """ + _colors_replace = ( + ('grey', 'gray'), # British --> American synonyms + ('ochre', 'ocher'), # ... + ('kelley', 'kelly'), # backwards compatibility to correct spelling + ) + + def __iter__(self): + yield from dict.__iter__(self) + + def __len__(self): + return dict.__len__(self) + + def __delitem__(self, key): + key = self._parse_key(key) + dict.__delitem__(self, key) + self.cache.clear() + + def __init__(self, mapping=None): + """ + Parameters + ---------- + mapping : dict-like, optional + The colors. + """ + # NOTE: Tested with and without standardization and speedup is marginal + self._cache = _ColorCache() + mapping = mapping or {} + for key, value in mapping.items(): + self.__setitem__(key, value) + + def __getitem__(self, key): + """ + Get a color. Translates ``grey`` into ``gray`` and supports retrieving + colors "on-the-fly" from registered colormaps and color cycles. + + * For a colormap, use e.g. ``color=('Blues', 0.8)``. + The number is the colormap index, and must be between 0 and 1. + * For a color cycle, use e.g. ``color=('colorblind', 2)``. + The number is the color list index. + + This works everywhere that colors are used in matplotlib, for + example as `color`, `edgecolor', or `facecolor` keyword arguments + passed to `~proplot.axes.PlotAxes` commands. + """ + key = self._parse_key(key) + return dict.__getitem__(self, key) + + def __setitem__(self, key, value): + """ + Add a color. Translates ``grey`` into ``gray`` and clears the + cache. The color must be a string. + """ + # Always standardize assignments. + key = self._parse_key(key) + dict.__setitem__(self, key, value) + self.cache.clear() + + def _parse_key(self, key): + """ + Parse the color key. Currently this just translates grays. + """ + if not isinstance(key, str): + raise ValueError(f'Invalid color name {key!r}. Must be string.') + if isinstance(key, str) and len(key) > 1: # ignore base colors + key = key.lower() + for sub, rep in self._colors_replace: + key = key.replace(sub, rep) + return key + + @property + def cache(self): + # Matplotlib uses 'cache' but treat '_cache' as synonym + # to guard against private API changes. + return self._cache + + +class ColormapDatabase(MutableMapping, dict): + """ + Dictionary subclass used to replace the matplotlib + colormap registry. See `~ColormapDatabase.__getitem__` and + `~ColormapDatabase.__setitem__` for details. + """ + _regex_grays = re.compile(r'\A(grays)(_r|_s)*\Z', flags=re.IGNORECASE) + _regex_suffix = re.compile(r'(_r|_s)*\Z', flags=re.IGNORECASE) + + def __iter__(self): + yield from dict.__iter__(self) + + def __len__(self): + return dict.__len__(self) + + def __delitem__(self, key): + key = self._parse_key(key, mirror=True) + dict.__delitem__(self, key) + + def __init__(self, kwargs): + """ + Parameters + ---------- + kwargs : dict-like + The source dictionary. + """ + for key, value in kwargs.items(): + self.__setitem__(key, value) + + def __getitem__(self, key): + """ + Retrieve the colormap associated with the sanitized key name. The + key name is case insensitive. + + * If the key ends in ``'_r'``, the result of ``cmap.reversed()`` is + returned for the colormap registered under the preceding name. + * If the key ends in ``'_s'``, the result of ``cmap.shifted(180)`` is + returned for the colormap registered under the preceding name. + * Reversed diverging colormaps can be requested with their "reversed" + name -- for example, ``'BuRd'`` is equivalent to ``'RdBu_r'``. + """ + return self._get_item(key) + + def __setitem__(self, key, value): + """ + Store the colormap under its lowercase name. If the object is a + `matplotlib.colors.ListedColormap` and ``cmap.N`` is smaller than + :rc:`cmap.listedthresh`, it is converted to a `proplot.colors.DiscreteColormap`. + Otherwise, it is converted to a `proplot.colors.ContinuousColormap`. + """ + self._set_item(key, value) + + def _translate_deprecated(self, key): + """ + Check if a colormap has been deprecated. + """ + # WARNING: Must search only for case-sensitive *capitalized* names or we would + # helpfully "redirect" user to SciVisColor cmap when they are trying to + # generate open-color monochromatic cmaps and would disallow some color names + if isinstance(key, str): + test = self._regex_suffix.sub('', key) + else: + test = None + if not self._has_item(test) and test in CMAPS_REMOVED: + version = CMAPS_REMOVED[test] + raise ValueError( + f'The colormap name {key!r} was removed in version {version}.' + ) + if not self._has_item(test) and test in CMAPS_RENAMED: + test_new, version = CMAPS_RENAMED[test] + warnings._warn_proplot( + f'The colormap name {test!r} was deprecated in version {version} ' + f'and may be removed in {warnings._next_release()}. Please use ' + f'the colormap name {test_new!r} instead.' + ) + key = re.sub(test, test_new, key, flags=re.IGNORECASE) + return key + + def _translate_key(self, key, mirror=True): + """ + Return the sanitized colormap name. Used for lookups and assignments. + """ + # Sanitize key + if not isinstance(key, str): + raise KeyError(f'Invalid key {key!r}. Key must be a string.') + key = key.lower() + key = self._regex_grays.sub(r'greys\2', key) + # Mirror diverging + reverse = key[-2:] == '_r' + if reverse: + key = key[:-2] + if mirror and not self._has_item(key): # avoid recursion here + key_mirror = CMAPS_DIVERGING.get(key, None) + if key_mirror and self._has_item(key_mirror): + reverse = not reverse + key = key_mirror + if reverse: + key = key + '_r' + return key + + def _has_item(self, key): + """ + Redirect to unsanitized `dict.__contains__`. + """ + return dict.__contains__(self, key) + + def _get_item(self, key): + """ + Get the colormap with flexible input keys. + """ + # Sanitize key + key = self._translate_deprecated(key) + key = self._translate_key(key, mirror=True) + shift = key[-2:] == '_s' and not self._has_item(key) + if shift: + key = key[:-2] + reverse = key[-2:] == '_r' and not self._has_item(key) + if reverse: + key = key[:-2] + # Retrieve colormap + try: + value = dict.__getitem__(self, key) # may raise keyerror + except KeyError: + raise KeyError( + f'Invalid colormap or color cycle name {key!r}. Options are: ' + + ', '.join(map(repr, self)) + + '.' + ) + # Modify colormap + if reverse: + value = value.reversed() + if shift: + value = value.shifted(180) + return value + + def _set_item(self, key, value): + """ + Add the colormap after validating and converting. + """ + if not isinstance(key, str): + raise KeyError(f'Invalid key {key!r}. Must be string.') + if not isinstance(value, mcolors.Colormap): + raise ValueError('Object is not a colormap.') + key = self._translate_key(key, mirror=False) + value = _translate_cmap(value) + dict.__setitem__(self, key, value) + + +# Initialize databases +_cmap_database = _init_cmap_database() +_color_database = _init_color_database() + +# Deprecated +( + ListedColormap, + LinearSegmentedColormap, + PerceptuallyUniformColormap, + LinearSegmentedNorm, +) = warnings._rename_objs( # noqa: E501 + '0.8.0', + ListedColormap=DiscreteColormap, + LinearSegmentedColormap=ContinuousColormap, + PerceptuallyUniformColormap=PerceptualColormap, + LinearSegmentedNorm=SegmentedNorm, +) diff --git a/proplot/colors/crayola.txt b/proplot/colors/crayola.txt deleted file mode 100644 index 4cb779566..000000000 --- a/proplot/colors/crayola.txt +++ /dev/null @@ -1,122 +0,0 @@ -# Crayola crayon colors -# https://en.wikipedia.org/wiki/List_of_Crayola_crayon_colors -almond: #efdecd -antique brass: #cd9575 -apricot: #fdd9b5 -aquamarine: #78dbe2 -asparagus: #87a96b -atomic tangerine: #ffa474 -banana mania: #fae7b5 -beaver: #9f8170 -bittersweet: #fd7c6e -black: #000000 -blue: #1f75fe -blue bell: #a2a2d0 -blue green: #0d98ba -blue violet: #7366bd -blush: #de5d83 -brick red: #cb4154 -brown: #b4674d -burnt orange: #ff7f49 -burnt sienna: #ea7e5d -cadet blue: #b0b7c6 -canary: #ffff99 -caribbean green: #00cc99 -carnation pink: #ffaacc -cerise: #dd4492 -cerulean: #1dacd6 -chestnut: #bc5d58 -copper: #dd9475 -cornflower: #9aceeb -cotton candy: #ffbcd9 -dandelion: #fddb6d -denim: #2b6cc4 -desert sand: #efcdb8 -eggplant: #6e5160 -electric lime: #ceff1d -fern: #71bc78 -forest green: #6dae81 -fuchsia: #c364c5 -fuzzy wuzzy: #cc6666 -gold: #e7c697 -goldenrod: #fcd975 -granny smith apple: #a8e4a0 -gray: #95918c -green: #1cac78 -green yellow: #f0e891 -hot magenta: #ff1dce -inchworm: #b2ec5d -indigo: #5d76cb -jazzberry jam: #ca3767 -jungle green: #3bb08f -laser lemon: #fefe22 -lavender: #fcb4d5 -macaroni and cheese: #ffbd88 -magenta: #f664af -mahogany: #cd4a4c -manatee: #979aaa -mango tango: #ff8243 -maroon: #c8385a -mauvelous: #ef98aa -melon: #fdbcb4 -midnight blue: #1a4876 -mountain meadow: #30ba8f -navy blue: #1974d2 -neon carrot: #ffa343 -olive green: #bab86c -orange: #ff7538 -orchid: #e6a8d7 -outer space: #414a4c -outrageous orange: #ff6e4a -pacific blue: #1ca9c9 -peach: #ffcfab -periwinkle: #c5d0e6 -piggy pink: #fddde6 -pine green: #158078 -pink flamingo: #fc74fd -pink sherbert: #f78fa7 -plum: #8e4585 -purple heart: #7442c8 -purple mountains majesty: #9d81ba -purple pizzazz: #fe4eda -radical red: #ff496c -raw sienna: #d68a59 -razzle dazzle rose: #ff48d0 -razzmatazz: #e3256b -red: #ee204d -red orange: #ff5349 -red violet: #c0448f -robins egg blue: #1fcecb -royal purple: #7851a9 -salmon: #ff9baa -scarlet: #fc2847 -screamin green: #76ff7a -sea green: #93dfb8 -sepia: #a5694f -shadow: #8a795d -shamrock: #45cea2 -shocking pink: #fb7efd -silver: #cdc5c2 -sky blue: #80daeb -spring green: #eceabe -sunglow: #ffcf48 -sunset orange: #fd5e53 -tan: #faa76c -tickle me pink: #fc89ac -timberwolf: #dbd7d2 -tropical rain forest: #17806d -tumbleweed: #deaa88 -turquoise blue: #77dde7 -unmellow yellow: #ffff66 -violet: #926eae -violet red: #f75394 -vivid tangerine: #ffa089 -vivid violet: #8f509d -white: #ffffff -wild blue yonder: #a2add0 -wild strawberry: #ff43a4 -wild watermelon: #fc6c85 -wisteria: #cda4de -yellow: #fce883 -yellow green: #c5e384 -yellow orange: #ffae42 diff --git a/proplot/config.py b/proplot/config.py new file mode 100644 index 000000000..1a8ce8df6 --- /dev/null +++ b/proplot/config.py @@ -0,0 +1,1779 @@ +#!/usr/bin/env python3 +""" +Tools for setting up proplot and configuring global settings. +See the :ref:`configuration guide ` for details. +""" +# NOTE: The matplotlib analogue to this file is actually __init__.py +# but it makes more sense to have all the setup actions in a separate file +# so the namespace of the top-level module is unpolluted. +# NOTE: Why also load colormaps and cycles in this file and not colors.py? +# Because I think it makes sense to have all the code that "runs" (i.e. not +# just definitions) in the same place, and I was having issues with circular +# dependencies and where import order of __init__.py was affecting behavior. +import logging +import os +import re +import sys +from collections import namedtuple +from collections.abc import MutableMapping +from numbers import Real + +import cycler +import matplotlib as mpl +import matplotlib.colors as mcolors +import matplotlib.font_manager as mfonts +import matplotlib.mathtext # noqa: F401 +import matplotlib.style.core as mstyle +import numpy as np +from matplotlib import RcParams + +from .internals import ic # noqa: F401 +from .internals import ( + _not_none, + _pop_kwargs, + _pop_props, + _translate_grid, + _version_mpl, + docstring, + rcsetup, + warnings, +) + +try: + from IPython import get_ipython +except ImportError: + def get_ipython(): + return + +# Suppress warnings emitted by mathtext.py (_mathtext.py in recent versions) +# when when substituting dummy unavailable glyph due to fallback disabled. +logging.getLogger('matplotlib.mathtext').setLevel(logging.ERROR) + +__all__ = [ + 'Configurator', + 'rc', + 'rc_proplot', + 'rc_matplotlib', + 'use_style', + 'config_inline_backend', + 'register_cmaps', + 'register_cycles', + 'register_colors', + 'register_fonts', + 'RcConfigurator', # deprecated + 'inline_backend_fmt', # deprecated +] + +# Constants +COLORS_KEEP = ( + 'red', + 'green', + 'blue', + 'cyan', + 'yellow', + 'magenta', + 'white', + 'black' +) + +# Configurator docstrings +_rc_docstring = """ +local : bool, default: True + Whether to load settings from the `~Configurator.local_files` file. +user : bool, default: True + Whether to load settings from the `~Configurator.user_file` file. +default : bool, default: True + Whether to reload built-in default proplot settings. +""" +docstring._snippet_manager['rc.params'] = _rc_docstring + +# Registration docstrings +_shared_docstring = """ +*args : path-spec or `~proplot.colors.{type}Colormap`, optional + The {objects} to register. These can be file paths containing + RGB data or `~proplot.colors.{type}Colormap` instances. By default, + if positional arguments are passed, then `user` is set to ``False``. + + Valid file extensions are listed in the below table. Note that {objects} + are registered according to their filenames -- for example, ``name.xyz`` + will be registered as ``'name'``. +""" # noqa: E501 +_cmap_exts_docstring = """ + =================== ========================================== + Extension Description + =================== ========================================== + ``.json`` JSON database of the channel segment data. + ``.hex`` Comma-delimited list of HEX strings. + ``.rgb``, ``.txt`` 3-4 column table of channel values. + =================== ========================================== +""" +_cycle_exts_docstring = """ + ================== ========================================== + Extension Description + ================== ========================================== + ``.hex`` Comma-delimited list of HEX strings. + ``.rgb``, ``.txt`` 3-4 column table of channel values. + ================== ========================================== +""" +_color_docstring = """ +*args : path-like or dict, optional + The colors to register. These can be file paths containing RGB data or + dictionary mappings of names to RGB values. By default, if positional + arguments are passed, then `user` is set to ``False``. Files must have + the extension ``.txt`` and should contain one line per color in the + format ``name : hex``. Whitespace is ignored. +""" +_font_docstring = """ +*args : path-like, optional + The font files to add. By default, if positional arguments are passed, then + `user` is set to ``False``. Files must have the extensions ``.ttf`` or ``.otf``. + See `this link \ +`__ + for a guide on converting other font files to ``.ttf`` and ``.otf``. +""" +_register_docstring = """ +user : bool, optional + Whether to reload {objects} from `~Configurator.user_folder`. Default is + ``False`` if positional arguments were passed and ``True`` otherwise. +local : bool, optional + Whether to reload {objects} from `~Configurator.local_folders`. Default is + ``False`` if positional arguments were passed and ``True`` otherwise. +default : bool, default: False + Whether to reload the default {objects} packaged with proplot. + Default is always ``False``. +""" +docstring._snippet_manager['rc.cmap_params'] = _register_docstring.format(objects='colormaps') # noqa: E501 +docstring._snippet_manager['rc.cycle_params'] = _register_docstring.format(objects='color cycles') # noqa: E501 +docstring._snippet_manager['rc.color_params'] = _register_docstring.format(objects='colors') # noqa: E501 +docstring._snippet_manager['rc.font_params'] = _register_docstring.format(objects='fonts') # noqa: E501 +docstring._snippet_manager['rc.cmap_args'] = _shared_docstring.format(objects='colormaps', type='Continuous') # noqa: E501 +docstring._snippet_manager['rc.cycle_args'] = _shared_docstring.format(objects='color cycles', type='Discrete') # noqa: E501 +docstring._snippet_manager['rc.color_args'] = _color_docstring +docstring._snippet_manager['rc.font_args'] = _font_docstring +docstring._snippet_manager['rc.cmap_exts'] = _cmap_exts_docstring +docstring._snippet_manager['rc.cycle_exts'] = _cycle_exts_docstring + + +def _init_user_file(): + """ + Initialize .proplotrc file. + """ + file = Configurator.user_file() + if not os.path.exists(file): + Configurator._save_yaml(file, comment=True) + + +def _init_user_folders(): + """ + Initialize .proplot folder. + """ + for subfolder in ('', 'cmaps', 'cycles', 'colors', 'fonts'): + folder = Configurator.user_folder(subfolder) + if not os.path.isdir(folder): + os.mkdir(folder) + + +def _get_data_folders(folder, user=True, local=True, default=True, reverse=False): + """ + Return data folder paths in reverse order of precedence. + """ + # When loading colormaps, cycles, and colors, files in the latter + # directories overwrite files in the former directories. When loading + # fonts, the resulting paths need to be *reversed*. + paths = [] + if default: + paths.append(os.path.join(os.path.dirname(__file__), folder)) + if user: + paths.append(Configurator.user_folder(folder)) + if local: + paths.extend(Configurator.local_folders(folder)) + if reverse: + paths = paths[::-1] + return paths + + +def _iter_data_objects(folder, *args, **kwargs): + """ + Iterate over input objects and files in the data folders that should be + registered. Also yield an index indicating whether these are user files. + """ + i = 0 # default files + for i, path in enumerate(_get_data_folders(folder, **kwargs)): + for dirname, dirnames, filenames in os.walk(path): + for filename in filenames: + if filename[0] == '.': # UNIX-style hidden files + continue + path = os.path.join(dirname, filename) + yield i, path + i += 1 # user files + for path in args: + path = os.path.expanduser(path) + if os.path.isfile(path): + yield i, path + else: + raise FileNotFoundError(f'Invalid file path {path!r}.') + + +def _filter_style_dict(rcdict, warn=True): + """ + Filter out blacklisted style parameters. + """ + # NOTE: This implements bugfix: https://github.com/matplotlib/matplotlib/pull/17252 + # This fix is *critical* for proplot because we always run style.use() + # when the configurator is made. Without fix backend is reset every time + # you import proplot in jupyter notebooks. So apply retroactively. + rcdict_filtered = {} + for key in rcdict: + if key in mstyle.STYLE_BLACKLIST: + if warn: + warnings._warn_proplot( + f'Dictionary includes a parameter, {key!r}, that is not related ' + 'to style. Ignoring.' + ) + else: + rcdict_filtered[key] = rcdict[key] + return rcdict_filtered + + +def _get_default_style_dict(): + """ + Get the default rc parameters dictionary with deprecated parameters filtered. + """ + # NOTE: Use RcParams update to filter and translate deprecated settings + # before actually applying them to rcParams down pipeline. This way we can + # suppress warnings for deprecated default params but still issue warnings + # when user-supplied stylesheets have deprecated params. + # WARNING: Some deprecated rc params remain in dictionary as None so we + # filter them out. Beware if hidden attribute changes. + # WARNING: The examples.directory deprecation was handled specially inside + # RcParams in early versions. Manually pop it out here. + rcdict = _filter_style_dict(mpl.rcParamsDefault, warn=False) + with warnings.catch_warnings(): + warnings.simplefilter('ignore', mpl.MatplotlibDeprecationWarning) + rcdict = dict(RcParams(rcdict)) + for attr in ('_deprecated_set', '_deprecated_remain_as_none'): + deprecated = getattr(mpl, attr, ()) + for key in deprecated: # _deprecated_set is in matplotlib < 3.4 + rcdict.pop(key, None) + rcdict.pop('examples.directory', None) # special case for matplotlib < 3.2 + return rcdict + + +def _get_style_dict(style, filter=True): + """ + Return a dictionary of settings belonging to the requested style(s). If `filter` + is ``True``, invalid style parameters like `backend` are filtered out. + """ + # NOTE: This is adapted from matplotlib source for the following changes: + # 1. Add an 'original' pseudo style. Like rcParamsOrig except we also reload + # from the user matplotlibrc file. + # 2. When the style is changed we reset to the default state ignoring matplotlibrc. + # By contrast matplotlib applies styles on top of current state (including + # matplotlibrc changes and runtime rcParams changes) but the word 'style' + # implies a rigid static format. This makes more sense. + # 3. Add a separate function that returns lists of style dictionaries so that + # we can modify the active style in a context block. Proplot context is more + # conservative than matplotlib's rc_context because it gets called a lot + # (e.g. every time you make an axes and every format() call). Instead of + # copying the entire rcParams dict we just track the keys that were changed. + style_aliases = { + '538': 'fivethirtyeight', + 'mpl20': 'default', + 'mpl15': 'classic', + 'original': mpl.matplotlib_fname(), + } + + # Always apply the default style *first* so styles are rigid + kw_matplotlib = _get_default_style_dict() + if style == 'default' or style is mpl.rcParamsDefault: + return kw_matplotlib + + # Apply limited deviations from the matplotlib style that we want to propagate to + # other styles. Want users selecting stylesheets to have few surprises, so + # currently just enforce the new aesthetically pleasing fonts. + kw_matplotlib['font.family'] = 'sans-serif' + for fmly in ('serif', 'sans-serif', 'monospace', 'cursive', 'fantasy'): + kw_matplotlib['font.' + fmly] = rcsetup._rc_matplotlib_default['font.' + fmly] + + # Apply user input style(s) one by one + if isinstance(style, str) or isinstance(style, dict): + styles = [style] + else: + styles = style + for style in styles: + if isinstance(style, dict): + kw = style + elif isinstance(style, str): + style = style_aliases.get(style, style) + if style in mstyle.library: + kw = mstyle.library[style] + else: + try: + kw = mpl.rc_params_from_file(style, use_default_template=False) + except IOError: + raise IOError( + f'Style {style!r} not found in the style library and input ' + 'is not a valid URL or file path. Available styles are: ' + + ', '.join(map(repr, mstyle.available)) + + '.' + ) + else: + raise ValueError(f'Invalid style {style!r}. Must be string or dictionary.') + if filter: + kw = _filter_style_dict(kw, warn=True) + kw_matplotlib.update(kw) + + return kw_matplotlib + + +def _infer_proplot_dict(kw_params): + """ + Infer values for proplot's "added" parameters from stylesheet parameters. + """ + kw_proplot = {} + mpl_to_proplot = { + 'xtick.labelsize': ( + 'tick.labelsize', 'grid.labelsize', + ), + 'ytick.labelsize': ( + 'tick.labelsize', 'grid.labelsize', + ), + 'axes.titlesize': ( + 'abc.size', 'suptitle.size', 'title.size', + 'leftlabel.size', 'rightlabel.size', + 'toplabel.size', 'bottomlabel.size', + ), + 'text.color': ( + 'abc.color', 'suptitle.color', 'title.color', + 'tick.labelcolor', 'grid.labelcolor', + 'leftlabel.color', 'rightlabel.color', + 'toplabel.color', 'bottomlabel.color', + ), + } + for key, params in mpl_to_proplot.items(): + if key in kw_params: + value = kw_params[key] + for param in params: + kw_proplot[param] = value + return kw_proplot + + +def config_inline_backend(fmt=None): + """ + Set up the ipython `inline backend display format \ +`__ + and ensure that inline figures always look the same as saved figures. + This runs the following ipython magic commands: + + .. code-block:: ipython + + %config InlineBackend.figure_formats = rc['inlineformat'] + %config InlineBackend.rc = {} # never override rc settings + %config InlineBackend.close_figures = True # cells start with no active figures + %config InlineBackend.print_figure_kwargs = {'bbox_inches': None} + + When the inline backend is inactive or unavailable, this has no effect. + This function is called when you modify the :rcraw:`inlineformat` property. + + Parameters + ---------- + fmt : str or sequence, default: :rc:`inlineformat` + The inline backend file format or a list thereof. Valid formats + include ``'jpg'``, ``'png'``, ``'svg'``, ``'pdf'``, and ``'retina'``. + + See also + -------- + Configurator + """ + # Note if inline backend is unavailable this will fail silently + ipython = get_ipython() + if ipython is None: + return + fmt = _not_none(fmt, rc_proplot['inlineformat']) + if isinstance(fmt, str): + fmt = [fmt] + elif np.iterable(fmt): + fmt = list(fmt) + else: + raise ValueError(f'Invalid inline backend format {fmt!r}. Must be string.') + ipython.magic('config InlineBackend.figure_formats = ' + repr(fmt)) + ipython.magic('config InlineBackend.rc = {}') + ipython.magic('config InlineBackend.close_figures = True') + ipython.magic("config InlineBackend.print_figure_kwargs = {'bbox_inches': None}") + + +def use_style(style): + """ + Apply the `matplotlib style(s) \ +`__ + with `matplotlib.style.use`. This function is + called when you modify the :rcraw:`style` property. + + Parameters + ---------- + style : str or sequence or dict-like + The matplotlib style name(s) or stylesheet filename(s), or dictionary(s) + of settings. Use ``'default'`` to apply matplotlib default settings and + ``'original'`` to include settings from your user ``matplotlibrc`` file. + + See also + -------- + Configurator + matplotlib.style.use + """ + # NOTE: This function is not really necessary but makes proplot's + # stylesheet-supporting features obvious. Plus changing the style does + # so much *more* than changing rc params or quick settings, so it is + # nice to have dedicated function instead of just another rc_param name. + kw_matplotlib = _get_style_dict(style) + rc_matplotlib.update(kw_matplotlib) + rc_proplot.update(_infer_proplot_dict(kw_matplotlib)) + + +@docstring._snippet_manager +def register_cmaps(*args, user=None, local=None, default=False): + """ + Register named colormaps. This is called on import. + + Parameters + ---------- + %(rc.cmap_args)s + + %(rc.cmap_exts)s + + %(rc.cmap_params)s + + See also + -------- + register_cycles + register_colors + register_fonts + proplot.demos.show_cmaps + """ + # Register input colormaps + from . import colors as pcolors + user = _not_none(user, not bool(args)) # skip user folder if input args passed + local = _not_none(local, not bool(args)) + paths = [] + for arg in args: + if isinstance(arg, mcolors.Colormap): + pcolors._cmap_database[arg.name] = arg + else: + paths.append(arg) + + # Register data files + for i, path in _iter_data_objects( + 'cmaps', *paths, user=user, local=local, default=default + ): + cmap = pcolors.ContinuousColormap.from_file(path, warn_on_failure=True) + if not cmap: + continue + if i == 0 and cmap.name.lower() in pcolors.CMAPS_CYCLIC: + cmap.set_cyclic(True) + pcolors._cmap_database[cmap.name] = cmap + + +@docstring._snippet_manager +def register_cycles(*args, user=None, local=None, default=False): + """ + Register named color cycles. This is called on import. + + Parameters + ---------- + %(rc.cycle_args)s + + %(rc.cycle_exts)s + + %(rc.cycle_params)s + + See also + -------- + register_cmaps + register_colors + register_fonts + proplot.demos.show_cycles + """ + # Register input color cycles + from . import colors as pcolors + user = _not_none(user, not bool(args)) # skip user folder if input args passed + local = _not_none(local, not bool(args)) + paths = [] + for arg in args: + if isinstance(arg, mcolors.Colormap): + pcolors._cmap_database[arg.name] = arg + else: + paths.append(arg) + + # Register data files + for _, path in _iter_data_objects( + 'cycles', *paths, user=user, local=local, default=default + ): + cmap = pcolors.DiscreteColormap.from_file(path, warn_on_failure=True) + if not cmap: + continue + pcolors._cmap_database[cmap.name] = cmap + + +@docstring._snippet_manager +def register_colors( + *args, user=None, local=None, default=False, space=None, margin=None, **kwargs +): + """ + Register named colors. This is called on import. + + Parameters + ---------- + %(rc.color_args)s + %(rc.color_params)s + space : {'hcl', 'hsl', 'hpl'}, optional + The colorspace used to pick "perceptually distinct" colors from + the `XKCD color survey `__. + If passed then `default` is set to ``True``. + margin : float, default: 0.1 + The margin used to pick "perceptually distinct" colors from the + `XKCD color survey `__. The normalized hue, + saturation, and luminance of each color must differ from the channel + values of the prededing colors by `margin` in order to be registered. + Must fall between ``0`` and ``1`` (``0`` will register all colors). + If passed then `default` is set to ``True``. + **kwargs + Additional color name specifications passed as keyword arguments rather + than positional argument dictionaries. + + See also + -------- + register_cmaps + register_cycles + register_fonts + proplot.demos.show_colors + """ + from . import colors as pcolors + default = default or space is not None or margin is not None + margin = _not_none(margin, 0.1) + space = _not_none(space, 'hcl') + + # Remove previously registered colors + # NOTE: Try not to touch matplotlib colors for compatibility + srcs = {'xkcd': pcolors.COLORS_XKCD, 'opencolor': pcolors.COLORS_OPEN} + if default: # possibly slow but not these dicts are empty on startup + for src in srcs.values(): + for key in src: + if key not in COLORS_KEEP: + pcolors._color_database.pop(key, None) # this also clears cache + src.clear() + + # Register input colors + user = _not_none(user, not bool(args) and not bool(kwargs)) # skip if args passed + local = _not_none(local, not bool(args) and not bool(kwargs)) + paths = [] + for arg in args: + if isinstance(arg, dict): + kwargs.update(arg) + else: + paths.append(arg) + for key, color in kwargs.items(): + if mcolors.is_color_like(color): + pcolors._color_database[key] = mcolors.to_rgba(color) + else: + raise ValueError(f'Invalid color {key}={color!r}.') + + # Load colors from file and get their HCL values + # NOTE: Colors that come *later* overwrite colors that come earlier. + for i, path in _iter_data_objects( + 'colors', *paths, user=user, local=local, default=default + ): + loaded = pcolors._load_colors(path, warn_on_failure=True) + if i == 0: + cat, _ = os.path.splitext(os.path.basename(path)) + if cat not in srcs: + raise RuntimeError(f'Unknown proplot color database {path!r}.') + src = srcs[cat] + if cat == 'xkcd': + for key in COLORS_KEEP: + loaded[key] = pcolors._color_database[key] # keep the same + loaded = pcolors._standardize_colors(loaded, space, margin) + src.clear() + src.update(loaded) # needed for demos.show_colors() + pcolors._color_database.update(loaded) + + +@docstring._snippet_manager +def register_fonts(*args, user=True, local=True, default=False): + """ + Register font families. This is called on import. + + Parameters + ---------- + %(rc.font_args)s + %(rc.font_params)s + + See also + -------- + register_cmaps + register_cycles + register_colors + proplot.demos.show_fonts + """ + # Find proplot fonts + # WARNING: Must search data files in reverse because font manager will + # not overwrite existing fonts with user-input fonts. + # WARNING: If you include a font file with an unrecognized style, + # matplotlib may use that font instead of the 'normal' one! Valid styles: + # 'ultralight', 'light', 'normal', 'regular', 'book', 'medium', 'roman', + # 'semibold', 'demibold', 'demi', 'bold', 'heavy', 'extra bold', 'black' + # https://matplotlib.org/api/font_manager_api.html + # For macOS the only fonts with 'Thin' in one of the .ttf file names + # are Helvetica Neue and .SF NS Display Condensed. Never try to use these! + paths_proplot = _get_data_folders( + 'fonts', user=user, local=local, default=default, reverse=True + ) + fnames_proplot = set(mfonts.findSystemFonts(paths_proplot)) + for path in args: + path = os.path.expanduser(path) + if os.path.isfile(path): + fnames_proplot.add(path) + else: + raise FileNotFoundError(f'Invalid font file path {path!r}.') + + # Detect user-input ttc fonts and issue warning + fnames_proplot_ttc = { + file for file in fnames_proplot if os.path.splitext(file)[1] == '.ttc' + } + if fnames_proplot_ttc: + warnings._warn_proplot( + 'Ignoring the following .ttc fonts because they cannot be ' + 'saved into PDF or EPS files (see matplotlib issue #3135): ' + + ', '.join(map(repr, sorted(fnames_proplot_ttc))) + + '. Please consider expanding them into separate .ttf files.' + ) + + # Rebuild font cache only if necessary! Can be >50% of total import time! + fnames_all = {font.fname for font in mfonts.fontManager.ttflist} + fnames_proplot -= fnames_proplot_ttc + if not fnames_all >= fnames_proplot: + warnings._warn_proplot( + 'Rebuilding font cache. This usually happens ' + 'after installing or updating proplot.' + ) + if hasattr(mfonts.fontManager, 'addfont'): + # Newer API lets us add font files manually and deprecates TTFPATH. However + # to cache fonts added this way, we must call json_dump explicitly. + # NOTE: Previously, cache filename was specified as _fmcache variable, but + # recently became inaccessible. Must reproduce mpl code instead. + # NOTE: Older mpl versions used fontList.json as the cache, but these + # versions also did not have 'addfont', so makes no difference. + for fname in fnames_proplot: + mfonts.fontManager.addfont(fname) + cache = os.path.join( + mpl.get_cachedir(), + f'fontlist-v{mfonts.FontManager.__version__}.json' + ) + mfonts.json_dump(mfonts.fontManager, cache) + else: + # Older API requires us to modify TTFPATH + # NOTE: Previously we tried to modify TTFPATH before importing + # font manager with hope that it would load proplot fonts on + # initialization. But 99% of the time font manager just imports + # the FontManager from cache, so we would have to rebuild anyway. + paths = ':'.join(paths_proplot) + if 'TTFPATH' not in os.environ: + os.environ['TTFPATH'] = paths + elif paths not in os.environ['TTFPATH']: + os.environ['TTFPATH'] += ':' + paths + mfonts._rebuild() + + # Remove ttc files and 'Thin' fonts *after* rebuild + # NOTE: 'Thin' filter is ugly kludge but without this matplotlib picks up on + # Roboto thin ttf files installed on the RTD server when compiling docs. + mfonts.fontManager.ttflist = [ + font + for font in mfonts.fontManager.ttflist + if os.path.splitext(font.fname)[1] != '.ttc' and ( + _version_mpl >= '3.3' + or 'Thin' not in os.path.basename(font.fname) + ) + ] + + +class Configurator(MutableMapping, dict): + """ + A dictionary-like class for managing `matplotlib settings + `__ + stored in `rc_matplotlib` and :ref:`proplot settings ` + stored in `rc_proplot`. This class is instantiated as the `rc` object + on import. See the :ref:`user guide ` for details. + """ + def __repr__(self): + cls = type('rc', (dict,), {}) # temporary class with short name + src = cls({key: val for key, val in rc_proplot.items() if '.' not in key}) + return type(rc_matplotlib).__repr__(src).strip()[:-1] + ',\n ...\n })' + + def __str__(self): + cls = type('rc', (dict,), {}) # temporary class with short name + src = cls({key: val for key, val in rc_proplot.items() if '.' not in key}) + return type(rc_matplotlib).__str__(src) + '\n...' + + def __iter__(self): + yield from rc_proplot # sorted proplot settings, ignoring deprecations + yield from rc_matplotlib # sorted matplotlib settings, ignoring deprecations + + def __len__(self): + return len(tuple(iter(self))) + + def __delitem__(self, key): # noqa: U100 + raise RuntimeError('rc settings cannot be deleted.') + + def __delattr__(self, attr): # noqa: U100 + raise RuntimeError('rc settings cannot be deleted.') + + @docstring._snippet_manager + def __init__(self, local=True, user=True, default=True, **kwargs): + """ + Parameters + ---------- + %(rc.params)s + """ + self._context = [] + self._init(local=local, user=user, default=default, **kwargs) + + def __getitem__(self, key): + """ + Return an `rc_matplotlib` or `rc_proplot` setting using dictionary notation + (e.g., ``value = pplt.rc[name]``). + """ + key, _ = self._validate_key(key) # might issue proplot removed/renamed error + try: + return rc_proplot[key] + except KeyError: + pass + return rc_matplotlib[key] # might issue matplotlib removed/renamed error + + def __setitem__(self, key, value): + """ + Modify an `rc_matplotlib` or `rc_proplot` setting using dictionary notation + (e.g., ``pplt.rc[name] = value``). + """ + kw_proplot, kw_matplotlib = self._get_item_dicts(key, value) + rc_proplot.update(kw_proplot) + rc_matplotlib.update(kw_matplotlib) + + def __getattr__(self, attr): + """ + Return an `rc_matplotlib` or `rc_proplot` setting using "dot" notation + (e.g., ``value = pplt.rc.name``). + """ + if attr[:1] == '_': + return super().__getattribute__(attr) # raise built-in error + else: + return self.__getitem__(attr) + + def __setattr__(self, attr, value): + """ + Modify an `rc_matplotlib` or `rc_proplot` setting using "dot" notation + (e.g., ``pplt.rc.name = value``). + """ + if attr[:1] == '_': + super().__setattr__(attr, value) + else: + self.__setitem__(attr, value) + + def __enter__(self): + """ + Apply settings from the most recent context block. + """ + if not self._context: + raise RuntimeError( + 'rc object must be initialized for context block using rc.context().' + ) + context = self._context[-1] + kwargs = context.kwargs + rc_new = context.rc_new # used for context-based _get_item_context + rc_old = context.rc_old # used to re-apply settings without copying whole dict + for key, value in kwargs.items(): + try: # TODO: consider moving setting validation to .context() + kw_proplot, kw_matplotlib = self._get_item_dicts(key, value) + except ValueError as error: + self.__exit__() + raise error + for rc_dict, kw_new in zip( + (rc_proplot, rc_matplotlib), + (kw_proplot, kw_matplotlib), + ): + for key, value in kw_new.items(): + rc_old[key] = rc_dict[key] + rc_new[key] = rc_dict[key] = value + + def __exit__(self, *args): # noqa: U100 + """ + Restore settings from the most recent context block. + """ + if not self._context: + raise RuntimeError( + 'rc object must be initialized for context block using rc.context().' + ) + context = self._context[-1] + for key, value in context.rc_old.items(): + kw_proplot, kw_matplotlib = self._get_item_dicts(key, value) + rc_proplot.update(kw_proplot) + rc_matplotlib.update(kw_matplotlib) + del self._context[-1] + + def _init(self, *, local, user, default, skip_cycle=False): + """ + Initialize the configurator. + """ + # Always remove context objects + self._context.clear() + + # Update from default settings + # NOTE: see _remove_blacklisted_style_params bugfix + if default: + rc_matplotlib.update(_get_style_dict('original', filter=False)) + rc_matplotlib.update(rcsetup._rc_matplotlib_default) + rc_proplot.update(rcsetup._rc_proplot_default) + for key, value in rc_proplot.items(): + kw_proplot, kw_matplotlib = self._get_item_dicts( + key, value, skip_cycle=skip_cycle + ) + rc_matplotlib.update(kw_matplotlib) + rc_proplot.update(kw_proplot) + + # Update from user home + user_path = None + if user: + user_path = self.user_file() + if os.path.isfile(user_path): + self.load(user_path) + + # Update from local paths + if local: + local_paths = self.local_files() + for path in local_paths: + if path == user_path: # local files always have precedence + continue + self.load(path) + + @staticmethod + def _validate_key(key, value=None): + """ + Validate setting names and handle `rc_proplot` deprecations. + """ + # NOTE: Not necessary to check matplotlib key here because... not sure why. + # Think deprecated matplotlib keys are not involved in any synced settings. + # Also note _check_key includes special handling for some renamed keys. + if not isinstance(key, str): + raise KeyError(f'Invalid key {key!r}. Must be string.') + key = key.lower() + if '.' not in key: + key = rcsetup._rc_nodots.get(key, key) + key, value = rc_proplot._check_key(key, value) # may issue deprecation warning + return key, value + + @staticmethod + def _validate_value(key, value): + """ + Validate setting values and convert numpy ndarray to list if possible. + """ + # NOTE: Ideally would implicitly validate on subsequent assignment to rc + # dictionaries, but must explicitly do it here, so _get_item_dicts can + # work with e.g. 'tick.lenratio', so _get_item_dicts does not have to include + # deprecated name handling in its if statements, and so _load_file can + # catch errors and emit warnings with line number indications as files + # are being read rather than after the end of the file reading. + if isinstance(value, np.ndarray): + value = value.item() if value.size == 1 else value.tolist() + validate_matplotlib = getattr(rc_matplotlib, 'validate', None) + validate_proplot = rc_proplot._validate + if validate_matplotlib is not None and key in validate_matplotlib: + value = validate_matplotlib[key](value) + elif key in validate_proplot: + value = validate_proplot[key](value) + return value + + def _get_item_context(self, key, mode=None): + """ + As with `~Configurator.__getitem__` but the search is limited based + on the context mode and ``None`` is returned if the key is not found. + """ + key, _ = self._validate_key(key) + if mode is None: + mode = self._context_mode + cache = tuple(context.rc_new for context in self._context) + if mode == 0: + rcdicts = (*cache, rc_proplot, rc_matplotlib) + elif mode == 1: + rcdicts = (*cache, rc_proplot) # added settings only! + elif mode == 2: + rcdicts = (*cache,) + else: + raise ValueError(f'Invalid caching mode {mode!r}.') + for rcdict in rcdicts: + if not rcdict: + continue + try: + return rcdict[key] + except KeyError: + continue + if mode == 0: # otherwise return None + raise KeyError(f'Invalid rc setting {key!r}.') + + def _get_item_dicts(self, key, value, skip_cycle=False): + """ + Return dictionaries for updating the `rc_proplot` and `rc_matplotlib` + properties associated with this key. Used when setting items, entering + context blocks, or loading files. + """ + # Get validated key, value, and child keys + key, value = self._validate_key(key, value) + value = self._validate_value(key, value) + keys = (key,) + rcsetup._rc_children.get(key, ()) # settings to change + contains = lambda *args: any(arg in keys for arg in args) # noqa: E731 + + # Fill dictionaries of matplotlib and proplot settings + # NOTE: Raise key error right away so it can be caught by _load_file(). + # Also ignore deprecation warnings so we only get them *once* on assignment + kw_proplot = {} # custom properties + kw_matplotlib = {} # builtin properties + with warnings.catch_warnings(): + warnings.simplefilter('ignore', mpl.MatplotlibDeprecationWarning) + warnings.simplefilter('ignore', warnings.ProplotWarning) + for key in keys: + if key in rc_matplotlib: + kw_matplotlib[key] = value + elif key in rc_proplot: + kw_proplot[key] = value + else: + raise KeyError(f'Invalid rc setting {key!r}.') + + # Special key: configure inline backend + if contains('inlineformat'): + config_inline_backend(value) + + # Special key: apply stylesheet + elif contains('style'): + if value is not None: + ikw_matplotlib = _get_style_dict(value) + kw_matplotlib.update(ikw_matplotlib) + kw_proplot.update(_infer_proplot_dict(ikw_matplotlib)) + + # Cycler + # NOTE: Have to skip this step during initial proplot import + elif contains('cycle') and not skip_cycle: + from .colors import _get_cmap_subtype + cmap = _get_cmap_subtype(value, 'discrete') + kw_matplotlib['axes.prop_cycle'] = cycler.cycler('color', cmap.colors) + kw_matplotlib['patch.facecolor'] = 'C0' + + # Turning bounding box on should turn border off and vice versa + elif contains('abc.bbox', 'title.bbox', 'abc.border', 'title.border'): + if value: + name, this = key.split('.') + other = 'border' if this == 'bbox' else 'bbox' + kw_proplot[name + '.' + other] = False + + # Fontsize + # NOTE: Re-application of e.g. size='small' uses the updated 'font.size' + elif contains('font.size'): + kw_proplot.update( + { + key: value for key, value in rc_proplot.items() + if key in rcsetup.FONT_KEYS and value in mfonts.font_scalings + } + ) + kw_matplotlib.update( + { + key: value for key, value in rc_matplotlib.items() + if key in rcsetup.FONT_KEYS and value in mfonts.font_scalings + } + ) + + # Tick length/major-minor tick length ratio + elif contains('tick.len', 'tick.lenratio'): + if contains('tick.len'): + ticklen = value + ratio = rc_proplot['tick.lenratio'] + else: + ticklen = rc_proplot['tick.len'] + ratio = value + kw_matplotlib['xtick.minor.size'] = ticklen * ratio + kw_matplotlib['ytick.minor.size'] = ticklen * ratio + + # Spine width/major-minor tick width ratio + elif contains('tick.width', 'tick.widthratio'): + if contains('tick.width'): + tickwidth = value + ratio = rc_proplot['tick.widthratio'] + else: + tickwidth = rc_proplot['tick.width'] + ratio = value + kw_matplotlib['xtick.minor.width'] = tickwidth * ratio + kw_matplotlib['ytick.minor.width'] = tickwidth * ratio + + # Gridline width + elif contains('grid.width', 'grid.widthratio'): + if contains('grid.width'): + gridwidth = value + ratio = rc_proplot['grid.widthratio'] + else: + gridwidth = rc_proplot['grid.width'] + ratio = value + kw_proplot['gridminor.linewidth'] = gridwidth * ratio + kw_proplot['gridminor.width'] = gridwidth * ratio + + # Gridline toggling + elif contains('grid', 'gridminor'): + b, which = _translate_grid( + value, 'gridminor' if contains('gridminor') else 'grid' + ) + kw_matplotlib['axes.grid'] = b + kw_matplotlib['axes.grid.which'] = which + + return kw_proplot, kw_matplotlib + + @staticmethod + def _get_axisbelow_zorder(axisbelow): + """ + Convert the `axisbelow` string to its corresponding `zorder`. + """ + if axisbelow is True: + zorder = 0.5 + elif axisbelow is False: + zorder = 2.5 + elif axisbelow in ('line', 'lines'): + zorder = 1.5 + else: + raise ValueError(f'Unexpected axisbelow value {axisbelow!r}.') + return zorder + + def _get_background_props(self, patch_kw=None, native=True, **kwargs): + """ + Return background properties, optionally filtering the output dictionary + based on the context. + """ + # Deprecated behavior + context = native or self._context_mode == 2 + if patch_kw: + warnings._warn_proplot( + "'patch_kw' is no longer necessary as of proplot v0.8. " + 'Pass the parameters as keyword arguments instead.' + ) + kwargs.update(patch_kw) + + # Get user-input properties and changed rc settings + # NOTE: Here we use 'color' as an alias for just 'edgecolor' rather than + # both 'edgecolor' and 'facecolor' to match 'xcolor' and 'ycolor' arguments. + props = _pop_props(kwargs, 'patch') + if 'color' in props: + props.setdefault('edgecolor', props.pop('color')) + for key in ('alpha', 'facecolor', 'linewidth', 'edgecolor'): + value = self.find('axes.' + key, context=context) + if value is not None: + props.setdefault(key, value) + + # Partition properties into face and edge + kw_face = _pop_kwargs(props, 'alpha', 'facecolor') + kw_edge = _pop_kwargs(props, 'edgecolor', 'linewidth', 'linestyle') + kw_edge['capstyle'] = 'projecting' # NOTE: needed to fix cartopy bounds + if 'color' in props: + kw_edge.setdefault('edgecolor', props.pop('color')) + if kwargs: + raise TypeError(f'Unexpected keyword argument(s): {kwargs!r}') + + return kw_face, kw_edge + + def _get_gridline_bool(self, grid=None, axis=None, which='major', native=True): + """ + Return major and minor gridline toggles from ``axes.grid``, ``axes.grid.which``, + and ``axes.grid.axis``, optionally returning `None` based on the context. + """ + # NOTE: If you pass 'grid' or 'gridminor' the native args are updated + # NOTE: Very careful to return not None only if setting was changed. + # Avoid unnecessarily triggering grid redraws (esp. bad for geo.py) + context = native or self._context_mode == 2 + grid_on = self.find('axes.grid', context=context) + which_on = self.find('axes.grid.which', context=context) + if grid_on is not None or which_on is not None: # if *one* was changed + axis_on = self['axes.grid.axis'] # always need this property + grid_on = _not_none(grid_on, self['axes.grid']) + which_on = _not_none(which_on, self['axes.grid.which']) + axis = _not_none(axis, 'x') + axis_on = axis is None or axis_on in (axis, 'both') + which_on = which_on in (which, 'both') + grid = _not_none(grid, grid_on and axis_on and which_on) + return grid + + def _get_gridline_props(self, which='major', native=True, rebuild=False): + """ + Return gridline properties, optionally filtering the output dictionary + based on the context. + """ + # Line properties + # NOTE: Gridline zorder is controlled automatically by matplotlib but + # must be controlled manually for geographic projections + key = 'grid' if which == 'major' else 'gridminor' + prefix = 'grid_' if native else '' # for native gridlines use this prefix + context = not rebuild and (native or self._context_mode == 2) + kwlines = self.fill( + { + f'{prefix}alpha': f'{key}.alpha', + f'{prefix}color': f'{key}.color', + f'{prefix}linewidth': f'{key}.linewidth', + f'{prefix}linestyle': f'{key}.linestyle', + }, + context=context, + ) + axisbelow = self.find('axes.axisbelow', context=context) + if axisbelow is not None: + if native: # this is a native plot so use set_axisbelow() down the line + kwlines['axisbelow'] = axisbelow + else: # this is a geographic plot so apply with zorder + kwlines['zorder'] = self._get_axisbelow_zorder(axisbelow) + return kwlines + + def _get_label_props(self, native=True, **kwargs): + """ + Return the axis label properties, optionally filtering the output dictionary + based on the context. + """ + # Get the label settings + # NOTE: This permits passing arbitrary additional args to set_[xy]label(). + context = native or self._context_mode == 2 + props = { + 'color': 'axes.labelcolor', + 'weight': 'axes.labelweight', + 'size': 'axes.labelsize', + 'family': 'font.family', + 'labelpad': 'axes.labelpad', # read by set_xlabel/set_ylabel + } + kw = self.fill(props, context=context) + for key, value in kwargs.items(): + if value is not None: # allow e.g. color=None + if key in props: + value = self._validate_value(props[key], value) + kw[key] = value + return kw + + def _get_loc_string(self, string, axis=None, native=True): + """ + Return `tickloc` and `spineloc` location strings from the `rc` boolean toggles, + optionally returning `None` based on the context. + """ + context = native or self._context_mode == 2 + axis = _not_none(axis, 'x') + opt1, opt2 = ('top', 'bottom') if axis == 'x' else ('left', 'right') + b1 = self.find(f'{string}.{opt1}', context=context) + b2 = self.find(f'{string}.{opt2}', context=context) + if b1 is None and b2 is None: + return None + elif b1 and b2: + return 'both' + elif b1: + return opt1 + elif b2: + return opt2 + else: + return 'neither' + + def _get_tickline_props(self, axis=None, which='major', native=True, rebuild=False): + """ + Return the tick line properties, optionally filtering the output dictionary + based on the context. + """ + # Tick properties obtained with rc.category + # NOTE: This loads 'size', 'width', 'pad', 'bottom', and 'top' + axis = _not_none(axis, 'x') + context = not rebuild and (native or self._context_mode == 2) + kwticks = self.category(f'{axis}tick.{which}', context=context) + kwticks.pop('visible', None) + for key in ('color', 'direction'): + value = self.find(f'{axis}tick.{key}', context=context) + if value is not None: + kwticks[key] = value + return kwticks + + def _get_ticklabel_props(self, axis=None, native=True, rebuild=False): + """ + Return the tick label properties, optionally filtering the output dictionary + based on the context. + """ + # NOTE: 'tick.label' properties are now synonyms of 'grid.label' properties + sprefix = axis or '' + cprefix = sprefix if _version_mpl >= '3.4' else '' # new settings + context = not rebuild and (native or self._context_mode == 2) + kwtext = self.fill( + { + 'color': f'{cprefix}tick.labelcolor', # native setting sometimes avail + 'size': f'{sprefix}tick.labelsize', # native setting always avail + 'weight': 'tick.labelweight', # native setting never avail + 'family': 'font.family', # apply manually + }, + context=context, + ) + if kwtext.get('color', None) == 'inherit': + # Inheritence is not automatic for geographic + # gridline labels so we apply inheritence here. + kwtext['color'] = self[f'{sprefix}tick.color'] + return kwtext + + @staticmethod + def local_files(): + """ + Return locations of files named ``proplotrc`` in this directory and in parent + directories. "Hidden" files with a leading dot are also recognized. These are + automatically loaded when proplot is imported. + + See also + -------- + Configurator.user_file + Configurator.local_folders + """ + cdir = os.getcwd() + paths = [] + while cdir: # i.e. not root + for name in ('proplotrc', '.proplotrc'): + path = os.path.join(cdir, name) + if os.path.isfile(path): + paths.append(path) + ndir = os.path.dirname(cdir) + if ndir == cdir: # root + break + cdir = ndir + return paths[::-1] # sort from decreasing to increasing importantce + + @staticmethod + def local_folders(subfolder=None): + """ + Return locations of folders named ``proplot_cmaps``, ``proplot_cycles``, + ``proplot_colors``, and ``proplot_fonts`` in this directory and in parent + directories. "Hidden" folders with a leading dot are also recognized. Files + in these directories are automatically loaded when proplot is imported. + + See also + -------- + Configurator.user_folder + Configurator.local_files + """ + cdir = os.getcwd() + paths = [] + if subfolder is None: + subfolder = ('cmaps', 'cycles', 'colors', 'fonts') + if isinstance(subfolder, str): + subfolder = (subfolder,) + while cdir: # i.e. not root + for prefix in ('proplot', '.proplot'): + for suffix in subfolder: + path = os.path.join(cdir, '_'.join((prefix, suffix))) + if os.path.isdir(path): + paths.append(path) + ndir = os.path.dirname(cdir) + if ndir == cdir: # root + break + cdir = ndir + return paths[::-1] + + @staticmethod + def _config_folder(): + """ + Get the XDG proplot folder. + """ + home = os.path.expanduser('~') + base = os.environ.get('XDG_CONFIG_HOME') + if not base: + base = os.path.join(home, '.config') + if sys.platform.startswith(('linux', 'freebsd')) and os.path.isdir(base): + configdir = os.path.join(base, 'proplot') + else: + configdir = os.path.join(home, '.proplot') + return configdir + + @staticmethod + def user_file(): + """ + Return location of the default proplotrc file. On Linux, this is either + ``$XDG_CONFIG_HOME/proplot/proplotrc`` or ``~/.config/proplot/proplotrc`` + if the `XDG directory `__ + is unset. On other operating systems, this is ``~/.proplot/proplotrc``. The + location ``~/.proplotrc`` or ``~/.proplot/proplotrc`` is always returned if the + file exists, regardless of the operating system. If multiple valid locations + are found, a warning is raised. + + See also + -------- + Configurator.user_folder + Configurator.local_files + """ + # Support both loose files and files inside .proplot + file = os.path.join(Configurator.user_folder(), 'proplotrc') + universal = os.path.join(os.path.expanduser('~'), '.proplotrc') + if os.path.isfile(universal): + if file != universal and os.path.isfile(file): + warnings._warn_proplot( + 'Found conflicting default user proplotrc files at ' + f'{universal!r} and {file!r}. Ignoring the second one.' + ) + file = universal + return file + + @staticmethod + def user_folder(subfolder=None): + """ + Return location of the default proplot folder. On Linux, this + is either ``$XDG_CONFIG_HOME/proplot`` or ``~/.config/proplot`` + if the `XDG directory `__ + is unset. On other operating systems, this is ``~/.proplot``. The location + ``~/.proplot`` is always returned if the folder exists, regardless of the + operating system. If multiple valid locations are found, a warning is raised. + + See also + -------- + Configurator.user_file + Configurator.local_folders + """ + # Try the XDG standard location + # NOTE: This is borrowed from matplotlib.get_configdir + home = os.path.expanduser('~') + universal = folder = os.path.join(home, '.proplot') + if sys.platform.startswith(('linux', 'freebsd')): + xdg = os.environ.get('XDG_CONFIG_HOME') + xdg = xdg or os.path.join(home, '.config') + folder = os.path.join(xdg, 'proplot') + # Fallback to the loose ~/.proplot if it is present + # NOTE: This is critical or we might ignore previously stored settings! + if os.path.isdir(universal): + if folder != universal and os.path.isdir(folder): + warnings._warn_proplot( + 'Found conflicting default user proplot folders at ' + f'{universal!r} and {folder!r}. Ignoring the second one.' + ) + folder = universal + # Return the folder + if subfolder: + folder = os.path.join(folder, subfolder) + return folder + + def context(self, *args, mode=0, file=None, **kwargs): + """ + Temporarily modify the rc settings in a "with as" block. + + Parameters + ---------- + *args + Dictionaries of `rc` keys and values. + file : path-like, optional + Filename from which settings should be loaded. + **kwargs + `rc` names and values passed as keyword arguments. + If the name has dots, simply omit them. + + Other parameters + ---------------- + mode : {0, 1, 2}, optional + The context mode. Dictates the behavior of `~Configurator.find`, + `~Configurator.fill`, and `~Configurator.category` within a + "with as" block when called with ``context=True``. + + The options are as follows: + + * ``mode=0``: Matplotlib's `rc_matplotlib` settings + and proplot's `rc_proplot` settings are all returned, + whether or not they are local to the "with as" block. + * ``mode=1``: Matplotlib's `rc_matplotlib` settings are only + returned if they are local to the "with as" block. For example, + if :rcraw:`axes.titlesize` was passed to `~Configurator.context`, + then ``pplt.rc.find('axes.titlesize', context=True)`` will return + this value, but ``pplt.rc.find('axes.titleweight', context=True)`` will + return ``None``. This is used internally when instantiating axes. + * ``mode=2``: Matplotlib's `rc_matplotlib` settings and proplot's + `rc_proplot` settings are only returned if they are local to the + "with as" block. This is used internally when formatting axes. + + Note + ---- + Context "modes" are primarily used internally but may also be useful for power + users. Mode ``1`` is used when `~proplot.axes.Axes.format` is called during + axes instantiation, and mode ``2`` is used when `~proplot.axes.Axes.format` + is manually called by users. The latter prevents successive calls to + `~proplot.axes.Axes.format` from constantly looking up and re-applying + unchanged settings and significantly increasing the runtime. + + Example + ------- + The below applies settings to axes in a specific figure using + `~Configurator.context`. + + >>> import proplot as pplt + >>> with pplt.rc.context(ticklen=5, metalinewidth=2): + >>> fig, ax = pplt.subplots() + >>> ax.plot(data) + + The below applies settings to a specific axes using + `~proplot.axes.Axes.format`, which uses `~Configurator.context` + internally. + + >>> import proplot as pplt + >>> fig, ax = pplt.subplots() + >>> ax.format(ticklen=5, metalinewidth=2) + """ + # Add input dictionaries + for arg in args: + if not isinstance(arg, dict): + raise ValueError(f'Non-dictionary argument {arg!r}.') + kwargs.update(arg) + + # Add settings from file + if file is not None: + kw = self._load_file(file) + kw = {key: value for key, value in kw.items() if key not in kwargs} + kwargs.update(kw) + + # Activate context object + if mode not in range(3): + raise ValueError(f'Invalid mode {mode!r}.') + cls = namedtuple('RcContext', ('mode', 'kwargs', 'rc_new', 'rc_old')) + context = cls(mode=mode, kwargs=kwargs, rc_new={}, rc_old={}) + self._context.append(context) + return self + + def category(self, cat, *, trimcat=True, context=False): + """ + Return a dictionary of settings beginning with the substring ``cat + '.'``. + Optionally limit the search to the context level. + + Parameters + ---------- + cat : str, optional + The `rc` setting category. + trimcat : bool, default: True + Whether to trim ``cat`` from the key names in the output dictionary. + context : bool, default: False + If ``True``, then settings not found in the context dictionaries + are omitted from the output dictionary. See `~Configurator.context`. + + See also + -------- + Configurator.find + Configurator.fill + """ + kw = {} + if cat not in rcsetup._rc_categories: + raise ValueError( + f'Invalid rc category {cat!r}. Valid categories are: ' + + ', '.join(map(repr, rcsetup._rc_categories)) + + '.' + ) + for key in self: + if not re.match(fr'\A{cat}\.[^.]+\Z', key): + continue + value = self._get_item_context(key, None if context else 0) + if value is None: + continue + if trimcat: + key = re.sub(fr'\A{cat}\.', '', key) + kw[key] = value + return kw + + def fill(self, props, *, context=False): + """ + Return a dictionary filled with settings whose names match the string values + in the input dictionary. Optionally limit the search to the context level. + + Parameters + ---------- + props : dict-like + Dictionary whose values are setting names -- for example + ``rc.fill({'edgecolor': 'axes.edgecolor', 'facecolor': 'axes.facecolor'})``. + context : bool, default: False + If ``True``, then settings not found in the context dictionaries + are omitted from the output dictionary. See `~Configurator.context`. + + See also + -------- + Configurator.category + Configurator.find + """ + kw = {} + for key, value in props.items(): + item = self._get_item_context(value, None if context else 0) + if item is not None: + kw[key] = item + return kw + + def find(self, key, *, context=False): + """ + Return a single setting. Optionally limit the search to the context level. + + Parameters + ---------- + key : str + The single setting name. + context : bool, default: False + If ``True``, then ``None`` is returned if the setting is not found + in the context dictionaries. See `~Configurator.context`. + + See also + -------- + Configurator.category + Configurator.fill + """ + return self._get_item_context(key, None if context else 0) + + def update(self, *args, **kwargs): + """ + Update several settings at once. + + Parameters + ---------- + *args : str or dict-like, optional + A dictionary containing `rc` keys and values. You can also pass + a "category" name as the first argument, in which case all + settings are prepended with ``'category.'``. For example, + ``rc.update('axes', labelsize=20, titlesize=20)`` changes the + :rcraw:`axes.labelsize` and :rcraw:`axes.titlesize` settings. + **kwargs + `rc` keys and values passed as keyword arguments. + If the name has dots, simply omit them. + + See also + -------- + Configurator.category + Configurator.fill + """ + prefix, kw = '', {} + if not args: + pass + elif len(args) == 1 and isinstance(args[0], str): + prefix = args[0] + elif len(args) == 1 and isinstance(args[0], dict): + kw = args[0] + elif len(args) == 2 and isinstance(args[0], str) and isinstance(args[1], dict): + prefix, kw = args + else: + raise ValueError( + f'Invalid arguments {args!r}. Usage is either ' + 'rc.update(dict), rc.update(kwy=value, ...), ' + 'rc.update(category, dict), or rc.update(category, key=value, ...).' + ) + prefix = prefix and prefix + '.' + kw.update(kwargs) + for key, value in kw.items(): + self.__setitem__(prefix + key, value) + + @docstring._snippet_manager + def reset(self, local=True, user=True, default=True, **kwargs): + """ + Reset the configurator to its initial state. + + Parameters + ---------- + %(rc.params)s + """ + self._init(local=local, user=user, default=default, **kwargs) + + def _load_file(self, path): + """ + Return dictionaries of proplot and matplotlib settings loaded from the file. + """ + # WARNING: Critical to not yet apply _get_item_dicts() syncing or else we + # can overwrite input settings (e.g. label.size followed by font.size). + path = os.path.expanduser(path) + added = set() + rcdict = {} + with open(path, 'r') as fh: + for idx, line in enumerate(fh): + # Strip comments + message = f'line #{idx + 1} in file {path!r}' + stripped = line.split('#', 1)[0].strip() + if not stripped: + pass # no warning + continue + # Parse the pair + pair = stripped.split(':', 1) + if len(pair) != 2: + warnings._warn_proplot(f'Illegal {message}:\n{line}"') + continue + # Detect duplicates + key, value = map(str.strip, pair) + if key in added: + warnings._warn_proplot(f'Duplicate rc key {key!r} on {message}.') + added.add(key) + # Get child dictionaries. Careful to have informative messages + with warnings.catch_warnings(): + warnings.simplefilter('error', warnings.ProplotWarning) + try: + key, value = self._validate_key(key, value) + value = self._validate_value(key, value) + except KeyError: + warnings.simplefilter('default', warnings.ProplotWarning) + warnings._warn_proplot(f'Invalid rc key {key!r} on {message}.') + continue + except ValueError as err: + warnings.simplefilter('default', warnings.ProplotWarning) + warnings._warn_proplot(f'Invalid rc value {value!r} for key {key!r} on {message}: {err}') # noqa: E501 + continue + except warnings.ProplotWarning as err: + warnings.simplefilter('default', warnings.ProplotWarning) + warnings._warn_proplot(f'Outdated rc key {key!r} on {message}: {err}') # noqa: E501 + warnings.simplefilter('ignore', warnings.ProplotWarning) + key, value = self._validate_key(key, value) + value = self._validate_value(key, value) + # Update the settings + rcdict[key] = value + + return rcdict + + def load(self, path): + """ + Load settings from the specified file. + + Parameters + ---------- + path : path-like + The file path. + + See also + -------- + Configurator.save + """ + rcdict = self._load_file(path) + for key, value in rcdict.items(): + self.__setitem__(key, value) + + @staticmethod + def _save_rst(path): + """ + Create an RST table file. Used for online docs. + """ + string = rcsetup._rst_table() + with open(path, 'w') as fh: + fh.write(string) + + @staticmethod + def _save_yaml(path, user_dict=None, *, comment=False, description=False): + """ + Create a YAML file. Used for online docs and default and user-generated + proplotrc files. Extra settings can be passed with the input dictionary. + """ + user_table = () + if user_dict: # add always-uncommented user settings + user_table = rcsetup._yaml_table(user_dict, comment=False) + user_table = ('# Changed settings', user_table, '') + proplot_dict = rcsetup._rc_proplot_table if description else rcsetup._rc_proplot_default # noqa: E501 + proplot_table = rcsetup._yaml_table(proplot_dict, comment=comment, description=description) # noqa: E501 + proplot_table = ('# Proplot settings', proplot_table, '') + matplotlib_dict = rcsetup._rc_matplotlib_default + matplotlib_table = rcsetup._yaml_table(matplotlib_dict, comment=comment) + matplotlib_table = ('# Matplotlib settings', matplotlib_table) + parts = ( + '#--------------------------------------------------------------------', + '# Use this file to change the default proplot and matplotlib settings.', + '# The syntax is identical to matplotlibrc syntax. For details see:', + '# https://proplot.readthedocs.io/en/latest/configuration.html', + '# https://matplotlib.org/stable/tutorials/introductory/customizing.html', + '#--------------------------------------------------------------------', + *user_table, # empty if nothing passed + *proplot_table, + *matplotlib_table, + ) + with open(path, 'w') as fh: + fh.write('\n'.join(parts)) + + def save(self, path=None, user=True, comment=None, backup=True, description=False): + """ + Save the current settings to a ``proplotrc`` file. This writes + the default values commented out plus the values that *differ* + from the defaults at the top of the file. + + Parameters + ---------- + path : path-like, default: 'proplotrc' + The file name and/or directory. The default file name is ``proplotrc`` + and the default directory is the current directory. + user : bool, default: True + If ``True`` then settings that have been `~Configurator.changed` from + the proplot defaults are shown uncommented at the top of the file. + backup : bool, default: True + Whether to "backup" an existing file by renaming with the suffix ``.bak`` + or overwrite an existing file. + comment : bool, optional + Whether to comment out the default settings. If not passed + this takes the same value as `user`. + description : bool, default: False + Whether to include descriptions of each setting (as seen in the + :ref:`user guide table `) as comments. + + See also + -------- + Configurator.load + Configurator.changed + """ + path = os.path.expanduser(path or '.') + if os.path.isdir(path): # includes '' + path = os.path.join(path, 'proplotrc') + if os.path.isfile(path) and backup: + backup = path + '.bak' + os.rename(path, backup) + warnings._warn_proplot(f'Existing file {path!r} was moved to {backup!r}.') + comment = _not_none(comment, user) + user_dict = self.changed if user else None + self._save_yaml(path, user_dict, comment=comment, description=description) + + @property + def _context_mode(self): + """ + Return the highest (least permissive) context mode. + """ + return max((context.mode for context in self._context), default=0) + + @property + def changed(self): + """ + A dictionary of settings that have changed from the proplot defaults. + + See also + -------- + Configurator.save + """ + # Carefully detect changed settings + rcdict = {} + for key, value in self.items(): + default = rcsetup._get_default_param(key) + if isinstance(value, Real) and isinstance(default, Real) and np.isclose(value, default): # noqa: E501 + pass + elif value == default: + pass + else: + rcdict[key] = value + # Ignore non-style-related settings. See mstyle.STYLE_BLACKLIST + # TODO: For now not sure how to detect if prop cycle changed since + # we cannot load it from _cmap_database in rcsetup. + rcdict.pop('interactive', None) # changed by backend + rcdict.pop('axes.prop_cycle', None) + return _filter_style_dict(rcdict, warn=False) + + # Renamed methods + load_file = warnings._rename_objs('0.8.0', load_file=load) + + +# Initialize locations +_init_user_folders() +_init_user_file() + +#: A dictionary-like container of matplotlib settings. Assignments are +#: validated and restricted to recognized setting names. +rc_matplotlib = mpl.rcParams # PEP8 4 lyfe + +#: A dictionary-like container of proplot settings. Assignments are +#: validated and restricted to recognized setting names. +rc_proplot = rcsetup._rc_proplot_default.copy() # a validated rcParams-style dict + +#: Instance of `Configurator`. This controls both `rc_matplotlib` and `rc_proplot` +#: settings. See the :ref:`configuration guide ` for details. +rc = Configurator(skip_cycle=True) + +# Deprecated +RcConfigurator = warnings._rename_objs( + '0.8.0', RcConfigurator=Configurator, +) +inline_backend_fmt = warnings._rename_objs( + '0.6.0', inline_backend_fmt=config_inline_backend +) diff --git a/proplot/constructor.py b/proplot/constructor.py new file mode 100644 index 000000000..17117f898 --- /dev/null +++ b/proplot/constructor.py @@ -0,0 +1,1565 @@ +#!/usr/bin/env python3 +""" +The constructor functions used to build class instances from simple shorthand arguments. +""" +# NOTE: These functions used to be in separate files like crs.py and +# ticker.py but makes more sense to group them together to ensure usage is +# consistent and so online documentation is easier to understand. Also in +# future version classes will not be imported into top-level namespace. This +# change will be easier to do with all constructor functions in separate file. +# NOTE: Used to include the raw variable names that define string keys as +# part of documentation, but this is redundant and pollutes the namespace. +# User should just inspect docstrings, use trial-error, or see online tables. +import copy +import os +import re +from functools import partial +from numbers import Number + +import cycler +import matplotlib.colors as mcolors +import matplotlib.dates as mdates +import matplotlib.projections.polar as mpolar +import matplotlib.scale as mscale +import matplotlib.ticker as mticker +import numpy as np + +from . import colors as pcolors +from . import proj as pproj +from . import scale as pscale +from . import ticker as pticker +from .config import rc +from .internals import ic # noqa: F401 +from .internals import _not_none, _pop_props, _version_cartopy, _version_mpl, warnings +from .utils import get_colors, to_hex, to_rgba + +try: + from mpl_toolkits.basemap import Basemap +except ImportError: + Basemap = object +try: + import cartopy.crs as ccrs + from cartopy.crs import Projection +except ModuleNotFoundError: + ccrs = None + Projection = object + +__all__ = [ + 'Proj', + 'Locator', + 'Formatter', + 'Scale', + 'Colormap', + 'Norm', + 'Cycle', + 'Colors', # deprecated +] + +# Color cycle constants +# TODO: Also automatically truncate the 'bright' end of colormaps +# when building color cycles from colormaps? Or add simple option. +DEFAULT_CYCLE_SAMPLES = 10 +DEFAULT_CYCLE_LUMINANCE = 90 + +# Normalizer registry +NORMS = { + 'none': mcolors.NoNorm, + 'null': mcolors.NoNorm, + 'div': pcolors.DivergingNorm, + 'diverging': pcolors.DivergingNorm, + 'segmented': pcolors.SegmentedNorm, + 'segments': pcolors.SegmentedNorm, + 'log': mcolors.LogNorm, + 'linear': mcolors.Normalize, + 'power': mcolors.PowerNorm, + 'symlog': mcolors.SymLogNorm, +} +if hasattr(mcolors, 'TwoSlopeNorm'): + NORMS['twoslope'] = mcolors.TwoSlopeNorm + +# Locator registry +# NOTE: Will raise error when you try to use degree-minute-second +# locators with cartopy < 0.18. +LOCATORS = { + 'none': mticker.NullLocator, + 'null': mticker.NullLocator, + 'auto': mticker.AutoLocator, + 'log': mticker.LogLocator, + 'maxn': mticker.MaxNLocator, + 'linear': mticker.LinearLocator, + 'multiple': mticker.MultipleLocator, + 'fixed': mticker.FixedLocator, + 'index': pticker.IndexLocator, + 'discrete': pticker.DiscreteLocator, + 'discreteminor': partial(pticker.DiscreteLocator, minor=True), + 'symlog': mticker.SymmetricalLogLocator, + 'logit': mticker.LogitLocator, + 'minor': mticker.AutoMinorLocator, + 'date': mdates.AutoDateLocator, + 'microsecond': mdates.MicrosecondLocator, + 'second': mdates.SecondLocator, + 'minute': mdates.MinuteLocator, + 'hour': mdates.HourLocator, + 'day': mdates.DayLocator, + 'weekday': mdates.WeekdayLocator, + 'month': mdates.MonthLocator, + 'year': mdates.YearLocator, + 'lon': partial(pticker.LongitudeLocator, dms=False), + 'lat': partial(pticker.LatitudeLocator, dms=False), + 'deglon': partial(pticker.LongitudeLocator, dms=False), + 'deglat': partial(pticker.LatitudeLocator, dms=False), +} +if hasattr(mpolar, 'ThetaLocator'): + LOCATORS['theta'] = mpolar.ThetaLocator +if _version_cartopy >= '0.18': + LOCATORS['dms'] = partial(pticker.DegreeLocator, dms=True) + LOCATORS['dmslon'] = partial(pticker.LongitudeLocator, dms=True) + LOCATORS['dmslat'] = partial(pticker.LatitudeLocator, dms=True) + +# Formatter registry +# NOTE: Critical to use SimpleFormatter for cardinal formatters rather than +# AutoFormatter because latter fails with Basemap formatting. +# NOTE: Define cartopy longitude/latitude formatters with dms=True because that +# is their distinguishing feature relative to proplot formatter. +# NOTE: Will raise error when you try to use degree-minute-second +# formatters with cartopy < 0.18. +FORMATTERS = { # note default LogFormatter uses ugly e+00 notation + 'none': mticker.NullFormatter, + 'null': mticker.NullFormatter, + 'auto': pticker.AutoFormatter, + 'date': mdates.AutoDateFormatter, + 'scalar': mticker.ScalarFormatter, + 'simple': pticker.SimpleFormatter, + 'fixed': mticker.FixedLocator, + 'index': pticker.IndexFormatter, + 'sci': pticker.SciFormatter, + 'sigfig': pticker.SigFigFormatter, + 'frac': pticker.FracFormatter, + 'func': mticker.FuncFormatter, + 'strmethod': mticker.StrMethodFormatter, + 'formatstr': mticker.FormatStrFormatter, + 'datestr': mdates.DateFormatter, + 'log': mticker.LogFormatterSciNotation, # NOTE: this is subclass of Mathtext class + 'logit': mticker.LogitFormatter, + 'eng': mticker.EngFormatter, + 'percent': mticker.PercentFormatter, + 'e': partial(pticker.FracFormatter, symbol=r'$e$', number=np.e), + 'pi': partial(pticker.FracFormatter, symbol=r'$\pi$', number=np.pi), + 'tau': partial(pticker.FracFormatter, symbol=r'$\tau$', number=2 * np.pi), + 'lat': partial(pticker.SimpleFormatter, negpos='SN'), + 'lon': partial(pticker.SimpleFormatter, negpos='WE', wraprange=(-180, 180)), + 'deg': partial(pticker.SimpleFormatter, suffix='\N{DEGREE SIGN}'), + 'deglat': partial(pticker.SimpleFormatter, suffix='\N{DEGREE SIGN}', negpos='SN'), + 'deglon': partial(pticker.SimpleFormatter, suffix='\N{DEGREE SIGN}', negpos='WE', wraprange=(-180, 180)), # noqa: E501 + 'math': mticker.LogFormatterMathtext, # deprecated (use SciNotation subclass) +} +if hasattr(mpolar, 'ThetaFormatter'): + FORMATTERS['theta'] = mpolar.ThetaFormatter +if hasattr(mdates, 'ConciseDateFormatter'): + FORMATTERS['concise'] = mdates.ConciseDateFormatter +if _version_cartopy >= '0.18': + FORMATTERS['dms'] = partial(pticker.DegreeFormatter, dms=True) + FORMATTERS['dmslon'] = partial(pticker.LongitudeFormatter, dms=True) + FORMATTERS['dmslat'] = partial(pticker.LatitudeFormatter, dms=True) + +# Scale registry and presets +SCALES = mscale._scale_mapping +SCALES_PRESETS = { + 'quadratic': ('power', 2,), + 'cubic': ('power', 3,), + 'quartic': ('power', 4,), + 'height': ('exp', np.e, -1 / 7, 1013.25, True), + 'pressure': ('exp', np.e, -1 / 7, 1013.25, False), + 'db': ('exp', 10, 1, 0.1, True), + 'idb': ('exp', 10, 1, 0.1, False), + 'np': ('exp', np.e, 1, 1, True), + 'inp': ('exp', np.e, 1, 1, False), +} +mscale.register_scale(pscale.CutoffScale) +mscale.register_scale(pscale.ExpScale) +mscale.register_scale(pscale.FuncScale) +mscale.register_scale(pscale.InverseScale) +mscale.register_scale(pscale.LogScale) +mscale.register_scale(pscale.LinearScale) +mscale.register_scale(pscale.LogitScale) +mscale.register_scale(pscale.MercatorLatitudeScale) +mscale.register_scale(pscale.PowerScale) +mscale.register_scale(pscale.SineLatitudeScale) +mscale.register_scale(pscale.SymmetricalLogScale) + +# Cartopy projection registry and basemap default keyword args +# NOTE: Normally basemap raises error if you omit keyword args +PROJ_DEFAULTS = { + 'geos': {'lon_0': 0}, + 'eck4': {'lon_0': 0}, + 'moll': {'lon_0': 0}, + 'hammer': {'lon_0': 0}, + 'kav7': {'lon_0': 0}, + 'sinu': {'lon_0': 0}, + 'vandg': {'lon_0': 0}, + 'mbtfpq': {'lon_0': 0}, + 'robin': {'lon_0': 0}, + 'ortho': {'lon_0': 0, 'lat_0': 0}, + 'nsper': {'lon_0': 0, 'lat_0': 0}, + 'aea': {'lon_0': 0, 'lat_0': 90, 'width': 15000e3, 'height': 15000e3}, + 'eqdc': {'lon_0': 0, 'lat_0': 90, 'width': 15000e3, 'height': 15000e3}, + 'cass': {'lon_0': 0, 'lat_0': 90, 'width': 15000e3, 'height': 15000e3}, + 'gnom': {'lon_0': 0, 'lat_0': 90, 'width': 15000e3, 'height': 15000e3}, + 'poly': {'lon_0': 0, 'lat_0': 0, 'width': 10000e3, 'height': 10000e3}, + 'npaeqd': {'lon_0': 0, 'boundinglat': 10}, # NOTE: everything breaks if you + 'nplaea': {'lon_0': 0, 'boundinglat': 10}, # try to set boundinglat to zero + 'npstere': {'lon_0': 0, 'boundinglat': 10}, + 'spaeqd': {'lon_0': 0, 'boundinglat': -10}, + 'splaea': {'lon_0': 0, 'boundinglat': -10}, + 'spstere': {'lon_0': 0, 'boundinglat': -10}, + 'lcc': { + 'lon_0': 0, 'lat_0': 40, 'lat_1': 35, 'lat_2': 45, # use cartopy defaults + 'width': 20000e3, 'height': 15000e3 + }, + 'tmerc': { + 'lon_0': 0, 'lat_0': 0, 'width': 10000e3, 'height': 10000e3 + }, + 'merc': { + 'llcrnrlat': -80, 'urcrnrlat': 84, 'llcrnrlon': -180, 'urcrnrlon': 180 + }, + 'omerc': { + 'lat_0': 0, 'lon_0': 0, 'lat_1': -10, 'lat_2': 10, + 'lon_1': 0, 'lon_2': 0, 'width': 10000e3, 'height': 10000e3 + }, +} +if ccrs is None: + PROJS = {} +else: + PROJS = { + 'aitoff': pproj.Aitoff, + 'hammer': pproj.Hammer, + 'kav7': pproj.KavrayskiyVII, + 'wintri': pproj.WinkelTripel, + 'npgnom': pproj.NorthPolarGnomonic, + 'spgnom': pproj.SouthPolarGnomonic, + 'npaeqd': pproj.NorthPolarAzimuthalEquidistant, + 'spaeqd': pproj.SouthPolarAzimuthalEquidistant, + 'nplaea': pproj.NorthPolarLambertAzimuthalEqualArea, + 'splaea': pproj.SouthPolarLambertAzimuthalEqualArea, + } + PROJS_MISSING = { + 'aea': 'AlbersEqualArea', + 'aeqd': 'AzimuthalEquidistant', + 'cyl': 'PlateCarree', # only basemap name not matching PROJ + 'eck1': 'EckertI', + 'eck2': 'EckertII', + 'eck3': 'EckertIII', + 'eck4': 'EckertIV', + 'eck5': 'EckertV', + 'eck6': 'EckertVI', + 'eqc': 'PlateCarree', # actual PROJ name + 'eqdc': 'EquidistantConic', + 'eqearth': 'EqualEarth', # better looking Robinson; not in basemap + 'euro': 'EuroPP', # Europe; not in basemap or PROJ + 'geos': 'Geostationary', + 'gnom': 'Gnomonic', + 'igh': 'InterruptedGoodeHomolosine', # not in basemap + 'laea': 'LambertAzimuthalEqualArea', + 'lcc': 'LambertConformal', + 'lcyl': 'LambertCylindrical', # not in basemap or PROJ + 'merc': 'Mercator', + 'mill': 'Miller', + 'moll': 'Mollweide', + 'npstere': 'NorthPolarStereo', # np/sp stuff not in PROJ + 'nsper': 'NearsidePerspective', + 'ortho': 'Orthographic', + 'osgb': 'OSGB', # UK; not in basemap or PROJ + 'osni': 'OSNI', # Ireland; not in basemap or PROJ + 'pcarree': 'PlateCarree', # common alternate name + 'robin': 'Robinson', + 'rotpole': 'RotatedPole', + 'sinu': 'Sinusoidal', + 'spstere': 'SouthPolarStereo', + 'stere': 'Stereographic', + 'tmerc': 'TransverseMercator', + 'utm': 'UTM', # not in basemap + } + for _key, _cls in tuple(PROJS_MISSING.items()): + if hasattr(ccrs, _cls): + PROJS[_key] = getattr(ccrs, _cls) + del PROJS_MISSING[_key] + if PROJS_MISSING: + warnings._warn_proplot( + 'The following cartopy projection(s) are unavailable: ' + + ', '.join(map(repr, PROJS_MISSING)) + + ' . Please consider updating cartopy.' + ) + PROJS_TABLE = ( + 'The known cartopy projection classes are:\n' + + '\n'.join( + ' ' + key + ' ' * (max(map(len, PROJS)) - len(key) + 10) + cls.__name__ + for key, cls in PROJS.items() + ) + ) + +# Geographic feature properties +FEATURES_CARTOPY = { # positional arguments passed to NaturalEarthFeature + 'land': ('physical', 'land'), + 'ocean': ('physical', 'ocean'), + 'lakes': ('physical', 'lakes'), + 'coast': ('physical', 'coastline'), + 'rivers': ('physical', 'rivers_lake_centerlines'), + 'borders': ('cultural', 'admin_0_boundary_lines_land'), + 'innerborders': ('cultural', 'admin_1_states_provinces_lakes'), +} +FEATURES_BASEMAP = { # names of relevant basemap methods + 'land': 'fillcontinents', + 'coast': 'drawcoastlines', + 'rivers': 'drawrivers', + 'borders': 'drawcountries', + 'innerborders': 'drawstates', +} + +# Resolution names +# NOTE: Maximum basemap resolutions are much finer than cartopy +RESOS_CARTOPY = { + 'lo': '110m', + 'med': '50m', + 'hi': '10m', + 'x-hi': '10m', # extra high + 'xx-hi': '10m', # extra extra high +} +RESOS_BASEMAP = { + 'lo': 'c', # coarse + 'med': 'l', + 'hi': 'i', # intermediate + 'x-hi': 'h', + 'xx-hi': 'f', # fine +} + + +def _modify_colormap(cmap, *, cut, left, right, reverse, shift, alpha, samples): + """ + Modify colormap using a variety of methods. + """ + if cut is not None or left is not None or right is not None: + if isinstance(cmap, pcolors.DiscreteColormap): + if cut is not None: + warnings._warn_proplot( + "Invalid argument 'cut' for ListedColormap. Ignoring." + ) + cmap = cmap.truncate(left=left, right=right) + else: + cmap = cmap.cut(cut, left=left, right=right) + if reverse: + cmap = cmap.reversed() + if shift is not None: + cmap = cmap.shifted(shift) + if alpha is not None: + cmap = cmap.copy(alpha=alpha) + if samples is not None: + if isinstance(cmap, pcolors.DiscreteColormap): + cmap = cmap.copy(N=samples) + else: + cmap = cmap.to_discrete(samples) + return cmap + + +@warnings._rename_kwargs( + '0.8.0', fade='saturation', shade='luminance', to_listed='discrete' +) +def Colormap( + *args, name=None, listmode='perceptual', filemode='continuous', discrete=False, + cycle=None, save=False, save_kw=None, **kwargs +): + """ + Generate, retrieve, modify, and/or merge instances of + `~proplot.colors.PerceptualColormap`, + `~proplot.colors.ContinuousColormap`, and + `~proplot.colors.DiscreteColormap`. + + Parameters + ---------- + *args : colormap-spec + Positional arguments that individually generate colormaps. If more + than one argument is passed, the resulting colormaps are *merged* with + `~proplot.colors.ContinuousColormap.append` + or `~proplot.colors.DiscreteColormap.append`. + The arguments are interpreted as follows: + + * If a registered colormap name, that colormap instance is looked up. + If colormap instance is a native matplotlib colormap class, it is + converted to a proplot colormap class. + * If a filename string with valid extension, the colormap data + is loaded with `proplot.colors.ContinuousColormap.from_file` or + `proplot.colors.DiscreteColormap.from_file` depending on the value of + `filemode` (see below). Default behavior is to load a + `~proplot.colors.ContinuousColormap`. + * If RGB tuple or color string, a `~proplot.colors.PerceptualColormap` + is generated with `~proplot.colors.PerceptualColormap.from_color`. + If the string ends in ``'_r'``, the monochromatic map will be + *reversed*, i.e. will go from dark to light instead of light to dark. + * If sequence of RGB tuples or color strings, a + `~proplot.colors.DiscreteColormap`, `~proplot.colors.PerceptualColormap`, + or `~proplot.colors.ContinuousColormap` is generated depending on + the value of `listmode` (see below). Default behavior is to generate a + `~proplot.colors.PerceptualColormap`. + * If dictionary, a `~proplot.colors.PerceptualColormap` is + generated with `~proplot.colors.PerceptualColormap.from_hsl`. + The dictionary should contain the keys ``'hue'``, ``'saturation'``, + ``'luminance'``, and optionally ``'alpha'``, or their aliases (see below). + + name : str, optional + Name under which the final colormap is registered. It can + then be reused by passing ``cmap='name'`` to plotting + functions. Names with leading underscores are ignored. + filemode : {'perceptual', 'continuous', 'discrete'}, optional + Controls how colormaps are generated when you input list(s) of colors. + The options are as follows: + + * If ``'perceptual'`` or ``'continuous'``, a colormap is generated using + `~proplot.colors.ContinuousColormap.from_file`. The resulting + colormap may be a `~proplot.colors.ContinuousColormap` or + `~proplot.colors.PerceptualColormap` depending on the data file. + * If ``'discrete'``, a `~proplot.colors.DiscreteColormap` is generated + using `~proplot.colors.ContinuousColormap.from_file`. + + Default is ``'continuous'`` when calling `Colormap` directly and + ``'discrete'`` when `Colormap` is called by `Cycle`. + listmode : {'perceptual', 'continuous', 'discrete'}, optional + Controls how colormaps are generated when you input sequence(s) + of colors. The options are as follows: + + * If ``'perceptual'``, a `~proplot.colors.PerceptualColormap` + is generated with `~proplot.colors.PerceptualColormap.from_list`. + * If ``'continuous'``, a `~proplot.colors.ContinuousColormap` is + generated with `~proplot.colors.ContinuousColormap.from_list`. + * If ``'discrete'``, a `~proplot.colors.DiscreteColormap` is generated + by simply passing the colors to the class. + + Default is ``'perceptual'`` when calling `Colormap` directly and + ``'discrete'`` when `Colormap` is called by `Cycle`. + samples : int or sequence of int, optional + For `~proplot.colors.ContinuousColormap`\\ s, this is used to + generate `~proplot.colors.DiscreteColormap`\\ s with + `~proplot.colors.ContinuousColormap.to_discrete`. For + `~proplot.colors.DiscreteColormap`\\ s, this is used to updates the + number of colors in the cycle. If `samples` is integer, it applies + to the final *merged* colormap. If it is a sequence of integers, + it applies to each input colormap individually. + discrete : bool, optional + If ``True``, when the final colormap is a + `~proplot.colors.DiscreteColormap`, we leave it alone, but when it is a + `~proplot.colors.ContinuousColormap`, we always call + `~proplot.colors.ContinuousColormap.to_discrete` with a + default `samples` value of ``10``. This argument is not + necessary if you provide the `samples` argument. + left, right : float or sequence of float, optional + Truncate the left or right edges of the colormap. + Passed to `~proplot.colors.ContinuousColormap.truncate`. + If float, these apply to the final *merged* colormap. If sequence + of float, these apply to each input colormap individually. + cut : float or sequence of float, optional + Cut out the center of the colormap. Passed to + `~proplot.colors.ContinuousColormap.cut`. If float, + this applies to the final *merged* colormap. If sequence of + float, these apply to each input colormap individually. + reverse : bool or sequence of bool, optional + Reverse the colormap. Passed to + `~proplot.colors.ContinuousColormap.reversed`. If + float, this applies to the final *merged* colormap. If + sequence of float, these apply to each input colormap individually. + shift : float or sequence of float, optional + Cyclically shift the colormap. + Passed to `~proplot.colors.ContinuousColormap.shifted`. + If float, this applies to the final *merged* colormap. If sequence + of float, these apply to each input colormap individually. + a + Shorthand for `alpha`. + alpha : float or color-spec or sequence, optional + The opacity of the colormap or the opacity gradation. Passed to + `proplot.colors.ContinuousColormap.set_alpha` + or `proplot.colors.DiscreteColormap.set_alpha`. If float, this applies + to the final *merged* colormap. If sequence of float, these apply to + each colormap individually. + h, s, l, c + Shorthands for `hue`, `luminance`, `saturation`, and `chroma`. + hue, saturation, luminance : float or color-spec or sequence, optional + The channel value(s) used to generate colormaps with + `~proplot.colors.PerceptualColormap.from_hsl` and + `~proplot.colors.PerceptualColormap.from_color`. + + * If you provided no positional arguments, these are used to create + an arbitrary perceptually uniform colormap with + `~proplot.colors.PerceptualColormap.from_hsl`. This + is an alternative to passing a dictionary as a positional argument + with `hue`, `saturation`, and `luminance` as dictionary keys (see `args`). + * If you did provide positional arguments, and any of them are + color specifications, these control the look of monochromatic colormaps + generated with `~proplot.colors.PerceptualColormap.from_color`. + To use different values for each colormap, pass a sequence of floats + instead of a single float. Note the default `luminance` is ``90`` if + `discrete` is ``True`` and ``100`` otherwise. + + chroma + Alias for `saturation`. + cycle : str, optional + The registered cycle name used to interpret color strings like ``'C0'`` + and ``'C2'``. Default is from the active property :rcraw:`cycle`. This lets + you make monochromatic colormaps using colors selected from arbitrary cycles. + save : bool, optional + Whether to call the colormap/color cycle save method, i.e. + `proplot.colors.ContinuousColormap.save` or + `proplot.colors.DiscreteColormap.save`. + save_kw : dict-like, optional + Ignored if `save` is ``False``. Passed to the colormap/color cycle + save method, i.e. `proplot.colors.ContinuousColormap.save` or + `proplot.colors.DiscreteColormap.save`. + + Other parameters + ---------------- + **kwargs + Passed to `proplot.colors.ContinuousColormap.copy`, + `proplot.colors.PerceptualColormap.copy`, or + `proplot.colors.DiscreteColormap.copy`. + + Returns + ------- + matplotlib.colors.Colormap + A `~proplot.colors.ContinuousColormap` or + `~proplot.colors.DiscreteColormap` instance. + + See also + -------- + matplotlib.colors.Colormap + matplotlib.colors.LinearSegmentedColormap + matplotlib.colors.ListedColormap + proplot.constructor.Norm + proplot.constructor.Cycle + proplot.utils.get_colors + """ + # Helper function + # NOTE: Very careful here! Try to support common use cases. For example + # adding opacity gradations to colormaps with Colormap('cmap', alpha=(0.5, 1)) + # or sampling maps with Colormap('cmap', samples=np.linspace(0, 1, 11)) should + # be allowable. + # If *args is singleton try to preserve it. + def _pop_modification(key): + value = kwargs.pop(key, None) + if not np.iterable(value) or isinstance(value, str): + values = (None,) * len(args) + elif len(args) == len(value): + values, value = tuple(value), None + elif len(args) == 1: # e.g. Colormap('cmap', alpha=(0.5, 1)) + values = (None,) + else: + raise ValueError( + f'Got {len(args)} colormap-specs ' + f'but {len(value)} values for {key!r}.' + ) + return value, values + + # Parse keyword args that can apply to the merged colormap or each one + hsla = _pop_props(kwargs, 'hsla') + if not args and hsla.keys() - {'alpha'}: + args = (hsla,) + else: + kwargs.update(hsla) + default_luminance = kwargs.pop('default_luminance', None) # used internally + cut, cuts = _pop_modification('cut') + left, lefts = _pop_modification('left') + right, rights = _pop_modification('right') + shift, shifts = _pop_modification('shift') + reverse, reverses = _pop_modification('reverse') + samples, sampless = _pop_modification('samples') + alpha, alphas = _pop_modification('alpha') + luminance, luminances = _pop_modification('luminance') + saturation, saturations = _pop_modification('saturation') + if luminance is not None: + luminances = (luminance,) * len(args) + if saturation is not None: + saturations = (saturation,) * len(args) + + # Issue warnings and errors + if not args: + raise ValueError( + 'Colormap() requires either positional arguments or ' + "'hue', 'chroma', 'saturation', and/or 'luminance' keywords." + ) + deprecated = {'listed': 'discrete', 'linear': 'continuous'} + if listmode in deprecated: + oldmode, listmode = listmode, deprecated[listmode] + warnings._warn_proplot( + f'Please use listmode={listmode!r} instead of listmode={oldmode!r}.' + 'Option was renamed in v0.8 and will be removed in a future relase.' + ) + options = {'discrete', 'continuous', 'perceptual'} + for key, mode in zip(('listmode', 'filemode'), (listmode, filemode)): + if mode not in options: + raise ValueError( + f'Invalid {key}={mode!r}. Options are: ' + + ', '.join(map(repr, options)) + + '.' + ) + + # Loop through colormaps + cmaps = [] + for arg, icut, ileft, iright, ireverse, ishift, isamples, iluminance, isaturation, ialpha in zip( # noqa: E501 + args, cuts, lefts, rights, reverses, shifts, sampless, luminances, saturations, alphas # noqa: E501 + ): + # Load registered colormaps and maps on file + # TODO: Document how 'listmode' also affects loaded files + if isinstance(arg, str): + if '.' in arg and os.path.isfile(arg): + if filemode == 'discrete': + arg = pcolors.DiscreteColormap.from_file(arg) + else: + arg = pcolors.ContinuousColormap.from_file(arg) + else: + try: + arg = pcolors._cmap_database[arg] + except KeyError: + pass + + # Convert matplotlib colormaps to subclasses + if isinstance(arg, mcolors.Colormap): + cmap = pcolors._translate_cmap(arg) + + # Dictionary of hue/sat/luminance values or 2-tuples + elif isinstance(arg, dict): + cmap = pcolors.PerceptualColormap.from_hsl(**arg) + + # List of color tuples or color strings, i.e. iterable of iterables + elif ( + not isinstance(arg, str) and np.iterable(arg) + and all(np.iterable(color) for color in arg) + ): + if listmode == 'discrete': + cmap = pcolors.DiscreteColormap(arg) + elif listmode == 'continuous': + cmap = pcolors.ContinuousColormap.from_list(arg) + else: + cmap = pcolors.PerceptualColormap.from_list(arg) + + # Monochrome colormap from input color + # NOTE: Do not print color names in error message. Too long to be useful. + else: + jreverse = isinstance(arg, str) and arg[-2:] == '_r' + if jreverse: + arg = arg[:-2] + try: + color = to_rgba(arg, cycle=cycle) + except (ValueError, TypeError): + message = f'Invalid colormap, color cycle, or color {arg!r}.' + if isinstance(arg, str) and arg[:1] != '#': + message += ( + ' Options include: ' + + ', '.join(sorted(map(repr, pcolors._cmap_database))) + + '.' + ) + raise ValueError(message) from None + iluminance = _not_none(iluminance, default_luminance) + cmap = pcolors.PerceptualColormap.from_color( + color, luminance=iluminance, saturation=isaturation + ) + ireverse = _not_none(ireverse, False) + ireverse = ireverse ^ jreverse # xor + + # Modify the colormap + cmap = _modify_colormap( + cmap, cut=icut, left=ileft, right=iright, + reverse=ireverse, shift=ishift, alpha=ialpha, samples=isamples, + ) + cmaps.append(cmap) + + # Merge the resulting colormaps + if len(cmaps) > 1: # more than one map and modify arbitrary properties + cmap = cmaps[0].append(*cmaps[1:], **kwargs) + else: + cmap = cmaps[0].copy(**kwargs) + + # Modify the colormap + if discrete and isinstance(cmap, pcolors.ContinuousColormap): # noqa: E501 + samples = _not_none(samples, DEFAULT_CYCLE_SAMPLES) + cmap = _modify_colormap( + cmap, cut=cut, left=left, right=right, + reverse=reverse, shift=shift, alpha=alpha, samples=samples + ) + + # Initialize + if not cmap._isinit: + cmap._init() + + # Register the colormap + if name is None: + name = cmap.name # may have been modified by e.g. reversed() + else: + cmap.name = name + if not isinstance(name, str): + raise ValueError('The colormap name must be a string.') + pcolors._cmap_database[name] = cmap + + # Save the colormap + if save: + save_kw = save_kw or {} + cmap.save(**save_kw) + + return cmap + + +def Cycle(*args, N=None, samples=None, name=None, **kwargs): + """ + Generate and merge `~cycler.Cycler` instances in a variety of ways. + + Parameters + ---------- + *args : colormap-spec or cycle-spec, optional + Positional arguments control the *colors* in the `~cycler.Cycler` + object. If zero arguments are passed, the single color ``'black'`` + is used. If more than one argument is passed, the resulting cycles + are merged. Arguments are interpreted as follows: + + * If a `~cycler.Cycler`, nothing more is done. + * If a sequence of RGB tuples or color strings, these colors are used. + * If a `~proplot.colors.DiscreteColormap`, colors from the ``colors`` + attribute are used. + * If a string cycle name, that `~proplot.colors.DiscreteColormap` + is looked up and its ``colors`` are used. + * In all other cases, the argument is passed to `Colormap`, and + colors from the resulting `~proplot.colors.ContinuousColormap` + are used. See the `samples` argument. + + If the last positional argument is numeric, it is used for the + `samples` keyword argument. + N + Shorthand for `samples`. + samples : float or sequence of float, optional + For `~proplot.colors.DiscreteColormap`\\ s, this is the number of + colors to select. For example, ``Cycle('538', 4)`` returns the first 4 + colors of the ``'538'`` color cycle. + For `~proplot.colors.ContinuousColormap`\\ s, this is either a + sequence of sample coordinates used to draw colors from the colormap, or + an integer number of colors to draw. If the latter, the sample coordinates + are ``np.linspace(0, 1, samples)``. For example, ``Cycle('Reds', 5)`` + divides the ``'Reds'`` colormap into five evenly spaced colors. + + Other parameters + ---------------- + c, color, colors : sequence of color-spec, optional + A sequence of colors passed as keyword arguments. This is equivalent + to passing a sequence of colors as the first positional argument and is + included for consistency with `~matplotlib.axes.Axes.set_prop_cycle`. + If positional arguments were passed, the colors in this list are + appended to the colors resulting from the positional arguments. + lw, ls, d, a, m, ms, mew, mec, mfc + Shorthands for the below keywords. + linewidth, linestyle, dashes, alpha, marker, markersize, markeredgewidth, \ +markeredgecolor, markerfacecolor : object or sequence of object, optional + Lists of `~matplotlib.lines.Line2D` properties that can be added to the + `~cycler.Cycler` instance. If the input was already a `~cycler.Cycler`, + these are added or appended to the existing cycle keys. If the lists have + unequal length, they are repeated to their least common multiple (unlike + `~cycler.cycler`, which throws an error in this case). For more info + on cyclers see `~matplotlib.axes.Axes.set_prop_cycle`. Also see + the `line style reference \ +`__, + the `marker reference \ +`__, + and the `custom dashes reference \ +`__. + linewidths, linestyles, dashes, alphas, markers, markersizes, markeredgewidths, \ +markeredgecolors, markerfacecolors + Aliases for the above keywords. + **kwargs + If the input is not already a `~cycler.Cycler` instance, these are passed + to `Colormap` and used to build the `~proplot.colors.DiscreteColormap` + from which the cycler will draw its colors. + + Returns + ------- + cycler.Cycler + A `~cycler.Cycler` instance that can be passed + to `~matplotlib.axes.Axes.set_prop_cycle`. + + See also + -------- + cycler.cycler + cycler.Cycler + matplotlib.axes.Axes.set_prop_cycle + proplot.constructor.Colormap + proplot.constructor.Norm + proplot.utils.get_colors + """ + # Parse keyword arguments that rotate through other properties + # besides color cycles. + props = _pop_props(kwargs, 'line') + if 'sizes' in kwargs: # special case, gets translated back by scatter() + props.setdefault('markersize', kwargs.pop('sizes')) + samples = _not_none(samples=samples, N=N) # trigger Colormap default + for key, value in tuple(props.items()): # permit in-place modification + if value is None: + return + elif not np.iterable(value) or isinstance(value, str): + value = (value,) + props[key] = list(value) # ensure mutable list + + # If args is non-empty, means we want color cycle; otherwise is black + if not args: + props.setdefault('color', ['black']) + if kwargs: + warnings._warn_proplot(f'Ignoring Cycle() keyword arg(s) {kwargs}.') + dicts = () + + # Merge cycler objects and/or update cycler objects with input kwargs + elif all(isinstance(arg, cycler.Cycler) for arg in args): + if kwargs: + warnings._warn_proplot(f'Ignoring Cycle() keyword arg(s) {kwargs}.') + if len(args) == 1 and not props: + return args[0] + dicts = tuple(arg.by_key() for arg in args) + + # Get a cycler from a colormap + # NOTE: Passing discrete=True does not imply default_luminance=90 because + # someone might be trying to make qualitative colormap for use in 2D plot + else: + if isinstance(args[-1], Number): + args, samples = args[:-1], _not_none(samples_positional=args[-1], samples=samples) # noqa: #501 + kwargs.setdefault('listmode', 'discrete') + kwargs.setdefault('filemode', 'discrete') + kwargs['discrete'] = True # triggers application of default 'samples' + kwargs['default_luminance'] = DEFAULT_CYCLE_LUMINANCE + cmap = Colormap(*args, name=name, samples=samples, **kwargs) + name = _not_none(name, cmap.name) + dict_ = {'color': [c if isinstance(c, str) else to_hex(c) for c in cmap.colors]} + dicts = (dict_,) + + # Update the cyler property + dicts = dicts + (props,) + props = {} + for dict_ in dicts: + for key, value in dict_.items(): + props.setdefault(key, []).extend(value) + + # Build cycler with matching property lengths + maxlen = np.lcm.reduce([len(value) for value in props.values()]) + props = {key: value * (maxlen // len(value)) for key, value in props.items()} + cycle = cycler.cycler(**props) + cycle.name = _not_none(name, '_no_name') + + return cycle + + +def Norm(norm, *args, **kwargs): + """ + Return an arbitrary `~matplotlib.colors.Normalize` instance. See this + `tutorial `__ + for an introduction to matplotlib normalizers. + + Parameters + ---------- + norm : str or `~matplotlib.colors.Normalize` + The normalizer specification. If a `~matplotlib.colors.Normalize` + instance already, a `copy.copy` of the instance is returned. + Otherwise, `norm` should be a string corresponding to one of + the "registered" colormap normalizers (see below table). + + If `norm` is a list or tuple and the first element is a "registered" + normalizer name, subsequent elements are passed to the normalizer class + as positional arguments. + + .. _norm_table: + + =============================== ===================================== + Key(s) Class + =============================== ===================================== + ``'null'``, ``'none'`` `~matplotlib.colors.NoNorm` + ``'diverging'``, ``'div'`` `~proplot.colors.DivergingNorm` + ``'segmented'``, ``'segments'`` `~proplot.colors.SegmentedNorm` + ``'linear'`` `~matplotlib.colors.Normalize` + ``'log'`` `~matplotlib.colors.LogNorm` + ``'power'`` `~matplotlib.colors.PowerNorm` + ``'symlog'`` `~matplotlib.colors.SymLogNorm` + =============================== ===================================== + + Other parameters + ---------------- + *args, **kwargs + Passed to the `~matplotlib.colors.Normalize` initializer. + + Returns + ------- + matplotlib.colors.Normalize + A `~matplotlib.colors.Normalize` instance. + + See also + -------- + matplotlib.colors.Normalize + proplot.colors.DiscreteNorm + proplot.constructor.Colormap + """ + if np.iterable(norm) and not isinstance(norm, str): + norm, *args = *norm, *args + if isinstance(norm, mcolors.Normalize): + return copy.copy(norm) + if not isinstance(norm, str): + raise ValueError(f'Invalid norm name {norm!r}. Must be string.') + if norm not in NORMS: + raise ValueError( + f'Unknown normalizer {norm!r}. Options are: ' + + ', '.join(map(repr, NORMS)) + + '.' + ) + if norm == 'symlog' and not args and 'linthresh' not in kwargs: + kwargs['linthresh'] = 1 # special case, needs argument + return NORMS[norm](*args, **kwargs) + + +def Locator(locator, *args, discrete=False, **kwargs): + """ + Return a `~matplotlib.ticker.Locator` instance. + + Parameters + ---------- + locator : `~matplotlib.ticker.Locator`, str, bool, float, or sequence + The locator specification, interpreted as follows: + + * If a `~matplotlib.ticker.Locator` instance already, + a `copy.copy` of the instance is returned. + * If ``False``, a `~matplotlib.ticker.NullLocator` is used, and if + ``True``, the default `~matplotlib.ticker.AutoLocator` is used. + * If a number, this specifies the *step size* between tick locations. + Returns a `~matplotlib.ticker.MultipleLocator`. + * If a sequence of numbers, these points are ticked. Returns + a `~matplotlib.ticker.FixedLocator` by default or a + `~proplot.ticker.DiscreteLocator` if `discrete` is ``True``. + + Otherwise, `locator` should be a string corresponding to one + of the "registered" locators (see below table). If `locator` is a + list or tuple and the first element is a "registered" locator name, + subsequent elements are passed to the locator class as positional + arguments. For example, ``pplt.Locator(('multiple', 5))`` is + equivalent to ``pplt.Locator('multiple', 5)``. + + .. _locator_table: + + ======================= ============================================ ===================================================================================== + Key Class Description + ======================= ============================================ ===================================================================================== + ``'null'``, ``'none'`` `~matplotlib.ticker.NullLocator` No ticks + ``'auto'`` `~matplotlib.ticker.AutoLocator` Major ticks at sensible locations + ``'minor'`` `~matplotlib.ticker.AutoMinorLocator` Minor ticks at sensible locations + ``'date'`` `~matplotlib.dates.AutoDateLocator` Default tick locations for datetime axes + ``'fixed'`` `~matplotlib.ticker.FixedLocator` Ticks at these exact locations + ``'discrete'`` `~proplot.ticker.DiscreteLocator` Major ticks restricted to these locations but subsampled depending on the axis length + ``'discreteminor'`` `~proplot.ticker.DiscreteLocator` Minor ticks restricted to these locations but subsampled depending on the axis length + ``'index'`` `~proplot.ticker.IndexLocator` Ticks on the non-negative integers + ``'linear'`` `~matplotlib.ticker.LinearLocator` Exactly ``N`` ticks encompassing axis limits, spaced as ``numpy.linspace(lo, hi, N)`` + ``'log'`` `~matplotlib.ticker.LogLocator` For log-scale axes + ``'logminor'`` `~matplotlib.ticker.LogLocator` For log-scale axes on the 1st through 9th multiples of each power of the base + ``'logit'`` `~matplotlib.ticker.LogitLocator` For logit-scale axes + ``'logitminor'`` `~matplotlib.ticker.LogitLocator` For logit-scale axes with ``minor=True`` passed to `~matplotlib.ticker.LogitLocator` + ``'maxn'`` `~matplotlib.ticker.MaxNLocator` No more than ``N`` ticks at sensible locations + ``'multiple'`` `~matplotlib.ticker.MultipleLocator` Ticks every ``N`` step away from zero + ``'symlog'`` `~matplotlib.ticker.SymmetricalLogLocator` For symlog-scale axes + ``'symlogminor'`` `~matplotlib.ticker.SymmetricalLogLocator` For symlog-scale axes on the 1st through 9th multiples of each power of the base + ``'theta'`` `~matplotlib.projections.polar.ThetaLocator` Like the base locator but default locations are every `numpy.pi` / 8 radians + ``'year'`` `~matplotlib.dates.YearLocator` Ticks every ``N`` years + ``'month'`` `~matplotlib.dates.MonthLocator` Ticks every ``N`` months + ``'weekday'`` `~matplotlib.dates.WeekdayLocator` Ticks every ``N`` weekdays + ``'day'`` `~matplotlib.dates.DayLocator` Ticks every ``N`` days + ``'hour'`` `~matplotlib.dates.HourLocator` Ticks every ``N`` hours + ``'minute'`` `~matplotlib.dates.MinuteLocator` Ticks every ``N`` minutes + ``'second'`` `~matplotlib.dates.SecondLocator` Ticks every ``N`` seconds + ``'microsecond'`` `~matplotlib.dates.MicrosecondLocator` Ticks every ``N`` microseconds + ``'lon'``, ``'deglon'`` `~proplot.ticker.LongitudeLocator` Longitude gridlines at sensible decimal locations + ``'lat'``, ``'deglat'`` `~proplot.ticker.LatitudeLocator` Latitude gridlines at sensible decimal locations + ``'dms'`` `~proplot.ticker.DegreeLocator` Gridlines on nice minute and second intervals + ``'dmslon'`` `~proplot.ticker.LongitudeLocator` Longitude gridlines on nice minute and second intervals + ``'dmslat'`` `~proplot.ticker.LatitudeLocator` Latitude gridlines on nice minute and second intervals + ======================= ============================================ ===================================================================================== + + Other parameters + ---------------- + *args, **kwargs + Passed to the `~matplotlib.ticker.Locator` class. + + Returns + ------- + matplotlib.ticker.Locator + A `~matplotlib.ticker.Locator` instance. + + See also + -------- + matplotlib.ticker.Locator + proplot.axes.CartesianAxes.format + proplot.axes.PolarAxes.format + proplot.axes.GeoAxes.format + proplot.axes.Axes.colorbar + proplot.constructor.Formatter + """ # noqa: E501 + if np.iterable(locator) and not isinstance(locator, str) and not all( + isinstance(num, Number) for num in locator + ): + locator, *args = *locator, *args + if isinstance(locator, mticker.Locator): + return copy.copy(locator) + if isinstance(locator, str): + if locator == 'index': # defaults + args = args or (1,) + if len(args) == 1: + args = (*args, 0) + elif locator in ('logminor', 'logitminor', 'symlogminor'): # presets + locator, _ = locator.split('minor') + if locator == 'logit': + kwargs.setdefault('minor', True) + else: + kwargs.setdefault('subs', np.arange(1, 10)) + if locator in LOCATORS: + locator = LOCATORS[locator](*args, **kwargs) + else: + raise ValueError( + f'Unknown locator {locator!r}. Options are: ' + + ', '.join(map(repr, LOCATORS)) + + '.' + ) + elif locator is True: + locator = mticker.AutoLocator(*args, **kwargs) + elif locator is False: + locator = mticker.NullLocator(*args, **kwargs) + elif isinstance(locator, Number): # scalar variable + locator = mticker.MultipleLocator(locator, *args, **kwargs) + elif np.iterable(locator): + locator = np.array(locator) + if discrete: + locator = pticker.DiscreteLocator(locator, *args, **kwargs) + else: + locator = mticker.FixedLocator(locator, *args, **kwargs) + else: + raise ValueError(f'Invalid locator {locator!r}.') + return locator + + +def Formatter(formatter, *args, date=False, index=False, **kwargs): + """ + Return a `~matplotlib.ticker.Formatter` instance. + + Parameters + ---------- + formatter : `~matplotlib.ticker.Formatter`, str, bool, callable, or sequence + The formatter specification, interpreted as follows: + + * If a `~matplotlib.ticker.Formatter` instance already, + a `copy.copy` of the instance is returned. + * If ``False``, a `~matplotlib.ticker.NullFormatter` is used, and if + ``True``, the default `~proplot.ticker.AutoFormatter` is used. + * If a function, the labels will be generated using this function. + Returns a `~matplotlib.ticker.FuncFormatter`. + * If sequence of strings, the ticks are labeled with these strings. + Returns a `~matplotlib.ticker.FixedFormatter` by default or + an `~proplot.ticker.IndexFormatter` if `index` is ``True``. + * If a string containing ``{x}`` or ``{x:...}``, ticks will be + formatted by calling ``string.format(x=number)``. Returns + a `~matplotlib.ticker.StrMethodFormatter`. + * If a string containing ``'%'`` and `date` is ``False``, ticks + will be formatted using the C-style ``string % number`` method. See + `this page `__ + for a review. Returns a `~matplotlib.ticker.FormatStrFormatter`. + * If a string containing ``'%'`` and `date` is ``True``, ticks + will be formatted using `~datetime.datetime.strfrtime`. See + `this page `__ + for a review. Returns a `~matplotlib.dates.DateFormatter`. + + Otherwise, `formatter` should be a string corresponding to one of the + "registered" formatters or formatter presets (see below table). If + `formatter` is a list or tuple and the first element is a "registered" + formatter name, subsequent elements are passed to the formatter class + as positional arguments. For example, ``pplt.Formatter(('sigfig', 3))`` is + equivalent to ``Formatter('sigfig', 3)``. + + + .. _tau: https://tauday.com/tau-manifesto + + .. _formatter_table: + + ====================== ============================================== ================================================================= + Key Class Description + ====================== ============================================== ================================================================= + ``'null'``, ``'none'`` `~matplotlib.ticker.NullFormatter` No tick labels + ``'auto'`` `~proplot.ticker.AutoFormatter` New default tick labels for axes + ``'sci'`` `~proplot.ticker.SciFormatter` Format ticks with scientific notation + ``'simple'`` `~proplot.ticker.SimpleFormatter` New default tick labels for e.g. contour labels + ``'sigfig'`` `~proplot.ticker.SigFigFormatter` Format labels using the first ``N`` significant digits + ``'frac'`` `~proplot.ticker.FracFormatter` Rational fractions + ``'date'`` `~matplotlib.dates.AutoDateFormatter` Default tick labels for datetime axes + ``'concise'`` `~matplotlib.dates.ConciseDateFormatter` More concise date labels introduced in matplotlib 3.1 + ``'datestr'`` `~matplotlib.dates.DateFormatter` Date formatting with C-style ``string % format`` notation + ``'eng'`` `~matplotlib.ticker.EngFormatter` Engineering notation + ``'fixed'`` `~matplotlib.ticker.FixedFormatter` List of strings + ``'formatstr'`` `~matplotlib.ticker.FormatStrFormatter` From C-style ``string % format`` notation + ``'func'`` `~matplotlib.ticker.FuncFormatter` Use an arbitrary function + ``'index'`` `~proplot.ticker.IndexFormatter` List of strings corresponding to non-negative integer positions + ``'log'`` `~matplotlib.ticker.LogFormatterSciNotation` For log-scale axes with scientific notation + ``'logit'`` `~matplotlib.ticker.LogitFormatter` For logistic-scale axes + ``'percent'`` `~matplotlib.ticker.PercentFormatter` Trailing percent sign + ``'scalar'`` `~matplotlib.ticker.ScalarFormatter` The default matplotlib formatter + ``'strmethod'`` `~matplotlib.ticker.StrMethodFormatter` From the ``string.format`` method + ``'theta'`` `~matplotlib.projections.polar.ThetaFormatter` Formats radians as degrees, with a degree symbol + ``'e'`` `~proplot.ticker.FracFormatter` preset Fractions of *e* + ``'pi'`` `~proplot.ticker.FracFormatter` preset Fractions of :math:`\\pi` + ``'tau'`` `~proplot.ticker.FracFormatter` preset Fractions of the `one true circle constant `_ :math:`\\tau` + ``'lat'`` `~proplot.ticker.AutoFormatter` preset Cardinal "SN" indicator + ``'lon'`` `~proplot.ticker.AutoFormatter` preset Cardinal "WE" indicator + ``'deg'`` `~proplot.ticker.AutoFormatter` preset Trailing degree symbol + ``'deglat'`` `~proplot.ticker.AutoFormatter` preset Trailing degree symbol and cardinal "SN" indicator + ``'deglon'`` `~proplot.ticker.AutoFormatter` preset Trailing degree symbol and cardinal "WE" indicator + ``'dms'`` `~proplot.ticker.DegreeFormatter` Labels with degree/minute/second support + ``'dmslon'`` `~proplot.ticker.LongitudeFormatter` Longitude labels with degree/minute/second support + ``'dmslat'`` `~proplot.ticker.LatitudeFormatter` Latitude labels with degree/minute/second support + ====================== ============================================== ================================================================= + + date : bool, optional + Toggles the behavior when `formatter` contains a ``'%'`` sign + (see above). + index : bool, optional + Controls the behavior when `formatter` is a sequence of strings + (see above). + + Other parameters + ---------------- + *args, **kwargs + Passed to the `~matplotlib.ticker.Formatter` class. + + Returns + ------- + matplotlib.ticker.Formatter + A `~matplotlib.ticker.Formatter` instance. + + See also + -------- + matplotlib.ticker.Formatter + proplot.axes.CartesianAxes.format + proplot.axes.PolarAxes.format + proplot.axes.GeoAxes.format + proplot.axes.Axes.colorbar + proplot.constructor.Locator + """ # noqa: E501 + if np.iterable(formatter) and not isinstance(formatter, str) and not all( + isinstance(item, str) for item in formatter + ): + formatter, *args = *formatter, *args + if isinstance(formatter, mticker.Formatter): + return copy.copy(formatter) + if isinstance(formatter, str): + if re.search(r'{x(:.+)?}', formatter): # str.format + formatter = mticker.StrMethodFormatter(formatter, *args, **kwargs) + elif '%' in formatter: # str % format + cls = mdates.DateFormatter if date else mticker.FormatStrFormatter + formatter = cls(formatter, *args, **kwargs) + elif formatter in FORMATTERS: + formatter = FORMATTERS[formatter](*args, **kwargs) + else: + raise ValueError( + f'Unknown formatter {formatter!r}. Options are: ' + + ', '.join(map(repr, FORMATTERS)) + + '.' + ) + elif formatter is True: + formatter = pticker.AutoFormatter(*args, **kwargs) + elif formatter is False: + formatter = mticker.NullFormatter(*args, **kwargs) + elif np.iterable(formatter): + formatter = (mticker.FixedFormatter, pticker.IndexFormatter)[index](formatter) + elif callable(formatter): + formatter = mticker.FuncFormatter(formatter, *args, **kwargs) + else: + raise ValueError(f'Invalid formatter {formatter!r}.') + return formatter + + +def Scale(scale, *args, **kwargs): + """ + Return a `~matplotlib.scale.ScaleBase` instance. + + Parameters + ---------- + scale : `~matplotlib.scale.ScaleBase`, str, or tuple + The axis scale specification. If a `~matplotlib.scale.ScaleBase` instance + already, a `copy.copy` of the instance is returned. Otherwise, `scale` + should be a string corresponding to one of the "registered" axis scales + or axis scale presets (see below table). + + If `scale` is a list or tuple and the first element is a + "registered" scale name, subsequent elements are passed to the + scale class as positional arguments. + + .. _scale_table: + + ================= ====================================== =============================================== + Key Class Description + ================= ====================================== =============================================== + ``'linear'`` `~proplot.scale.LinearScale` Linear + ``'log'`` `~proplot.scale.LogScale` Logarithmic + ``'symlog'`` `~proplot.scale.SymmetricalLogScale` Logarithmic beyond finite space around zero + ``'logit'`` `~proplot.scale.LogitScale` Logistic + ``'inverse'`` `~proplot.scale.InverseScale` Inverse + ``'function'`` `~proplot.scale.FuncScale` Arbitrary forward and backwards transformations + ``'sine'`` `~proplot.scale.SineLatitudeScale` Sine function (in degrees) + ``'mercator'`` `~proplot.scale.MercatorLatitudeScale` Mercator latitude function (in degrees) + ``'exp'`` `~proplot.scale.ExpScale` Arbitrary exponential function + ``'power'`` `~proplot.scale.PowerScale` Arbitrary power function + ``'cutoff'`` `~proplot.scale.CutoffScale` Arbitrary piecewise linear transformations + ``'quadratic'`` `~proplot.scale.PowerScale` (preset) Quadratic function + ``'cubic'`` `~proplot.scale.PowerScale` (preset) Cubic function + ``'quartic'`` `~proplot.scale.PowerScale` (preset) Quartic function + ``'db'`` `~proplot.scale.ExpScale` (preset) Ratio expressed as `decibels `_ + ``'np'`` `~proplot.scale.ExpScale` (preset) Ratio expressed as `nepers `_ + ``'idb'`` `~proplot.scale.ExpScale` (preset) `Decibels `_ expressed as ratio + ``'inp'`` `~proplot.scale.ExpScale` (preset) `Nepers `_ expressed as ratio + ``'pressure'`` `~proplot.scale.ExpScale` (preset) Height (in km) expressed linear in pressure + ``'height'`` `~proplot.scale.ExpScale` (preset) Pressure (in hPa) expressed linear in height + ================= ====================================== =============================================== + + .. _db: https://en.wikipedia.org/wiki/Decibel + .. _np: https://en.wikipedia.org/wiki/Neper + + Other parameters + ---------------- + *args, **kwargs + Passed to the `~matplotlib.scale.ScaleBase` class. + + Returns + ------- + matplotlib.scale.ScaleBase + A `~matplotlib.scale.ScaleBase` instance. + + See also + -------- + matplotlib.scale.ScaleBase + proplot.scale.LinearScale + proplot.axes.CartesianAxes.format + proplot.axes.CartesianAxes.dualx + proplot.axes.CartesianAxes.dualy + """ # noqa: E501 + # NOTE: Why not try to interpret FuncScale arguments, like when lists + # of numbers are passed to Locator? Because FuncScale *itself* accepts + # ScaleBase classes as arguments... but constructor functions cannot + # do anything but return the class instance upon receiving one. + if np.iterable(scale) and not isinstance(scale, str): + scale, *args = *scale, *args + if isinstance(scale, mscale.ScaleBase): + return copy.copy(scale) + if not isinstance(scale, str): + raise ValueError(f'Invalid scale name {scale!r}. Must be string.') + scale = scale.lower() + if scale in SCALES_PRESETS: + if args or kwargs: + warnings._warn_proplot( + f'Scale {scale!r} is a scale *preset*. Ignoring positional ' + 'argument(s): {args} and keyword argument(s): {kwargs}. ' + ) + scale, *args = SCALES_PRESETS[scale] + if scale in SCALES: + scale = SCALES[scale] + else: + raise ValueError( + f'Unknown scale or preset {scale!r}. Options are: ' + + ', '.join(map(repr, (*SCALES, *SCALES_PRESETS))) + + '.' + ) + return scale(*args, **kwargs) + + +def Proj( + name, backend=None, + lon0=None, lon_0=None, lat0=None, lat_0=None, lonlim=None, latlim=None, **kwargs +): + """ + Return a `cartopy.crs.Projection` or `~mpl_toolkits.basemap.Basemap` instance. + + Parameters + ---------- + name : str, `cartopy.crs.Projection`, or `~mpl_toolkits.basemap.Basemap` + The projection name or projection class instance. If the latter, it + is simply returned. If the former, it must correspond to one of the + `PROJ `__ projection name shorthands, like in + basemap. + + The following table lists the valid projection name shorthands, + their full names (with links to the relevant `PROJ documentation + `__), + and whether they are available in the cartopy and basemap packages. + (added) indicates a projection class that proplot has "added" to + cartopy using the cartopy API. + + .. _proj_table: + + ============= =============================================== ========= ======= + Key Name Cartopy Basemap + ============= =============================================== ========= ======= + ``'aea'`` `Albers Equal Area `_ ✓ ✓ + ``'aeqd'`` `Azimuthal Equidistant `_ ✓ ✓ + ``'aitoff'`` `Aitoff `_ ✓ (added) ✗ + ``'cass'`` `Cassini-Soldner `_ ✗ ✓ + ``'cea'`` `Cylindrical Equal Area `_ ✗ ✓ + ``'cyl'`` `Cylindrical Equidistant `_ ✓ ✓ + ``'eck1'`` `Eckert I `_ ✓ ✗ + ``'eck2'`` `Eckert II `_ ✓ ✗ + ``'eck3'`` `Eckert III `_ ✓ ✗ + ``'eck4'`` `Eckert IV `_ ✓ ✓ + ``'eck5'`` `Eckert V `_ ✓ ✗ + ``'eck6'`` `Eckert VI `_ ✓ ✗ + ``'eqdc'`` `Equidistant Conic `_ ✓ ✓ + ``'eqc'`` `Cylindrical Equidistant `_ ✓ ✓ + ``'eqearth'`` `Equal Earth `_ ✓ ✗ + ``'europp'`` Euro PP (Europe) ✓ ✗ + ``'gall'`` `Gall Stereographic Cylindrical `_ ✗ ✓ + ``'geos'`` `Geostationary `_ ✓ ✓ + ``'gnom'`` `Gnomonic `_ ✓ ✓ + ``'hammer'`` `Hammer `_ ✓ (added) ✓ + ``'igh'`` `Interrupted Goode Homolosine `_ ✓ ✗ + ``'kav7'`` `Kavrayskiy VII `_ ✓ (added) ✓ + ``'laea'`` `Lambert Azimuthal Equal Area `_ ✓ ✓ + ``'lcc'`` `Lambert Conformal `_ ✓ ✓ + ``'lcyl'`` Lambert Cylindrical ✓ ✗ + ``'mbtfpq'`` `McBryde-Thomas Flat-Polar Quartic `_ ✗ ✓ + ``'merc'`` `Mercator `_ ✓ ✓ + ``'mill'`` `Miller Cylindrical `_ ✓ ✓ + ``'moll'`` `Mollweide `_ ✓ ✓ + ``'npaeqd'`` North-Polar Azimuthal Equidistant ✓ (added) ✓ + ``'npgnom'`` North-Polar Gnomonic ✓ (added) ✗ + ``'nplaea'`` North-Polar Lambert Azimuthal ✓ (added) ✓ + ``'npstere'`` North-Polar Stereographic ✓ ✓ + ``'nsper'`` `Near-Sided Perspective `_ ✓ ✓ + ``'osni'`` OSNI (Ireland) ✓ ✗ + ``'osgb'`` OSGB (UK) ✓ ✗ + ``'omerc'`` `Oblique Mercator `_ ✗ ✓ + ``'ortho'`` `Orthographic `_ ✓ ✓ + ``'pcarree'`` `Cylindrical Equidistant `_ ✓ ✓ + ``'poly'`` `Polyconic `_ ✗ ✓ + ``'rotpole'`` Rotated Pole ✓ ✓ + ``'sinu'`` `Sinusoidal `_ ✓ ✓ + ``'spaeqd'`` South-Polar Azimuthal Equidistant ✓ (added) ✓ + ``'spgnom'`` South-Polar Gnomonic ✓ (added) ✗ + ``'splaea'`` South-Polar Lambert Azimuthal ✓ (added) ✓ + ``'spstere'`` South-Polar Stereographic ✓ ✓ + ``'stere'`` `Stereographic `_ ✓ ✓ + ``'tmerc'`` `Transverse Mercator `_ ✓ ✓ + ``'utm'`` `Universal Transverse Mercator `_ ✓ ✗ + ``'vandg'`` `van der Grinten `_ ✗ ✓ + ``'wintri'`` `Winkel tripel `_ ✓ (added) ✗ + ============= =============================================== ========= ======= + + backend : {'cartopy', 'basemap'}, default: :rc:`geo.backend` + Whether to return a cartopy `~cartopy.crs.Projection` instance + or a basemap `~mpl_toolkits.basemap.Basemap` instance. + lon0, lat0 : float, optional + The central projection longitude and latitude. These are translated to + `central_longitude`, `central_latitude` for cartopy projections. + lon_0, lat_0 : float, optional + Aliases for `lon0`, `lat0`. + lonlim : 2-tuple of float, optional + The longitude limits. Translated to `min_longitude` and `max_longitude` for + cartopy projections and `llcrnrlon` and `urcrnrlon` for basemap projections. + latlim : 2-tuple of float, optional + The latitude limits. Translated to `min_latitude` and `max_latitude` for + cartopy projections and `llcrnrlon` and `urcrnrlon` for basemap projections. + + Other parameters + ---------------- + **kwargs + Passed to the cartopy `~cartopy.crs.Projection` or + basemap `~mpl_toolkits.basemap.Basemap` class. + + Returns + ------- + proj : mpl_toolkits.basemap.Basemap or cartopy.crs.Projection + A cartopy or basemap projection instance. + + See also + -------- + mpl_toolkits.basemap.Basemap + cartopy.crs.Projection + proplot.ui.subplots + proplot.axes.GeoAxes + + References + ---------- + For more information on map projections, see the + `wikipedia page `__ and the + `PROJ `__ documentation. + + .. _aea: https://proj.org/operations/projections/aea.html + .. _aeqd: https://proj.org/operations/projections/aeqd.html + .. _aitoff: https://proj.org/operations/projections/aitoff.html + .. _cass: https://proj.org/operations/projections/cass.html + .. _cea: https://proj.org/operations/projections/cea.html + .. _eqc: https://proj.org/operations/projections/eqc.html + .. _eck1: https://proj.org/operations/projections/eck1.html + .. _eck2: https://proj.org/operations/projections/eck2.html + .. _eck3: https://proj.org/operations/projections/eck3.html + .. _eck4: https://proj.org/operations/projections/eck4.html + .. _eck5: https://proj.org/operations/projections/eck5.html + .. _eck6: https://proj.org/operations/projections/eck6.html + .. _eqdc: https://proj.org/operations/projections/eqdc.html + .. _eqc: https://proj.org/operations/projections/eqc.html + .. _eqearth: https://proj.org/operations/projections/eqearth.html + .. _gall: https://proj.org/operations/projections/gall.html + .. _geos: https://proj.org/operations/projections/geos.html + .. _gnom: https://proj.org/operations/projections/gnom.html + .. _hammer: https://proj.org/operations/projections/hammer.html + .. _igh: https://proj.org/operations/projections/igh.html + .. _kav7: https://proj.org/operations/projections/kav7.html + .. _laea: https://proj.org/operations/projections/laea.html + .. _lcc: https://proj.org/operations/projections/lcc.html + .. _mbtfpq: https://proj.org/operations/projections/mbtfpq.html + .. _merc: https://proj.org/operations/projections/merc.html + .. _mill: https://proj.org/operations/projections/mill.html + .. _moll: https://proj.org/operations/projections/moll.html + .. _nsper: https://proj.org/operations/projections/nsper.html + .. _omerc: https://proj.org/operations/projections/omerc.html + .. _ortho: https://proj.org/operations/projections/ortho.html + .. _eqc: https://proj.org/operations/projections/eqc.html + .. _poly: https://proj.org/operations/projections/poly.html + .. _sinu: https://proj.org/operations/projections/sinu.html + .. _stere: https://proj.org/operations/projections/stere.html + .. _tmerc: https://proj.org/operations/projections/tmerc.html + .. _utm: https://proj.org/operations/projections/utm.html + .. _vandg: https://proj.org/operations/projections/vandg.html + .. _wintri: https://proj.org/operations/projections/wintri.html + """ # noqa: E501 + # Parse input arguments + # NOTE: Underscores are permitted for consistency with cartopy only here. + # In format() underscores are not allowed for constistency with reset of API. + lon0 = _not_none(lon0=lon0, lon_0=lon_0) + lat0 = _not_none(lat0=lat0, lat_0=lat_0) + lonlim = _not_none(lonlim, default=(None, None)) + latlim = _not_none(latlim, default=(None, None)) + is_crs = Projection is not object and isinstance(name, Projection) + is_basemap = Basemap is not object and isinstance(name, Basemap) + include_axes = kwargs.pop('include_axes', False) # for error message + if backend is not None and backend not in ('cartopy', 'basemap'): + raise ValueError( + f"Invalid backend={backend!r}. Options are 'cartopy' or 'basemap'." + ) + if not is_crs and not is_basemap: + backend = _not_none(backend, rc['geo.backend']) + if not isinstance(name, str): + raise ValueError( + f'Unexpected projection {name!r}. Must be PROJ string name, ' + 'cartopy.crs.Projection, or mpl_toolkits.basemap.Basemap.' + ) + for key_proj, key_cartopy, value in ( + ('lon_0', 'central_longitude', lon0), + ('lat_0', 'central_latitude', lat0), + ('llcrnrlon', 'min_longitude', lonlim[0]), + ('urcrnrlon', 'max_longitude', lonlim[1]), + ('llcrnrlat', 'min_latitude', latlim[0]), + ('urcrnrlat', 'max_latitude', latlim[1]), + ): + if value is None: + continue + if backend == 'basemap' and key_proj == 'lon_0' and value > 0: + value -= 360 # see above comment + kwargs[key_proj if backend == 'basemap' else key_cartopy] = value + + # Projection instances + if is_crs or is_basemap: + if backend is not None: + kwargs['backend'] = backend + if kwargs: + warnings._warn_proplot(f'Ignoring Proj() keyword arg(s): {kwargs!r}.') + proj = name + backend = 'cartopy' if is_crs else 'basemap' + + # Cartopy name + # NOTE: Error message matches basemap invalid projection message + elif backend == 'cartopy': + # Parse keywoard arguments + import cartopy # ensure present # noqa: F401 + for key in ('round', 'boundinglat'): + value = kwargs.pop(key, None) + if value is not None: + raise ValueError( + 'Ignoring Proj() keyword {key}={value!r}. Must be passed ' + 'to GeoAxes.format() when cartopy is the backend.' + ) + + # Retrieve projection and initialize with nice error message + try: + crs = PROJS[name] + except KeyError: + message = f'{name!r} is an unknown cartopy projection class.\n' + message += 'The known cartopy projection classes are:\n' + message += '\n'.join( + ' ' + key + ' ' * (max(map(len, PROJS)) - len(key) + 10) + cls.__name__ + for key, cls in PROJS.items() + ) + if include_axes: + from . import axes as paxes # avoid circular imports + message = message.replace('class.', 'class or axes subclass.') + message += '\nThe known axes subclasses are:\n' + paxes._cls_table + raise ValueError(message) from None + if name == 'geos': # fix common mistake + kwargs.pop('central_latitude', None) + proj = crs(**kwargs) + + # Basemap name + # NOTE: Known issue that basemap sometimes produces backwards maps: + # https://stackoverflow.com/q/56299971/4970632 + # NOTE: We set rsphere to fix non-conda installed basemap issue: + # https://github.com/matplotlib/basemap/issues/361 + # NOTE: Adjust lon_0 to fix issues with Robinson (and related?) projections + # https://stackoverflow.com/questions/56299971/ (also triggers 'no room for axes') + # NOTE: Unlike cartopy, basemap resolution is configured + # on initialization and controls *all* features. + else: + # Parse input arguments + from mpl_toolkits import basemap # ensure present # noqa: F401 + if name in ('eqc', 'pcarree'): + name = 'cyl' # PROJ package aliases + defaults = {'fix_aspect': True, **PROJ_DEFAULTS.get(name, {})} + if name[:2] in ('np', 'sp'): + defaults['round'] = rc['geo.round'] + if name == 'geos': + defaults['rsphere'] = (6378137.00, 6356752.3142) + for key, value in defaults.items(): + if kwargs.get(key, None) is None: # allow e.g. boundinglat=None + kwargs[key] = value + + # Initialize + if _version_mpl >= '3.3': + raise RuntimeError( + 'Basemap is no longer maintained and is incompatible with ' + 'matplotlib >= 3.3. Please use cartopy as your geographic ' + 'plotting backend or downgrade to matplotlib < 3.3.' + ) + reso = _not_none( + reso=kwargs.pop('reso', None), + resolution=kwargs.pop('resolution', None), + default=rc['reso'] + ) + if reso in RESOS_BASEMAP: + reso = RESOS_BASEMAP[reso] + else: + raise ValueError( + f'Invalid resolution {reso!r}. Options are: ' + + ', '.join(map(repr, RESOS_BASEMAP)) + + '.' + ) + kwargs.update({'resolution': reso, 'projection': name}) + try: + proj = Basemap(**kwargs) # will raise helpful warning + except ValueError as err: + message = str(err) + message = message.strip() + message = message.replace('projection', 'basemap projection') + message = message.replace('supported', 'known') + if include_axes: + from . import axes as paxes # avoid circular imports + message = message.replace('projection.', 'projection or axes subclass.') + message += '\nThe known axes subclasses are:\n' + paxes._cls_table + raise ValueError(message) from None + + proj._proj_backend = backend + return proj + + +# Deprecated +Colors = warnings._rename_objs( + '0.8.0', Colors=get_colors +) diff --git a/proplot/cycles/bmh.hex b/proplot/cycles/bmh.hex new file mode 100644 index 000000000..d2bd7830e --- /dev/null +++ b/proplot/cycles/bmh.hex @@ -0,0 +1,2 @@ +# BMH +'#348ABD', '#A60628', '#7A68A6', '#467821', '#D55E00', '#CC79A7', '#56B4E9', '#009E73', '#F0E442', '#0072B2' diff --git a/proplot/cycles/colorblind.hex b/proplot/cycles/colorblind.hex index 23c0ad4fa..71470d217 100644 --- a/proplot/cycles/colorblind.hex +++ b/proplot/cycles/colorblind.hex @@ -1,2 +1,2 @@ -# Seaborn and proplot default style +# Proplot default style '#0072B2', '#D55E00', '#009E73', '#CC79A7', '#F0E442', '#56B4E9', diff --git a/proplot/cycles/seaborn.hex b/proplot/cycles/seaborn.hex new file mode 100644 index 000000000..745f38ff1 --- /dev/null +++ b/proplot/cycles/seaborn.hex @@ -0,0 +1,2 @@ +# Seaborn +'#4C72B0', '#55A868', '#C44E52', '#8172B2', '#CCB974', '#64B5CD', diff --git a/proplot/cycles/tableau.hex b/proplot/cycles/tableau.hex new file mode 100644 index 000000000..de21bf355 --- /dev/null +++ b/proplot/cycles/tableau.hex @@ -0,0 +1,2 @@ +# Tableau colorblind +'#006BA4', '#FF800E', '#ABABAB', '#595959', '#5F9ED1', '#C85200', '#898989', '#A2C8EC', '#FFBC79', '#CFCFCF' diff --git a/proplot/demos.py b/proplot/demos.py new file mode 100644 index 000000000..7011838de --- /dev/null +++ b/proplot/demos.py @@ -0,0 +1,959 @@ +#!/usr/bin/env python3 +""" +Functions for displaying colors and fonts. +""" +import os +import re + +import cycler +import matplotlib.colors as mcolors +import matplotlib.font_manager as mfonts +import numpy as np + +from . import colors as pcolors +from . import constructor, ui +from .config import _get_data_folders, rc +from .internals import ic # noqa: F401 +from .internals import _not_none, _version_mpl, docstring, warnings +from .utils import to_rgb, to_xyz + +__all__ = [ + 'show_cmaps', + 'show_channels', + 'show_colors', + 'show_colorspaces', + 'show_cycles', + 'show_fonts', +] + + +# Tables and constants +FAMILY_TEXGYRE = ( + 'TeX Gyre Heros', # sans-serif + 'TeX Gyre Schola', # serif + 'TeX Gyre Bonum', + 'TeX Gyre Termes', + 'TeX Gyre Pagella', + 'TeX Gyre Chorus', # cursive + 'TeX Gyre Adventor', # fantasy + 'TeX Gyre Cursor', # monospace +) +COLOR_TABLE = { + # NOTE: Just want the names but point to the dictionaries because + # they don't get filled until after __init__ imports this module. + 'base': mcolors.BASE_COLORS, + 'css4': mcolors.CSS4_COLORS, + 'opencolor': pcolors.COLORS_OPEN, + 'xkcd': pcolors.COLORS_XKCD, +} +CYCLE_TABLE = { + 'Matplotlib defaults': ( + 'default', 'classic', + ), + 'Matplotlib stylesheets': ( + # NOTE: Do not include 'solarized' because colors are terrible for + # colorblind folks. + 'colorblind', 'colorblind10', 'tableau', 'ggplot', '538', 'seaborn', 'bmh', + ), + 'ColorBrewer2.0 qualitative': ( + 'Accent', 'Dark2', + 'Paired', 'Pastel1', 'Pastel2', + 'Set1', 'Set2', 'Set3', + 'tab10', 'tab20', 'tab20b', 'tab20c', + ), + 'Other qualitative': ( + 'FlatUI', 'Qual1', 'Qual2', + ), +} +CMAP_TABLE = { + # NOTE: No longer rename colorbrewer greys map, just redirect 'grays' + # to 'greys' in colormap database. + 'Grayscale': ( # assorted origin, but they belong together + 'Greys', 'Mono', 'MonoCycle', + ), + 'Matplotlib sequential': ( + 'viridis', 'plasma', 'inferno', 'magma', 'cividis', + ), + 'Matplotlib cyclic': ( + 'twilight', + ), + 'Seaborn sequential': ( + 'Rocket', 'Flare', 'Mako', 'Crest', + ), + 'Seaborn diverging': ( + 'IceFire', 'Vlag', + ), + 'Proplot sequential': ( + 'Fire', + 'Stellar', + 'Glacial', + 'Dusk', + 'Marine', + 'Boreal', + 'Sunrise', + 'Sunset', + ), + 'Proplot diverging': ( + 'Div', 'NegPos', 'DryWet', + ), + 'Other sequential': ( + 'cubehelix', 'turbo' + ), + 'Other diverging': ( + 'BR', 'ColdHot', 'CoolWarm', + ), + 'cmOcean sequential': ( + 'Oxy', 'Thermal', 'Dense', 'Ice', 'Haline', + 'Deep', 'Algae', 'Tempo', 'Speed', 'Turbid', 'Solar', 'Matter', + 'Amp', + ), + 'cmOcean diverging': ( + 'Balance', 'Delta', 'Curl', + ), + 'cmOcean cyclic': ( + 'Phase', + ), + 'Scientific colour maps sequential': ( + 'batlow', 'batlowK', 'batlowW', + 'devon', 'davos', 'oslo', 'lapaz', 'acton', + 'lajolla', 'bilbao', 'tokyo', 'turku', 'bamako', 'nuuk', + 'hawaii', 'buda', 'imola', + 'oleron', 'bukavu', 'fes', + ), + 'Scientific colour maps diverging': ( + 'roma', 'broc', 'cork', 'vik', 'bam', 'lisbon', 'tofino', 'berlin', 'vanimo', + ), + 'Scientific colour maps cyclic': ( + 'romaO', 'brocO', 'corkO', 'vikO', 'bamO', + ), + 'ColorBrewer2.0 sequential': ( + 'Purples', 'Blues', 'Greens', 'Oranges', 'Reds', + 'YlOrBr', 'YlOrRd', 'OrRd', 'PuRd', 'RdPu', 'BuPu', + 'PuBu', 'PuBuGn', 'BuGn', 'GnBu', 'YlGnBu', 'YlGn' + ), + 'ColorBrewer2.0 diverging': ( + 'Spectral', 'PiYG', 'PRGn', 'BrBG', 'PuOr', 'RdGY', + 'RdBu', 'RdYlBu', 'RdYlGn', + ), + 'SciVisColor blues': ( + 'Blues1', 'Blues2', 'Blues3', 'Blues4', 'Blues5', + 'Blues6', 'Blues7', 'Blues8', 'Blues9', 'Blues10', 'Blues11', + ), + 'SciVisColor greens': ( + 'Greens1', 'Greens2', 'Greens3', 'Greens4', 'Greens5', + 'Greens6', 'Greens7', 'Greens8', + ), + 'SciVisColor yellows': ( + 'Yellows1', 'Yellows2', 'Yellows3', 'Yellows4', + ), + 'SciVisColor oranges': ( + 'Oranges1', 'Oranges2', 'Oranges3', 'Oranges4', + ), + 'SciVisColor browns': ( + 'Browns1', 'Browns2', 'Browns3', 'Browns4', 'Browns5', + 'Browns6', 'Browns7', 'Browns8', 'Browns9', + ), + 'SciVisColor reds': ( + 'Reds1', 'Reds2', 'Reds3', 'Reds4', 'Reds5', + ), + 'SciVisColor purples': ( + 'Purples1', 'Purples2', 'Purples3', + ), + # Builtin colormaps that re hidden by default. Some are really bad, some + # are segmented maps that should be cycles, and some are just uninspiring. + 'MATLAB': ( + 'bone', 'cool', 'copper', 'autumn', 'flag', 'prism', + 'jet', 'hsv', 'hot', 'spring', 'summer', 'winter', 'pink', 'gray', + ), + 'GNUplot': ( + 'gnuplot', 'gnuplot2', 'ocean', 'afmhot', 'rainbow', + ), + 'GIST': ( + 'gist_earth', 'gist_gray', 'gist_heat', 'gist_ncar', + 'gist_rainbow', 'gist_stern', 'gist_yarg', + ), + 'Other': ( + 'binary', 'bwr', 'brg', # appear to be custom matplotlib + 'Wistia', 'CMRmap', # individually released + 'seismic', 'terrain', 'nipy_spectral', # origin ambiguous + 'tab10', 'tab20', 'tab20b', 'tab20c', # merged colormap cycles + ) +} + +# Docstring snippets +_colorbar_docstring = """ +length : unit-spec, optional + The length of each colorbar. + %(units.in)s +width : float or str, optional + The width of each colorbar. + %(units.in)s +rasterized : bool, default: :rc:`colorbar.rasterized` + Whether to rasterize the colorbar solids. This increases rendering + time and decreases file sizes for vector graphics. +""" +docstring._snippet_manager['demos.cmaps'] = ', '.join(f'``{s!r}``' for s in CMAP_TABLE) +docstring._snippet_manager['demos.cycles'] = ', '.join(f'``{s!r}``' for s in CYCLE_TABLE) # noqa: E501 +docstring._snippet_manager['demos.colors'] = ', '.join(f'``{s!r}``' for s in COLOR_TABLE) # noqa: E501 +docstring._snippet_manager['demos.colorbar'] = _colorbar_docstring + + +def show_channels( + *args, N=100, rgb=False, saturation=True, + minhue=0, maxsat=500, width=100, refwidth=1.7 +): + """ + Show how arbitrary colormap(s) vary with respect to the hue, chroma, + luminance, HSL saturation, and HPL saturation channels, and optionally + the red, blue and green channels. Adapted from `this example \ +`__. + + Parameters + ---------- + *args : colormap-spec, default: :rc:`image.cmap` + Positional arguments are colormap names or objects. + N : int, optional + The number of markers to draw for each colormap. + rgb : bool, optional + Whether to also show the red, green, and blue channels in the bottom row. + saturation : bool, optional + Whether to show the HSL and HPL saturation channels alongside the raw chroma. + minhue : float, optional + The minimum hue. This lets you rotate the hue plot cyclically. + maxsat : float, optional + The maximum saturation. Use this to truncate large saturation values. + width : int, optional + The width of each colormap line in points. + refwidth : int or str, optional + The width of each subplot. Passed to `~proplot.ui.subplots`. + + Returns + ------- + proplot.figure.Figure + The figure. + proplot.gridspec.SubplotGrid + The subplot grid. + + See also + -------- + show_cmaps + show_colorspaces + """ + # Figure and plot + if not args: + raise ValueError('At least one positional argument required.') + array = [[1, 1, 2, 2, 3, 3]] + labels = ('Hue', 'Chroma', 'Luminance') + if saturation: + array += [[0, 4, 4, 5, 5, 0]] + labels += ('HSL saturation', 'HPL saturation') + if rgb: + array += [np.array([4, 4, 5, 5, 6, 6]) + 2 * int(saturation)] + labels += ('Red', 'Green', 'Blue') + fig, axs = ui.subplots( + array=array, refwidth=refwidth, wratios=(1.5, 1, 1, 1, 1, 1.5), + share='labels', span=False, innerpad=1, + ) + # Iterate through colormaps + mc = ms = mp = 0 + cmaps = [] + for cmap in args: + # Get colormap and avoid registering new names + name = cmap if isinstance(cmap, str) else getattr(cmap, 'name', None) + cmap = constructor.Colormap(cmap, N=N) # arbitrary cmap argument + if name is not None: + cmap.name = name + cmap._init() + cmaps.append(cmap) + + # Get clipped RGB table + x = np.linspace(0, 1, N) + lut = cmap._lut[:-3, :3].copy() + rgb_data = lut.T # 3 by N + hcl_data = np.array([to_xyz(color, space='hcl') for color in lut]).T # 3 by N + hsl_data = [to_xyz(color, space='hsl')[1] for color in lut] + hpl_data = [to_xyz(color, space='hpl')[1] for color in lut] + + # Plot channels + # If rgb is False, the zip will just truncate the other iterables + data = tuple(hcl_data) + if saturation: + data += (hsl_data, hpl_data) + if rgb: + data += tuple(rgb_data) + for ax, y, label in zip(axs, data, labels): + ylim, ylocator = None, None + if label in ('Red', 'Green', 'Blue'): + ylim = (0, 1) + ylocator = 0.2 + elif label == 'Luminance': + ylim = (0, 100) + ylocator = 20 + elif label == 'Hue': + ylim = (minhue, minhue + 360) + ylocator = 90 + y = y - 720 + for _ in range(3): # rotate up to 1080 degrees + y[y < minhue] += 360 + else: + if 'HSL' in label: + m = ms = max(min(max(ms, max(y)), maxsat), 100) + elif 'HPL' in label: + m = mp = max(min(max(mp, max(y)), maxsat), 100) + else: + m = mc = max(min(max(mc, max(y)), maxsat), 100) + ylim = (0, m) + ylocator = ('maxn', 5) + ax.scatter(x, y, c=x, cmap=cmap, s=width, linewidths=0) + ax.format(title=label, ylim=ylim, ylocator=ylocator) + + # Formatting + suptitle = ( + ', '.join(repr(cmap.name) for cmap in cmaps[:-1]) + + (', and ' if len(cmaps) > 2 else ' and ' if len(cmaps) == 2 else ' ') + + f'{repr(cmaps[-1].name)} colormap' + + ('s' if len(cmaps) > 1 else '') + ) + axs.format( + xlocator=0.25, xformatter='null', + suptitle=f'{suptitle} by channel', ylim=None, ytickminor=False, + ) + + # Colorbar on the bottom + for cmap in cmaps: + fig.colorbar( + cmap, loc='b', span=(2, 5), + locator='null', label=cmap.name, labelweight='bold' + ) + return fig, axs + + +def show_colorspaces(*, luminance=None, saturation=None, hue=None, refwidth=2): + """ + Generate hue-saturation, hue-luminance, and luminance-saturation + cross-sections for the HCL, HSL, and HPL colorspaces. + + Parameters + ---------- + luminance : float, default: 50 + If passed, saturation-hue cross-sections are drawn for + this luminance. Must be between ``0`` and ``100``. + saturation : float, optional + If passed, luminance-hue cross-sections are drawn for this + saturation. Must be between ``0`` and ``100``. + hue : float, optional + If passed, luminance-saturation cross-sections + are drawn for this hue. Must be between ``0`` and ``360``. + refwidth : str or float, optional + Average width of each subplot. Units are interpreted by + `~proplot.utils.units`. + + Returns + ------- + proplot.figure.Figure + The figure. + proplot.gridspec.SubplotGrid + The subplot grid. + + See also + -------- + show_cmaps + show_channels + """ + # Get colorspace properties + hues = np.linspace(0, 360, 361) + sats = np.linspace(0, 120, 120) + lums = np.linspace(0, 99.99, 101) + if luminance is None and saturation is None and hue is None: + luminance = 50 + _not_none(luminance=luminance, saturation=saturation, hue=hue) # warning + if luminance is not None: + hsl = np.concatenate(( + np.repeat(hues[:, None], len(sats), axis=1)[..., None], + np.repeat(sats[None, :], len(hues), axis=0)[..., None], + np.ones((len(hues), len(sats)))[..., None] * luminance, + ), axis=2) + suptitle = f'Hue-saturation cross-section for luminance {luminance}' + xlabel, ylabel = 'hue', 'saturation' + xloc, yloc = 60, 20 + elif saturation is not None: + hsl = np.concatenate(( + np.repeat(hues[:, None], len(lums), axis=1)[..., None], + np.ones((len(hues), len(lums)))[..., None] * saturation, + np.repeat(lums[None, :], len(hues), axis=0)[..., None], + ), axis=2) + suptitle = f'Hue-luminance cross-section for saturation {saturation}' + xlabel, ylabel = 'hue', 'luminance' + xloc, yloc = 60, 20 + elif hue is not None: + hsl = np.concatenate(( + np.ones((len(lums), len(sats)))[..., None] * hue, + np.repeat(sats[None, :], len(lums), axis=0)[..., None], + np.repeat(lums[:, None], len(sats), axis=1)[..., None], + ), axis=2) + suptitle = 'Luminance-saturation cross-section' + xlabel, ylabel = 'luminance', 'saturation' + xloc, yloc = 20, 20 + + # Make figure, with black indicating invalid values + # Note we invert the x-y ordering for imshow + fig, axs = ui.subplots(refwidth=refwidth, ncols=3, share=False, innerpad=0.5) + for ax, space in zip(axs, ('hcl', 'hsl', 'hpl')): + rgba = np.ones((*hsl.shape[:2][::-1], 4)) # RGBA + for j in range(hsl.shape[0]): + for k in range(hsl.shape[1]): + rgb_jk = to_rgb(hsl[j, k, :], space) + if not all(0 <= c <= 1 for c in rgb_jk): + rgba[k, j, 3] = 0 # black cell + else: + rgba[k, j, :3] = rgb_jk + ax.imshow(rgba, origin='lower', aspect='auto') + ax.format( + xlabel=xlabel, ylabel=ylabel, suptitle=suptitle, + grid=False, xtickminor=False, ytickminor=False, + xlocator=xloc, ylocator=yloc, facecolor='k', + title=space.upper(), + ) + return fig, axs + + +@warnings._rename_kwargs('0.8.0', categories='include') +@warnings._rename_kwargs('0.10.0', rasterize='rasterized') +def _draw_bars( + cmaps, *, source, unknown='User', include=None, ignore=None, + length=4.0, width=0.2, N=None, rasterized=None, +): + """ + Draw colorbars for "colormaps" and "color cycles". This is called by + `show_cycles` and `show_cmaps`. + """ + # Categorize the input names + table = {unknown: []} if unknown else {} + table.update({cat: [None] * len(names) for cat, names in source.items()}) + for cmap in cmaps: + cat = None + name = cmap.name or '_no_name' + name = name.lower() + for opt, names in source.items(): + names = list(map(str.lower, names)) + if name in names: + i, cat = names.index(name), opt + if cat: + table[cat][i] = cmap + elif unknown: + table[unknown].append(cmap) + + # Filter out certain categories + options = set(map(str.lower, source)) + if ignore is None: + ignore = ('matlab', 'gnuplot', 'gist', 'other') + if isinstance(include, str): + include = (include,) + if isinstance(ignore, str): + ignore = (ignore,) + if include is None: + include = options - set(map(str.lower, ignore)) + else: + include = set(map(str.lower, include)) + if any(cat not in options and cat != unknown for cat in include): + raise ValueError( + f'Invalid categories {include!r}. Options are: ' + + ', '.join(map(repr, source)) + '.' + ) + for cat in tuple(table): + table[cat][:] = [cmap for cmap in table[cat] if cmap is not None] + if not table[cat] or cat.lower() not in include and cat != unknown: + del table[cat] + + # Draw figure + # Allocate two colorbar widths for each title of sections + naxs = 2 * len(table) + sum(map(len, table.values())) + fig, axs = ui.subplots( + refwidth=length, refheight=width, + nrows=naxs, share=False, hspace='2pt', top='-1em', + ) + i = -1 + nheads = nbars = 0 # for deciding which axes to plot in + for cat, cmaps in table.items(): + nheads += 1 + for j, cmap in enumerate(cmaps): + i += 1 + if j + nheads + nbars > naxs: + break + if j == 0: # allocate this axes for title + i += 2 + for ax in axs[i - 2:i]: + ax.set_visible(False) + ax = axs[i] + if N is not None: + cmap = cmap.copy(N=N) + label = cmap.name + label = re.sub(r'\A_*', '', label) + label = re.sub(r'(_copy)*\Z', '', label) + ax.colorbar( + cmap, loc='fill', orientation='horizontal', + locator='null', linewidth=0, rasterized=rasterized, + ) + ax.text( + 0 - (rc['axes.labelpad'] / 72) / length, 0.45, label, + ha='right', va='center', transform='axes', + ) + if j == 0: + ax.set_title(cat, weight='bold') + nbars += len(cmaps) + + return fig, axs + + +@docstring._snippet_manager +def show_cmaps(*args, **kwargs): + """ + Generate a table of the registered colormaps or the input colormaps + categorized by source. Adapted from `this example \ +`__. + + Parameters + ---------- + *args : colormap-spec, optional + Colormap names or objects. + N : int, default: :rc:`image.lut` + The number of levels in each colorbar. + unknown : str, default: 'User' + Category name for colormaps that are unknown to proplot. + Set this to ``False`` to hide unknown colormaps. + include : str or sequence of str, default: None + Category names to be shown in the table. Use this to limit + the table to a subset of categories. Valid categories are + %(demos.cmaps)s. + ignore : str or sequence of str, default: 'MATLAB', 'GNUplot', 'GIST', 'Other' + Used only if `include` was not passed. Category names to be removed from the + table. Use of the default ignored colormaps is discouraged because they contain + non-uniform color transitions (see the :ref:`user guide `). + %(demos.colorbar)s + + Returns + ------- + proplot.figure.Figure + The figure. + proplot.gridspec.SubplotGrid + The subplot grid. + + See also + -------- + show_colorspaces + show_channels + show_cycles + show_colors + show_fonts + """ + # Get the list of colormaps + if args: + cmaps = list(map(constructor.Colormap, args)) + cmaps = [ + cmap if isinstance(cmap, mcolors.LinearSegmentedColormap) + else pcolors._get_cmap_subtype(cmap, 'continuous') for cmap in args + ] + ignore = () + else: + cmaps = [ + cmap for cmap in pcolors._cmap_database.values() + if isinstance(cmap, pcolors.ContinuousColormap) + and not (cmap.name or '_')[:1] == '_' + ] + ignore = None + + # Return figure of colorbars + kwargs.setdefault('source', CMAP_TABLE) + kwargs.setdefault('ignore', ignore) + return _draw_bars(cmaps, **kwargs) + + +@docstring._snippet_manager +def show_cycles(*args, **kwargs): + """ + Generate a table of registered color cycles or the input color cycles + categorized by source. Adapted from `this example \ +`__. + + Parameters + ---------- + *args : colormap-spec, optional + Cycle names or objects. + unknown : str, default: 'User' + Category name for cycles that are unknown to proplot. + Set this to ``False`` to hide unknown colormaps. + include : str or sequence of str, default: None + Category names to be shown in the table. Use this to limit + the table to a subset of categories. Valid categories are + %(demos.cycles)s. + ignore : str or sequence of str, default: None + Used only if `include` was not passed. Category names to be removed + from the table. + %(demos.colorbar)s + + Returns + ------- + proplot.figure.Figure + The figure. + proplot.gridspec.SubplotGrid + The subplot grid. + + See also + -------- + show_cmaps + show_colors + show_fonts + """ + # Get the list of cycles + if args: + cycles = [ + pcolors.DiscreteColormap( + cmap.by_key().get('color', ['k']), name=getattr(cmap, 'name', None) + ) + if isinstance(cmap, cycler.Cycler) + else cmap if isinstance(cmap, mcolors.ListedColormap) + else pcolors._get_cmap_subtype(cmap, 'discrete') for cmap in args + ] + ignore = () + else: + cycles = [ + cmap for cmap in pcolors._cmap_database.values() + if isinstance(cmap, pcolors.DiscreteColormap) + and not (cmap.name or '_')[:1] == '_' + ] + ignore = None + + # Return figure of colorbars + kwargs.setdefault('source', CYCLE_TABLE) + kwargs.setdefault('ignore', ignore) + return _draw_bars(cycles, **kwargs) + + +def _filter_colors(hcl, ihue, nhues, minsat): + """ + Filter colors into categories. + + Parameters + ---------- + hcl : tuple + The data. + ihue : int + The hue column. + nhues : int + The total number of hues. + minsat : float + The minimum saturation used for the "grays" column. + """ + breakpoints = np.linspace(0, 360, nhues) + gray = hcl[1] <= minsat + if ihue == 0: + return gray + color = breakpoints[ihue - 1] <= hcl[0] < breakpoints[ihue] + if ihue == nhues - 1: + color = color or color == breakpoints[ihue] # endpoint inclusive + return not gray and color + + +@docstring._snippet_manager +def show_colors(*, nhues=17, minsat=10, unknown='User', include=None, ignore=None): + """ + Generate tables of the registered color names. Adapted from + `this example `__. + + Parameters + ---------- + nhues : int, optional + The number of breaks between hues for grouping "like colors" in the + color table. + minsat : float, optional + The threshold saturation, between ``0`` and ``100``, for designating + "gray colors" in the color table. + unknown : str, default: 'User' + Category name for color names that are unknown to proplot. + Set this to ``False`` to hide unknown color names. + include : str or sequence of str, default: None + Category names to be shown in the table. Use this to limit + the table to a subset of categories. Valid categories are + %(demos.colors)s. + ignore : str or sequence of str, default: 'CSS4' + Used only if `include` was not passed. Category names to be removed + from the colormap table. + + Returns + ------- + proplot.figure.Figure + The figure. + proplot.gridspec.SubplotGrid + The subplot grid. + """ + # Tables of known colors to be plotted + colordict = {} + if ignore is None: + ignore = 'css4' + if isinstance(include, str): + include = (include.lower(),) + if isinstance(ignore, str): + ignore = (ignore.lower(),) + if include is None: + include = COLOR_TABLE.keys() + include -= set(map(str.lower, ignore)) + for cat in sorted(include): + if cat not in COLOR_TABLE: + raise ValueError( + f'Invalid categories {include!r}. Options are: ' + + ', '.join(map(repr, COLOR_TABLE)) + '.' + ) + colordict[cat] = list(COLOR_TABLE[cat]) # copy the names + + # Add "unknown" colors + if unknown: + unknown_colors = [ + color for color in map(repr, pcolors._color_database) + if 'xkcd:' not in color and 'tableau:' not in color + and not any(color in list_ for list_ in COLOR_TABLE) + ] + if unknown_colors: + colordict[unknown] = unknown_colors + + # Divide colors into columns and rows + # For base and open colors, tables are already organized into like + # colors, so just reshape them into grids. For other colors, we group + # them by hue in descending order of luminance. + namess = {} + for cat in sorted(include): + if cat == 'base': + names = np.asarray(colordict[cat]) + ncols, nrows = len(names), 1 + elif cat == 'opencolor': + names = np.asarray(colordict[cat]) + ncols, nrows = 7, 20 + else: + hclpairs = [(name, to_xyz(name, 'hcl')) for name in colordict[cat]] + hclpairs = [ + sorted( + [ + pair for pair in hclpairs + if _filter_colors(pair[1], ihue, nhues, minsat) + ], + key=lambda x: x[1][2] # sort by luminance + ) + for ihue in range(nhues) + ] + names = np.array([name for ipairs in hclpairs for name, _ in ipairs]) + ncols, nrows = 4, len(names) // 4 + 1 + + names.resize((ncols, nrows)) # fill empty slots with empty string + namess[cat] = names + + # Draw figures for different groups of colors + # NOTE: Aspect ratios should be number of columns divided by number + # of rows, times the aspect ratio of the slot for each swatch-name + # pair, which we set to 5. + shape = tuple(namess.values())[0].shape # sample *first* group + figwidth = 6.5 + refaspect = (figwidth * 72) / (10 * shape[1]) # points + maxcols = max(names.shape[0] for names in namess.values()) + hratios = tuple(names.shape[1] for names in namess.values()) + fig, axs = ui.subplots( + figwidth=figwidth, + refaspect=refaspect, + nrows=len(include), + hratios=hratios, + ) + title_dict = { + 'css4': 'CSS4 colors', + 'base': 'Base colors', + 'opencolor': 'Open color', + 'xkcd': 'XKCD colors', + } + for ax, (cat, names) in zip(axs, namess.items()): + # Format axes + ax.format( + title=title_dict.get(cat, cat), + titleweight='bold', + xlim=(0, maxcols - 1), + ylim=(0, names.shape[1]), + grid=False, yloc='neither', xloc='neither', + alpha=0, + ) + + # Draw swatches as lines + lw = 8 # best to just use trial and error + swatch = 0.45 # percent of column reserved for swatch + ncols, nrows = names.shape + for col, inames in enumerate(names): + for row, name in enumerate(inames): + if not name: + continue + y = nrows - row - 1 # start at top + x1 = col * (maxcols - 1) / ncols # e.g. idx 3 --> idx 7 + x2 = x1 + swatch # portion of column + xtext = x1 + 1.1 * swatch + ax.text( + xtext, y, name, ha='left', va='center', + transform='data', clip_on=False, + ) + ax.plot( + [x1, x2], [y, y], + color=name, lw=lw, + solid_capstyle='butt', # do not stick out + clip_on=False, + ) + + return fig, axs + + +def show_fonts( + *args, family=None, user=None, text=None, math=False, fallback=False, **kwargs +): + """ + Generate a table of fonts. If a glyph for a particular font is unavailable, + it is replaced with the "¤" dummy character. + + Parameters + ---------- + *args : str or `~matplotlib.font_manager.FontProperties` + The font specs, font names, or `~matplotlib.font_manager.FontProperties`\\ s + to show. If no positional arguments are passed and the `family` argument is + not passed, then the fonts found in `~proplot.config.Configurator.user_folder` + and `~proplot.config.Configurator.local_folders` and the *available* + :rcraw:`font.sans-serif` fonts are shown. + family \ +: {'tex-gyre', 'sans-serif', 'serif', 'monospace', 'cursive', 'fantasy'}, optional + The family from which *available* fonts are shown. Default is ``'sans-serif'`` + if no arguments were provided. Otherwise the default is to not show family + fonts. The fonts belonging to each family are listed under :rcraw:`font.serif`, + :rcraw:`font.sans-serif`, :rcraw:`font.monospace`, :rcraw:`font.cursive`, and + :rcraw:`font.fantasy`. The special family ``'tex-gyre'`` includes the + `TeX Gyre `__ fonts. + user : bool, optional + Whether to include fonts in `~proplot.config.Configurator.user_folder` and + `~proplot.config.Configurator.local_folders` at the top of the table. Default + is ``True`` if called without any arguments and ``False`` otherwise. + text : str, optional + The sample text shown for each font. If not passed then default math or + non-math sample text is used. + math : bool, default: False + Whether the default sample text should show non-math Latin characters or + or math equations and Greek letters. + fallback : bool, default: False + Whether to use the fallback font :rcraw:`mathtext.fallback` for unavailable + characters. If ``False`` the dummy glyph "¤" is shown for missing characters. + **kwargs + Additional font properties passed to `~matplotlib.font_manager.FontProperties`. + Default size is ``12`` and default weight, style, and strength are ``'normal'``. + + Other parameters + ---------------- + size : float, default: 12 + The font size. + weight : str, default: 'normal' + The font weight. + style : str, default: 'normal' + The font style. + stretch : str, default: 'normal' + The font stretch. + + Returns + ------- + proplot.figure.Figure + The figure. + proplot.gridspec.SubplotGrid + The subplot grid. + + See also + -------- + show_cmaps + show_cycles + show_colors + """ + # Parse user input fonts and translate into FontProperties. + s = set() + props = [] # should be string names + all_fonts = sorted(mfonts.fontManager.ttflist, key=lambda font: font.name) + all_fonts = [font for font in all_fonts if font.name not in s and not s.add(font.name)] # noqa: E501 + all_names = [font.name for font in all_fonts] + for arg in args: + if isinstance(arg, str): + arg = mfonts.FontProperties(arg, **kwargs) # possibly a fontspec + elif not isinstance(arg, mfonts.FontProperties): + raise TypeError(f'Expected string or FontProperties but got {type(arg)}.') + opts = arg.get_family() # usually a singleton list + if opts and opts[0] in all_names: + props.append(arg) + else: + warnings._warn_proplot(f'Input font name {opts[:1]!r} not found. Skipping.') + + # Add user and family FontProperties. + user = _not_none(user, not args and family is None) + family = _not_none(family, None if args else 'sans-serif') + if user: + paths = _get_data_folders('fonts', default=False) + for font in all_fonts: # fonts sorted by unique name + if os.path.dirname(font.fname) in paths: + props.append(mfonts.FontProperties(font.name, **kwargs)) + if family is not None: + options = ('serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', 'tex-gyre') + if family not in options: + raise ValueError( + f'Invalid font family {family!r}. Options are: ' + + ', '.join(map(repr, options)) + '.' + ) + names = FAMILY_TEXGYRE if family == 'tex-gyre' else rc['font.' + family] + for name in names: + if name in all_names: # valid font name + props.append(mfonts.FontProperties(name, **kwargs)) + + # The default sample text + linespacing = 0.8 if text is None and math else 1.2 + if text is None: + if not math: + text = ( + 'the quick brown fox jumps over a lazy dog 01234 ; . , + - * ^ () ||' + '\n' + 'THE QUICK BROWN FOX JUMPS OVER A LAZY DOG 56789 : ! ? & # % $ [] {}' + ) + else: + text = ( + '\n' + r'$\alpha\beta$ $\Gamma\gamma$ $\Delta\delta$ ' + r'$\epsilon\zeta\eta$ $\Theta\theta$ $\kappa\mu\nu$ ' + r'$\Lambda\lambda$ $\Pi\pi$ $\xi\rho\tau\chi$ $\Sigma\sigma$ ' + r'$\Phi\phi$ $\Psi\psi$ $\Omega\omega$ ' + r'$\{ \; \}^i$ $[ \; ]_j$ $( \; )^k$ $\left< \right>_n$' + '\n' + r'$0^a + 1_b - 2^c \times 3_d = ' + r'4.0^e \equiv 5.0_f \approx 6.0^g \sim 7_h \leq 8^i \geq 9_j' + r'\ll \prod \, P \gg \sum \, Q \, ' + r'\int \, Y \mathrm{d}y \propto \oint \;\, Z \mathrm{d}z$' + ) + + # Settings for rendering math text + ctx = {'mathtext.fontset': 'custom'} + if not fallback: + if _version_mpl < '3.4': + ctx['mathtext.fallback_to_cm'] = False + else: + ctx['mathtext.fallback'] = None + if 'size' not in kwargs: + for prop in props: + if prop.get_size() == rc['font.size']: + prop.set_size(12) # only if fontspec did not change the size + + # Create figure + refsize = props[0].get_size_in_points() if props else rc['font.size'] + refheight = 1.2 * (text.count('\n') + 2.5) * refsize / 72 + fig, axs = ui.subplots( + refwidth=4.5, refheight=refheight, nrows=len(props), ncols=1, space=0, + ) + fig._render_context.update(ctx) + fig.format( + xloc='neither', yloc='neither', xlocator='null', ylocator='null', alpha=0 + ) + for ax, prop in zip(axs, props): + name = prop.get_family()[0] + ax.text( + 0, 0.5, f'{name}:\n{text} ', ha='left', va='center', + linespacing=linespacing, fontproperties=prop + ) + return fig, axs diff --git a/proplot/external/__init__.py b/proplot/external/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/proplot/externals/__init__.py b/proplot/externals/__init__.py new file mode 100644 index 000000000..ed2aee163 --- /dev/null +++ b/proplot/externals/__init__.py @@ -0,0 +1,5 @@ +#!/usr/bin/env python3 +""" +External utilities adapted for proplot. +""" +from . import hsluv # noqa: F401 diff --git a/proplot/external/hsluv.py b/proplot/externals/hsluv.py similarity index 94% rename from proplot/external/hsluv.py rename to proplot/externals/hsluv.py index b3a4c17d5..73dfdf4a0 100644 --- a/proplot/external/hsluv.py +++ b/proplot/externals/hsluv.py @@ -1,37 +1,33 @@ #!/usr/bin/env python3 """ -Tools for converting between various colorspaces. Adapted from `seaborn -`__ -and `hsluv-python -`__. -For more info on colorspaces see the -`CIULUV specification `__, the -`CIE 1931 colorspace `__, -the `HCL colorspace `__, -and the `HSLuv system `__. - -Provided by matplotlib: - -* `~matplotlib.colors.to_rgb` -* `~matplotlib.colors.rgb_to_hsv` -* `~matplotlib.colors.hsv_to_rgb` - -New conversion tools: +Utilities for converting between colorspaces. Includes the following: -* `rgb_to_hsl` (same as `~matplotlib.colors.rgb_to_hsv`) -* `hsl_to_rgb` (same as `~matplotlib.colors.hsv_to_rgb`) +* `rgb_to_hsl` (same as `matplotlib.colors.rgb_to_hsv`) +* `hsl_to_rgb` (same as `matplotlib.colors.hsv_to_rgb`) * `hcl_to_rgb` * `rgb_to_hcl` * `hsluv_to_rgb` * `rgb_to_hsluv` * `hpluv_to_rgb` * `rgb_to_hpluv` + +Note +---- +This file is adapted from `seaborn +`__ +and `hsluv-python +`__. +For more information on colorspaces see the +`CIULUV specification `__, the +`CIE 1931 colorspace `__, +the `HCL colorspace `__, +and the `HSLuv system `__. """ -# Imports (below functions are just meant to be used by user) -# See: https://stackoverflow.com/a/2353265/4970632 +# Imports. See: https://stackoverflow.com/a/2353265/4970632 # The HLS is actually HCL import math from colorsys import hls_to_rgb, rgb_to_hls + # Coefficients or something m = [ [3.2406, -1.5372, -0.4986], diff --git a/proplot/figure.py b/proplot/figure.py new file mode 100644 index 000000000..bc0ff6f0d --- /dev/null +++ b/proplot/figure.py @@ -0,0 +1,1959 @@ +#!/usr/bin/env python3 +""" +The figure class used for all proplot figures. +""" +import functools +import inspect +import os +from numbers import Integral + +import matplotlib.axes as maxes +import matplotlib.figure as mfigure +import matplotlib.gridspec as mgridspec +import matplotlib.projections as mproj +import matplotlib.text as mtext +import matplotlib.transforms as mtransforms +import numpy as np + +from . import axes as paxes +from . import constructor +from . import gridspec as pgridspec +from .config import rc, rc_matplotlib +from .internals import ic # noqa: F401 +from .internals import ( + _not_none, + _pop_params, + _pop_rc, + _translate_loc, + context, + docstring, + labels, + warnings, +) +from .utils import units + +__all__ = [ + 'Figure', +] + + +# Preset figure widths or sizes based on academic journal recommendations +# NOTE: Please feel free to add to this! +JOURNAL_SIZES = { + 'aaas1': '5.5cm', + 'aaas2': '12cm', + 'agu1': ('95mm', '115mm'), + 'agu2': ('190mm', '115mm'), + 'agu3': ('95mm', '230mm'), + 'agu4': ('190mm', '230mm'), + 'ams1': 3.2, + 'ams2': 4.5, + 'ams3': 5.5, + 'ams4': 6.5, + 'nat1': '89mm', + 'nat2': '183mm', + 'pnas1': '8.7cm', + 'pnas2': '11.4cm', + 'pnas3': '17.8cm', +} + + +# Figure docstring +_figure_docstring = """ +refnum : int, optional + The reference subplot number. The `refwidth`, `refheight`, and `refaspect` + keyword args are applied to this subplot, and the aspect ratio is conserved + for this subplot in the `~Figure.auto_layout`. The default is the first + subplot created in the figure. +refaspect : float or 2-tuple of float, optional + The reference subplot aspect ratio. If scalar, this indicates the width + divided by height. If 2-tuple, this indicates the (width, height). Ignored + if both `figwidth` *and* `figheight` or both `refwidth` *and* `refheight` were + passed. The default value is ``1`` or the "data aspect ratio" if the latter + is explicitly fixed (as with `~proplot.axes.PlotAxes.imshow` plots and + `~proplot.axes.Axes.GeoAxes` projections; see `~matplotlib.axes.Axes.set_aspect`). +refwidth, refheight : unit-spec, default: :rc:`subplots.refwidth` + The width, height of the reference subplot. + %(units.in)s + Ignored if `figwidth`, `figheight`, or `figsize` was passed. If you + specify just one, `refaspect` will be respected. +ref, aspect, axwidth, axheight + Aliases for `refnum`, `refaspect`, `refwidth`, `refheight`. + *These may be deprecated in a future release.* +figwidth, figheight : unit-spec, optional + The figure width and height. Default behavior is to use `refwidth`. + %(units.in)s + If you specify just one, `refaspect` will be respected. +width, height + Aliases for `figwidth`, `figheight`. +figsize : 2-tuple, optional + Tuple specifying the figure ``(width, height)``. +sharex, sharey, share \ +: {0, False, 1, 'labels', 'labs', 2, 'limits', 'lims', 3, True, 4, 'all'}, \ +default: :rc:`subplots.share` + The axis sharing "level" for the *x* axis, *y* axis, or both + axes. Options are as follows: + + * ``0`` or ``False``: No axis sharing. This also sets the default `spanx` + and `spany` values to ``False``. + * ``1`` or ``'labels'`` or ``'labs'``: Only draw axis labels on the bottommost + row or leftmost column of subplots. Tick labels still appear on every subplot. + * ``2`` or ``'limits'`` or ``'lims'``: As above but force the axis limits, scales, + and tick locations to be identical. Tick labels still appear on every subplot. + * ``3`` or ``True``: As above but only show the tick labels on the bottommost + row and leftmost column of subplots. + * ``4`` or ``'all'``: As above but also share the axis limits, scales, and + tick locations between subplots not in the same row or column. + +spanx, spany, span : bool or {0, 1}, default: :rc:`subplots.span` + Whether to use "spanning" axis labels for the *x* axis, *y* axis, or both + axes. Default is ``False`` if `sharex`, `sharey`, or `share` are ``0`` or + ``False``. When ``True``, a single, centered axis label is used for all axes + with bottom and left edges in the same row or column. This can considerably + redundancy in your figure. "Spanning" labels integrate with "shared" axes. For + example, for a 3-row, 3-column figure, with ``sharey > 1`` and ``spany == True``, + your figure will have 1 y axis label instead of 9 y axis labels. +alignx, aligny, align : bool or {0, 1}, default: :rc:`subplots.align` + Whether to `"align" axis labels \ +`__ + for the *x* axis, *y* axis, or both axes. Aligned labels always appear in the same + row or column. This is ignored if `spanx`, `spany`, or `span` are ``True``. +%(gridspec.shared)s +%(gridspec.scalar)s +tight : bool, default: :rc`subplots.tight` + Whether automatic calls to `~Figure.auto_layout` should include + :ref:`tight layout adjustments `. If you manually specified a spacing + in the call to `~proplot.ui.subplots`, it will be used to override the tight + layout spacing. For example, with ``left=1``, the left margin is set to 1 + em-width, while the remaining margin widths are calculated automatically. +%(gridspec.tight)s +journal : str, optional + String corresponding to an academic journal standard used to control the figure + width `figwidth` and, if specified, the figure height `figheight`. See the below + table. Feel free to add to this table by submitting a pull request. + + .. _journal_table: + + =========== ==================== \ +=============================================================================== + Key Size description Organization + =========== ==================== \ +=============================================================================== + ``'aaas1'`` 1-column \ +`American Association for the Advancement of Science `_ (e.g. *Science*) + ``'aaas2'`` 2-column ” + ``'agu1'`` 1-column `American Geophysical Union `_ + ``'agu2'`` 2-column ” + ``'agu3'`` full height 1-column ” + ``'agu4'`` full height 2-column ” + ``'ams1'`` 1-column `American Meteorological Society `_ + ``'ams2'`` small 2-column ” + ``'ams3'`` medium 2-column ” + ``'ams4'`` full 2-column ” + ``'nat1'`` 1-column `Nature Research `_ + ``'nat2'`` 2-column ” + ``'pnas1'`` 1-column \ +`Proceedings of the National Academy of Sciences `_ + ``'pnas2'`` 2-column ” + ``'pnas3'`` landscape page ” + =========== ==================== \ +=============================================================================== + + .. _aaas: \ +https://www.sciencemag.org/authors/instructions-preparing-initial-manuscript + .. _agu: \ +https://www.agu.org/Publish-with-AGU/Publish/Author-Resources/Graphic-Requirements + .. _ams: \ +https://www.ametsoc.org/ams/index.cfm/publications/authors/journal-and-bams-authors/figure-information-for-authors/ + .. _nat: \ +https://www.nature.com/nature/for-authors/formatting-guide + .. _pnas: \ +https://www.pnas.org/page/authors/format +""" +docstring._snippet_manager['figure.figure'] = _figure_docstring + + +# Multiple subplots +_subplots_params_docstring = """ +array : `proplot.gridspec.GridSpec` or array-like of int, optional + The subplot grid specifier. If a `~proplot.gridspec.GridSpec`, one subplot is + drawn for each unique `~proplot.gridspec.GridSpec` slot. If a 2D array of integers, + one subplot is drawn for each unique integer in the array. Think of this array as + a "picture" of the subplot grid -- for example, the array ``[[1, 1], [2, 3]]`` + creates one long subplot in the top row, two smaller subplots in the bottom row. + Integers must range from 1 to the number of plots, and ``0`` indicates an + empty space -- for example, ``[[1, 1, 1], [2, 0, 3]]`` creates one long subplot + in the top row with two subplots in the bottom row separated by a space. +nrows, ncols : int, default: 1 + The number of rows and columns in the subplot grid. Ignored + if `array` was passed. Use these arguments for simple subplot grids. +order : {'C', 'F'}, default: 'C' + Whether subplots are numbered in column-major (``'C'``) or row-major (``'F'``) + order. Analogous to `numpy.array` ordering. This controls the order that + subplots appear in the `SubplotGrid` returned by this function, and the order + of subplot a-b-c labels (see `~proplot.axes.Axes.format`). +%(axes.proj)s + + To use different projections for different subplots, you have + two options: + + * Pass a *list* of projection specifications, one for each subplot. + For example, ``pplt.subplots(ncols=2, proj=('cart', 'robin'))``. + * Pass a *dictionary* of projection specifications, where the + keys are integers or tuples of integers that indicate the projection + to use for the corresponding subplot number(s). If a key is not + provided, the default projection ``'cartesian'`` is used. For example, + ``pplt.subplots(ncols=4, proj={2: 'cyl', (3, 4): 'stere'})`` creates + a figure with a default Cartesian axes for the first subplot, a Mercator + projection for the second subplot, and a Stereographic projection + for the third and fourth subplots. + +%(axes.proj_kw)s + If dictionary of properties, applies globally. If list or dictionary of + dictionaries, applies to specific subplots, as with `proj`. For example, + ``pplt.subplots(ncols=2, proj='cyl', proj_kw=({'lon_0': 0}, {'lon_0': 180})`` + centers the projection in the left subplot on the prime meridian and in the + right subplot on the international dateline. +%(axes.backend)s + If string, applies to all subplots. If list or dict, applies to specific + subplots, as with `proj`. +%(gridspec.shared)s +%(gridspec.vector)s +%(gridspec.tight)s +""" +docstring._snippet_manager['figure.subplots_params'] = _subplots_params_docstring + + +# Extra args docstring +_axes_params_docstring = """ +**kwargs + Passed to the proplot class `proplot.axes.CartesianAxes`, `proplot.axes.PolarAxes`, + `proplot.axes.GeoAxes`, or `proplot.axes.ThreeAxes`. This can include keyword + arguments for projection-specific ``format`` commands. +""" +docstring._snippet_manager['figure.axes_params'] = _axes_params_docstring + + +# Multiple subplots docstring +_subplots_docstring = """ +Add an arbitrary grid of subplots to the figure. + +Parameters +---------- +%(figure.subplots_params)s + +Other parameters +---------------- +%(figure.figure)s +%(figure.axes_params)s + +Returns +------- +axs : SubplotGrid + The axes instances stored in a `SubplotGrid`. + +See also +-------- +proplot.ui.figure +proplot.ui.subplots +proplot.figure.Figure.subplot +proplot.figure.Figure.add_subplot +proplot.gridspec.SubplotGrid +proplot.axes.Axes +""" +docstring._snippet_manager['figure.subplots'] = _subplots_docstring + + +# Single subplot docstring +_subplot_docstring = """ +Add a subplot axes to the figure. + +Parameters +---------- +*args : int, tuple, or `~matplotlib.gridspec.SubplotSpec`, optional + The subplot location specifier. Your options are: + + * A single 3-digit integer argument specifying the number of rows, + number of columns, and gridspec number (using row-major indexing). + * Three positional arguments specifying the number of rows, number of + columns, and gridspec number (int) or number range (2-tuple of int). + * A `~matplotlib.gridspec.SubplotSpec` instance generated by indexing + a proplot `~proplot.gridspec.GridSpec`. + + For integer input, the implied geometry must be compatible with the implied + geometry from previous calls -- for example, ``fig.add_subplot(331)`` followed + by ``fig.add_subplot(132)`` is valid because the 1 row of the second input can + be tiled into the 3 rows of the the first input, but ``fig.add_subplot(232)`` + will raise an error because 2 rows cannot be tiled into 3 rows. For + `~matplotlib.gridspec.SubplotSpec` input, the `~matplotlig.gridspec.SubplotSpec` + must be derived from the `~proplot.gridspec.GridSpec` used in previous calls. + + These restrictions arise because we allocate a single, + unique `~Figure.gridspec` for each figure. +number : int, optional + The axes number used for a-b-c labeling. See `~proplot.axes.Axes.format` for + details. By default this is incremented automatically based on the other subplots + in the figure. Use e.g. ``number=None`` or ``number=False`` to ensure the subplot + has no a-b-c label. Note the number corresponding to ``a`` is ``1``, not ``0``. +autoshare : bool, default: True + Whether to automatically share the *x* and *y* axes with subplots spanning the + same rows and columns based on the figure-wide `sharex` and `sharey` settings. + This has no effect if :rcraw:`subplots.share` is ``False`` or if ``sharex=False`` + or ``sharey=False`` were passed to the figure. +%(axes.proj)s +%(axes.proj_kw)s +%(axes.backend)s + +Other parameters +---------------- +%(figure.axes_params)s + +See also +-------- +proplot.figure.Figure.add_axes +proplot.figure.Figure.subplots +proplot.figure.Figure.add_subplots +""" +docstring._snippet_manager['figure.subplot'] = _subplot_docstring + + +# Single axes +_axes_docstring = """ +Add a non-subplot axes to the figure. + +Parameters +---------- +rect : 4-tuple of float + The (left, bottom, width, height) dimensions of the axes in + figure-relative coordinates. +%(axes.proj)s +%(axes.proj_kw)s +%(axes.backend)s + +Other parameters +---------------- +%(figure.axes_params)s + +See also +-------- +proplot.figure.Figure.subplot +proplot.figure.Figure.add_subplot +proplot.figure.Figure.subplots +proplot.figure.Figure.add_subplots +""" +docstring._snippet_manager['figure.axes'] = _axes_docstring + + +# Colorbar or legend panel docstring +_space_docstring = """ +loc : str, optional + The {name} location. Valid location keys are as follows. + +%(axes.panel_loc)s + +space : float or str, default: None + The fixed space between the {name} and the subplot grid edge. + %(units.em)s + When the :ref:`tight layout algorithm ` is active for the figure, + `space` is computed automatically (see `pad`). Otherwise, `space` is set to + a suitable default. +pad : float or str, default: :rc:`subplots.innerpad` or :rc:`subplots.panelpad` + The :ref:`tight layout padding ` between the {name} and the + subplot grid. Default is :rcraw:`subplots.innerpad` for the first {name} + and :rcraw:`subplots.panelpad` for subsequently "stacked" {name}s. + %(units.em)s +row, rows + Aliases for `span` for {name}s on the left or right side. +col, cols + Aliases for `span` for {name}s on the top or bottom side. +span : int or 2-tuple of int, default: None + Integer(s) indicating the span of the {name} across rows and columns of + subplots. For example, ``fig.{name}(loc='b', col=1)`` draws a {name} beneath + the leftmost column of subplots, and ``fig.{name}(loc='b', cols=(1, 2))`` + draws a {name} beneath the left two columns of subplots. By default + the {name} will span every subplot row and column. +align : {{'center', 'top', 't', 'bottom', 'b', 'left', 'l', 'right', 'r'}}, optional + For outer {name}s only. How to align the {name} against the + subplot edge. The values ``'top'`` and ``'bottom'`` are valid for left and + right {name}s and ``'left'`` and ``'right'`` are valid for top and bottom + {name}s. The default is always ``'center'``. +""" +docstring._snippet_manager['figure.legend_space'] = _space_docstring.format(name='legend') # noqa: E501 +docstring._snippet_manager['figure.colorbar_space'] = _space_docstring.format(name='colorbar') # noqa: E501 + + +# Save docstring +_save_docstring = """ +Save the figure. + +Parameters +---------- +path : path-like, optional + The file path. User paths are expanded with `os.path.expanduser`. +**kwargs + Passed to `~matplotlib.figure.Figure.savefig` + +See also +-------- +Figure.save +Figure.savefig +matplotlib.figure.Figure.savefig +""" +docstring._snippet_manager['figure.save'] = _save_docstring + + +def _get_journal_size(preset): + """ + Return the width and height corresponding to the given preset. + """ + value = JOURNAL_SIZES.get(preset, None) + if value is None: + raise ValueError( + f'Unknown preset figure size specifier {preset!r}. ' + 'Current options are: ' + + ', '.join(map(repr, JOURNAL_SIZES.keys())) + ) + figwidth = figheight = None + try: + figwidth, figheight = value + except (TypeError, ValueError): + figwidth = value + return figwidth, figheight + + +def _add_canvas_preprocessor(canvas, method, cache=False): + """ + Return a pre-processer that can be used to override instance-level + canvas draw() and print_figure() methods. This applies tight layout + and aspect ratio-conserving adjustments and aligns labels. Required + so canvas methods instantiate renderers with the correct dimensions. + """ + # NOTE: Renderer must be (1) initialized with the correct figure size or + # (2) changed inplace during draw, but vector graphic renderers *cannot* + # be changed inplace. So options include (1) monkey patch + # canvas.get_width_height, overriding figure.get_size_inches, and exploit + # the FigureCanvasAgg.get_renderer() implementation (because FigureCanvasAgg + # queries the bbox directly rather than using get_width_height() so requires + # workaround), (2) override bbox and bbox_inches as *properties* (but these + # are really complicated, dangerous, and result in unnecessary extra draws), + # or (3) simply override canvas draw methods. Our choice is #3. + def _canvas_preprocess(self, *args, **kwargs): + fig = self.figure # update even if not stale! needed after saves + func = getattr(type(self), method) # the original method + + # Bail out if we are already adjusting layout + # NOTE: The _is_adjusting check necessary when inserting new + # gridspec rows or columns with the qt backend. + # NOTE: Return value for macosx _draw is the renderer, for qt draw is + # nothing, and for print_figure is some figure object, but this block + # has never been invoked when calling print_figure. + if fig._is_adjusting: + if method == '_draw': # macosx backend + return fig._get_renderer() + else: + return + + # Adjust layout + # NOTE: The authorized_context is needed because some backends disable + # constrained layout or tight layout before printing the figure. + ctx1 = fig._context_adjusting(cache=cache) + ctx2 = fig._context_authorized() # skip backend set_constrained_layout() + ctx3 = rc.context(fig._render_context) # draw with figure-specific setting + with ctx1, ctx2, ctx3: + fig.auto_layout() + return func(self, *args, **kwargs) + + # Add preprocessor + setattr(canvas, method, _canvas_preprocess.__get__(canvas)) + return canvas + + +class Figure(mfigure.Figure): + """ + The `~matplotlib.figure.Figure` subclass used by proplot. + """ + # Shared error and warning messages + _share_message = ( + 'Axis sharing level can be 0 or False (share nothing), ' + "1 or 'labels' or 'labs' (share axis labels), " + "2 or 'limits' or 'lims' (share axis limits and axis labels), " + '3 or True (share axis limits, axis labels, and tick labels), ' + "or 4 or 'all' (share axis labels and tick labels in the same gridspec " + 'rows and columns and share axis limits across all subplots).' + ) + _space_message = ( + 'To set the left, right, bottom, top, wspace, or hspace gridspec values, ' + 'pass them as keyword arguments to pplt.figure() or pplt.subplots(). Please ' + 'note they are now specified in physical units, with strings interpreted by ' + 'pplt.units() and floats interpreted as font size-widths.' + ) + _tight_message = ( + 'Proplot uses its own tight layout algorithm that is activated by default. ' + "To disable it, set pplt.rc['subplots.tight'] to False or pass tight=False " + 'to pplt.subplots(). For details, see fig.auto_layout().' + ) + _warn_interactive = True # disabled after first warning + + def __repr__(self): + opts = {} + for attr in ('refaspect', 'refwidth', 'refheight', 'figwidth', 'figheight'): + value = getattr(self, '_' + attr) + if value is not None: + opts[attr] = np.round(value, 2) + geom = '' + if self.gridspec: + nrows, ncols = self.gridspec.get_geometry() + geom = f'nrows={nrows}, ncols={ncols}, ' + opts = ', '.join(f'{key}={value!r}' for key, value in opts.items()) + return f'Figure({geom}{opts})' + + # NOTE: If _rename_kwargs argument is an invalid identifier, it is + # simply used in the warning message. + @docstring._obfuscate_kwargs + @docstring._snippet_manager + @warnings._rename_kwargs( + '0.7.0', axpad='innerpad', autoformat='pplt.rc.autoformat = {}' + ) + def __init__( + self, *, refnum=None, ref=None, refaspect=None, aspect=None, + refwidth=None, refheight=None, axwidth=None, axheight=None, + figwidth=None, figheight=None, width=None, height=None, journal=None, + sharex=None, sharey=None, share=None, # used for default spaces + spanx=None, spany=None, span=None, + alignx=None, aligny=None, align=None, + left=None, right=None, top=None, bottom=None, + wspace=None, hspace=None, space=None, + tight=None, outerpad=None, innerpad=None, panelpad=None, + wpad=None, hpad=None, pad=None, + wequal=None, hequal=None, equal=None, + wgroup=None, hgroup=None, group=None, + **kwargs + ): + """ + Parameters + ---------- + %(figure.figure)s + + Other parameters + ---------------- + %(figure.format)s + **kwargs + Passed to `matplotlib.figure.Figure`. + + See also + -------- + Figure.format + proplot.ui.figure + proplot.ui.subplots + matplotlib.figure.Figure + """ + # Add figure sizing settings + # NOTE: We cannot catpure user-input 'figsize' here because it gets + # automatically filled by the figure manager. See ui.figure(). + # NOTE: The figure size is adjusted according to these arguments by the + # canvas preprocessor. Although in special case where both 'figwidth' and + # 'figheight' were passes we update 'figsize' to limit side effects. + refnum = _not_none(refnum=refnum, ref=ref, default=1) # never None + refaspect = _not_none(refaspect=refaspect, aspect=aspect) + refwidth = _not_none(refwidth=refwidth, axwidth=axwidth) + refheight = _not_none(refheight=refheight, axheight=axheight) + figwidth = _not_none(figwidth=figwidth, width=width) + figheight = _not_none(figheight=figheight, height=height) + messages = [] + if journal is not None: + jwidth, jheight = _get_journal_size(journal) + if jwidth is not None and figwidth is not None: + messages.append(('journal', journal, 'figwidth', figwidth)) + if jheight is not None and figheight is not None: + messages.append(('journal', journal, 'figheight', figheight)) + figwidth = _not_none(jwidth, figwidth) + figheight = _not_none(jheight, figheight) + if figwidth is not None and refwidth is not None: + messages.append(('figwidth', figwidth, 'refwidth', refwidth)) + refwidth = None + if figheight is not None and refheight is not None: + messages.append(('figheight', figheight, 'refheight', refheight)) + refheight = None + if figwidth is None and figheight is None and refwidth is None and refheight is None: # noqa: E501 + refwidth = rc['subplots.refwidth'] # always inches + if np.iterable(refaspect): + refaspect = refaspect[0] / refaspect[1] + for key1, val1, key2, val2 in messages: + warnings._warn_proplot( + f'Got conflicting figure size arguments {key1}={val1!r} and ' + f'{key2}={val2!r}. Ignoring {key2!r}.' + ) + self._refnum = refnum + self._refaspect = refaspect + self._refaspect_default = 1 # updated for imshow and geographic plots + self._refwidth = units(refwidth, 'in') + self._refheight = units(refheight, 'in') + self._figwidth = figwidth = units(figwidth, 'in') + self._figheight = figheight = units(figheight, 'in') + + # Add special consideration for interactive backends + backend = _not_none(rc.backend, '') + backend = backend.lower() + interactive = 'nbagg' in backend or 'ipympl' in backend + if not interactive: + pass + elif figwidth is None or figheight is None: + figsize = rc['figure.figsize'] # modified by proplot + self._figwidth = figwidth = _not_none(figwidth, figsize[0]) + self._figheight = figheight = _not_none(figheight, figsize[1]) + self._refwidth = self._refheight = None # critical! + if self._warn_interactive: + Figure._warn_interactive = False # set class attribute + warnings._warn_proplot( + 'Auto-sized proplot figures are not compatible with interactive ' + "backends like '%matplotlib widget' and '%matplotlib notebook'. " + f'Reverting to the figure size ({figwidth}, {figheight}). To make ' + 'auto-sized figures, please consider using the non-interactive ' + '(default) backend. This warning message is shown the first time ' + 'you create a figure without explicitly specifying the size.' + ) + + # Add space settings + # NOTE: This is analogous to 'subplotpars' but we don't worry about + # user mutability. Think it's perfectly fine to ask users to simply + # pass these to pplt.figure() or pplt.subplots(). Also overriding + # 'subplots_adjust' would be confusing since we switch to absolute + # units and that function is heavily used outside of proplot. + params = { + 'left': left, 'right': right, 'top': top, 'bottom': bottom, + 'wspace': wspace, 'hspace': hspace, 'space': space, + 'wequal': wequal, 'hequal': hequal, 'equal': equal, + 'wgroup': wgroup, 'hgroup': hgroup, 'group': group, + 'wpad': wpad, 'hpad': hpad, 'pad': pad, + 'outerpad': outerpad, 'innerpad': innerpad, 'panelpad': panelpad, + } + self._gridspec_params = params # used to initialize the gridspec + for key, value in tuple(params.items()): + if not isinstance(value, str) and np.iterable(value) and len(value) > 1: + raise ValueError( + f'Invalid gridspec parameter {key}={value!r}. Space parameters ' + 'passed to Figure() must be scalar. For vector spaces use ' + 'GridSpec() or pass space parameters to subplots().' + ) + + # Add tight layout setting and ignore native settings + pars = kwargs.pop('subplotpars', None) + if pars is not None: + warnings._warn_proplot( + f'Ignoring subplotpars={pars!r}. ' + self._space_message + ) + if kwargs.pop('tight_layout', None): + warnings._warn_proplot( + 'Ignoring tight_layout=True. ' + self._tight_message + ) + if kwargs.pop('constrained_layout', None): + warnings._warn_proplot( + 'Ignoring constrained_layout=True. ' + self._tight_message + ) + if rc_matplotlib.get('figure.autolayout', False): + warnings._warn_proplot( + "Setting rc['figure.autolayout'] to False. " + self._tight_message + ) + if rc_matplotlib.get('figure.constrained_layout.use', False): + warnings._warn_proplot( + "Setting rc['figure.constrained_layout.use'] to False. " + self._tight_message # noqa: E501 + ) + try: + rc_matplotlib['figure.autolayout'] = False # this is rcParams + except KeyError: + pass + try: + rc_matplotlib['figure.constrained_layout.use'] = False # this is rcParams + except KeyError: + pass + self._tight_active = _not_none(tight, rc['subplots.tight']) + + # Translate share settings + translate = {'labels': 1, 'labs': 1, 'limits': 2, 'lims': 2, 'all': 4} + sharex = _not_none(sharex, share, rc['subplots.share']) + sharey = _not_none(sharey, share, rc['subplots.share']) + sharex = 3 if sharex is True else translate.get(sharex, sharex) + sharey = 3 if sharey is True else translate.get(sharey, sharey) + if sharex not in range(5): + raise ValueError(f'Invalid sharex={sharex!r}. ' + self._share_message) + if sharey not in range(5): + raise ValueError(f'Invalid sharey={sharey!r}. ' + self._share_message) + self._sharex = int(sharex) + self._sharey = int(sharey) + + # Translate span and align settings + spanx = _not_none(spanx, span, False if not sharex else None, rc['subplots.span']) # noqa: E501 + spany = _not_none(spany, span, False if not sharey else None, rc['subplots.span']) # noqa: E501 + if spanx and (alignx or align): # only warn when explicitly requested + warnings._warn_proplot('"alignx" has no effect when spanx=True.') + if spany and (aligny or align): + warnings._warn_proplot('"aligny" has no effect when spany=True.') + self._spanx = bool(spanx) + self._spany = bool(spany) + alignx = _not_none(alignx, align, rc['subplots.align']) + aligny = _not_none(aligny, align, rc['subplots.align']) + self._alignx = bool(alignx) + self._aligny = bool(aligny) + + # Initialize the figure + # NOTE: Super labels are stored inside {axes: text} dictionaries + self._gridspec = None + self._panel_dict = {'left': [], 'right': [], 'bottom': [], 'top': []} + self._subplot_dict = {} # subplots indexed by number + self._subplot_counter = 0 # avoid add_subplot() returning an existing subplot + self._is_adjusting = False + self._is_authorized = False + self._includepanels = None + self._render_context = {} + rc_kw, rc_mode = _pop_rc(kwargs) + kw_format = _pop_params(kwargs, self._format_signature) + if figwidth is not None and figheight is not None: + kwargs['figsize'] = (figwidth, figheight) + with self._context_authorized(): + super().__init__(**kwargs) + + # Super labels. We don't rely on private matplotlib _suptitle attribute and + # _align_axis_labels supports arbitrary spanning labels for subplot groups. + # NOTE: Don't use 'anchor' rotation mode otherwise switching to horizontal + # left and right super labels causes overlap. Current method is fine. + self._suptitle = self.text(0.5, 0.95, '', ha='center', va='bottom') + self._supxlabel_dict = {} # an axes: label mapping + self._supylabel_dict = {} # an axes: label mapping + self._suplabel_dict = {'left': {}, 'right': {}, 'bottom': {}, 'top': {}} + self._suptitle_pad = rc['suptitle.pad'] + d = self._suplabel_props = {} # store the super label props + d['left'] = {'va': 'center', 'ha': 'right'} + d['right'] = {'va': 'center', 'ha': 'left'} + d['bottom'] = {'va': 'top', 'ha': 'center'} + d['top'] = {'va': 'bottom', 'ha': 'center'} + d = self._suplabel_pad = {} # store the super label padding + d['left'] = rc['leftlabel.pad'] + d['right'] = rc['rightlabel.pad'] + d['bottom'] = rc['bottomlabel.pad'] + d['top'] = rc['toplabel.pad'] + + # Format figure + # NOTE: This ignores user-input rc_mode. + self.format(rc_kw=rc_kw, rc_mode=1, skip_axes=True, **kw_format) + + def _context_adjusting(self, cache=True): + """ + Prevent re-running auto layout steps due to draws triggered by figure + resizes. Otherwise can get infinite loops. + """ + kw = {'_is_adjusting': True} + if not cache: + kw['_cachedRenderer'] = None # temporarily ignore it + return context._state_context(self, **kw) + + def _context_authorized(self): + """ + Prevent warning message when internally calling no-op methods. Otherwise + emit warnings to help new users. + """ + return context._state_context(self, _is_authorized=True) + + @staticmethod + def _parse_backend(backend=None, basemap=None): + """ + Handle deprication of basemap and cartopy package. + """ + if basemap is not None: + backend = ('cartopy', 'basemap')[bool(basemap)] + warnings._warn_proplot( + f"The 'basemap' keyword was deprecated in version 0.10.0 and will be " + f'removed in a future release. Please use backend={backend!r} instead.' + ) + return backend + + def _parse_proj( + self, proj=None, projection=None, + proj_kw=None, projection_kw=None, backend=None, basemap=None, **kwargs + ): + """ + Translate the user-input projection into a registered matplotlib + axes class. Input projection can be a string, `matplotlib.axes.Axes`, + `cartopy.crs.Projection`, or `mpl_toolkits.basemap.Basemap`. + """ + # Parse arguments + proj = _not_none(proj=proj, projection=projection, default='cartesian') + proj_kw = _not_none(proj_kw=proj_kw, projection_kw=projection_kw, default={}) + backend = self._parse_backend(backend, basemap) + if isinstance(proj, str): + proj = proj.lower() + if isinstance(self, paxes.Axes): + proj = self._name + elif isinstance(self, maxes.Axes): + raise ValueError('Matplotlib axes cannot be added to proplot figures.') + + # Search axes projections + name = None + if isinstance(proj, str): + try: + mproj.get_projection_class('proplot_' + proj) + except (KeyError, ValueError): + pass + else: + name = proj + # Helpful error message + if ( + name is None + and backend is None + and isinstance(proj, str) + and constructor.Projection is object + and constructor.Basemap is object + ): + raise ValueError( + f'Invalid projection name {proj!r}. If you are trying to generate a ' + 'GeoAxes with a cartopy.crs.Projection or mpl_toolkits.basemap.Basemap ' + 'then cartopy or basemap must be installed. Otherwise the known axes ' + f'subclasses are:\n{paxes._cls_table}' + ) + # Search geographic projections + # NOTE: Also raises errors due to unexpected projection type + if name is None: + proj = constructor.Proj(proj, backend=backend, include_axes=True, **proj_kw) + name = proj._proj_backend + kwargs['map_projection'] = proj + + kwargs['projection'] = 'proplot_' + name + return kwargs + + def _get_align_axes(self, side): + """ + Return the main axes along the edge of the figure. + """ + x, y = 'xy' if side in ('left', 'right') else 'yx' + axs = self._subplot_dict.values() + if not axs: + return [] + ranges = np.array([ax._range_subplotspec(x) for ax in axs]) + edge = ranges[:, 0].min() if side in ('left', 'top') else ranges[:, 1].max() + idx = 0 if side in ('left', 'top') else 1 + axs = [ax for ax in axs if ax._range_subplotspec(x)[idx] == edge] + axs = [ax for ax in sorted(axs, key=lambda ax: ax._range_subplotspec(y)[0])] + axs = [ax for ax in axs if ax.get_visible()] + return axs + + def _get_align_coord(self, side, axs, includepanels=False): + """ + Return the figure coordinate for centering spanning axis labels or super titles. + """ + # Get position in figure relative coordinates + if not all(isinstance(ax, paxes.Axes) for ax in axs): + raise RuntimeError('Axes must be proplot axes.') + if not all(isinstance(ax, maxes.SubplotBase) for ax in axs): + raise RuntimeError('Axes must be subplots.') + s = 'y' if side in ('left', 'right') else 'x' + axs = [ax._panel_parent or ax for ax in axs] # deflect to main axes + if includepanels: # include panel short axes? + axs = [_ for ax in axs for _ in ax._iter_axes(panels=True, children=False)] + ranges = np.array([ax._range_subplotspec(s) for ax in axs]) + min_, max_ = ranges[:, 0].min(), ranges[:, 1].max() + ax_lo = axs[np.where(ranges[:, 0] == min_)[0][0]] + ax_hi = axs[np.where(ranges[:, 1] == max_)[0][0]] + box_lo = ax_lo.get_subplotspec().get_position(self) + box_hi = ax_hi.get_subplotspec().get_position(self) + if s == 'x': + pos = 0.5 * (box_lo.x0 + box_hi.x1) + else: + pos = 0.5 * (box_lo.y1 + box_hi.y0) # 'lo' is actually on top of figure + ax = axs[(np.argmin(ranges[:, 0]) + np.argmax(ranges[:, 1])) // 2] + ax = ax._panel_parent or ax # always use main subplot for spanning labels + return pos, ax + + def _get_offset_coord(self, side, axs, renderer, *, pad=None, extra=None): + """ + Return the figure coordinate for offsetting super labels and super titles. + """ + s = 'x' if side in ('left', 'right') else 'y' + cs = [] + objs = tuple(_ for ax in axs for _ in ax._iter_axes(panels=True, children=True, hidden=True)) # noqa: E501 + objs = objs + (extra or ()) # e.g. top super labels + for obj in objs: + bbox = obj.get_tightbbox(renderer) # cannot use cached bbox + attr = s + 'max' if side in ('top', 'right') else s + 'min' + c = getattr(bbox, attr) + c = (c, 0) if side in ('left', 'right') else (0, c) + c = self.transFigure.inverted().transform(c) + c = c[0] if side in ('left', 'right') else c[1] + cs.append(c) + width, height = self.get_size_inches() + if pad is None: + pad = self._suplabel_pad[side] / 72 + pad = pad / width if side in ('left', 'right') else pad / height + return min(cs) - pad if side in ('left', 'bottom') else max(cs) + pad + + def _get_renderer(self): + """ + Get a renderer at all costs. See matplotlib's tight_layout.py. + """ + if self._cachedRenderer: + renderer = self._cachedRenderer + else: + canvas = self.canvas + if canvas and hasattr(canvas, 'get_renderer'): + renderer = canvas.get_renderer() + else: + from matplotlib.backends.backend_agg import FigureCanvasAgg + canvas = FigureCanvasAgg(self) + renderer = canvas.get_renderer() + return renderer + + def _add_axes_panel(self, ax, side=None, **kwargs): + """ + Add an axes panel. + """ + # Interpret args + # NOTE: Axis sharing not implemented for figure panels, 99% of the + # time this is just used as construct for adding global colorbars and + # legends, really not worth implementing axis sharing + ax = ax._altx_parent or ax + ax = ax._alty_parent or ax + if not isinstance(ax, paxes.Axes): + raise RuntimeError('Cannot add panels to non-proplot axes.') + if not isinstance(ax, maxes.SubplotBase): + raise RuntimeError('Cannot add panels to non-subplot axes.') + orig = ax._panel_side + if orig is None: + pass + elif side is None or side == orig: + ax, side = ax._panel_parent, orig + else: + raise RuntimeError(f'Cannot add {side!r} panel to existing {orig!r} panel.') + side = _translate_loc(side, 'panel', default=_not_none(orig, 'right')) + + # Add and setup the panel accounting for index changes + # NOTE: Always put tick labels on the 'outside' and permit arbitrary + # keyword arguments passed from the user. + gs = self.gridspec + if not gs: + raise RuntimeError('The gridspec must be active.') + kw = _pop_params(kwargs, gs._insert_panel_slot) + ss, share = gs._insert_panel_slot(side, ax, **kw) + kwargs['autoshare'] = False + kwargs.setdefault('number', False) # power users might number panels + pax = self.add_subplot(ss, **kwargs) + pax._panel_side = side + pax._panel_share = share + pax._panel_parent = ax + ax._panel_dict[side].append(pax) + ax._apply_auto_share() + axis = pax.yaxis if side in ('left', 'right') else pax.xaxis + getattr(axis, 'tick_' + side)() # set tick and tick label position + axis.set_label_position(side) # set label position + return pax + + def _add_figure_panel( + self, side=None, span=None, row=None, col=None, rows=None, cols=None, **kwargs + ): + """ + Add a figure panel. + """ + # Interpret args and enforce sensible keyword args + side = _translate_loc(side, 'panel', default='right') + if side in ('left', 'right'): + for key, value in (('col', col), ('cols', cols)): + if value is not None: + raise ValueError(f'Invalid keyword {key!r} for {side!r} panel.') + span = _not_none(span=span, row=row, rows=rows) + else: + for key, value in (('row', row), ('rows', rows)): + if value is not None: + raise ValueError(f'Invalid keyword {key!r} for {side!r} panel.') + span = _not_none(span=span, col=col, cols=cols) + + # Add and setup panel + # NOTE: This is only called internally by colorbar and legend so + # do not need to pass aribtrary axes keyword arguments. + gs = self.gridspec + if not gs: + raise RuntimeError('The gridspec must be active.') + ss, _ = gs._insert_panel_slot(side, span, filled=True, **kwargs) + pax = self.add_subplot(ss, autoshare=False, number=False) + plist = self._panel_dict[side] + plist.append(pax) + pax._panel_side = side + pax._panel_share = False + pax._panel_parent = None + return pax + + def _add_subplot(self, *args, **kwargs): + """ + The driver function for adding single subplots. + """ + # Parse arguments + kwargs = self._parse_proj(**kwargs) + args = args or (1, 1, 1) + gs = self.gridspec + + # Integer arg + if len(args) == 1 and isinstance(args[0], Integral): + if not 111 <= args[0] <= 999: + raise ValueError(f'Input {args[0]} must fall between 111 and 999.') + args = tuple(map(int, str(args[0]))) + + # Subplot spec + if ( + len(args) == 1 + and isinstance(args[0], (maxes.SubplotBase, mgridspec.SubplotSpec)) + ): + ss = args[0] + if isinstance(ss, maxes.SubplotBase): + ss = ss.get_subplotspec() + if gs is None: + gs = ss.get_topmost_subplotspec().get_gridspec() + if not isinstance(gs, pgridspec.GridSpec): + raise ValueError( + 'Input subplotspec must be derived from a proplot.GridSpec.' + ) + if ss.get_topmost_subplotspec().get_gridspec() is not gs: + raise ValueError( + 'Input subplotspec must be derived from the active figure gridspec.' + ) + + # Row and column spec + # TODO: How to pass spacing parameters to gridspec? Consider overriding + # subplots adjust? Or require using gridspec manually? + elif ( + len(args) == 3 + and all(isinstance(arg, Integral) for arg in args[:2]) + and all(isinstance(arg, Integral) for arg in np.atleast_1d(args[2])) + ): + nrows, ncols, num = args + i, j = np.resize(num, 2) + if gs is None: + gs = pgridspec.GridSpec(nrows, ncols) + orows, ocols = gs.get_geometry() + if orows % nrows: + raise ValueError( + f'The input number of rows {nrows} does not divide the ' + f'figure gridspec number of rows {orows}.' + ) + if ocols % ncols: + raise ValueError( + f'The input number of columns {ncols} does not divide the ' + f'figure gridspec number of columns {ocols}.' + ) + if any(_ < 1 or _ > nrows * ncols for _ in (i, j)): + raise ValueError( + 'The input subplot indices must fall between ' + f'1 and {nrows * ncols}. Instead got {i} and {j}.' + ) + rowfact, colfact = orows // nrows, ocols // ncols + irow, icol = divmod(i - 1, ncols) # convert to zero-based + jrow, jcol = divmod(j - 1, ncols) + irow, icol = irow * rowfact, icol * colfact + jrow, jcol = (jrow + 1) * rowfact - 1, (jcol + 1) * colfact - 1 + ss = gs[irow:jrow + 1, icol:jcol + 1] + + # Otherwise + else: + raise ValueError(f'Invalid add_subplot positional arguments {args!r}.') + + # Add the subplot + # NOTE: Pass subplotspec as keyword arg for mpl >= 3.4 workaround + # NOTE: Must assign unique label to each subplot or else subsequent calls + # to add_subplot() in mpl < 3.4 may return an already-drawn subplot in the + # wrong location due to gridspec override. Is against OO package design. + self.gridspec = gs # trigger layout adjustment + self._subplot_counter += 1 # unique label for each subplot + kwargs.setdefault('label', f'subplot_{self._subplot_counter}') + kwargs.setdefault('number', 1 + max(self._subplot_dict, default=0)) + ax = super().add_subplot(ss, _subplot_spec=ss, **kwargs) + if ax.number: + self._subplot_dict[ax.number] = ax + return ax + + def _add_subplots( + self, array=None, nrows=1, ncols=1, order='C', proj=None, projection=None, + proj_kw=None, projection_kw=None, backend=None, basemap=None, **kwargs + ): + """ + The driver function for adding multiple subplots. + """ + # Clunky helper function + # TODO: Consider deprecating and asking users to use add_subplot() + def _axes_dict(naxs, input, kw=False, default=None): + # First build up dictionary + if not kw: # 'string' or {1: 'string1', (2, 3): 'string2'} + if np.iterable(input) and not isinstance(input, (str, dict)): + input = {num + 1: item for num, item in enumerate(input)} + elif not isinstance(input, dict): + input = {range(1, naxs + 1): input} + else: # {key: value} or {1: {key: value1}, (2, 3): {key: value2}} + nested = [isinstance(_, dict) for _ in input.values()] + if not any(nested): # any([]) == False + input = {range(1, naxs + 1): input.copy()} + elif not all(nested): + raise ValueError(f'Invalid input {input!r}.') + # Unfurl keys that contain multiple axes numbers + output = {} + for nums, item in input.items(): + nums = np.atleast_1d(nums) + for num in nums.flat: + output[num] = item.copy() if kw else item + # Fill with default values + for num in range(1, naxs + 1): + if num not in output: + output[num] = {} if kw else default + if output.keys() != set(range(1, naxs + 1)): + raise ValueError( + f'Have {naxs} axes, but {input!r} includes props for the axes: ' + + ', '.join(map(repr, sorted(output))) + '.' + ) + return output + + # Build the subplot array + # NOTE: Currently this may ignore user-input nrows/ncols without warning + if order not in ('C', 'F'): # better error message + raise ValueError(f"Invalid order={order!r}. Options are 'C' or 'F'.") + gs = None + if array is None or isinstance(array, mgridspec.GridSpec): + if array is not None: + gs, nrows, ncols = array, array.nrows, array.ncols + array = np.arange(1, nrows * ncols + 1)[..., None] + array = array.reshape((nrows, ncols), order=order) + else: + array = np.atleast_1d(array) + array[array == None] = 0 # None or 0 both valid placeholders # noqa: E711 + array = array.astype(int) + if array.ndim == 1: # interpret as single row or column + array = array[None, :] if order == 'C' else array[:, None] + elif array.ndim != 2: + raise ValueError(f'Expected 1D or 2D array of integers. Got {array}.') + + # Parse input format, gridspec, and projection arguments + # NOTE: Permit figure format keywords for e.g. 'collabels' (more intuitive) + nums = np.unique(array[array != 0]) + naxs = len(nums) + if any(num < 0 or not isinstance(num, Integral) for num in nums.flat): + raise ValueError(f'Expected array of positive integers. Got {array}.') + proj = _not_none(projection=projection, proj=proj) + proj = _axes_dict(naxs, proj, kw=False, default='cartesian') + proj_kw = _not_none(projection_kw=projection_kw, proj_kw=proj_kw) or {} + proj_kw = _axes_dict(naxs, proj_kw, kw=True) + backend = self._parse_backend(backend, basemap) + backend = _axes_dict(naxs, backend, kw=False) + axes_kw = { + num: {'proj': proj[num], 'proj_kw': proj_kw[num], 'backend': backend[num]} + for num in proj + } + for key in ('gridspec_kw', 'subplot_kw'): + kw = kwargs.pop(key, None) + if not kw: + continue + warnings._warn_proplot( + f'{key!r} is not necessary in proplot. Pass the ' + 'parameters as keyword arguments instead.' + ) + kwargs.update(kw or {}) + figure_kw = _pop_params(kwargs, self._format_signature) + gridspec_kw = _pop_params(kwargs, pgridspec.GridSpec._update_params) + + # Create or update the gridspec and add subplots with subplotspecs + # NOTE: The gridspec is added to the figure when we pass the subplotspec + if gs is None: + gs = pgridspec.GridSpec(*array.shape, **gridspec_kw) + else: + gs.update(**gridspec_kw) + axs = naxs * [None] # list of axes + axids = [np.where(array == i) for i in np.sort(np.unique(array)) if i > 0] + axcols = np.array([[x.min(), x.max()] for _, x in axids]) + axrows = np.array([[y.min(), y.max()] for y, _ in axids]) + for idx in range(naxs): + num = idx + 1 + x0, x1 = axcols[idx, 0], axcols[idx, 1] + y0, y1 = axrows[idx, 0], axrows[idx, 1] + ss = gs[y0:y1 + 1, x0:x1 + 1] + kw = {**kwargs, **axes_kw[num], 'number': num} + axs[idx] = self.add_subplot(ss, **kw) + + self.format(skip_axes=True, **figure_kw) + return pgridspec.SubplotGrid(axs) + + def _align_axis_label(self, x): + """ + Align *x* and *y* axis labels in the perpendicular and parallel directions. + """ + # NOTE: Always use 'align' if 'span' is True to get correct offset + # NOTE: Must trigger axis sharing here so that super label alignment + # with tight=False is valid. Kind of kludgey but oh well. + seen = set() + span = getattr(self, '_span' + x) + align = getattr(self, '_align' + x) + for ax in self._subplot_dict.values(): + if isinstance(ax, paxes.CartesianAxes): + ax._apply_axis_sharing() # always! + else: + continue + pos = getattr(ax, x + 'axis').get_label_position() + if ax in seen or pos not in ('bottom', 'left'): + continue # already aligned or cannot align + axs = ax._get_span_axes(pos, panels=False) # returns panel or main axes + if any(getattr(ax, '_share' + x) for ax in axs): + continue # nothing to align or axes have parents + seen.update(axs) + if span or align: + if hasattr(self, '_align_label_groups'): + group = self._align_label_groups[x] + else: + group = getattr(self, '_align_' + x + 'label_grp', None) + if group is not None: # fail silently to avoid fragile API changes + for ax in axs[1:]: + group.join(axs[0], ax) # add to grouper + if span: + self._update_axis_label(pos, axs) + + def _align_super_labels(self, side, renderer): + """ + Adjust the position of super labels. + """ + # NOTE: Ensure title is offset only here. + for ax in self._subplot_dict.values(): + ax._apply_title_above() + if side not in ('left', 'right', 'bottom', 'top'): + raise ValueError(f'Invalid side {side!r}.') + labs = self._suplabel_dict[side] + axs = tuple(ax for ax, lab in labs.items() if lab.get_text()) + if not axs: + return + c = self._get_offset_coord(side, axs, renderer) + for lab in labs.values(): + s = 'x' if side in ('left', 'right') else 'y' + lab.update({s: c}) + + def _align_super_title(self, renderer): + """ + Adjust the position of the super title. + """ + if not self._suptitle.get_text(): + return + axs = self._get_align_axes('top') # returns outermost panels + if not axs: + return + labs = tuple(t for t in self._suplabel_dict['top'].values() if t.get_text()) + pad = (self._suptitle_pad / 72) / self.get_size_inches()[1] + x, _ = self._get_align_coord('top', axs, includepanels=self._includepanels) + y = self._get_offset_coord('top', axs, renderer, pad=pad, extra=labs) + self._suptitle.set_ha('center') + self._suptitle.set_va('bottom') + self._suptitle.set_position((x, y)) + + def _update_axis_label(self, side, axs): + """ + Update the aligned axis label for the input axes. + """ + # Get the central axis and the spanning label (initialize if it does not exist) + # NOTE: Previously we secretly used matplotlib axis labels for spanning labels, + # offsetting them between two subplots if necessary. Now we track designated + # 'super' labels and replace the actual labels with spaces so they still impact + # the tight bounding box and thus allocate space for the spanning label. + x, y = 'xy' if side in ('bottom', 'top') else 'yx' + c, ax = self._get_align_coord(side, axs, includepanels=self._includepanels) + axlab = getattr(ax, x + 'axis').label # the central label + suplabs = getattr(self, '_sup' + x + 'label_dict') # dict of spanning labels + suplab = suplabs.get(ax, None) + if suplab is None and not axlab.get_text().strip(): + return # nothing to transfer from the normal label + if suplab is not None and not suplab.get_text().strip(): + return # nothing to update on the super label + if suplab is None: + props = ('ha', 'va', 'rotation', 'rotation_mode') + suplab = suplabs[ax] = self.text(0, 0, '') + suplab.update({prop: getattr(axlab, 'get_' + prop)() for prop in props}) + + # Copy text from the central label to the spanning label + # NOTE: Must use spaces rather than newlines, otherwise tight layout + # won't make room. Reason is Text implementation (see Text._get_layout()) + labels._transfer_label(axlab, suplab) # text, color, and font properties + count = 1 + suplab.get_text().count('\n') + space = '\n'.join(' ' * count) + for ax in axs: # includes original 'axis' + axis = getattr(ax, x + 'axis') + axis.label.set_text(space) + + # Update spanning label position then add simple monkey patch + # NOTE: Simply using axis._update_label_position() when this is + # called is not sufficient. Fails with e.g. inline backend. + t = mtransforms.IdentityTransform() # set in pixels + cx, cy = axlab.get_position() + if x == 'x': + trans = mtransforms.blended_transform_factory(self.transFigure, t) + coord = (c, cy) + else: + trans = mtransforms.blended_transform_factory(t, self.transFigure) + coord = (cx, c) + suplab.set_transform(trans) + suplab.set_position(coord) + setpos = getattr(mtext.Text, 'set_' + y) + def _set_coord(self, *args, **kwargs): # noqa: E306 + setpos(self, *args, **kwargs) + setpos(suplab, *args, **kwargs) + setattr(axlab, 'set_' + y, _set_coord.__get__(axlab)) + + def _update_super_labels(self, side, labels, **kwargs): + """ + Assign the figure super labels and update settings. + """ + # Update the label parameters + if side not in ('left', 'right', 'bottom', 'top'): + raise ValueError(f'Invalid side {side!r}.') + kw = rc.fill( + { + 'color': side + 'label.color', + 'rotation': side + 'label.rotation', + 'size': side + 'label.size', + 'weight': side + 'label.weight', + 'family': 'font.family', + }, + context=True, + ) + kw.update(kwargs) # used when updating *existing* labels + props = self._suplabel_props[side] + props.update(kw) # used when creating *new* labels + + # Get the label axes + # WARNING: In case users added labels then changed the subplot geometry we + # have to remove labels whose axes don't match the current 'align' axes. + axs = self._get_align_axes(side) + if not axs: + return # occurs if called while adding axes + if not labels: + labels = [None for _ in axs] # indicates that text should not be updated + if not kw and all(_ is None for _ in labels): + return # nothing to update + if len(labels) != len(axs): + raise ValueError( + f'Got {len(labels)} {side} labels but found {len(axs)} axes ' + f'along the {side} side of the figure.' + ) + src = self._suplabel_dict[side] + extra = src.keys() - set(axs) + for ax in extra: # e.g. while adding axes + text = src[ax].get_text() + if text: + warnings._warn_proplot( + f'Removing {side} label with text {text!r} from axes {ax.number}.' + ) + src[ax].remove() # remove from the figure + + # Update the label text + tf = self.transFigure + for ax, label in zip(axs, labels): + if ax in src: + obj = src[ax] + elif side in ('left', 'right'): + trans = mtransforms.blended_transform_factory(tf, ax.transAxes) + obj = src[ax] = self.text(0, 0.5, '', transform=trans) + obj.update(props) + else: + trans = mtransforms.blended_transform_factory(ax.transAxes, tf) + obj = src[ax] = self.text(0.5, 0, '', transform=trans) + obj.update(props) + if kw: + obj.update(kw) + if label is not None: + obj.set_text(label) + + def _update_super_title(self, title, **kwargs): + """ + Assign the figure super title and update settings. + """ + kw = rc.fill( + { + 'size': 'suptitle.size', + 'weight': 'suptitle.weight', + 'color': 'suptitle.color', + 'family': 'font.family' + }, + context=True, + ) + kw.update(kwargs) + if kw: + self._suptitle.update(kw) + if title is not None: + self._suptitle.set_text(title) + + @docstring._concatenate_inherited + @docstring._snippet_manager + def add_axes(self, rect, **kwargs): + """ + %(figure.axes)s + """ + kwargs = self._parse_proj(**kwargs) + return super().add_axes(rect, **kwargs) + + @docstring._concatenate_inherited + @docstring._snippet_manager + def add_subplot(self, *args, **kwargs): + """ + %(figure.subplot)s + """ + return self._add_subplot(*args, **kwargs) + + @docstring._snippet_manager + def subplot(self, *args, **kwargs): # shorthand + """ + %(figure.subplot)s + """ + return self._add_subplot(*args, **kwargs) + + @docstring._snippet_manager + def add_subplots(self, *args, **kwargs): + """ + %(figure.subplots)s + """ + return self._add_subplots(*args, **kwargs) + + @docstring._snippet_manager + def subplots(self, *args, **kwargs): + """ + %(figure.subplots)s + """ + return self._add_subplots(*args, **kwargs) + + def auto_layout(self, renderer=None, aspect=None, tight=None, resize=None): + """ + Automatically adjust the figure size and subplot positions. This is + triggered automatically whenever the figure is drawn. + + Parameters + ---------- + renderer : `~matplotlib.backend_bases.RendererBase`, optional + The renderer. If ``None`` a default renderer will be produced. + aspect : bool, optional + Whether to update the figure size based on the reference subplot aspect + ratio. By default, this is ``True``. This only has an effect if the + aspect ratio is fixed (e.g., due to an image plot or geographic projection). + tight : bool, optional + Whether to update the figuer size and subplot positions according to + a "tight layout". By default, this takes on the value of `tight` passed + to `Figure`. If nothing was passed, it is :rc:`subplots.tight`. + resize : bool, optional + If ``False``, the current figure dimensions are fixed and automatic + figure resizing is disabled. By default, the figure size may change + unless both `figwidth` and `figheight` or `figsize` were passed + to `~Figure.subplots`, `~Figure.set_size_inches` was called manually, + or the figure was resized manually with an interactive backend. + """ + # *Impossible* to get notebook backend to work with auto resizing so we + # just do the tight layout adjustments and skip resizing. + gs = self.gridspec + renderer = self._get_renderer() + if aspect is None: + aspect = True + if tight is None: + tight = self._tight_active + if resize is False: # fix the size + self._figwidth, self._figheight = self.get_size_inches() + self._refwidth = self._refheight = None # critical! + + # Helper functions + # NOTE: Have to draw legends and colorbars early (before reaching axes + # draw methods) because we have to take them into account for alignment. + # Also requires another figure resize (which triggers a gridspec update). + def _draw_content(): + for ax in self._iter_axes(hidden=False, children=True): + ax._add_queued_guides() # may trigger resizes if panels are added + def _align_content(): # noqa: E306 + for axis in 'xy': + self._align_axis_label(axis) + for side in ('left', 'right', 'top', 'bottom'): + self._align_super_labels(side, renderer) + self._align_super_title(renderer) + + # Update the layout + # WARNING: Tried to avoid two figure resizes but made + # subsequent tight layout really weird. Have to resize twice. + _draw_content() + if not gs: + return + if aspect: + gs._auto_layout_aspect() + _align_content() + if tight: + gs._auto_layout_tight(renderer) + _align_content() + + @warnings._rename_kwargs( + '0.10.0', mathtext_fallback='pplt.rc.mathtext_fallback = {}' + ) + @docstring._snippet_manager + def format( + self, axs=None, *, + figtitle=None, suptitle=None, suptitle_kw=None, + llabels=None, leftlabels=None, leftlabels_kw=None, + rlabels=None, rightlabels=None, rightlabels_kw=None, + blabels=None, bottomlabels=None, bottomlabels_kw=None, + tlabels=None, toplabels=None, toplabels_kw=None, + rowlabels=None, collabels=None, # aliases + includepanels=None, **kwargs, + ): + """ + Modify figure-wide labels and call ``format`` for the + input axes. By default the numbered subplots are used. + + Parameters + ---------- + axs : sequence of `~proplot.axes.Axes`, optional + The axes to format. Default is the numbered subplots. + %(figure.format)s + + Important + --------- + `leftlabelpad`, `toplabelpad`, `rightlabelpad`, and `bottomlabelpad` + keywords are actually :ref:`configuration settings `. + We explicitly document these arguments here because it is common to + change them for specific figures. But many :ref:`other configuration + settings ` can be passed to ``format`` too. + + Other parameters + ---------------- + %(axes.format)s + %(cartesian.format)s + %(polar.format)s + %(geo.format)s + %(rc.format)s + + See also + -------- + proplot.axes.Axes.format + proplot.axes.CartesianAxes.format + proplot.axes.PolarAxes.format + proplot.axes.GeoAxes.format + proplot.gridspec.SubplotGrid.format + proplot.config.Configurator.context + """ + # Initiate context block + axs = axs or self._subplot_dict.values() + skip_axes = kwargs.pop('skip_axes', False) # internal keyword arg + rc_kw, rc_mode = _pop_rc(kwargs) + with rc.context(rc_kw, mode=rc_mode): + # Update background patch + kw = rc.fill({'facecolor': 'figure.facecolor'}, context=True) + self.patch.update(kw) + + # Update super title and label spacing + pad = rc.find('suptitle.pad', context=True) # super title + if pad is not None: + self._suptitle_pad = pad + for side in tuple(self._suplabel_pad): # super labels + pad = rc.find(side + 'label.pad', context=True) + if pad is not None: + self._suplabel_pad[side] = pad + if includepanels is not None: + self._includepanels = includepanels + + # Update super title and labels text and settings + suptitle_kw = suptitle_kw or {} + leftlabels_kw = leftlabels_kw or {} + rightlabels_kw = rightlabels_kw or {} + bottomlabels_kw = bottomlabels_kw or {} + toplabels_kw = toplabels_kw or {} + self._update_super_title( + _not_none(figtitle=figtitle, suptitle=suptitle), + **suptitle_kw, + ) + self._update_super_labels( + 'left', + _not_none(rowlabels=rowlabels, leftlabels=leftlabels, llabels=llabels), + **leftlabels_kw, + ) + self._update_super_labels( + 'right', + _not_none(rightlabels=rightlabels, rlabels=rlabels), + **rightlabels_kw, + ) + self._update_super_labels( + 'bottom', + _not_none(bottomlabels=bottomlabels, blabels=blabels), + **bottomlabels_kw, + ) + self._update_super_labels( + 'top', + _not_none(collabels=collabels, toplabels=toplabels, tlabels=tlabels), + **toplabels_kw, + ) + + # Update the main axes + if skip_axes: # avoid recursion + return + kws = { + cls: _pop_params(kwargs, sig) + for cls, sig in paxes.Axes._format_signatures.items() + } + classes = set() # track used dictionaries + for ax in axs: + kw = { + key: value for cls, kw in kws.items() + for key, value in kw.items() + if isinstance(ax, cls) and not classes.add(cls) + } + ax.format(rc_kw=rc_kw, rc_mode=rc_mode, skip_figure=True, **kw, **kwargs) + + # Warn unused keyword argument(s) + kw = { + key: value for name in kws.keys() - classes + for key, value in kws[name].items() + } + if kw: + warnings._warn_proplot( + f'Ignoring unused projection-specific format() keyword argument(s): {kw}' # noqa: E501 + ) + + @docstring._concatenate_inherited + @docstring._snippet_manager + def colorbar( + self, mappable, values=None, loc=None, location=None, + row=None, col=None, rows=None, cols=None, span=None, + space=None, pad=None, width=None, **kwargs + ): + """ + Add a colorbar along the side of the figure. + + Parameters + ---------- + %(axes.colorbar_args)s + length : float, default: :rc:`colorbar.length` + The colorbar length. Units are relative to the span of the rows and + columns of subplots. + shrink : float, optional + Alias for `length`. This is included for consistency with + `matplotlib.figure.Figure.colorbar`. + width : unit-spec, default: :rc:`colorbar.width` + The colorbar width. + %(units.in)s + %(figure.colorbar_space)s + Has no visible effect if `length` is ``1``. + + Other parameters + ---------------- + %(axes.colorbar_kwargs)s + + See also + -------- + proplot.axes.Axes.colorbar + matplotlib.figure.Figure.colorbar + """ + # Backwards compatibility + ax = kwargs.pop('ax', None) + cax = kwargs.pop('cax', None) + if isinstance(values, maxes.Axes): + cax = _not_none(cax_positional=values, cax=cax) + values = None + if isinstance(loc, maxes.Axes): + ax = _not_none(ax_positional=loc, ax=ax) + loc = None + # Helpful warning + if kwargs.pop('use_gridspec', None) is not None: + warnings._warn_proplot( + "Ignoring the 'use_gridspec' keyword. Proplot always allocates " + 'additional space for colorbars using the figure gridspec ' + "rather than 'stealing space' from the parent subplot." + ) + # Fill this axes + if cax is not None: + with context._state_context(cax, _internal_call=True): # do not wrap pcolor + cb = super().colorbar(mappable, cax=cax, **kwargs) + # Axes panel colorbar + elif ax is not None: + cb = ax.colorbar( + mappable, values, space=space, pad=pad, width=width, **kwargs + ) + # Figure panel colorbar + else: + loc = _not_none(loc=loc, location=location, default='r') + ax = self._add_figure_panel( + loc, row=row, col=col, rows=rows, cols=cols, span=span, + width=width, space=space, pad=pad, + ) + cb = ax.colorbar(mappable, values, loc='fill', **kwargs) + return cb + + @docstring._concatenate_inherited + @docstring._snippet_manager + def legend( + self, handles=None, labels=None, loc=None, location=None, + row=None, col=None, rows=None, cols=None, span=None, + space=None, pad=None, width=None, **kwargs + ): + """ + Add a legend along the side of the figure. + + Parameters + ---------- + %(axes.legend_args)s + %(figure.legend_space)s + width : unit-spec, optional + The space allocated for the legend box. This does nothing if + the :ref:`tight layout algorithm ` is active for the figure. + %(units.in)s + + Other parameters + ---------------- + %(axes.legend_kwargs)s + + See also + -------- + proplot.axes.Axes.legend + matplotlib.axes.Axes.legend + """ + ax = kwargs.pop('ax', None) + # Axes panel legend + if ax is not None: + leg = ax.legend( + handles, labels, space=space, pad=pad, width=width, **kwargs + ) + # Figure panel legend + else: + loc = _not_none(loc=loc, location=location, default='r') + ax = self._add_figure_panel( + loc, row=row, col=col, rows=rows, cols=cols, span=span, + width=width, space=space, pad=pad, + ) + leg = ax.legend(handles, labels, loc='fill', **kwargs) + return leg + + @docstring._snippet_manager + def save(self, filename, **kwargs): + """ + %(figure.save)s + """ + return self.savefig(filename, **kwargs) + + @docstring._concatenate_inherited + @docstring._snippet_manager + def savefig(self, filename, **kwargs): + """ + %(figure.save)s + """ + # Automatically expand the user name. Undocumented because we + # do not want to overwrite the matplotlib docstring. + if isinstance(filename, str): + filename = os.path.expanduser(filename) + super().savefig(filename, **kwargs) + + @docstring._concatenate_inherited + def set_canvas(self, canvas): + """ + Set the figure canvas. Add monkey patches for the instance-level + `~matplotlib.backend_bases.FigureCanvasBase.draw` and + `~matplotlib.backend_bases.FigureCanvasBase.print_figure` methods. + + Parameters + ---------- + canvas : `~matplotlib.backend_bases.FigureCanvasBase` + The figure canvas. + + See also + -------- + matplotlib.figure.Figure.set_canvas + """ + # NOTE: Use the _draw method if it exists, e.g. for osx backends. Critical + # or else wrong renderer size is used. + # NOTE: See _add_canvas_preprocessor for details. Critical to not add cache + # print_figure renderer when the print method (print_pdf, print_png, etc.) + # calls Figure.draw(). Otherwise have issues where (1) figure size and/or + # bounds are incorrect after saving figure *then* displaying it in qt or inline + # notebook backends, and (2) figure fails to update correctly after successively + # modifying and displaying within inline notebook backend (previously worked + # around this by forcing additional draw() call in this function before + # proceeding with print_figure). Set the canvas and add monkey patches + # to the instance-level draw and print_figure methods. + method = '_draw' if callable(getattr(canvas, '_draw', None)) else 'draw' + _add_canvas_preprocessor(canvas, 'print_figure', cache=False) # saves, inlines + _add_canvas_preprocessor(canvas, method, cache=True) # renderer displays + super().set_canvas(canvas) + + def _is_same_size(self, figsize, eps=None): + """ + Test if the figure size is unchanged up to some tolerance in inches. + """ + eps = _not_none(eps, 0.01) + figsize_active = self.get_size_inches() + if figsize is None: # e.g. GridSpec._calc_figsize() returned None + return True + else: + return np.all(np.isclose(figsize, figsize_active, rtol=0, atol=eps)) + + @docstring._concatenate_inherited + def set_size_inches(self, w, h=None, *, forward=True, internal=False, eps=None): + """ + Set the figure size. If this is being called manually or from an interactive + backend, update the default layout with this fixed size. If the figure size is + unchanged or this is an internal call, do not update the default layout. + + Parameters + ---------- + *args : float + The width and height passed as positional arguments or a 2-tuple. + forward : bool, optional + Whether to update the canvas. + internal : bool, optional + Whether this is an internal resize. + eps : float, optional + The deviation from the current size in inches required to treat this + as a user-triggered figure resize that fixes the layout. + + See also + -------- + matplotlib.figure.Figure.set_size_inches + """ + # Parse input args + figsize = w if h is None else (w, h) + if not np.all(np.isfinite(figsize)): + raise ValueError(f'Figure size must be finite, not {figsize}.') + + # Fix the figure size if this is a user action from an interactive backend + # NOTE: If we fail to detect 'user' resize from the user, not only will + # result be incorrect, but qt backend will crash because it detects a + # recursive size change, since preprocessor size will differ. + # NOTE: Bitmap renderers calculate the figure size in inches from + # int(Figure.bbox.[width|height]) which rounds to whole pixels. When + # renderer calls set_size_inches, size may be effectively the same, but + # slightly changed due to roundoff error! Therefore only compare approx size. + attrs = ('_is_idle_drawing', '_is_drawing', '_draw_pending') + backend = any(getattr(self.canvas, attr, None) for attr in attrs) + internal = internal or self._is_adjusting + samesize = self._is_same_size(figsize, eps) + ctx = context._empty_context() # context not necessary most of the time + if not backend and not internal and not samesize: + ctx = self._context_adjusting() # do not trigger layout solver + self._figwidth, self._figheight = figsize + self._refwidth = self._refheight = None # critical! + + # Apply the figure size + # NOTE: If size changes we always update the gridspec to enforce fixed spaces + # and panel widths (necessary since axes use figure relative coords) + with ctx: # avoid recursion + super().set_size_inches(figsize, forward=forward) + if not samesize: # gridspec positions will resolve differently + self.gridspec.update() + + def _iter_axes(self, hidden=False, children=False, panels=True): + """ + Iterate over all axes and panels in the figure belonging to the + `~proplot.axes.Axes` class. Exclude inset and twin axes. + + Parameters + ---------- + hidden : bool, optional + Whether to include "hidden" panels. + children : bool, optional + Whether to include child axes. Note this now includes "twin" axes. + panels : bool or str or sequence of str, optional + Whether to include panels or the panels to include. + """ + # Parse panels + if panels is False: + panels = () + elif panels is True or panels is None: + panels = ('left', 'right', 'bottom', 'top') + elif isinstance(panels, str): + panels = (panels,) + if not set(panels) <= {'left', 'right', 'bottom', 'top'}: + raise ValueError(f'Invalid sides {panels!r}.') + # Iterate + axs = ( + *self._subplot_dict.values(), + *(ax for side in panels for ax in self._panel_dict[side]), + ) + for ax in axs: + if not hidden and ax._panel_hidden: + continue # ignore hidden panel and its colorbar/legend child + yield from ax._iter_axes(hidden=hidden, children=children, panels=panels) + + @property + def gridspec(self): + """ + The single `~proplot.gridspec.GridSpec` instance used for all + subplots in the figure. + + See also + -------- + proplot.figure.Figure.subplotgrid + proplot.gridspec.GridSpec.figure + proplot.gridspec.SubplotGrid.gridspec + """ + return self._gridspec + + @gridspec.setter + def gridspec(self, gs): + if not isinstance(gs, pgridspec.GridSpec): + raise ValueError('Gridspec must be a proplot.GridSpec instance.') + self._gridspec = gs + gs.figure = self # trigger copying settings from the figure + + @property + def subplotgrid(self): + """ + A `~proplot.gridspec.SubplotGrid` containing the numbered subplots in the + figure. The subplots are ordered by increasing `~proplot.axes.Axes.number`. + + See also + -------- + proplot.figure.Figure.gridspec + proplot.gridspec.SubplotGrid.figure + """ + return pgridspec.SubplotGrid([s for _, s in sorted(self._subplot_dict.items())]) + + @property + def tight(self): + """ + Whether the :ref:`tight layout algorithm ` is active for the + figure. This value is passed to `~proplot.figure.Figure.auto_layout` + every time the figure is drawn. Can be changed e.g. ``fig.tight = False``. + + See also + -------- + proplot.figure.Figure.auto_layout + """ + return self._tight_active + + @tight.setter + def tight(self, b): + self._tight_active = bool(b) + + # Apply signature obfuscation after getting keys + # NOTE: This is needed for axes and figure instantiation. + _format_signature = inspect.signature(format) + format = docstring._obfuscate_kwargs(format) + + +# Add deprecated properties. There are *lots* of properties we pass to Figure +# and do not like idea of publicly tracking every single one of them. If we +# want to improve user introspection consider modifying Figure.__repr__. +for _attr in ('alignx', 'aligny', 'sharex', 'sharey', 'spanx', 'spany', 'tight', 'ref'): + def _get_deprecated(self, attr=_attr): + warnings._warn_proplot( + f'The property {attr!r} is no longer public as of v0.8. It will be ' + 'removed in a future release.' + ) + return getattr(self, '_' + attr) + _getter = property(_get_deprecated) + setattr(Figure, _attr, property(_get_deprecated)) + + +# Disable native matplotlib layout and spacing functions when called +# manually and emit warning message to help new users. +for _attr, _msg in ( + ('set_tight_layout', Figure._tight_message), + ('set_constrained_layout', Figure._tight_message), + ('tight_layout', Figure._tight_message), + ('init_layoutbox', Figure._tight_message), + ('execute_constrained_layout', Figure._tight_message), + ('subplots_adjust', Figure._space_message), +): + _func = getattr(Figure, _attr, None) + if _func is None: + continue + @functools.wraps(_func) # noqa: E301 + def _disable_method(self, *args, func=_func, message=_msg, **kwargs): + message = f'fig.{func.__name__}() has no effect on proplot figures. ' + message + if self._is_authorized: + return func(self, *args, **kwargs) + else: + warnings._warn_proplot(message) # noqa: E501, U100 + _disable_method.__doc__ = None # remove docs + setattr(Figure, _attr, _disable_method) diff --git a/proplot/fonts/FiraSans-Black.ttf b/proplot/fonts/FiraSans-Black.ttf new file mode 100644 index 000000000..3087a31bc Binary files /dev/null and b/proplot/fonts/FiraSans-Black.ttf differ diff --git a/proplot/fonts/FiraSans-BlackItalic.ttf b/proplot/fonts/FiraSans-BlackItalic.ttf new file mode 100644 index 000000000..9a9ef5e84 Binary files /dev/null and b/proplot/fonts/FiraSans-BlackItalic.ttf differ diff --git a/proplot/fonts/FiraSans-Bold.ttf b/proplot/fonts/FiraSans-Bold.ttf new file mode 100644 index 000000000..0fb896aec Binary files /dev/null and b/proplot/fonts/FiraSans-Bold.ttf differ diff --git a/proplot/fonts/FiraSans-BoldItalic.ttf b/proplot/fonts/FiraSans-BoldItalic.ttf new file mode 100644 index 000000000..e7e936f7b Binary files /dev/null and b/proplot/fonts/FiraSans-BoldItalic.ttf differ diff --git a/proplot/fonts/FiraSans-ExtraBold.ttf b/proplot/fonts/FiraSans-ExtraBold.ttf new file mode 100644 index 000000000..4b29d6f8a Binary files /dev/null and b/proplot/fonts/FiraSans-ExtraBold.ttf differ diff --git a/proplot/fonts/FiraSans-ExtraBoldItalic.ttf b/proplot/fonts/FiraSans-ExtraBoldItalic.ttf new file mode 100644 index 000000000..de3b83b48 Binary files /dev/null and b/proplot/fonts/FiraSans-ExtraBoldItalic.ttf differ diff --git a/proplot/fonts/FiraSans-ExtraLight.ttf b/proplot/fonts/FiraSans-ExtraLight.ttf new file mode 100644 index 000000000..e5755da8d Binary files /dev/null and b/proplot/fonts/FiraSans-ExtraLight.ttf differ diff --git a/proplot/fonts/FiraSans-ExtraLightItalic.ttf b/proplot/fonts/FiraSans-ExtraLightItalic.ttf new file mode 100644 index 000000000..890524e9f Binary files /dev/null and b/proplot/fonts/FiraSans-ExtraLightItalic.ttf differ diff --git a/proplot/fonts/FiraSans-Italic.ttf b/proplot/fonts/FiraSans-Italic.ttf new file mode 100644 index 000000000..36efca2a7 Binary files /dev/null and b/proplot/fonts/FiraSans-Italic.ttf differ diff --git a/proplot/fonts/FiraSans-Light.ttf b/proplot/fonts/FiraSans-Light.ttf new file mode 100644 index 000000000..fac4edf54 Binary files /dev/null and b/proplot/fonts/FiraSans-Light.ttf differ diff --git a/proplot/fonts/FiraSans-LightItalic.ttf b/proplot/fonts/FiraSans-LightItalic.ttf new file mode 100644 index 000000000..1daa0bcce Binary files /dev/null and b/proplot/fonts/FiraSans-LightItalic.ttf differ diff --git a/proplot/fonts/FiraSans-Medium.ttf b/proplot/fonts/FiraSans-Medium.ttf new file mode 100644 index 000000000..eeb8f8f0b Binary files /dev/null and b/proplot/fonts/FiraSans-Medium.ttf differ diff --git a/proplot/fonts/FiraSans-MediumItalic.ttf b/proplot/fonts/FiraSans-MediumItalic.ttf new file mode 100644 index 000000000..328b53b2c Binary files /dev/null and b/proplot/fonts/FiraSans-MediumItalic.ttf differ diff --git a/proplot/fonts/FiraSans-Regular.ttf b/proplot/fonts/FiraSans-Regular.ttf new file mode 100644 index 000000000..c4cfa5975 Binary files /dev/null and b/proplot/fonts/FiraSans-Regular.ttf differ diff --git a/proplot/fonts/FiraSans-SemiBold.ttf b/proplot/fonts/FiraSans-SemiBold.ttf new file mode 100644 index 000000000..954a2cab8 Binary files /dev/null and b/proplot/fonts/FiraSans-SemiBold.ttf differ diff --git a/proplot/fonts/FiraSans-SemiBoldItalic.ttf b/proplot/fonts/FiraSans-SemiBoldItalic.ttf new file mode 100644 index 000000000..55e812c66 Binary files /dev/null and b/proplot/fonts/FiraSans-SemiBoldItalic.ttf differ diff --git a/proplot/fonts/LICENSE_FIRASANS.txt b/proplot/fonts/LICENSE_FIRASANS.txt new file mode 100644 index 000000000..70e4f161e --- /dev/null +++ b/proplot/fonts/LICENSE_FIRASANS.txt @@ -0,0 +1,97 @@ +Copyright (c) , (), +with Reserved Font Name . +Copyright (c) , (), +with Reserved Font Name . +Copyright (c) , (). + +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 + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/proplot/fonts/LICENSE_NOTOSERIF.txt b/proplot/fonts/LICENSE_NOTOSERIF.txt new file mode 100644 index 000000000..fce27fb35 --- /dev/null +++ b/proplot/fonts/LICENSE_NOTOSERIF.txt @@ -0,0 +1,93 @@ +Copyright 2012 Google Inc. All Rights Reserved. + +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 + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/proplot/fonts/LICENSE_SOURCESERIF.txt b/proplot/fonts/LICENSE_SOURCESERIF.txt new file mode 100644 index 000000000..e6efdaf8e --- /dev/null +++ b/proplot/fonts/LICENSE_SOURCESERIF.txt @@ -0,0 +1,93 @@ +Copyright 2014 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries. + +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 + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/proplot/fonts/NotoSerif-Bold.ttf b/proplot/fonts/NotoSerif-Bold.ttf new file mode 100644 index 000000000..156cfcbe0 Binary files /dev/null and b/proplot/fonts/NotoSerif-Bold.ttf differ diff --git a/proplot/fonts/NotoSerif-BoldItalic.ttf b/proplot/fonts/NotoSerif-BoldItalic.ttf new file mode 100644 index 000000000..d82186466 Binary files /dev/null and b/proplot/fonts/NotoSerif-BoldItalic.ttf differ diff --git a/proplot/fonts/NotoSerif-Italic.ttf b/proplot/fonts/NotoSerif-Italic.ttf new file mode 100644 index 000000000..101bb8c2d Binary files /dev/null and b/proplot/fonts/NotoSerif-Italic.ttf differ diff --git a/proplot/fonts/NotoSerif-Regular.ttf b/proplot/fonts/NotoSerif-Regular.ttf new file mode 100644 index 000000000..e5587fc7a Binary files /dev/null and b/proplot/fonts/NotoSerif-Regular.ttf differ diff --git a/proplot/fonts/SourceSerifPro-Black.ttf b/proplot/fonts/SourceSerifPro-Black.ttf new file mode 100644 index 000000000..4562a7ff3 Binary files /dev/null and b/proplot/fonts/SourceSerifPro-Black.ttf differ diff --git a/proplot/fonts/SourceSerifPro-BlackItalic.ttf b/proplot/fonts/SourceSerifPro-BlackItalic.ttf new file mode 100644 index 000000000..ff6397a71 Binary files /dev/null and b/proplot/fonts/SourceSerifPro-BlackItalic.ttf differ diff --git a/proplot/fonts/SourceSerifPro-Bold.ttf b/proplot/fonts/SourceSerifPro-Bold.ttf new file mode 100644 index 000000000..bf782fb56 Binary files /dev/null and b/proplot/fonts/SourceSerifPro-Bold.ttf differ diff --git a/proplot/fonts/SourceSerifPro-BoldItalic.ttf b/proplot/fonts/SourceSerifPro-BoldItalic.ttf new file mode 100644 index 000000000..1461f4a45 Binary files /dev/null and b/proplot/fonts/SourceSerifPro-BoldItalic.ttf differ diff --git a/proplot/fonts/SourceSerifPro-ExtraLight.ttf b/proplot/fonts/SourceSerifPro-ExtraLight.ttf new file mode 100644 index 000000000..f1cca5513 Binary files /dev/null and b/proplot/fonts/SourceSerifPro-ExtraLight.ttf differ diff --git a/proplot/fonts/SourceSerifPro-ExtraLightItalic.ttf b/proplot/fonts/SourceSerifPro-ExtraLightItalic.ttf new file mode 100644 index 000000000..d1bde716d Binary files /dev/null and b/proplot/fonts/SourceSerifPro-ExtraLightItalic.ttf differ diff --git a/proplot/fonts/SourceSerifPro-Italic.ttf b/proplot/fonts/SourceSerifPro-Italic.ttf new file mode 100644 index 000000000..4baaf1d0d Binary files /dev/null and b/proplot/fonts/SourceSerifPro-Italic.ttf differ diff --git a/proplot/fonts/SourceSerifPro-Light.ttf b/proplot/fonts/SourceSerifPro-Light.ttf new file mode 100644 index 000000000..ff60489b9 Binary files /dev/null and b/proplot/fonts/SourceSerifPro-Light.ttf differ diff --git a/proplot/fonts/SourceSerifPro-LightItalic.ttf b/proplot/fonts/SourceSerifPro-LightItalic.ttf new file mode 100644 index 000000000..b202bd5f6 Binary files /dev/null and b/proplot/fonts/SourceSerifPro-LightItalic.ttf differ diff --git a/proplot/fonts/SourceSerifPro-Regular.ttf b/proplot/fonts/SourceSerifPro-Regular.ttf new file mode 100644 index 000000000..e6c5dff97 Binary files /dev/null and b/proplot/fonts/SourceSerifPro-Regular.ttf differ diff --git a/proplot/fonts/SourceSerifPro-SemiBold.ttf b/proplot/fonts/SourceSerifPro-SemiBold.ttf new file mode 100644 index 000000000..b0f8edc60 Binary files /dev/null and b/proplot/fonts/SourceSerifPro-SemiBold.ttf differ diff --git a/proplot/fonts/SourceSerifPro-SemiBoldItalic.ttf b/proplot/fonts/SourceSerifPro-SemiBoldItalic.ttf new file mode 100644 index 000000000..040e7fc7a Binary files /dev/null and b/proplot/fonts/SourceSerifPro-SemiBoldItalic.ttf differ diff --git a/proplot/gridspec.py b/proplot/gridspec.py new file mode 100644 index 000000000..717d6bb79 --- /dev/null +++ b/proplot/gridspec.py @@ -0,0 +1,1618 @@ +#!/usr/bin/env python3 +""" +The gridspec and subplot grid classes used throughout proplot. +""" +import inspect +import itertools +import re +from collections.abc import MutableSequence +from numbers import Integral + +import matplotlib.axes as maxes +import matplotlib.gridspec as mgridspec +import matplotlib.transforms as mtransforms +import numpy as np + +from . import axes as paxes +from .config import rc +from .internals import ic # noqa: F401 +from .internals import _not_none, docstring, warnings +from .utils import _fontsize_to_pt, units + +__all__ = [ + 'GridSpec', + 'SubplotGrid', + 'SubplotsContainer' # deprecated +] + + +# Gridspec vector arguments +# Valid for figure() and GridSpec() +_shared_docstring = """ +left, right, top, bottom : unit-spec, default: None + The fixed space between the subplots and the figure edge. + %(units.em)s + If ``None``, the space is determined automatically based on the tick and + label settings. If :rcraw:`subplots.tight` is ``True`` or ``tight=True`` was + passed to the figure, the space is determined by the tight layout algorithm. +""" +_scalar_docstring = """ +wspace, hspace, space : unit-spec, default: None + The fixed space between grid columns, rows, or both. + %(units.em)s + If ``None``, the space is determined automatically based on the font size and axis + sharing settings. If :rcraw:`subplots.tight` is ``True`` or ``tight=True`` was + passed to the figure, the space is determined by the tight layout algorithm. +""" +_vector_docstring = """ +wspace, hspace, space : unit-spec or sequence, default: None + The fixed space between grid columns, rows, and both, respectively. If + float, string, or ``None``, this value is expanded into lists of length + ``ncols - 1`` (for `wspace`) or length ``nrows - 1`` (for `hspace`). If + a sequence, its length must match these lengths. + %(units.em)s + + For elements equal to ``None``, the space is determined automatically based + on the tick and label settings. If :rcraw:`subplots.tight` is ``True`` or + ``tight=True`` was passed to the figure, the space is determined by the tight + layout algorithm. For example, ``subplots(ncols=3, tight=True, wspace=(2, None))`` + fixes the space between columns 1 and 2 but lets the tight layout algorithm + determine the space between columns 2 and 3. +wratios, hratios : float or sequence, optional + Passed to `~proplot.gridspec.GridSpec`, denotes the width and height + ratios for the subplot grid. Length of `wratios` must match the number + of columns, and length of `hratios` must match the number of rows. +width_ratios, height_ratios + Aliases for `wratios`, `hratios`. Included for + consistency with `matplotlib.gridspec.GridSpec`. +wpad, hpad, pad : unit-spec or sequence, optional + The tight layout padding between columns, rows, and both, respectively. + Unlike ``space``, these control the padding between subplot content + (including text, ticks, etc.) rather than subplot edges. As with + ``space``, these can be scalars or arrays optionally containing ``None``. + For elements equal to ``None``, the default is `innerpad`. + %(units.em)s +""" +_tight_docstring = """ +wequal, hequal, equal : bool, default: :rc:`subplots.equalspace` + Whether to make the tight layout algorithm apply equal spacing + between columns, rows, or both. +wgroup, hgroup, group : bool, default: :rc:`subplots.groupspace` + Whether to make the tight layout algorithm just consider spaces between + adjacent subplots instead of entire columns and rows of subplots. +outerpad : unit-spec, default: :rc:`subplots.outerpad` + The scalar tight layout padding around the left, right, top, bottom figure edges. + %(units.em)s +innerpad : unit-spec, default: :rc:`subplots.innerpad` + The scalar tight layout padding between columns and rows. Synonymous with `pad`. + %(units.em)s +panelpad : unit-spec, default: :rc:`subplots.panelpad` + The scalar tight layout padding between subplots and their panels, + colorbars, and legends and between "stacks" of these objects. + %(units.em)s +""" +docstring._snippet_manager['gridspec.shared'] = _shared_docstring +docstring._snippet_manager['gridspec.scalar'] = _scalar_docstring +docstring._snippet_manager['gridspec.vector'] = _vector_docstring +docstring._snippet_manager['gridspec.tight'] = _tight_docstring + + +def _disable_method(attr): + """ + Disable the inherited method. + """ + def _dummy_method(*args): + raise RuntimeError(f'Method {attr}() is disabled on proplot gridspecs.') + _dummy_method.__name__ = attr + return _dummy_method + + +class _SubplotSpec(mgridspec.SubplotSpec): + """ + A thin `~matplotlib.gridspec.SubplotSpec` subclass with a nice string + representation and a few helper methods. + """ + def __repr__(self): + # NOTE: Also include panel obfuscation here to avoid confusion. If this + # is a panel slot generated internally then show zero info. + try: + nrows, ncols, num1, num2 = self._get_geometry() + except (IndexError, ValueError, AttributeError): + return 'SubplotSpec(unknown)' + else: + return f'SubplotSpec(nrows={nrows}, ncols={ncols}, index=({num1}, {num2}))' + + def _get_geometry(self): + """ + Return the geometry and scalar indices relative to the "unhidden" non-panel + geometry. May trigger error if this is in a "hidden" panel slot. + """ + gs = self.get_gridspec() + num1, num2 = self.num1, self.num2 + if isinstance(gs, GridSpec): + nrows, ncols = gs.get_geometry() + num1, num2 = gs._decode_indices(num1, num2) # may trigger error + return nrows, ncols, num1, num2 + + def _get_rows_columns(self, ncols=None): + """ + Return the row and column indices. The resulting indices include + "hidden" panel rows and columns. See `GridSpec.get_grid_positions`. + """ + # NOTE: Sort of confusing that this doesn't have 'total' in name but that + # is by analogy with get_grid_positions(). This is used for grid positioning. + gs = self.get_gridspec() + if isinstance(gs, GridSpec): + ncols = _not_none(ncols, gs.ncols_total) + else: + ncols = _not_none(ncols, gs.ncols) + row1, col1 = divmod(self.num1, ncols) + row2, col2 = divmod(self.num2, ncols) + return row1, row2, col1, col2 + + def get_position(self, figure, return_all=False): + # Silent override. Older matplotlib versions can create subplots + # with negative heights and widths that crash on instantiation. + # Instead better to dynamically adjust the bounding box and hope + # that subsequent adjustments will correct the subplot position. + gs = self.get_gridspec() + if isinstance(gs, GridSpec): + nrows, ncols = gs.get_total_geometry() + else: + nrows, ncols = gs.get_geometry() + rows, cols = np.unravel_index([self.num1, self.num2], (nrows, ncols)) + bottoms, tops, lefts, rights = gs.get_grid_positions(figure) + bottom = bottoms[rows].min() + top = max(bottom, tops[rows].max()) + left = lefts[cols].min() + right = max(left, rights[cols].max()) + bbox = mtransforms.Bbox.from_extents(left, bottom, right, top) + if return_all: + return bbox, rows[0], cols[0], nrows, ncols + else: + return bbox + + +class GridSpec(mgridspec.GridSpec): + """ + A `~matplotlib.gridspec.GridSpec` subclass that permits variable spacing + between successive rows and columns and hides "panel slots" from indexing. + """ + def __repr__(self): + nrows, ncols = self.get_geometry() + prows, pcols = self.get_panel_geometry() + params = {'nrows': nrows, 'ncols': ncols} + if prows: + params['nrows_panel'] = prows + if pcols: + params['ncols_panel'] = pcols + params = ', '.join(f'{key}={value!r}' for key, value in params.items()) + return f'GridSpec({params})' + + def __getattr__(self, attr): + # Redirect to private 'layout' attributes that are fragile w.r.t. + # matplotlib version. Cannot set these by calling super().__init__() + # because we make spacing arguments non-settable properties. + if 'layout' in attr: + return None + super().__getattribute__(attr) # native error message + + @docstring._snippet_manager + def __init__(self, nrows=1, ncols=1, **kwargs): + """ + Parameters + ---------- + nrows : int, optional + The number of rows in the subplot grid. + ncols : int, optional + The number of columns in the subplot grid. + + Other parameters + ---------------- + %(gridspec.shared)s + %(gridspec.vector)s + %(gridspec.tight)s + + See also + -------- + proplot.ui.figure + proplot.figure.Figure + proplot.ui.subplots + proplot.figure.Figure.subplots + proplot.figure.Figure.add_subplots + matplotlib.gridspec.GridSpec + + Important + --------- + Adding axes panels, axes or figure colorbars, and axes or figure legends + quietly augments the gridspec geometry by inserting "panel slots". However, + subsequently indexing the gridspec with ``gs[num]`` or ``gs[row, col]`` will + ignore the "panel slots". This permits adding new subplots by passing + ``gs[num]`` or ``gs[row, col]`` to `~proplot.figure.Figure.add_subplot` + even in the presence of panels (see `~GridSpec.__getitem__` for details). + This also means that each `GridSpec` is `~proplot.figure.Figure`-specific, + i.e. it can only be used once (if you are working with `GridSpec` instances + manually and want the same geometry for multiple figures, you must create + a copy with `GridSpec.copy` before working on the subsequent figure). + """ + # Fundamental GridSpec properties + self._nrows_total = nrows + self._ncols_total = ncols + self._left = None + self._right = None + self._bottom = None + self._top = None + self._hspace_total = [None] * (nrows - 1) + self._wspace_total = [None] * (ncols - 1) + self._hratios_total = [1] * nrows + self._wratios_total = [1] * ncols + self._left_default = None + self._right_default = None + self._bottom_default = None + self._top_default = None + self._hspace_total_default = [None] * (nrows - 1) + self._wspace_total_default = [None] * (ncols - 1) + self._figure = None # initial state + + # Capture rc settings used for default spacing + # NOTE: This is consistent with conversion of 'em' units to inches on gridspec + # instantiation. In general it seems strange for future changes to rc settings + # to magically update an existing gridspec layout. This also may improve draw + # time as manual or auto figure resizes repeatedly call get_grid_positions(). + scales = {'in': 0, 'inout': 0.5, 'out': 1, None: 1} + self._xtickspace = scales[rc['xtick.direction']] * rc['xtick.major.size'] + self._ytickspace = scales[rc['ytick.direction']] * rc['ytick.major.size'] + self._xticklabelspace = _fontsize_to_pt(rc['xtick.labelsize']) + rc['xtick.major.pad'] # noqa: E501 + self._yticklabelspace = 2 * _fontsize_to_pt(rc['ytick.labelsize']) + rc['ytick.major.pad'] # noqa: E501 + self._labelspace = _fontsize_to_pt(rc['axes.labelsize']) + rc['axes.labelpad'] + self._titlespace = _fontsize_to_pt(rc['axes.titlesize']) + rc['axes.titlepad'] + + # Tight layout and panel-related properties + # NOTE: The wpanels and hpanels contain empty strings '' (indicating main axes), + # or one of 'l', 'r', 'b', 't' (indicating axes panels) or 'f' (figure panels) + outerpad = _not_none(kwargs.pop('outerpad', None), rc['subplots.outerpad']) + innerpad = _not_none(kwargs.pop('innerpad', None), rc['subplots.innerpad']) + panelpad = _not_none(kwargs.pop('panelpad', None), rc['subplots.panelpad']) + pad = _not_none(kwargs.pop('pad', None), innerpad) # alias of innerpad + self._outerpad = units(outerpad, 'em', 'in') + self._innerpad = units(innerpad, 'em', 'in') + self._panelpad = units(panelpad, 'em', 'in') + self._hpad_total = [units(pad, 'em', 'in')] * (nrows - 1) + self._wpad_total = [units(pad, 'em', 'in')] * (ncols - 1) + self._hequal = rc['subplots.equalspace'] + self._wequal = rc['subplots.equalspace'] + self._hgroup = rc['subplots.groupspace'] + self._wgroup = rc['subplots.groupspace'] + self._hpanels = [''] * nrows # axes and figure panel identification + self._wpanels = [''] * ncols + self._fpanels = { # array representation of figure panel spans + 'left': np.empty((0, nrows), dtype=bool), + 'right': np.empty((0, nrows), dtype=bool), + 'bottom': np.empty((0, ncols), dtype=bool), + 'top': np.empty((0, ncols), dtype=bool), + } + self._update_params(pad=pad, **kwargs) + + def __getitem__(self, key): + """ + Get a `~matplotlib.gridspec.SubplotSpec`. "Hidden" slots allocated for axes + panels, colorbars, and legends are ignored. For example, given a gridspec with + 2 subplot rows, 3 subplot columns, and a "panel" row between the subplot rows, + calling ``gs[1, 1]`` returns a `~matplotlib.gridspec.SubplotSpec` corresponding + to the central subplot on the second row rather than a "panel" slot. + """ + return self._make_subplot_spec(key, includepanels=False) + + def _make_subplot_spec(self, key, includepanels=False): + """ + Generate a subplotspec either ignoring panels or including panels. + """ + # Convert the indices into endpoint-inclusive (start, stop) + def _normalize_index(key, size, axis=None): # noqa: E306 + if isinstance(key, slice): + start, stop, _ = key.indices(size) + if stop > start: + return start, stop - 1 + else: + if key < 0: + key += size + if 0 <= key < size: + return key, key # endpoing inclusive + extra = 'for gridspec' if axis is None else f'along axis {axis}' + raise IndexError(f'Invalid index {key} {extra} with size {size}.') + + # Normalize the indices + if includepanels: + nrows, ncols = self.get_total_geometry() + else: + nrows, ncols = self.get_geometry() + if not isinstance(key, tuple): # usage gridspec[1,2] + num1, num2 = _normalize_index(key, nrows * ncols) + elif len(key) == 2: + k1, k2 = key + num1 = _normalize_index(k1, nrows, axis=0) + num2 = _normalize_index(k2, ncols, axis=1) + num1, num2 = np.ravel_multi_index((num1, num2), (nrows, ncols)) + else: + raise ValueError(f'Invalid index {key!r}.') + + # Return the subplotspec + if not includepanels: + num1, num2 = self._encode_indices(num1, num2) + return _SubplotSpec(self, num1, num2) + + def _encode_indices(self, *args, which=None): + """ + Convert indices from the "unhidden" gridspec geometry into indices for the + total geometry. If `which` is not passed these should be flattened indices. + """ + nums = [] + idxs = self._get_indices(which) + for arg in args: + try: + nums.append(idxs[arg]) + except (IndexError, TypeError): + raise ValueError(f'Invalid gridspec index {arg}.') + return nums[0] if len(nums) == 1 else nums + + def _decode_indices(self, *args, which=None): + """ + Convert indices from the total geometry into the "unhidden" gridspec + geometry. If `which` is not passed these should be flattened indices. + """ + nums = [] + idxs = self._get_indices(which) + for arg in args: + try: + nums.append(idxs.index(arg)) + except ValueError: + raise ValueError(f'Invalid gridspec index {arg}.') + return nums[0] if len(nums) == 1 else nums + + def _filter_indices(self, key, panel=False): + """ + Filter the vector attribute for "unhidden" or "hidden" slots. + """ + # NOTE: Currently this is just used for unused internal properties, + # defined for consistency with the properties ending in "total". + # These may be made public in a future version. + which = key[0] + space = 'space' in key or 'pad' in key + idxs = self._get_indices(which=which, space=space, panel=panel) + vector = getattr(self, key + '_total') + return [vector[i] for i in idxs] + + def _get_indices(self, which=None, space=False, panel=False): + """ + Get the indices associated with "unhidden" or "hidden" slots. + """ + if which: + panels = getattr(self, f'_{which}panels') + else: + panels = [h + w for h, w in itertools.product(self._hpanels, self._wpanels)] + if not space: + idxs = [ + i for i, p in enumerate(panels) if p + ] + else: + idxs = [ + i for i, (p1, p2) in enumerate(zip(panels[:-1], panels[1:])) + if p1 == p2 == 'f' + or p1 in ('l', 't') and p2 in ('l', 't', '') + or p1 in ('r', 'b', '') and p2 in ('r', 'b') + ] + if not panel: + length = len(panels) - 1 if space else len(panels) + idxs = [i for i in range(length) if i not in idxs] + return idxs + + def _modify_subplot_geometry(self, newrow=None, newcol=None): + """ + Update the axes subplot specs by inserting rows and columns as specified. + """ + fig = self.figure + ncols = self._ncols_total - int(newcol is not None) # previous columns + inserts = (newrow, newrow, newcol, newcol) + for ax in fig._iter_axes(hidden=True, children=True): + # Get old index + # NOTE: Endpoints are inclusive, not exclusive! + if not isinstance(ax, maxes.SubplotBase): + continue + gs = ax.get_subplotspec().get_gridspec() + ss = ax.get_subplotspec().get_topmost_subplotspec() + # Get a new subplotspec + coords = list(ss._get_rows_columns(ncols=ncols)) + for i in range(4): + if inserts[i] is not None and coords[i] >= inserts[i]: + coords[i] += 1 + row1, row2, col1, col2 = coords + key1 = slice(row1, row2 + 1) + key2 = slice(col1, col2 + 1) + ss_new = self._make_subplot_spec((key1, key2), includepanels=True) + # Apply new subplotspec + # NOTE: We should only have one possible level of GridSpecFromSubplotSpec + # nesting -- from making side colorbars with length less than 1. + if ss is ax.get_subplotspec(): + ax.set_subplotspec(ss_new) + elif ss is getattr(gs, '_subplot_spec', None): + gs._subplot_spec = ss_new + else: + raise RuntimeError('Unexpected GridSpecFromSubplotSpec nesting.') + ax._reposition_subplot() + + def _parse_panel_arg(self, side, arg): + """ + Return the indices associated with a new figure panel on the specified side. + Try to find room in the current mosaic of figure panels. + """ + # Add a subplot panel. Index depends on the side + # NOTE: This always "stacks" new panels on old panels + if isinstance(arg, maxes.SubplotBase) and isinstance(arg, paxes.Axes): + slot = side[0] + ss = arg.get_subplotspec().get_topmost_subplotspec() + offset = len(arg._panel_dict[side]) + 1 + row1, row2, col1, col2 = ss._get_rows_columns() + if side in ('left', 'right'): + iratio = col1 - offset if side == 'left' else col2 + offset + start, stop = row1, row2 + else: + iratio = row1 - offset if side == 'top' else row2 + offset + start, stop = col1, col2 + + # Add a figure panel. Index depends on the side and the input 'span' + # NOTE: Here the 'span' indices start at '1' by analogy with add_subplot() + # integers and with main subplot numbers. Also *ignores panel slots*. + # NOTE: This only "stacks" panels if requested slots are filled. Slots are + # tracked with figure panel array (a boolean mask where each row corresponds + # to a panel, moving toward the outside, and True indicates a slot is filled). + elif ( + arg is None or isinstance(arg, Integral) + or np.iterable(arg) and all(isinstance(_, Integral) for _ in arg) + ): + slot = 'f' + array = self._fpanels[side] + nacross = self._ncols_total if side in ('left', 'right') else self._nrows_total # noqa: E501 + npanels, nalong = array.shape + arg = np.atleast_1d(_not_none(arg, (1, nalong))) + if arg.size not in (1, 2): + raise ValueError(f'Invalid span={arg!r}. Must be scalar or 2-tuple of coordinates.') # noqa: E501 + if any(s < 1 or s > nalong for s in arg): + raise ValueError(f'Invalid span={arg!r}. Coordinates must satisfy 1 <= c <= {nalong}.') # noqa: E501 + start, stop = arg[0] - 1, arg[-1] # non-inclusive starting at zero + iratio = -1 if side in ('left', 'top') else nacross # default values + for i in range(npanels): # possibly use existing panel slot + if not any(array[i, start:stop]): + array[i, start:stop] = True + if side in ('left', 'top'): # descending moves us closer to 0 + iratio = npanels - 1 - i # index in ratios array + else: # descending array moves us closer to nacross - 1 + iratio = nacross - (npanels - i) # index in ratios array + break + if iratio == -1 or iratio == nacross: # no slots so we must add to array + iarray = np.zeros((1, nalong), dtype=bool) + iarray[0, start:stop] = True + array = np.concatenate((array, iarray), axis=0) + self._fpanels[side] = array # replace array + which = 'h' if side in ('left', 'right') else 'w' + start, stop = self._encode_indices(start, stop - 1, which=which) + + else: + raise ValueError(f'Invalid panel argument {arg!r}.') + + # Return subplotspec indices + # NOTE: Convert using the lengthwise indices + return slot, iratio, slice(start, stop + 1) + + def _insert_panel_slot( + self, side, arg, *, share=None, width=None, space=None, pad=None, filled=False, + ): + """ + Insert a panel slot into the existing gridspec. The `side` is the panel side + and the `arg` is either an axes instance or the figure row-column span. + """ + # Parse input args and get user-input properties, default properties + fig = self.figure + if fig is None: + raise RuntimeError('Figure must be assigned to gridspec.') + if side not in ('left', 'right', 'bottom', 'top'): + raise ValueError(f'Invalid side {side}.') + slot, idx, span = self._parse_panel_arg(side, arg) + pad = units(pad, 'em', 'in') + space = units(space, 'em', 'in') + width = units(width, 'in') + share = False if filled else share if share is not None else True + which = 'w' if side in ('left', 'right') else 'h' + panels = getattr(self, f'_{which}panels') + pads = getattr(self, f'_{which}pad_total') # no copies! + ratios = getattr(self, f'_{which}ratios_total') + spaces = getattr(self, f'_{which}space_total') + spaces_default = getattr(self, f'_{which}space_total_default') + new_outer_slot = idx in (-1, len(panels)) + new_inner_slot = not new_outer_slot and panels[idx] != slot + + # Retrieve default spaces + # NOTE: Cannot use 'wspace' and 'hspace' for top and right colorbars because + # that adds an unnecessary tick space. So bypass _get_default_space totally. + pad_default = ( + self._panelpad + if slot != 'f' + or side in ('left', 'top') and panels[0] == 'f' + or side in ('right', 'bottom') and panels[-1] == 'f' + else self._innerpad + ) + inner_space_default = ( + _not_none(pad, pad_default) + if side in ('top', 'right') + else self._get_default_space( + 'hspace_total' if side == 'bottom' else 'wspace_total', + title=False, # no title between subplot and panel + share=3 if share else 0, # space for main subplot labels + pad=_not_none(pad, pad_default), + ) + ) + outer_space_default = self._get_default_space( + 'bottom' if not share and side == 'top' + else 'left' if not share and side == 'right' + else side, + title=True, # room for titles deflected above panels + pad=self._outerpad if new_outer_slot else self._innerpad, + ) + if new_inner_slot: + outer_space_default += self._get_default_space( + 'hspace_total' if side in ('bottom', 'top') else 'wspace_total', + share=None, # use external share setting + pad=0, # use no additional padding + ) + width_default = units( + rc['colorbar.width' if filled else 'subplots.panelwidth'], 'in' + ) + + # Adjust space, ratio, and panel indicator arrays + # If slot exists, overwrite width, pad, space if they were provided by the user + # If slot does not exist, modify gemoetry and add insert new spaces + attr = 'ncols' if side in ('left', 'right') else 'nrows' + idx_offset = int(side in ('top', 'left')) + idx_inner_space = idx - int(side in ('bottom', 'right')) # inner colorbar space + idx_outer_space = idx - int(side in ('top', 'left')) # outer colorbar space + if new_outer_slot or new_inner_slot: + idx += idx_offset + idx_inner_space += idx_offset + idx_outer_space += idx_offset + newcol, newrow = (idx, None) if attr == 'ncols' else (None, idx) + setattr(self, f'_{attr}_total', 1 + getattr(self, f'_{attr}_total')) + panels.insert(idx, slot) + ratios.insert(idx, _not_none(width, width_default)) + pads.insert(idx_inner_space, _not_none(pad, pad_default)) + spaces.insert(idx_inner_space, space) + spaces_default.insert(idx_inner_space, inner_space_default) + if new_inner_slot: + spaces_default.insert(idx_outer_space, outer_space_default) + else: + setattr(self, f'_{side}_default', outer_space_default) + else: + newrow = newcol = None + spaces_default[idx_inner_space] = inner_space_default + if width is not None: + ratios[idx] = width + if pad is not None: + pads[idx_inner_space] = pad + if space is not None: + spaces[idx_inner_space] = space + + # Update the figure and axes and return a SubplotSpec + # NOTE: For figure panels indices are determined by user-input spans. + self._modify_subplot_geometry(newrow, newcol) + figsize = self._update_figsize() + if figsize is not None: + fig.set_size_inches(figsize, internal=True, forward=False) + else: + self.update() + key = (span, idx) if side in ('left', 'right') else (idx, span) + ss = self._make_subplot_spec(key, includepanels=True) # bypass obfuscation + return ss, share + + def _get_space(self, key): + """ + Return the currently active vector inner space or scalar outer space + accounting for both default values and explicit user overrides. + """ + # NOTE: Default panel spaces should have been filled by _insert_panel_slot. + # They use 'panelpad' and the panel-local 'share' setting. This function + # instead fills spaces between subplots depending on sharing setting. + fig = self.figure + if not fig: + raise ValueError('Figure must be assigned to get grid positions.') + attr = f'_{key}' # user-specified + attr_default = f'_{key}_default' # default values + value = getattr(self, attr) + value_default = getattr(self, attr_default) + if key in ('left', 'right', 'bottom', 'top'): + if value_default is None: + value_default = self._get_default_space(key) + setattr(self, attr_default, value_default) + return _not_none(value, value_default) + elif key in ('wspace_total', 'hspace_total'): + result = [] + for i, (val, val_default) in enumerate(zip(value, value_default)): + if val_default is None: + val_default = self._get_default_space(key) + value_default[i] = val_default + result.append(_not_none(val, val_default)) + return result + else: + raise ValueError(f'Unknown space parameter {key!r}.') + + def _get_default_space(self, key, pad=None, share=None, title=True): + """ + Return suitable default scalar inner or outer space given a shared axes + setting. This is only relevant when "tight layout" is disabled. + """ + # NOTE: Internal spacing args are stored in inches to simplify the + # get_grid_positions() calculations. + fig = self.figure + if fig is None: + raise RuntimeError('Figure must be assigned.') + if key == 'right': + pad = _not_none(pad, self._outerpad) + space = 0 + elif key == 'top': + pad = _not_none(pad, self._outerpad) + space = self._titlespace if title else 0 + elif key == 'left': + pad = _not_none(pad, self._outerpad) + space = self._labelspace + self._yticklabelspace + self._ytickspace + elif key == 'bottom': + pad = _not_none(pad, self._outerpad) + space = self._labelspace + self._xticklabelspace + self._xtickspace + elif key == 'wspace_total': + pad = _not_none(pad, self._innerpad) + share = _not_none(share, fig._sharey, 0) + space = self._ytickspace + if share < 3: + space += self._yticklabelspace + if share < 1: + space += self._labelspace + elif key == 'hspace_total': + pad = _not_none(pad, self._innerpad) + share = _not_none(share, fig._sharex, 0) + space = self._xtickspace + if title: + space += self._titlespace + if share < 3: + space += self._xticklabelspace + if share < 1: + space += self._labelspace + else: + raise ValueError(f'Invalid space key {key!r}.') + return pad + space / 72 + + def _get_tight_space(self, w): + """ + Get tight layout spaces between the input subplot rows or columns. + """ + # Get constants + fig = self.figure + if not fig: + return + if w == 'w': + x, y = 'xy' + group = self._wgroup + nacross = self.nrows_total + space = self.wspace_total + pad = self.wpad_total + else: + x, y = 'yx' + group = self._hgroup + nacross = self.ncols_total + space = self.hspace_total + pad = self.hpad_total + + # Iterate along each row or column space + axs = tuple(fig._iter_axes(hidden=True, children=False)) + space = list(space) # a copy + ralong = np.array([ax._range_subplotspec(x) for ax in axs]) + racross = np.array([ax._range_subplotspec(y) for ax in axs]) + for i, (s, p) in enumerate(zip(space, pad)): + # Find axes that abutt aginst this row or column space + groups = [] + for j in range(nacross): # e.g. each row + # Get the indices for axes that meet this row or column edge. + # NOTE: Rigorously account for empty and overlapping slots here + filt = (racross[:, 0] <= j) & (j <= racross[:, 1]) + if sum(filt) < 2: + continue # no interface + ii = i + idx1 = idx2 = np.array(()) + while ii >= 0 and idx1.size == 0: + filt1 = ralong[:, 1] == ii # i.e. r / b edge abutts against this + idx1, = np.where(filt & filt1) + ii -= 1 + ii = i + 1 + while ii <= len(space) and idx2.size == 0: + filt2 = ralong[:, 0] == ii # i.e. l / t edge abutts against this + idx2, = np.where(filt & filt2) + ii += 1 + # Put axes into unique groups and store as (l, r) or (b, t) pairs. + axs1, axs2 = [axs[_] for _ in idx1], [axs[_] for _ in idx2] + if x != 'x': # order bottom-to-top + axs1, axs2 = axs2, axs1 + for (group1, group2) in groups: + if any(_ in group1 for _ in axs1) or any(_ in group2 for _ in axs2): + group1.update(axs1) + group2.update(axs2) + break + else: + if axs1 and axs2: + groups.append((set(axs1), set(axs2))) # form new group + # Determing the spaces using cached tight bounding boxes + # NOTE: Set gridspec space to zero if there are no adjacent edges + if not group: + groups = [( + set(ax for (group1, _) in groups for ax in group1), + set(ax for (_, group2) in groups for ax in group2), + )] + margins = [] + for (group1, group2) in groups: + x1 = max(ax._range_tightbbox(x)[1] for ax in group1) + x2 = min(ax._range_tightbbox(x)[0] for ax in group2) + margins.append((x2 - x1) / self.figure.dpi) + s = 0 if not margins else max(0, s - min(margins) + p) + space[i] = s + + return space + + def _auto_layout_aspect(self): + """ + Update the underlying default aspect ratio. + """ + # Get the axes + fig = self.figure + if not fig: + return + ax = fig._subplot_dict.get(fig._refnum, None) + if ax is None: + return + + # Get aspect ratio + ratio = ax.get_aspect() # the aspect ratio in *data units* + if ratio == 'auto': + return + elif ratio == 'equal': + ratio = 1 + elif isinstance(ratio, str): + raise RuntimeError(f'Unknown aspect ratio mode {ratio!r}.') + else: + ratio = 1 / ratio + + # Compare to current aspect after scaling by data ratio + # Noat matplotlib 3.2.0 expanded get_data_ratio to work for all axis scales: + # https://github.com/matplotlib/matplotlib/commit/87c742b99dc6b9a190f8c89bc6256ced72f5ab80 # noqa: E501 + aspect = ratio / ax.get_data_ratio() + if fig._refaspect is not None: + return # fixed by user + if np.isclose(aspect, fig._refaspect_default): + return # close enough to the default aspect + fig._refaspect_default = aspect + + # Update the layout + figsize = self._update_figsize() + if not fig._is_same_size(figsize): + fig.set_size_inches(figsize, internal=True) + + def _auto_layout_tight(self, renderer): + """ + Update the underlying spaces with tight layout values. If `resize` is + ``True`` and the auto figure size has changed then update the figure + size. Either way always update the subplot positions. + """ + # Initial stuff + fig = self.figure + if not fig: + return + if not any(fig._iter_axes(hidden=True, children=False)): + return # skip tight layout if there are no subplots in the figure + + # Get the tight bounding box around the whole figure. + # NOTE: This triggers proplot.axes.Axes.get_tightbbox which *caches* the + # computed bounding boxes used by _range_tightbbox below. + pad = self._outerpad + obox = fig.bbox_inches # original bbox + bbox = fig.get_tightbbox(renderer) + + # Calculate new figure margins + # NOTE: Negative spaces are common where entire rows/columns of gridspec + # are empty but it seems to result in wrong figure size + grid positions. Not + # worth correcting so instead enforce positive margin sizes. Will leave big + # empty slot but that is probably what should happen under this scenario. + left = self.left + bottom = self.bottom + right = self.right + top = self.top + self._left_default = max(0, left - (bbox.xmin - 0) + pad) + self._bottom_default = max(0, bottom - (bbox.ymin - 0) + pad) + self._right_default = max(0, right - (obox.xmax - bbox.xmax) + pad) + self._top_default = max(0, top - (obox.ymax - bbox.ymax) + pad) + + # Calculate new subplot row and column spaces. Enforce equal + # default spaces between main subplot edges if requested. + hspace = self._get_tight_space('h') + wspace = self._get_tight_space('w') + if self._hequal: + idxs = self._get_indices('h', space=True) + space = max(hspace[i] for i in idxs) + for i in idxs: + hspace[i] = space + if self._wequal: + idxs = self._get_indices('w', space=True) + space = max(wspace[i] for i in idxs) + for i in idxs: + wspace[i] = space + self._hspace_total_default = hspace + self._wspace_total_default = wspace + + # Update the layout + # NOTE: fig.set_size_inches() always updates the gridspec to enforce fixed + # spaces (necessary since native position coordinates are figure-relative) + # and to enforce fixed panel ratios. So only self.update() if we skip resize. + figsize = self._update_figsize() + if not fig._is_same_size(figsize): + fig.set_size_inches(figsize, internal=True) + else: + self.update() + + def _update_figsize(self): + """ + Return an updated auto layout figure size accounting for the + gridspec and figure parameters. May or may not need to be applied. + """ + fig = self.figure + if fig is None: # drawing before subplots are added? + return + ax = fig._subplot_dict.get(fig._refnum, None) + if ax is None: # drawing before subplots are added? + return + ss = ax.get_subplotspec().get_topmost_subplotspec() + y1, y2, x1, x2 = ss._get_rows_columns() + refhspace = sum(self.hspace_total[y1:y2]) + refwspace = sum(self.wspace_total[x1:x2]) + refhpanel = sum(self.hratios_total[i] for i in range(y1, y2 + 1) if self._hpanels[i]) # noqa: E501 + refwpanel = sum(self.wratios_total[i] for i in range(x1, x2 + 1) if self._wpanels[i]) # noqa: E501 + refhsubplot = sum(self.hratios_total[i] for i in range(y1, y2 + 1) if not self._hpanels[i]) # noqa: E501 + refwsubplot = sum(self.wratios_total[i] for i in range(x1, x2 + 1) if not self._wpanels[i]) # noqa: E501 + + # Get the reference sizes + # NOTE: The sizing arguments should have been normalized already + figwidth, figheight = fig._figwidth, fig._figheight + refwidth, refheight = fig._refwidth, fig._refheight + refaspect = _not_none(fig._refaspect, fig._refaspect_default) + if refheight is None and figheight is None: + if figwidth is not None: + gridwidth = figwidth - self.spacewidth - self.panelwidth + refwidth = gridwidth * refwsubplot / self.gridwidth + if refwidth is not None: # WARNING: do not change to elif! + refheight = refwidth / refaspect + else: + raise RuntimeError('Figure size arguments are all missing.') + if refwidth is None and figwidth is None: + if figheight is not None: + gridheight = figheight - self.spaceheight - self.panelheight + refheight = gridheight * refhsubplot / self.gridheight + if refheight is not None: + refwidth = refheight * refaspect + else: + raise RuntimeError('Figure size arguments are all missing.') + + # Get the auto figure size. Might trigger 'not enough room' error later + # NOTE: For e.g. [[1, 1, 2, 2], [0, 3, 3, 0]] we make sure to still scale the + # reference axes like a square even though takes two columns of gridspec. + if refheight is not None: + refheight -= refhspace + refhpanel + gridheight = refheight * self.gridheight / refhsubplot + figheight = gridheight + self.spaceheight + self.panelheight + if refwidth is not None: + refwidth -= refwspace + refwpanel + gridwidth = refwidth * self.gridwidth / refwsubplot + figwidth = gridwidth + self.spacewidth + self.panelwidth + + # Return the figure size + figsize = (figwidth, figheight) + if all(np.isfinite(figsize)): + return figsize + else: + warnings._warn_proplot(f'Auto resize failed. Invalid figsize {figsize}.') + + def _update_params( + self, *, + left=None, bottom=None, right=None, top=None, + wspace=None, hspace=None, space=None, + wpad=None, hpad=None, pad=None, + wequal=None, hequal=None, equal=None, + wgroup=None, hgroup=None, group=None, + outerpad=None, innerpad=None, panelpad=None, + hratios=None, wratios=None, width_ratios=None, height_ratios=None, + ): + """ + Update the user-specified properties. + """ + # Assign scalar args + # WARNING: The key signature here is critical! Used in ui.py to + # separate out figure keywords and gridspec keywords. + def _assign_scalar(key, value, convert=True): + if value is None: + return + if not np.isscalar(value): + raise ValueError(f'Unexpected {key}={value!r}. Must be scalar.') + if convert: + value = units(value, 'em', 'in') + setattr(self, f'_{key}', value) + hequal = _not_none(hequal, equal) + wequal = _not_none(wequal, equal) + hgroup = _not_none(hgroup, group) + wgroup = _not_none(wgroup, group) + _assign_scalar('left', left) + _assign_scalar('right', right) + _assign_scalar('bottom', bottom) + _assign_scalar('top', top) + _assign_scalar('panelpad', panelpad) + _assign_scalar('outerpad', outerpad) + _assign_scalar('innerpad', innerpad) + _assign_scalar('hequal', hequal, convert=False) + _assign_scalar('wequal', wequal, convert=False) + _assign_scalar('hgroup', hgroup, convert=False) + _assign_scalar('wgroup', wgroup, convert=False) + + # Assign vector args + # NOTE: Here we employ obfuscation that skips 'panel' indices. So users could + # still call self.update(wspace=[1, 2]) even if there is a right-axes panel + # between each subplot. To control panel spaces users should instead pass + # 'pad' or 'space' to panel_axes(), colorbar(), or legend() on creation. + def _assign_vector(key, values, space): + if values is None: + return + idxs = self._get_indices(key[0], space=space) + nidxs = len(idxs) + values = np.atleast_1d(values) + if values.size == 1: + values = np.repeat(values, nidxs) + if values.size != nidxs: + raise ValueError(f'Expected len({key}) == {nidxs}. Got {values.size}.') + list_ = getattr(self, f'_{key}_total') + for i, value in enumerate(values): + if value is None: + continue + list_[idxs[i]] = value + if pad is not None and not np.isscalar(pad): + raise ValueError(f'Parameter pad={pad!r} must be scalar.') + if space is not None and not np.isscalar(space): + raise ValueError(f'Parameter space={space!r} must be scalar.') + hpad = _not_none(hpad, pad) + wpad = _not_none(wpad, pad) + hpad = units(hpad, 'em', 'in') + wpad = units(wpad, 'em', 'in') + hspace = _not_none(hspace, space) + wspace = _not_none(wspace, space) + hspace = units(hspace, 'em', 'in') + wspace = units(wspace, 'em', 'in') + hratios = _not_none(hratios=hratios, height_ratios=height_ratios) + wratios = _not_none(wratios=wratios, width_ratios=width_ratios) + _assign_vector('hpad', hpad, space=True) + _assign_vector('wpad', wpad, space=True) + _assign_vector('hspace', hspace, space=True) + _assign_vector('wspace', wspace, space=True) + _assign_vector('hratios', hratios, space=False) + _assign_vector('wratios', wratios, space=False) + + @docstring._snippet_manager + def copy(self, **kwargs): + """ + Return a copy of the `GridSpec` with the `~proplot.figure.Figure`-specific + "panel slots" removed. This can be useful if you want to draw multiple + figures with the same geometry. Properties are inherited from this + `GridSpec` by default but can be changed by passing keyword arguments. + + Parameters + ---------- + %(gridspec.shared)s + %(gridspec.vector)s + %(gridspec.tight)s + + See also + -------- + GridSpec.update + """ + # WARNING: For some reason copy.copy() fails. Updating e.g. wpanels + # and hpanels on the copy also updates this object. No idea why. + nrows, ncols = self.get_geometry() + gs = GridSpec(nrows, ncols) + hidxs = self._get_indices('h') + widxs = self._get_indices('w') + gs._hratios_total = [self._hratios_total[i] for i in hidxs] + gs._wratios_total = [self._wratios_total[i] for i in widxs] + hidxs = self._get_indices('h', space=True) + widxs = self._get_indices('w', space=True) + gs._hpad_total = [self._hpad_total[i] for i in hidxs] + gs._wpad_total = [self._wpad_total[i] for i in widxs] + gs._hspace_total = [self._hspace_total[i] for i in hidxs] + gs._wspace_total = [self._wspace_total[i] for i in widxs] + gs._hspace_total_default = [self._hspace_total_default[i] for i in hidxs] + gs._wspace_total_default = [self._wspace_total_default[i] for i in widxs] + for key in ( + 'left', 'right', 'bottom', 'top', 'labelspace', 'titlespace', + 'xtickspace', 'ytickspace', 'xticklabelspace', 'yticklabelspace', + 'outerpad', 'innerpad', 'panelpad', 'hequal', 'wequal', + ): + value = getattr(self, '_' + key) + setattr(gs, '_' + key, value) + gs.update(**kwargs) + return gs + + def get_geometry(self): + """ + Return the number of "unhidden" non-panel rows and columns in the grid + (see `GridSpec` for details). + + See also + -------- + GridSpec.get_panel_geometry + GridSpec.get_total_geometry + """ + nrows, ncols = self.get_total_geometry() + nrows_panels, ncols_panels = self.get_panel_geometry() + return nrows - nrows_panels, ncols - ncols_panels + + def get_panel_geometry(self): + """ + Return the number of "hidden" panel rows and columns in the grid + (see `GridSpec` for details). + + See also + -------- + GridSpec.get_geometry + GridSpec.get_total_geometry + """ + nrows = sum(map(bool, self._hpanels)) + ncols = sum(map(bool, self._wpanels)) + return nrows, ncols + + def get_total_geometry(self): + """ + Return the total number of "unhidden" and "hidden" rows and columns + in the grid (see `GridSpec` for details). + + See also + -------- + GridSpec.get_geometry + GridSpec.get_panel_geometry + GridSpec.get_grid_positions + """ + return self._nrows_total, self._ncols_total + + def get_grid_positions(self, figure=None): + """ + Return the subplot grid positions allowing for variable inter-subplot + spacing and using physical units for the spacing terms. The resulting + positions include "hidden" panel rows and columns. + + Note + ---- + The physical units for positioning grid cells are converted from em-widths to + inches when the `GridSpec` is instantiated. This means that subsequent changes + to :rcraw:`font.size` will have no effect on the spaces. This is consistent + with :rcraw:`font.size` having no effect on already-instantiated figures. + + See also + -------- + GridSpec.get_total_geometry + """ + # Grab the figure size + if not self.figure: + self._figure = figure + if not self.figure: + raise RuntimeError('Figure must be assigned to gridspec.') + if figure is not self.figure: + raise RuntimeError(f'Input figure {figure} does not match gridspec figure {self.figure}.') # noqa: E501 + fig = _not_none(figure, self.figure) + figwidth, figheight = fig.get_size_inches() + spacewidth, spaceheight = self.spacewidth, self.spaceheight + panelwidth, panelheight = self.panelwidth, self.panelheight + hratios, wratios = self.hratios_total, self.wratios_total + hidxs, widxs = self._get_indices('h'), self._get_indices('w') + + # Scale the subplot slot ratios and keep the panel slots fixed + hsubplot = np.array([hratios[i] for i in hidxs]) + wsubplot = np.array([wratios[i] for i in widxs]) + hsubplot = (figheight - panelheight - spaceheight) * hsubplot / np.sum(hsubplot) + wsubplot = (figwidth - panelwidth - spacewidth) * wsubplot / np.sum(wsubplot) + for idx, ratio in zip(hidxs, hsubplot): + hratios[idx] = ratio # modify the main subplot ratios + for idx, ratio in zip(widxs, wsubplot): + wratios[idx] = ratio + + # Calculate accumulated heights of columns + norm = (figheight - spaceheight) / (figheight * sum(hratios)) + if norm < 0: + raise RuntimeError( + 'Not enough room for axes. Try increasing the figure height or ' + "decreasing the 'top', 'bottom', or 'hspace' gridspec spaces." + ) + cell_heights = [r * norm for r in hratios] + sep_heights = [0] + [s / figheight for s in self.hspace_total] + heights = np.cumsum(np.column_stack([sep_heights, cell_heights]).flat) + + # Calculate accumulated widths of rows + norm = (figwidth - spacewidth) / (figwidth * sum(wratios)) + if norm < 0: + raise RuntimeError( + 'Not enough room for axes. Try increasing the figure width or ' + "decreasing the 'left', 'right', or 'wspace' gridspec spaces." + ) + cell_widths = [r * norm for r in wratios] + sep_widths = [0] + [s / figwidth for s in self.wspace_total] + widths = np.cumsum(np.column_stack([sep_widths, cell_widths]).flat) + + # Return the figure coordinates + tops, bottoms = (1 - self.top / figheight - heights).reshape((-1, 2)).T + lefts, rights = (self.left / figwidth + widths).reshape((-1, 2)).T + return bottoms, tops, lefts, rights + + @docstring._snippet_manager + def update(self, **kwargs): + """ + Update the gridspec with arbitrary initialization keyword arguments + and update the subplot positions. + + Parameters + ---------- + %(gridspec.shared)s + %(gridspec.vector)s + %(gridspec.tight)s + + See also + -------- + GridSpec.copy + """ + # Apply positions to all axes + # NOTE: This uses the current figure size to fix panel widths + # and determine physical grid spacing. + self._update_params(**kwargs) + fig = self.figure + if fig is None: + return + for ax in fig.axes: + if not isinstance(ax, maxes.SubplotBase): + continue + ss = ax.get_subplotspec().get_topmost_subplotspec() + if ss.get_gridspec() is not self: # should be impossible + continue + ax._reposition_subplot() + fig.stale = True + + @property + def figure(self): + """ + The `proplot.figure.Figure` uniquely associated with this `GridSpec`. + On assignment the gridspec parameters and figure size are updated. + + See also + -------- + proplot.gridspec.SubplotGrid.figure + proplot.figure.Figure.gridspec + """ + return self._figure + + @figure.setter + def figure(self, fig): + from .figure import Figure + if not isinstance(fig, Figure): + raise ValueError('Figure must be a proplot figure.') + if self._figure and self._figure is not fig: + raise ValueError( + 'Cannot use the same gridspec for multiple figures. ' + 'Please use gridspec.copy() to make a copy.' + ) + self._figure = fig + self._update_params(**fig._gridspec_params) + fig._gridspec_params.clear() + figsize = self._update_figsize() + if figsize is not None: + fig.set_size_inches(figsize, internal=True, forward=False) + else: + self.update() + + # Delete attributes. Don't like having special setters and getters for some + # settings and not others. Width and height ratios can be updated with update(). + # Also delete obsolete 'subplotpars' and built-in tight layout function. + tight_layout = _disable_method('tight_layout') # instead use custom tight layout + subgridspec = _disable_method('subgridspec') # instead use variable spaces + get_width_ratios = _disable_method('get_width_ratios') + get_height_ratios = _disable_method('get_height_ratios') + set_width_ratios = _disable_method('set_width_ratios') + set_height_ratios = _disable_method('set_height_ratios') + get_subplot_params = _disable_method('get_subplot_params') + locally_modified_subplot_params = _disable_method('locally_modified_subplot_params') + + # Immutable helper properties used to calculate figure size and subplot positions + # NOTE: The spaces are auto-filled with defaults wherever user left them unset + gridheight = property(lambda self: sum(self.hratios)) + gridwidth = property(lambda self: sum(self.wratios)) + panelheight = property(lambda self: sum(self.hratios_panel)) + panelwidth = property(lambda self: sum(self.wratios_panel)) + spaceheight = property(lambda self: self.bottom + self.top + sum(self.hspace_total)) + spacewidth = property(lambda self: self.left + self.right + sum(self.wspace_total)) + + # Geometry properties. These are included for consistency with get_geometry + # functions (would be really confusing if self.nrows, self.ncols disagree). + nrows = property(lambda self: self._nrows_total - sum(map(bool, self._hpanels)), doc='') # noqa: E501 + ncols = property(lambda self: self._ncols_total - sum(map(bool, self._wpanels)), doc='') # noqa: E501 + nrows_panel = property(lambda self: sum(map(bool, self._hpanels))) + ncols_panel = property(lambda self: sum(map(bool, self._wpanels))) + nrows_total = property(lambda self: self._nrows_total) + ncols_total = property(lambda self: self._ncols_total) + + # Make formerly public instance-level attributes immutable and redirect space + # properties so they try to retrieve user settings then fallback to defaults. + # NOTE: These are undocumented for the time being. Generally properties should + # be changed with update() and introspection not really necessary. + left = property(lambda self: self._get_space('left')) + bottom = property(lambda self: self._get_space('bottom')) + right = property(lambda self: self._get_space('right')) + top = property(lambda self: self._get_space('top')) + hratios = property(lambda self: self._filter_indices('hratios', panel=False)) + wratios = property(lambda self: self._filter_indices('wratios', panel=False)) + hratios_panel = property(lambda self: self._filter_indices('hratios', panel=True)) + wratios_panel = property(lambda self: self._filter_indices('wratios', panel=True)) + hratios_total = property(lambda self: list(self._hratios_total)) + wratios_total = property(lambda self: list(self._wratios_total)) + hspace = property(lambda self: self._filter_indices('hspace', panel=False)) + wspace = property(lambda self: self._filter_indices('wspace', panel=False)) + hspace_panel = property(lambda self: self._filter_indices('hspace', panel=True)) + wspace_panel = property(lambda self: self._filter_indices('wspace', panel=True)) + hspace_total = property(lambda self: self._get_space('hspace_total')) + wspace_total = property(lambda self: self._get_space('wspace_total')) + hpad = property(lambda self: self._filter_indices('hpad', panel=False)) + wpad = property(lambda self: self._filter_indices('wpad', panel=False)) + hpad_panel = property(lambda self: self._filter_indices('hpad', panel=True)) + wpad_panel = property(lambda self: self._filter_indices('wpad', panel=True)) + hpad_total = property(lambda self: list(self._hpad_total)) + wpad_total = property(lambda self: list(self._wpad_total)) + + +class SubplotGrid(MutableSequence, list): + """ + List-like, array-like object used to store subplots returned by + `~proplot.figure.Figure.subplots`. 1D indexing uses the underlying list of + `~proplot.axes.Axes` while 2D indexing uses the `~SubplotGrid.gridspec`. + See `~SubplotGrid.__getitem__` for details. + """ + def __repr__(self): + if not self: + return 'SubplotGrid(length=0)' + length = len(self) + nrows, ncols = self.gridspec.get_geometry() + return f'SubplotGrid(nrows={nrows}, ncols={ncols}, length={length})' + + def __str__(self): + return self.__repr__() + + def __len__(self): + return list.__len__(self) + + def insert(self, key, value): # required for MutableSequence + value = self._validate_item(value, scalar=True) + list.insert(self, key, value) + + def __init__(self, sequence=None, **kwargs): + """ + Parameters + ---------- + sequence : sequence + A sequence of `proplot.axes.Axes` subplots or their children. + + See also + -------- + proplot.ui.subplots + proplot.figure.Figure.subplots + proplot.figure.Figure.add_subplots + """ + n = kwargs.pop('n', None) + order = kwargs.pop('order', None) + if n is not None or order is not None: + warnings._warn_proplot( + f'Ignoring n={n!r} and order={order!r}. As of v0.8 SubplotGrid ' + 'handles 2D indexing by leveraging the subplotspec extents rather than ' + 'directly emulating 2D array indexing. These arguments are no longer ' + 'needed and will be removed in a future release.' + ) + sequence = _not_none(sequence, []) + sequence = self._validate_item(sequence, scalar=False) + super().__init__(sequence, **kwargs) + + def __getattr__(self, attr): + """ + Get a missing attribute. Simply redirects to the axes if the `SubplotGrid` + is singleton and raises an error otherwise. This can be convenient for + single-axes figures generated with `~proplot.figure.Figure.subplots`. + """ + # Redirect to the axes + if not self or attr[:1] == '_': + return super().__getattribute__(attr) # trigger default error + if len(self) == 1: + return getattr(self[0], attr) + + # Obscure deprecated behavior + # WARNING: This is now deprecated! Instead we dynamically define a few + # dedicated relevant commands that can be called from the grid (see below). + import functools + warnings._warn_proplot( + 'Calling arbitrary axes methods from SubplotGrid was deprecated in v0.8 ' + 'and will be removed in a future release. Please index the grid or loop ' + 'over the grid instead.' + ) + if not self: + return None + objs = tuple(getattr(ax, attr) for ax in self) # may raise error + if not any(map(callable, objs)): + return objs[0] if len(self) == 1 else objs + elif all(map(callable, objs)): + @functools.wraps(objs[0]) + def _iterate_subplots(*args, **kwargs): + result = [] + for func in objs: + result.append(func(*args, **kwargs)) + if len(self) == 1: + return result[0] + elif all(res is None for res in result): + return None + elif all(isinstance(res, paxes.Axes) for res in result): + return SubplotGrid(result, n=self._n, order=self._order) + else: + return tuple(result) + _iterate_subplots.__doc__ = inspect.getdoc(objs[0]) + return _iterate_subplots + else: + raise AttributeError(f'Found mixed types for attribute {attr!r}.') + + def __getitem__(self, key): + """ + Get an axes. + + Parameters + ---------- + key : int, slice, or 2-tuple + The index. If 1D then the axes in the corresponding + sublist are returned. If 2D then the axes that intersect + the corresponding `~SubplotGrid.gridspec` slots are returned. + + Returns + ------- + axs : proplot.axes.Axes or SubplotGrid + The axes. If the index included slices then + another `SubplotGrid` is returned. + + Example + ------- + >>> import proplot as pplt + >>> fig, axs = pplt.subplots(nrows=3, ncols=3) + >>> axs[5] # the subplot in the second row, third column + >>> axs[1, 2] # the subplot in the second row, third column + >>> axs[:, 0] # a SubplotGrid containing the subplots in the first column + """ + if isinstance(key, tuple) and len(key) == 1: + key = key[0] + # List-style indexing + if isinstance(key, (Integral, slice)): + slices = isinstance(key, slice) + objs = list.__getitem__(self, key) + # Gridspec-style indexing + elif ( + isinstance(key, tuple) + and len(key) == 2 + and all(isinstance(ikey, (Integral, slice)) for ikey in key) + ): + # WARNING: Permit no-op slicing of empty grids here + slices = any(isinstance(ikey, slice) for ikey in key) + objs = [] + if self: + gs = self.gridspec + ss_key = gs._make_subplot_spec(key) # obfuscates panels + row1_key, col1_key = divmod(ss_key.num1, gs.ncols) + row2_key, col2_key = divmod(ss_key.num2, gs.ncols) + for ax in self: + ss = ax._get_topmost_axes().get_subplotspec().get_topmost_subplotspec() + row1, col1 = divmod(ss.num1, gs.ncols) + row2, col2 = divmod(ss.num2, gs.ncols) + inrow = row1_key <= row1 <= row2_key or row1_key <= row2 <= row2_key + incol = col1_key <= col1 <= col2_key or col1_key <= col2 <= col2_key + if inrow and incol: + objs.append(ax) + if not slices and len(objs) == 1: # accounts for overlapping subplots + objs = objs[0] + else: + raise IndexError(f'Invalid index {key!r}.') + if isinstance(objs, list): + return SubplotGrid(objs) + else: + return objs + + def __setitem__(self, key, value): + """ + Add an axes. + + Parameters + ---------- + key : int or slice + The 1D index. + value : `proplot.axes.Axes` + The proplot subplot or its child or panel axes, + or a sequence thereof if the index was a slice. + """ + if isinstance(key, Integral): + value = self._validate_item(value, scalar=True) + elif isinstance(key, slice): + value = self._validate_item(value, scalar=False) + else: + raise IndexError('Multi dimensional item assignment is not supported.') + return super().__setitem__(key, value) # could be list[:] = [1, 2, 3] + + @classmethod + def _add_command(cls, src, name): + """ + Add a `SubplotGrid` method that iterates through axes methods. + """ + # Create the method + def _grid_command(self, *args, **kwargs): + objs = [] + for ax in self: + obj = getattr(ax, name)(*args, **kwargs) + objs.append(obj) + return SubplotGrid(objs) + + # Clean the docstring + cmd = getattr(src, name) + doc = inspect.cleandoc(cmd.__doc__) # dedents + dot = doc.find('.') + if dot != -1: + doc = doc[:dot] + ' for every axes in the grid' + doc[dot:] + doc = re.sub( + r'^(Returns\n-------\n)(.+)(\n\s+)(.+)', + r'\1SubplotGrid\2A grid of the resulting axes.', + doc + ) + + # Apply the method + _grid_command.__qualname__ = f'SubplotGrid.{name}' + _grid_command.__name__ = name + _grid_command.__doc__ = doc + setattr(cls, name, _grid_command) + + def _validate_item(self, items, scalar=False): + """ + Validate assignments. Accept diverse iterable inputs. + """ + gridspec = None + message = ( + 'SubplotGrid can only be filled with proplot subplots ' + 'belonging to the same GridSpec. Instead got {}.' + ) + items = np.atleast_1d(items) + if self: + gridspec = self.gridspec # compare against existing gridspec + for item in items.flat: + if not isinstance(item, paxes.Axes): + raise ValueError(message.format(f'the object {item!r}')) + item = item._get_topmost_axes() + if not isinstance(item, maxes.SubplotBase): + raise ValueError(message.format(f'the axes {item!r}')) + gs = item.get_subplotspec().get_topmost_subplotspec().get_gridspec() + if not isinstance(gs, GridSpec): + raise ValueError(message.format(f'the GridSpec {gs!r}')) + if gridspec and gs is not gridspec: + raise ValueError(message.format('at least two different GridSpecs')) + gridspec = gs + if not scalar: + items = tuple(items.flat) + elif items.size == 1: + items = items.flat[0] + else: + raise ValueError('Input must be a single proplot axes.') + return items + + @docstring._snippet_manager + def format(self, **kwargs): + """ + Call the ``format`` command for the `~SubplotGrid.figure` + and every axes in the grid. + + Parameters + ---------- + %(axes.format)s + **kwargs + Passed to the projection-specific ``format`` command for each axes. + Valid only if every axes in the grid belongs to the same class. + + Other parameters + ---------------- + %(figure.format)s + %(cartesian.format)s + %(polar.format)s + %(geo.format)s + %(rc.format)s + + See also + -------- + proplot.axes.Axes.format + proplot.axes.CartesianAxes.format + proplot.axes.PolarAxes.format + proplot.axes.GeoAxes.format + proplot.figure.Figure.format + proplot.config.Configurator.context + """ + self.figure.format(axs=self, **kwargs) + + @property + def figure(self): + """ + The `proplot.figure.Figure` uniquely associated with this `SubplotGrid`. + This is used with the `SubplotGrid.format` command. + + See also + -------- + proplot.gridspec.GridSpec.figure + proplot.gridspec.SubplotGrid.gridspec + proplot.figure.Figure.subplotgrid + """ + return self.gridspec.figure + + @property + def gridspec(self): + """ + The `~proplot.gridspec.GridSpec` uniquely associated with this `SubplotGrid`. + This is used to resolve 2D indexing. See `~SubplotGrid.__getitem__` for details. + + See also + -------- + proplot.figure.Figure.gridspec + proplot.gridspec.SubplotGrid.figure + proplot.gridspec.SubplotGrid.shape + """ + # Return the gridspec associatd with the grid + if not self: + raise ValueError('Unknown gridspec for empty SubplotGrid.') + ax = self[0] + ax = ax._get_topmost_axes() + return ax.get_subplotspec().get_topmost_subplotspec().get_gridspec() + + @property + def shape(self): + """ + The shape of the `~proplot.gridspec.GridSpec` associated with the grid. + See `~SubplotGrid.__getitem__` for details. + + See also + -------- + proplot.gridspec.SubplotGrid.gridspec + """ + # NOTE: Considered deprecating this but on second thought since this is + # a 2D array-like object it should definitely have a shape attribute. + return self.gridspec.get_geometry() + + +# Dynamically add commands to generate twin or inset axes +# TODO: Add commands that plot the input data for every +# axes in the grid along a third dimension. +for _src, _name in ( + (paxes.Axes, 'panel'), + (paxes.Axes, 'panel_axes'), + (paxes.Axes, 'inset'), + (paxes.Axes, 'inset_axes'), + (paxes.CartesianAxes, 'altx'), + (paxes.CartesianAxes, 'alty'), + (paxes.CartesianAxes, 'dualx'), + (paxes.CartesianAxes, 'dualy'), + (paxes.CartesianAxes, 'twinx'), + (paxes.CartesianAxes, 'twiny'), +): + SubplotGrid._add_command(_src, _name) + +# Deprecated +SubplotsContainer = warnings._rename_objs('0.8.0', SubplotsContainer=SubplotGrid) diff --git a/proplot/internals/__init__.py b/proplot/internals/__init__.py new file mode 100644 index 000000000..4d335ac86 --- /dev/null +++ b/proplot/internals/__init__.py @@ -0,0 +1,487 @@ +#!/usr/bin/env python3 +""" +Internal utilities. +""" +# Import statements +import inspect +from numbers import Integral, Real + +import numpy as np +from matplotlib import rcParams as rc_matplotlib + +try: # print debugging (used with internal modules) + from icecream import ic +except ImportError: # graceful fallback if IceCream isn't installed + ic = lambda *args: print(*args) # noqa: E731 + + +def _not_none(*args, default=None, **kwargs): + """ + Return the first non-``None`` value. This is used with keyword arg aliases and + for setting default values. Use `kwargs` to issue warnings when multiple passed. + """ + first = default + if args and kwargs: + raise ValueError('_not_none can only be used with args or kwargs.') + elif args: + for arg in args: + if arg is not None: + first = arg + break + elif kwargs: + for name, arg in list(kwargs.items()): + if arg is not None: + first = arg + break + kwargs = {name: arg for name, arg in kwargs.items() if arg is not None} + if len(kwargs) > 1: + warnings._warn_proplot( + f'Got conflicting or duplicate keyword arguments: {kwargs}. ' + 'Using the first keyword argument.' + ) + return first + + +# Internal import statements +# WARNING: Must come after _not_none because this is leveraged inside other funcs +from . import ( # noqa: F401 + benchmarks, + context, + docstring, + fonts, + guides, + inputs, + labels, + rcsetup, + versions, + warnings +) +from .versions import _version_mpl, _version_cartopy # noqa: F401 +from .warnings import ProplotWarning # noqa: F401 + + +# Style aliases. We use this rather than matplotlib's normalize_kwargs and _alias_maps. +# NOTE: We add aliases 'edgewidth' and 'fillcolor' for patch edges and faces +# NOTE: Alias cannot appear as key or else _translate_kwargs will overwrite with None! +_alias_maps = { + 'rgba': { + 'red': ('r',), + 'green': ('g',), + 'blue': ('b',), + 'alpha': ('a',), + }, + 'hsla': { + 'hue': ('h',), + 'saturation': ('s', 'c', 'chroma'), + 'luminance': ('l',), + 'alpha': ('a',), + }, + 'patch': { + 'alpha': ('a', 'alphas', 'fa', 'facealpha', 'facealphas', 'fillalpha', 'fillalphas'), # noqa: E501 + 'color': ('c', 'colors'), + 'edgecolor': ('ec', 'edgecolors'), + 'facecolor': ('fc', 'facecolors', 'fillcolor', 'fillcolors'), + 'hatch': ('h', 'hatching'), + 'linestyle': ('ls', 'linestyles'), + 'linewidth': ('lw', 'linewidths', 'ew', 'edgewidth', 'edgewidths'), + 'zorder': ('z', 'zorders'), + }, + 'line': { # copied from lines.py but expanded to include plurals + 'alpha': ('a', 'alphas'), + 'color': ('c', 'colors'), + 'dashes': ('d', 'dash'), + 'drawstyle': ('ds', 'drawstyles'), + 'fillstyle': ('fs', 'fillstyles', 'mfs', 'markerfillstyle', 'markerfillstyles'), + 'linestyle': ('ls', 'linestyles'), + 'linewidth': ('lw', 'linewidths'), + 'marker': ('m', 'markers'), + 'markersize': ('s', 'ms', 'markersizes'), # WARNING: no 'sizes' here for barb + 'markeredgewidth': ('ew', 'edgewidth', 'edgewidths', 'mew', 'markeredgewidths'), + 'markeredgecolor': ('ec', 'edgecolor', 'edgecolors', 'mec', 'markeredgecolors'), + 'markerfacecolor': ( + 'fc', 'facecolor', 'facecolors', 'fillcolor', 'fillcolors', + 'mc', 'markercolor', 'markercolors', 'mfc', 'markerfacecolors' + ), + 'zorder': ('z', 'zorders'), + }, + 'collection': { # WARNING: face color ignored for line collections + 'alpha': ('a', 'alphas'), # WARNING: collections and contours use singular! + 'colors': ('c', 'color'), + 'edgecolors': ('ec', 'edgecolor', 'mec', 'markeredgecolor', 'markeredgecolors'), + 'facecolors': ( + 'fc', 'facecolor', 'fillcolor', 'fillcolors', + 'mc', 'markercolor', 'markercolors', 'mfc', 'markerfacecolor', 'markerfacecolors' # noqa: E501 + ), + 'linestyles': ('ls', 'linestyle'), + 'linewidths': ('lw', 'linewidth', 'ew', 'edgewidth', 'edgewidths', 'mew', 'markeredgewidth', 'markeredgewidths'), # noqa: E501 + 'marker': ('m', 'markers'), + 'sizes': ('s', 'ms', 'markersize', 'markersizes'), + 'zorder': ('z', 'zorders'), + }, + 'text': { + 'color': ('c', 'fontcolor'), # NOTE: see text.py source code + 'fontfamily': ('family', 'name', 'fontname'), + 'fontsize': ('size',), + 'fontstretch': ('stretch',), + 'fontstyle': ('style',), + 'fontvariant': ('variant',), + 'fontweight': ('weight',), + 'fontproperties': ('fp', 'font', 'font_properties'), + 'zorder': ('z', 'zorders'), + }, +} + + +# Unit docstrings +# NOTE: Try to fit this into a single line. Cannot break up with newline as that will +# mess up docstring indentation since this is placed in indented param lines. +_units_docstring = 'If float, units are {units}. If string, interpreted by `~proplot.utils.units`.' # noqa: E501 +docstring._snippet_manager['units.pt'] = _units_docstring.format(units='points') +docstring._snippet_manager['units.in'] = _units_docstring.format(units='inches') +docstring._snippet_manager['units.em'] = _units_docstring.format(units='em-widths') + + +# Style docstrings +# NOTE: These are needed in a few different places +_line_docstring = """ +lw, linewidth, linewidths : unit-spec, default: :rc:`lines.linewidth` + The width of the line(s). + %(units.pt)s +ls, linestyle, linestyles : str, default: :rc:`lines.linestyle` + The style of the line(s). +c, color, colors : color-spec, optional + The color of the line(s). The property `cycle` is used by default. +a, alpha, alphas : float, optional + The opacity of the line(s). Inferred from `color` by default. +""" +_patch_docstring = """ +lw, linewidth, linewidths : unit-spec, default: :rc:`patch.linewidth` + The edge width of the patch(es). + %(units.pt)s +ls, linestyle, linestyles : str, default: '-' + The edge style of the patch(es). +ec, edgecolor, edgecolors : color-spec, default: '{edgecolor}' + The edge color of the patch(es). +fc, facecolor, facecolors, fillcolor, fillcolors : color-spec, optional + The face color of the patch(es). The property `cycle` is used by default. +a, alpha, alphas : float, optional + The opacity of the patch(es). Inferred from `facecolor` and `edgecolor` by default. +""" +_pcolor_collection_docstring = """ +lw, linewidth, linewidths : unit-spec, default: 0.3 + The width of lines between grid boxes. + %(units.pt)s +ls, linestyle, linestyles : str, default: '-' + The style of lines between grid boxes. +ec, edgecolor, edgecolors : color-spec, default: 'k' + The color of lines between grid boxes. +a, alpha, alphas : float, optional + The opacity of the grid boxes. Inferred from `cmap` by default. +""" +_contour_collection_docstring = """ +lw, linewidth, linewidths : unit-spec, default: 0.3 or :rc:`lines.linewidth` + The width of the line contours. Default is ``0.3`` when adding to filled contours + or :rc:`lines.linewidth` otherwise. %(units.pt)s +ls, linestyle, linestyles : str, default: '-' or :rc:`contour.negative_linestyle` + The style of the line contours. Default is ``'-'`` for positive contours and + :rcraw:`contour.negative_linestyle` for negative contours. +ec, edgecolor, edgecolors : color-spec, default: 'k' or inferred + The color of the line contours. Default is ``'k'`` when adding to filled contours + or inferred from `color` or `cmap` otherwise. +a, alpha, alpha : float, optional + The opacity of the contours. Inferred from `edgecolor` by default. +""" +_text_docstring = """ +name, fontname, family, fontfamily : str, optional + The font typeface name (e.g., ``'Fira Math'``) or font family name (e.g., + ``'serif'``). Matplotlib falls back to the system default if not found. +size, fontsize : unit-spec or str, optional + The font size. %(units.pt)s + This can also be a string indicating some scaling relative to + :rcraw:`font.size`. The sizes and scalings are shown below. The + scalings ``'med'``, ``'med-small'``, and ``'med-large'`` are + added by proplot while the rest are native matplotlib sizes. + + .. _font_table: + + ========================== ===== + Size Scale + ========================== ===== + ``'xx-small'`` 0.579 + ``'x-small'`` 0.694 + ``'small'``, ``'smaller'`` 0.833 + ``'med-small'`` 0.9 + ``'med'``, ``'medium'`` 1.0 + ``'med-large'`` 1.1 + ``'large'``, ``'larger'`` 1.2 + ``'x-large'`` 1.440 + ``'xx-large'`` 1.728 + ``'larger'`` 1.2 + ========================== ===== + +""" +docstring._snippet_manager['artist.line'] = _line_docstring +docstring._snippet_manager['artist.text'] = _text_docstring +docstring._snippet_manager['artist.patch'] = _patch_docstring.format(edgecolor='none') +docstring._snippet_manager['artist.patch_black'] = _patch_docstring.format(edgecolor='black') # noqa: E501 +docstring._snippet_manager['artist.collection_pcolor'] = _pcolor_collection_docstring +docstring._snippet_manager['artist.collection_contour'] = _contour_collection_docstring + + +def _get_aliases(category, *keys): + """ + Get all available aliases. + """ + aliases = [] + for key in keys: + aliases.append(key) + aliases.extend(_alias_maps[category][key]) + return tuple(aliases) + + +def _kwargs_to_args(options, *args, allow_extra=False, **kwargs): + """ + Translate keyword arguments to positional arguments. Permit omitted + arguments so that plotting functions can infer values. + """ + nargs, nopts = len(args), len(options) + if nargs > nopts and not allow_extra: + raise ValueError(f'Expected up to {nopts} positional arguments. Got {nargs}.') + args = list(args) # WARNING: Axes.text() expects return type of list + args.extend(None for _ in range(nopts - nargs)) # fill missing args + for idx, keys in enumerate(options): + if isinstance(keys, str): + keys = (keys,) + opts = {} + if args[idx] is not None: # positional args have first priority + opts[keys[0] + '_positional'] = args[idx] + for key in keys: # keyword args + opts[key] = kwargs.pop(key, None) + args[idx] = _not_none(**opts) # may reassign None + return args, kwargs + + +def _pop_kwargs(kwargs, *keys, **aliases): + """ + Pop the input properties and return them in a new dictionary. + """ + output = {} + aliases.update({key: () for key in keys}) + for key, aliases in aliases.items(): + aliases = (aliases,) if isinstance(aliases, str) else aliases + aliases = tuple(alias for alias in aliases if alias != key) # prevent dev errs + opts = {key: kwargs.pop(key, None) for key in (key, *aliases)} + value = _not_none(**opts) + if value is not None: + output[key] = value + return output + + +def _pop_params(kwargs, *funcs, ignore_internal=False): + """ + Pop parameters of the input functions or methods. + """ + internal_params = { + 'default_cmap', + 'default_discrete', + 'inbounds', + 'plot_contours', + 'plot_lines', + 'skip_autolev', + 'to_centers', + } + output = {} + for func in funcs: + if isinstance(func, inspect.Signature): + sig = func + elif callable(func): + sig = inspect.signature(func) + elif func is None: + continue + else: + raise RuntimeError(f'Internal error. Invalid function {func!r}.') + for key in sig.parameters: + value = kwargs.pop(key, None) + if ignore_internal and key in internal_params: + continue + if value is not None: + output[key] = value + return output + + +def _pop_props(input, *categories, prefix=None, ignore=None, skip=None): + """ + Pop the registered properties and return them in a new dictionary. + """ + output = {} + skip = skip or () + ignore = ignore or () + if isinstance(skip, str): # e.g. 'sizes' for barbs() input + skip = (skip,) + if isinstance(ignore, str): # e.g. 'marker' to ignore marker properties + ignore = (ignore,) + prefix = prefix or '' # e.g. 'box' for boxlw, boxlinewidth, etc. + for category in categories: + for key, aliases in _alias_maps[category].items(): + if isinstance(aliases, str): + aliases = (aliases,) + opts = { + prefix + alias: input.pop(prefix + alias, None) + for alias in (key, *aliases) + if alias not in skip + } + prop = _not_none(**opts) + if prop is None: + continue + if any(string in key for string in ignore): + warnings._warn_proplot(f'Ignoring property {key}={prop!r}.') + continue + if isinstance(prop, str): # ad-hoc unit conversion + if key in ('fontsize',): + from ..utils import _fontsize_to_pt + prop = _fontsize_to_pt(prop) + if key in ('linewidth', 'linewidths', 'markersize'): + from ..utils import units + prop = units(prop, 'pt') + output[key] = prop + return output + + +def _pop_rc(src, *, ignore_conflicts=True): + """ + Pop the rc setting names and mode for a `~Configurator.context` block. + """ + # NOTE: Must ignore deprected or conflicting rc params + # NOTE: rc_mode == 2 applies only the updated params. A power user + # could use ax.format(rc_mode=0) to re-apply all the current settings + conflict_params = ( + 'alpha', + 'color', + 'facecolor', + 'edgecolor', + 'linewidth', + 'basemap', + 'backend', + 'share', + 'span', + 'tight', + 'span', + ) + kw = src.pop('rc_kw', None) or {} + if 'mode' in src: + src['rc_mode'] = src.pop('mode') + warnings._warn_proplot( + "Keyword 'mode' was deprecated in v0.6. Please use 'rc_mode' instead." + ) + mode = src.pop('rc_mode', None) + mode = _not_none(mode, 2) # only apply updated params by default + for key, value in tuple(src.items()): + name = rcsetup._rc_nodots.get(key, None) + if ignore_conflicts and name in conflict_params: + name = None # former renamed settings + if name is not None: + kw[name] = src.pop(key) + return kw, mode + + +def _translate_loc(loc, mode, *, default=None, **kwargs): + """ + Translate the location string `loc` into a standardized form. The `mode` + must be a string for which there is a :rcraw:`mode.loc` setting. Additional + options can be added with keyword arguments. + """ + # Create specific options dictionary + # NOTE: This is not inside validators.py because it is also used to + # validate various user-input locations. + if mode == 'align': + loc_dict = rcsetup.ALIGN_LOCS + elif mode == 'panel': + loc_dict = rcsetup.PANEL_LOCS + elif mode == 'legend': + loc_dict = rcsetup.LEGEND_LOCS + elif mode == 'colorbar': + loc_dict = rcsetup.COLORBAR_LOCS + elif mode == 'text': + loc_dict = rcsetup.TEXT_LOCS + else: + raise ValueError(f'Invalid mode {mode!r}.') + loc_dict = loc_dict.copy() + loc_dict.update(kwargs) + + # Translate location + if loc in (None, True): + loc = default + elif isinstance(loc, (str, Integral)): + try: + loc = loc_dict[loc] + except KeyError: + raise KeyError( + f'Invalid {mode} location {loc!r}. Options are: ' + + ', '.join(map(repr, loc_dict)) + + '.' + ) + elif ( + mode == 'legend' + and np.iterable(loc) + and len(loc) == 2 + and all(isinstance(l, Real) for l in loc) + ): + loc = tuple(loc) + else: + raise KeyError(f'Invalid {mode} location {loc!r}.') + + # Kludge / white lie + # TODO: Implement 'best' colorbar location + if mode == 'colorbar' and loc == 'best': + loc = 'lower right' + + return loc + + +def _translate_grid(b, key): + """ + Translate an instruction to turn either major or minor gridlines on or off into a + boolean and string applied to :rcraw:`axes.grid` and :rcraw:`axes.grid.which`. + """ + ob = rc_matplotlib['axes.grid'] + owhich = rc_matplotlib['axes.grid.which'] + + # Instruction is to turn off gridlines + if not b: + # Gridlines are already off, or they are on for the particular + # ones that we want to turn off. Instruct to turn both off. + if ( + not ob + or key == 'grid' and owhich == 'major' + or key == 'gridminor' and owhich == 'minor' + ): + which = 'both' # disable both sides + # Gridlines are currently on for major and minor ticks, so we + # instruct to turn on gridlines for the one we *don't* want off + elif owhich == 'both': # and ob is True, as already tested + # if gridminor=False, enable major, and vice versa + b = True + which = 'major' if key == 'gridminor' else 'minor' + # Gridlines are on for the ones that we *didn't* instruct to + # turn off, and off for the ones we do want to turn off. This + # just re-asserts the ones that are already on. + else: + b = True + which = owhich + + # Instruction is to turn on gridlines + else: + # Gridlines are already both on, or they are off only for the + # ones that we want to turn on. Turn on gridlines for both. + if ( + owhich == 'both' + or key == 'grid' and owhich == 'minor' + or key == 'gridminor' and owhich == 'major' + ): + which = 'both' + # Gridlines are off for both, or off for the ones that we + # don't want to turn on. We can just turn on these ones. + else: + which = owhich + + return b, which diff --git a/proplot/internals/benchmarks.py b/proplot/internals/benchmarks.py new file mode 100644 index 000000000..086b8313c --- /dev/null +++ b/proplot/internals/benchmarks.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +""" +Utilities for benchmarking proplot performance. +""" +import time + +from . import ic # noqa: F401 + +BENCHMARK = False # toggle this to turn on benchmarking (see timers.py) + + +class _benchmark(object): + """ + Context object for timing arbitrary blocks of code. + """ + def __init__(self, message): + self.message = message + + def __enter__(self): + if BENCHMARK: + self.time = time.perf_counter() + + def __exit__(self, *args): # noqa: U100 + if BENCHMARK: + print(f'{self.message}: {time.perf_counter() - self.time}s') diff --git a/proplot/internals/context.py b/proplot/internals/context.py new file mode 100644 index 000000000..494254d4b --- /dev/null +++ b/proplot/internals/context.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +""" +Utilities for manging context. +""" +from . import ic # noqa: F401 + + +class _empty_context(object): + """ + A dummy context manager. + """ + def __init__(self): + pass + + def __enter__(self): + pass + + def __exit__(self, *args): # noqa: U100 + pass + + +class _state_context(object): + """ + Temporarily modify attribute(s) for an arbitrary object. + """ + def __init__(self, obj, **kwargs): + self._obj = obj + self._attrs_new = kwargs + self._attrs_prev = { + key: getattr(obj, key) for key in kwargs if hasattr(obj, key) + } + + def __enter__(self): + for key, value in self._attrs_new.items(): + setattr(self._obj, key, value) + + def __exit__(self, *args): # noqa: U100 + for key in self._attrs_new.keys(): + if key in self._attrs_prev: + setattr(self._obj, key, self._attrs_prev[key]) + else: + delattr(self._obj, key) diff --git a/proplot/internals/docstring.py b/proplot/internals/docstring.py new file mode 100644 index 000000000..d1589d42e --- /dev/null +++ b/proplot/internals/docstring.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python3 +""" +Utilities for modifying proplot docstrings. +""" +# WARNING: To check every docstring in the package for +# unfilled snippets simply use the following code: +# >>> import proplot as pplt +# ... seen = set() +# ... def _iter_doc(objs): +# ... if objs in seen: +# ... return +# ... seen.add(objs) +# ... for attr in dir(objs): +# ... obj = getattr(objs, attr, None) +# ... if callable(obj) and hasattr(obj, '__doc__'): +# ... if obj in seen: +# ... continue +# ... seen.add(obj) +# ... if obj.__doc__ and '%(' in obj.__doc__: +# ... yield obj.__name__ +# ... yield from _iter_doc(obj) +# ... print(*_iter_doc(pplt)) +import inspect +import re + +import matplotlib.axes as maxes +import matplotlib.figure as mfigure +from matplotlib import rcParams as rc_matplotlib + +from . import ic # noqa: F401 + + +def _obfuscate_kwargs(func): + """ + Obfuscate keyword args. + """ + return _obfuscate_signature(func, lambda **kwargs: None) + + +def _obfuscate_params(func): + """ + Obfuscate all parameters. + """ + return _obfuscate_signature(func, lambda *args, **kwargs: None) + + +def _obfuscate_signature(func, dummy): + """ + Obfuscate a misleading or incomplete call signature. + Instead users should inspect the parameter table. + """ + # Obfuscate signature by converting to *args **kwargs. Note this does + # not change behavior of function! Copy parameters from a dummy function + # because I'm too lazy to figure out inspect.Parameters API + # See: https://stackoverflow.com/a/33112180/4970632 + sig = inspect.signature(func) + sig_repl = inspect.signature(dummy) + func.__signature__ = sig.replace(parameters=tuple(sig_repl.parameters.values())) + return func + + +def _concatenate_inherited(func, prepend_summary=False): + """ + Concatenate docstrings from a matplotlib axes method with a proplot + axes method and obfuscate the call signature. + """ + # Get matplotlib axes func + # NOTE: Do not bother inheriting from cartopy GeoAxes. Cartopy completely + # truncates the matplotlib docstrings (which is kind of not great). + qual = func.__qualname__ + if 'Axes' in qual: + cls = maxes.Axes + elif 'Figure' in qual: + cls = mfigure.Figure + else: + raise ValueError(f'Unexpected method {qual!r}. Must be Axes or Figure method.') + doc = inspect.getdoc(func) or '' # also dedents + func_orig = getattr(cls, func.__name__, None) + doc_orig = inspect.getdoc(func_orig) + if not doc_orig: # should never happen + return func + + # Optionally prepend the function summary + # Concatenate docstrings only if this is not generated for website + regex = re.search(r'\.( | *\n|\Z)', doc_orig) + if regex and prepend_summary: + doc = doc_orig[:regex.start() + 1] + '\n\n' + doc + if not rc_matplotlib['docstring.hardcopy']: + doc = f""" +===================== +Proplot documentation +===================== + +{doc} + +======================== +Matplotlib documentation +======================== + +{doc_orig} +""" + + # Return docstring + # NOTE: Also obfuscate parameters to avoid partial coverage of call signatures + func.__doc__ = inspect.cleandoc(doc) + func = _obfuscate_params(func) + return func + + +class _SnippetManager(dict): + """ + A simple database for handling documentation snippets. + """ + def __call__(self, obj): + """ + Add snippets to the string or object using ``%(name)s`` substitution. Here + ``%(name)s`` is used rather than ``.format`` to support invalid identifiers. + """ + if isinstance(obj, str): + obj %= self # add snippets to a string + else: + obj.__doc__ = inspect.getdoc(obj) # also dedents the docstring + if obj.__doc__: + obj.__doc__ %= self # insert snippets after dedent + return obj + + def __setitem__(self, key, value): + """ + Populate input strings with other snippets and strip newlines. Developers + should take care to import modules in the correct order. + """ + value = self(value) + value = value.strip('\n') + super().__setitem__(key, value) + + +# Initiate snippets database +_snippet_manager = _SnippetManager() diff --git a/proplot/internals/fonts.py b/proplot/internals/fonts.py new file mode 100644 index 000000000..f9659a0b9 --- /dev/null +++ b/proplot/internals/fonts.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +""" +Overrides related to math fonts. +""" +import matplotlib as mpl +from matplotlib.font_manager import findfont, ttfFontProperty +from matplotlib.mathtext import MathTextParser + +from . import warnings + +try: # newer versions + from matplotlib._mathtext import UnicodeFonts +except ImportError: # older versions + from matplotlib.mathtext import UnicodeFonts + +# Global constant +WARN_MATHPARSER = True + + +class _UnicodeFonts(UnicodeFonts): + """ + A simple `~matplotlib._mathtext.UnicodeFonts` subclass that + interprets ``rc['mathtext.default'] != 'regular'`` in the presence of + ``rc['mathtext.fontset'] == 'custom'`` as possibly modifying the active font. + + Works by permitting the ``rc['mathtext.rm']``, ``rc['mathtext.it']``, + etc. settings to have the dummy value ``'regular'`` instead of a valid family + name, e.g. ``rc['mathtext.it'] == 'regular:italic'`` (permitted through an + override of the `~matplotlib.rcsetup.validate_font_properties` validator). + When this dummy value is detected then the font properties passed to + `~matplotlib._mathtext.TrueTypeFont` are taken by replacing ``'regular'`` + in the "math" fontset with the active font name. + """ + def __init__(self, *args, **kwargs): + # Initialize font + # NOTE: Could also capture the 'default_font_prop' passed as positional + # argument but want to guard against keyword changes. This entire API is + # private and it is easier to do graceful fallback with _fonts dictionary. + ctx = {} # rc context + regular = {} # styles + for texfont in ('cal', 'rm', 'tt', 'it', 'bf', 'sf'): + key = 'mathtext.' + texfont + prop = mpl.rcParams[key] + if prop.startswith('regular'): + ctx[key] = prop.replace('regular', 'sans', 1) + regular[texfont] = prop + with mpl.rc_context(ctx): + super().__init__(*args, **kwargs) + # Apply current font replacements + global WARN_MATHPARSER + if ( + hasattr(self, 'fontmap') + and hasattr(self, '_fonts') + and 'regular' in self._fonts + ): + font = self._fonts['regular'] # an ft2font.FT2Font instance + font = ttfFontProperty(font) + for texfont, prop in regular.items(): + prop = prop.replace('regular', font.name) + self.fontmap[texfont] = findfont(prop, fallback_to_default=False) + elif WARN_MATHPARSER: + # Suppress duplicate warnings in case API changes + warnings._warn_proplot('Failed to update the math text parser.') + WARN_MATHPARSER = False + + +# Replace the parser +try: + mapping = MathTextParser._font_type_mapping + if mapping['custom'] is UnicodeFonts: + mapping['custom'] = _UnicodeFonts +except (KeyError, AttributeError): + warnings._warn_proplot('Failed to update math text parser.') + WARN_MATHPARSER = False diff --git a/proplot/internals/guides.py b/proplot/internals/guides.py new file mode 100644 index 000000000..e2b620853 --- /dev/null +++ b/proplot/internals/guides.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python3 +""" +Utilties related to legends and colorbars. +""" +import matplotlib.artist as martist +import matplotlib.colorbar as mcolorbar +import matplotlib.legend as mlegend # noqa: F401 +import matplotlib.ticker as mticker +import numpy as np + +from . import ic # noqa: F401 +from . import warnings + +# Global constants +REMOVE_AFTER_FLUSH = ( + 'pad', 'space', 'width', 'length', 'shrink', 'align', 'queue', +) +GUIDE_ALIASES = ( + ('title', 'label'), + ('locator', 'ticks'), + ('format', 'formatter', 'ticklabels') +) + + +def _add_guide_kw(name, kwargs, **opts): + """ + Add to the `colorbar_kw` or `legend_kw` dict if there are no conflicts. + """ + # NOTE: Here we *do not* want to overwrite properties in dictionary. Indicates + # e.g. default locator inferred from levels or default title inferred from metadata. + attr = f'{name}_kw' + if not opts: + return + if not kwargs.get(attr, None): + kwargs[attr] = {} # permit e.g. colorbar_kw=None + guide_kw = kwargs[attr] + _update_kw(guide_kw, overwrite=False, **opts) + + +def _cache_guide_kw(obj, name, kwargs): + """ + Cache settings on the object from the input keyword arguments. + """ + # NOTE: Here we overwrite the hidden dictionary if it already exists. + # This is only called once in _update_guide() so its fine. + try: + setattr(obj, f'_{name}_kw', kwargs) + except AttributeError: + pass + if isinstance(obj, (tuple, list, np.ndarray)): + for member in obj: + _cache_guide_kw(member, name, kwargs) + + +def _flush_guide_kw(obj, name, kwargs): + """ + Flux settings cached on the object into the keyword arguments. + """ + # NOTE: Here we *do not* overwrite properties in the dictionary by default. + # Indicates e.g. calling colorbar(extend='both') after pcolor(extend='neither'). + # NOTE: Previously had problems reusing same keyword arguments for more than one + # colorbar() because locator or formatter axis would get reset. Old solution was + # to delete the _guide_kw but that destroyed default behavior. New solution is + # to keep _guide_kw but have constructor functions return shallow copies. + guide_kw = getattr(obj, f'_{name}_kw', None) + if guide_kw: + _update_kw(kwargs, overwrite=False, **guide_kw) + for key in REMOVE_AFTER_FLUSH: + guide_kw.pop(key, None) + if isinstance(obj, (tuple, list, np.ndarray)): + for member in obj: # possibly iterate over matplotlib tuple/list subclasses + _flush_guide_kw(member, name, kwargs) + return kwargs + + +def _update_kw(kwargs, overwrite=False, **opts): + """ + Add the keyword arguments to the dictionary if not already present. + """ + for key, value in opts.items(): + if value is None: + continue + keys = tuple(k for opts in GUIDE_ALIASES for k in opts if key in opts) + keys = keys or (key,) # e.g. 'extend' or something + keys_found = tuple(key for key in keys if kwargs.get(key) is not None) + if not keys_found: + kwargs[key] = value + elif overwrite: # overwrite existing key + kwargs[keys_found[0]] = value + + +def _iter_children(*args): + """ + Iterate through `_children` of `HPacker`, `VPacker`, and `DrawingArea`. + This is used to update legend handle properties. + """ + for arg in args: + if hasattr(arg, '_children'): + yield from _iter_children(*arg._children) + elif arg is not None: + yield arg + + +def _iter_iterables(*args): + """ + Iterate over arbitrary nested lists of iterables. Used for deciphering legend input. + Things can get complicated with e.g. bar colunns plus negative-positive colors. + """ + for arg in args: + if np.iterable(arg): + yield from _iter_iterables(*arg) + elif arg is not None: + yield arg + + +def _update_ticks(self, manual_only=False): + """ + Refined colorbar tick updater without subclassing. + """ + # TODO: Add this to generalized colorbar subclass? + # NOTE: Matplotlib 3.5+ does not define _use_auto_colorbar_locator since + # ticks are always automatically adjusted by its colorbar subclass. This + # override is thus backwards and forwards compatible. + attr = '_use_auto_colorbar_locator' + if not hasattr(self, attr) or getattr(self, attr)(): + if manual_only: + pass + else: + mcolorbar.Colorbar.update_ticks(self) # AutoMinorLocator auto updates + else: + mcolorbar.Colorbar.update_ticks(self) # update necessary + minorlocator = getattr(self, 'minorlocator', None) + if minorlocator is None: + pass + elif hasattr(self, '_ticker'): + ticks, *_ = self._ticker(self.minorlocator, mticker.NullFormatter()) + axis = self.ax.yaxis if self.orientation == 'vertical' else self.ax.xaxis + axis.set_ticks(ticks, minor=True) + axis.set_ticklabels([], minor=True) + else: + warnings._warn_proplot(f'Cannot use user-input colorbar minor locator {minorlocator!r} (older matplotlib version). Turning on minor ticks instead.') # noqa: E501 + self.minorlocator = None + self.minorticks_on() # at least turn them on + + +class _InsetColorbar(martist.Artist): + """ + Legend-like class for managing inset colorbars. + """ + # TODO: Write this! + + +class _CenteredLegend(martist.Artist): + """ + Legend-like class for managing centered-row legends. + """ + # TODO: Write this! diff --git a/proplot/internals/inputs.py b/proplot/internals/inputs.py new file mode 100644 index 000000000..cbbb56235 --- /dev/null +++ b/proplot/internals/inputs.py @@ -0,0 +1,799 @@ +#!/usr/bin/env python3 +""" +Utilities for processing input data passed to plotting commands. +""" +import functools +import sys + +import numpy as np +import numpy.ma as ma + +from . import ic # noqa: F401 +from . import _not_none, warnings + +try: + from cartopy.crs import PlateCarree +except ModuleNotFoundError: + PlateCarree = object + + +# Constants +BASEMAP_FUNCS = ( # default latlon=True + 'barbs', 'contour', 'contourf', 'hexbin', + 'imshow', 'pcolor', 'pcolormesh', 'plot', + 'quiver', 'scatter', 'streamplot', 'step', +) +CARTOPY_FUNCS = ( # default transform=PlateCarree() + 'barbs', 'contour', 'contourf', + 'fill', 'fill_between', 'fill_betweenx', # NOTE: not sure if these work + 'imshow', 'pcolor', 'pcolormesh', 'plot', + 'quiver', 'scatter', 'streamplot', 'step', + 'tricontour', 'tricontourf', 'tripcolor', # NOTE: not sure why these work +) + + +def _load_objects(): + """ + Load array-like objects. + """ + # NOTE: We just want to detect if *input arrays* belong to these types -- and if + # this is the case, it means the module has already been imported! So, we only + # try loading these classes within autoformat calls. This saves >500ms of import + # time. We use ndarray as the default value for unimported types and in loops we + # are careful to check membership to np.ndarray before anything else. + global ndarray, DataArray, DataFrame, Series, Index, Quantity + ndarray = np.ndarray + DataArray = getattr(sys.modules.get('xarray', None), 'DataArray', ndarray) + DataFrame = getattr(sys.modules.get('pandas', None), 'DataFrame', ndarray) + Series = getattr(sys.modules.get('pandas', None), 'Series', ndarray) + Index = getattr(sys.modules.get('pandas', None), 'Index', ndarray) + Quantity = getattr(sys.modules.get('pint', None), 'Quantity', ndarray) + + +_load_objects() + + +# Type utilities +def _is_numeric(data): + """ + Test whether input is numeric array rather than datetime or strings. + """ + array = _to_numpy_array(data) + return len(data) and ( + np.issubdtype(array.dtype, np.number) + or np.issubdtype(array.dtype, object) + and all(isinstance(_, np.number) for _ in array.flat) + ) + + +def _is_categorical(data): + """ + Test whether input is array of strings. + """ + array = _to_numpy_array(data) + return len(data) and ( + np.issubdtype(array.dtype, str) + or np.issubdtype(array.dtype, object) + and any(isinstance(_, str) for _ in array.flat) + ) + + +def _is_descending(data): + """ + Test whether the input data is descending. This is used for auto axis reversal. + """ + # NOTE: Want this to work with e.g. datetime object arrays and numpy datetime + # arrays so use try except clause. + data = _to_numpy_array(data) + if data.ndim > 1 or data.size < 2: + return False + try: + return all(x != abs(x) for x in np.diff(data)) + except TypeError: + return False + + +def _to_duck_array(data, strip_units=False): + """ + Convert arbitrary input to duck array. Preserve array containers with metadata. + """ + _load_objects() + if data is None: + raise ValueError('Invalid data None.') + if ( + not isinstance(data, (ndarray, DataArray, DataFrame, Series, Index, Quantity)) + or not np.iterable(data) + ): + # WARNING: this strips e.g. scalar DataArray metadata + data = _to_numpy_array(data) + if strip_units: # used for z coordinates that cannot have units + if isinstance(data, (ndarray, Quantity)): + if Quantity is not ndarray and isinstance(data, Quantity): + data = data.magnitude + elif isinstance(data, DataArray): + if Quantity is not ndarray and isinstance(data.data, Quantity): + data = data.copy(deep=False) + data.data = data.data.magnitude + return data + + +def _to_numpy_array(data, strip_units=False): + """ + Convert arbitrary input to numpy array. Preserve masked arrays and unit arrays. + """ + _load_objects() + if data is None: + raise ValueError('Invalid data None.') + if isinstance(data, ndarray): + pass + elif isinstance(data, DataArray): + data = data.data # support pint quantities that get unit-stripped later + elif isinstance(data, (DataFrame, Series, Index)): + data = data.values + if Quantity is not ndarray and isinstance(data, Quantity): + units = None if strip_units else data.units + data = np.atleast_1d(data.magnitude) + else: + units = None + data = np.atleast_1d(data) # natively preserves masked arrays + if np.issubdtype(data.dtype, bool): + data = data.view(np.uint8) + if units is not None: + data = data * units + return data + + +def _to_masked_array(data, *, copy=False): + """ + Convert numpy array to masked array with consideration for datetimes and quantities. + """ + units = None + if ndarray is not Quantity and isinstance(data, Quantity): + data, units = data.magnitude, data.units + else: + data = _to_numpy_array(data) + if data.dtype == 'O': + data = ma.array(data, mask=False) + else: + data = ma.masked_invalid(data, copy=copy) + if np.issubdtype(data.dtype, np.integer): + data = data.astype(np.float64) + if np.issubdtype(data.dtype, np.number): + data.fill_value *= np.nan # default float fill_value is 1e+20 or 1e+20 + 0j + else: + pass # leave with default fill_value (e.g. NaT for datetime data) + return data, units + + +# Input data transformations +def _to_edges(x, y, z): + """ + Enforce that coordinates are edges. Convert from centers if possible. + """ + from ..utils import edges, edges2d + xlen, ylen = x.shape[-1], y.shape[0] + if z.ndim == 2 and z.shape[1] == xlen and z.shape[0] == ylen: + # Get edges given centers + if all(z.ndim == 1 and z.size > 1 and _is_numeric(z) for z in (x, y)): + x = edges(x) + y = edges(y) + else: + if x.ndim == 2 and x.shape[0] > 1 and x.shape[1] > 1 and _is_numeric(x): + x = edges2d(x) + if y.ndim == 2 and y.shape[0] > 1 and y.shape[1] > 1 and _is_numeric(y): + y = edges2d(y) + elif z.shape[-1] != xlen - 1 or z.shape[0] != ylen - 1: + # Helpful error message + raise ValueError( + f'Input shapes x {x.shape} and y {y.shape} must match ' + f'array centers {z.shape} or ' + f'array borders {tuple(i + 1 for i in z.shape)}.' + ) + return x, y + + +def _to_centers(x, y, z): + """ + Enforce that coordinates are centers. Convert from edges if possible. + """ + xlen, ylen = x.shape[-1], y.shape[0] + if z.ndim == 2 and z.shape[1] == xlen - 1 and z.shape[0] == ylen - 1: + # Get centers given edges + if all(z.ndim == 1 and z.size > 1 and _is_numeric(z) for z in (x, y)): + x = 0.5 * (x[1:] + x[:-1]) + y = 0.5 * (y[1:] + y[:-1]) + else: + if x.ndim == 2 and x.shape[0] > 1 and x.shape[1] > 1 and _is_numeric(x): + x = 0.25 * (x[:-1, :-1] + x[:-1, 1:] + x[1:, :-1] + x[1:, 1:]) + if y.ndim == 2 and y.shape[0] > 1 and y.shape[1] > 1 and _is_numeric(y): + y = 0.25 * (y[:-1, :-1] + y[:-1, 1:] + y[1:, :-1] + y[1:, 1:]) + elif z.shape[-1] != xlen or z.shape[0] != ylen: + # Helpful error message + raise ValueError( + f'Input shapes x {x.shape} and y {y.shape} ' + f'must match z centers {z.shape} ' + f'or z borders {tuple(i+1 for i in z.shape)}.' + ) + return x, y + + +# Input argument processing +def _from_data(data, *args): + """ + Try to convert positional `key` arguments to `data[key]`. If argument is string + it could be a valid positional argument like `fmt` so do not raise error. + """ + if data is None: + return + args = list(args) + for i, arg in enumerate(args): + if isinstance(arg, str): + try: + array = data[arg] + except KeyError: + pass + else: + args[i] = array + return args + + +def _preprocess_or_redirect(*keys, keywords=None, allow_extra=True): + """ + Redirect internal plotting calls to native matplotlib methods. Also convert + keyword args to positional and pass arguments through 'data' dictionary. + """ + # Keyword arguments processed through 'data' + # Positional arguments are always processed through data + keywords = keywords or () + if isinstance(keywords, str): + keywords = (keywords,) + + def _decorator(func): + name = func.__name__ + from . import _kwargs_to_args + + @functools.wraps(func) + def _preprocess_or_redirect(self, *args, **kwargs): + if getattr(self, '_internal_call', None): + # Redirect internal matplotlib call to native function + from ..axes import PlotAxes + func_native = getattr(super(PlotAxes, self), name) + return func_native(*args, **kwargs) + else: + # Impose default coordinate system + from ..constructor import Proj + if self._name == 'basemap' and name in BASEMAP_FUNCS: + if kwargs.get('latlon', None) is None: + kwargs['latlon'] = True + if self._name == 'cartopy' and name in CARTOPY_FUNCS: + if kwargs.get('transform', None) is None: + kwargs['transform'] = PlateCarree() + else: + kwargs['transform'] = Proj(kwargs['transform']) + + # Process data args + # NOTE: Raises error if there are more args than keys + args, kwargs = _kwargs_to_args( + keys, *args, allow_extra=allow_extra, **kwargs + ) + data = kwargs.pop('data', None) + if data is not None: + args = _from_data(data, *args) + for key in set(keywords) & set(kwargs): + kwargs[key] = _from_data(data, kwargs[key]) + + # Auto-setup matplotlib with the input unit registry + _load_objects() + for arg in args: + if ndarray is not DataArray and isinstance(arg, DataArray): + arg = arg.data + if ndarray is not Quantity and isinstance(arg, Quantity): + ureg = getattr(arg, '_REGISTRY', None) + if hasattr(ureg, 'setup_matplotlib'): + ureg.setup_matplotlib(True) + + # Call main function + return func(self, *args, **kwargs) # call unbound method + + return _preprocess_or_redirect + + return _decorator + + +# Stats utiltiies +def _dist_clean(distribution): + """ + Clean the distrubtion data for processing by `boxplot` or `violinplot`. + Without this invalid values break the algorithm. + """ + if distribution.ndim == 1: + distribution = distribution[:, None] + distribution, units = _to_masked_array(distribution) # no copy needed + distribution = tuple( + distribution[..., i].compressed() for i in range(distribution.shape[-1]) + ) + if units is not None: + distribution = tuple(dist * units for dist in distribution) + return distribution + + +def _dist_reduce(data, *, mean=None, means=None, median=None, medians=None, **kwargs): + """ + Reduce statistical distributions to means and medians. Tack on a + distribution keyword argument for processing down the line. + """ + # TODO: Permit 3D array with error dimension coming first + means = _not_none(mean=mean, means=means) + medians = _not_none(median=median, medians=medians) + if means and medians: + warnings._warn_proplot( + 'Cannot have both means=True and medians=True. Using former.' + ) + medians = None + if means or medians: + distribution, units = _to_masked_array(data) + distribution = distribution.filled() + if distribution.ndim != 2: + raise ValueError( + f'Expected 2D array for means=True. Got {distribution.ndim}D.' + ) + if units is not None: + distribution = distribution * units + if means: + data = np.nanmean(distribution, axis=0) + else: + data = np.nanmedian(distribution, axis=0) + kwargs['distribution'] = distribution + + # Save argument passed to _error_bars + return (data, kwargs) + + +def _dist_range( + data, distribution, *, errdata=None, absolute=False, label=False, + stds=None, pctiles=None, stds_default=None, pctiles_default=None, +): + """ + Return a plottable characteristic range for the statistical distribution + relative to the input coordinate (generally a mean or median). + """ + # Parse stds arguments + # NOTE: Have to guard against "truth value of an array is ambiguous" errors + if stds is True: + stds = stds_default + elif stds is False or stds is None: + stds = None + else: + stds = np.atleast_1d(stds) + if stds.size == 1: + stds = sorted((-stds.item(), stds.item())) + elif stds.size != 2: + raise ValueError('Expected scalar or length-2 stdev specification.') + + # Parse pctiles arguments + if pctiles is True: + pctiles = pctiles_default + elif pctiles is False or pctiles is None: + pctiles = None + else: + pctiles = np.atleast_1d(pctiles) + if pctiles.size == 1: + delta = (100 - pctiles.item()) / 2.0 + pctiles = sorted((delta, 100 - delta)) + elif pctiles.size != 2: + raise ValueError('Expected scalar or length-2 pctiles specification.') + + # Incompatible settings + if distribution is None and any(_ is not None for _ in (stds, pctiles)): + raise ValueError( + 'To automatically compute standard deviations or percentiles on ' + 'columns of data you must pass means=True or medians=True.' + ) + if stds is not None and pctiles is not None: + warnings._warn_proplot( + 'Got both a standard deviation range and a percentile range for ' + 'auto error indicators. Using the standard deviation range.' + ) + pctiles = None + if distribution is not None and errdata is not None: + stds = pctiles = None + warnings._warn_proplot( + 'You explicitly provided the error bounds but also requested ' + 'automatically calculating means or medians on data columns. ' + 'It may make more sense to use the "stds" or "pctiles" keyword args ' + 'and have *proplot* calculate the error bounds.' + ) + + # Compute error data in format that can be passed to maxes.Axes.errorbar() + # NOTE: Include option to pass symmetric deviation from central points + if errdata is not None: + # Manual error data + if data.ndim != 1: + raise ValueError( + "Passing both 2D data coordinates and 'errdata' is not yet supported." + ) + label_default = 'uncertainty' + err = _to_numpy_array(errdata) + if ( + err.ndim not in (1, 2) + or err.shape[-1] != data.size + or err.ndim == 2 and err.shape[0] != 2 + ): + raise ValueError( + f"Input 'errdata' has shape {err.shape}. Expected (2, {data.size})." + ) + if err.ndim == 1: + abserr = err + err = np.empty((2, err.size)) + err[0, :] = data - abserr # translated back to absolute deviations below + err[1, :] = data + abserr + elif stds is not None: + # Standard deviations + # NOTE: Invalid values were handled by _dist_reduce + label_default = fr'{abs(stds[1])}$\sigma$ range' + stds = _to_numpy_array(stds)[:, None] + err = data + stds * np.nanstd(distribution, axis=0) + elif pctiles is not None: + # Percentiles + # NOTE: Invalid values were handled by _dist_reduce + label_default = f'{pctiles[1] - pctiles[0]}% range' + err = np.nanpercentile(distribution, pctiles, axis=0) + else: + warnings._warn_proplot( + 'Error indications are missing from the dataset reduced by a ' + 'mean or median operation. Consider passing e.g. bars=True.' + ) + err = None + + # Adjust error bounds + if err is not None and not absolute: # for errorbar() ingestion + err = err - data + err[0, :] *= -1 # absolute deviations from central points + + # Apply legend entry + if isinstance(label, str): + pass + elif label: # e.g. label=True says to use a default label + label = label_default + else: + label = None + + return err, label + + +def _safe_mask(mask, *args): + """ + Safely apply the mask to the input arrays, accounting for existing masked + or invalid values. Values matching ``False`` are set to `np.nan`. + """ + # NOTE: Could also convert unmasked data to masked. But other way around is + # easier becase np.ma gives us correct fill values for data subtypes. + _load_objects() + invalid = ~mask # True if invalid + args_masked = [] + for data in args: + data, units = _to_masked_array(data, copy=True) + nan = data.fill_value + data = data.filled() + if data.size > 1 and data.shape != invalid.shape: + raise ValueError( + f'Mask shape {mask.shape} incompatible with array shape {data.shape}.' + ) + if data.size == 1 or invalid.size == 1: # NOTE: happens with _restrict_inbounds + pass + elif invalid.size == 1: + data = nan if invalid.item() else data + elif data.size > 1: + data[invalid] = nan + if units is not None: + data = data * units + args_masked.append(data) + return args_masked[0] if len(args_masked) == 1 else args_masked + + +def _safe_range(data, lo=0, hi=100): + """ + Safely return the minimum and maximum (default) or percentile range accounting + for masked values. Use min and max functions when possible for speed. Return + ``None`` if we fail to get a valid range. + """ + _load_objects() + data, units = _to_masked_array(data) + data = data.compressed() # remove all invalid values + min_ = max_ = None + if data.size: + min_ = np.min(data) if lo <= 0 else np.percentile(data, lo) + if hasattr(min_, 'dtype') and np.issubdtype(min_.dtype, np.integer): + min_ = np.float64(min_) + try: + is_finite = np.isfinite(min_) + except TypeError: + is_finite = True + if not is_finite: + min_ = None + elif units is not None: + min_ *= units + if data.size: + max_ = np.max(data) if hi >= 100 else np.percentile(data, hi) + if hasattr(max_, 'dtype') and np.issubdtype(max_.dtype, np.integer): + max_ = np.float64(max_) + try: + is_finite = np.isfinite(min_) + except TypeError: + is_finite = True + if not is_finite: + max_ = None + elif units is not None: + max_ *= units + return min_, max_ + + +# Metadata utilities +def _meta_coords(*args, which='x', **kwargs): + """ + Return the index arrays associated with string coordinates and + keyword arguments updated with index locators and formatters. + """ + # NOTE: Why FixedLocator and not IndexLocator? The ticks chosen by the latter + # depend on other plotted content. + # NOTE: Why IndexFormatter and not FixedFormatter? The former ensures labels + # correspond to indices while the latter can mysteriously truncate labels. + from ..constructor import Formatter, Locator + res = [] + for data in args: + data = _to_duck_array(data) + if not _is_categorical(data): + res.append(data) + continue + if data.ndim > 1: + raise ValueError('Non-1D string coordinate input is unsupported.') + ticks = np.arange(len(data)) + labels = list(map(str, data)) + kwargs.setdefault(which + 'locator', Locator(ticks)) + kwargs.setdefault(which + 'formatter', Formatter(labels, index=True)) + kwargs.setdefault(which + 'minorlocator', Locator('null')) + res.append(ticks) # use these as data coordinates + return (*res, kwargs) + + +def _meta_labels(data, axis=0, always=True): + """ + Return the array-like "labels" along axis `axis`. If `always` is ``False`` + we return ``None`` for simple ndarray input. + """ + # NOTE: Previously inferred 'axis 1' metadata of 1D variable using the + # data values metadata but that is incorrect. The paradigm for 1D plots + # is we have row coordinates representing x, data values representing y, + # and column coordinates representing individual series. + _load_objects() + labels = None + if axis not in (0, 1, 2): + raise ValueError(f'Invalid axis {axis}.') + if isinstance(data, (ndarray, Quantity)): + if not always: + pass + elif axis < data.ndim: + labels = np.arange(data.shape[axis]) + else: # requesting 'axis 1' on a 1D array + labels = np.array([0]) + # Xarray object + # NOTE: Even if coords not present .coords[dim] auto-generates indices + elif isinstance(data, DataArray): + if axis < data.ndim: + labels = data.coords[data.dims[axis]] + elif not always: + pass + else: + labels = np.array([0]) + # Pandas object + elif isinstance(data, (DataFrame, Series, Index)): + if axis == 0 and isinstance(data, (DataFrame, Series)): + labels = data.index + elif axis == 1 and isinstance(data, (DataFrame,)): + labels = data.columns + elif not always: + pass + else: # beyond dimensionality + labels = np.array([0]) + # Everything else + # NOTE: Ensure data is at least 1D in _to_duck_array so this covers everything + else: + raise ValueError(f'Unrecognized array type {type(data)}.') + return labels + + +def _meta_title(data, include_units=True): + """ + Return the "title" of an array-like object with metadata. + Include units in the title if `include_units` is ``True``. + """ + _load_objects() + title = units = None + if isinstance(data, ndarray): + pass + # Xarray object with possible long_name, standard_name, and units attributes. + # Output depends on if units is True. Prefer long_name (come last in loop). + elif isinstance(data, DataArray): + title = getattr(data, 'name', None) + for key in ('standard_name', 'long_name'): + title = data.attrs.get(key, title) + if include_units: + units = _meta_units(data) + # Pandas object. DataFrame has no native name attribute but user can add one + # See: https://github.com/pandas-dev/pandas/issues/447 + elif isinstance(data, (DataFrame, Series, Index)): + title = getattr(data, 'name', None) or None + # Pint Quantity + elif isinstance(data, Quantity): + if include_units: + units = _meta_units(data) + # Add units or return units alone + if title and units: + title = f'{title} ({units})' + else: + title = title or units + if title is not None: + title = str(title).strip() + return title + + +def _meta_units(data): + """ + Get the unit string from the `xarray.DataArray` attributes or the + `pint.Quantity`. Format the latter with :rcraw:`unitformat`. + """ + _load_objects() + # Get units from the attributes + if ndarray is not DataArray and isinstance(data, DataArray): + units = data.attrs.get('units', None) + data = data.data + if units is not None: + return units + # Get units from the quantity + if ndarray is not Quantity and isinstance(data, Quantity): + from ..config import rc + fmt = rc.unitformat + try: + units = format(data.units, fmt) + except (TypeError, ValueError): + warnings._warn_proplot( + f'Failed to format pint quantity with format string {fmt!r}.' + ) + else: + if 'L' in fmt: # auto-apply LaTeX math indicator + units = '$' + units + '$' + return units + + +# Geographic utiltiies +def _geo_basemap_1d(x, *ys, xmin=-180, xmax=180): + """ + Fix basemap geographic 1D data arrays. + """ + ys = _geo_clip(*ys) + x_orig, ys_orig, ys = x, ys, [] + for y_orig in ys_orig: + x, y = _geo_inbounds(x_orig, y_orig, xmin=xmin, xmax=xmax) + ys.append(y) + return (x, *ys) + + +def _geo_basemap_2d(x, y, *zs, xmin=-180, xmax=180, globe=False): + """ + Fix basemap geographic 2D data arrays. + """ + y = _geo_clip(y) + x_orig, y_orig, zs_orig, zs = x, y, zs, [] + for z_orig in zs_orig: + x, y, z = x_orig, y_orig, z_orig + x, z = _geo_inbounds(x, z, xmin=xmin, xmax=xmax) + if globe and z is not None and x.ndim == 1 and y.ndim == 1: + x, y, z = _geo_globe(x, y, z, xmin=xmin, modulo=False) + zs.append(z) + return (x, y, *zs) + + +def _geo_cartopy_1d(x, *ys): + """ + Fix cartopy geographic 1D data arrays. + """ + ys = _geo_clip(ys) + return (x, *ys) + + +def _geo_cartopy_2d(x, y, *zs, globe=False): + """ + Fix cartopy geographic 2D data arrays. + """ + y = _geo_clip(y) + x_orig, y_orig, zs_orig = x, y, zs + zs = [] + for z_orig in zs_orig: + x, y, z = x_orig, y_orig, z_orig + if globe and z is not None and x.ndim == 1 and y.ndim == 1: + x, y, z = _geo_globe(x, y, z, modulo=True) + zs.append(z) + return (x, y, *zs) + + +def _geo_clip(*ys): + """ + Ensure latitudes fall within ``-90`` to ``90``. Important if we + add graticule edges with `edges`. + """ + ys = tuple(np.clip(y, -90, 90) for y in ys) + return ys[0] if len(ys) == 1 else ys + + +def _geo_inbounds(x, y, xmin=-180, xmax=180): + """ + Fix conflicts with map coordinates by rolling the data to fall between the + minimum and maximum longitudes and masking out-of-bounds data points. + """ + # Roll in same direction if some points on right-edge extend + # more than 360 above min longitude; *they* should be on left side + if x.ndim != 1: + return x, y + lonroll = np.where(x > xmin + 360)[0] # tuple of ids + if lonroll.size: # non-empty + roll = x.size - lonroll.min() + x = np.roll(x, roll) + y = np.roll(y, roll, axis=-1) + x[:roll] -= 360 # make monotonic + # Set NaN where data not in range xmin, xmax. Must be done for regional smaller + # projections or get weird side-effects from valid data outside boundaries + y, units = _to_masked_array(y) + nan = y.fill_value + y = y.filled() + if not y.shape: + pass + elif x.size - 1 == y.shape[-1]: # test western/eastern grid cell edges + mask = (x[1:] < xmin) | (x[:-1] > xmax) + y[..., mask] = nan + elif x.size == y.shape[-1]: # test the centers and pad by one for safety + where, = np.where((x < xmin) | (x > xmax)) + y[..., where[1:-1]] = nan + return x, y + + +def _geo_globe(x, y, z, xmin=-180, modulo=False): + """ + Ensure global coverage by fixing gaps over poles and across + longitude seams. Increases the size of the arrays. + """ + # Cover gaps over poles by appending polar data + with np.errstate(all='ignore'): + p1 = np.mean(z[0, :]) # do not ignore NaN if present + p2 = np.mean(z[-1, :]) + ps = (-90, 90) if (y[0] < y[-1]) else (90, -90) + z1 = np.repeat(p1, z.shape[1]) + z2 = np.repeat(p2, z.shape[1]) + y = ma.concatenate((ps[:1], y, ps[1:])) + z = ma.concatenate((z1[None, :], z, z2[None, :]), axis=0) + # Cover gaps over cartopy longitude seam + # Ensure coordinates span 360 after modulus + if modulo: + if x[0] % 360 != (x[-1] + 360) % 360: + x = ma.concatenate((x, (x[0] + 360,))) + z = ma.concatenate((z, z[:, :1]), axis=1) + # Cover gaps over basemap longitude seam + # Ensure coordinates span exactly 360 + else: + # Interpolate coordinate centers to seam. Size possibly augmented by 2 + if x.size == z.shape[1]: + if x[0] + 360 != x[-1]: + xi = np.array([x[-1], x[0] + 360]) # input coordinates + xq = xmin + 360 # query coordinate + zq = ma.concatenate((z[:, -1:], z[:, :1]), axis=1) + zq = (zq[:, :1] * (xi[1] - xq) + zq[:, 1:] * (xq - xi[0])) / (xi[1] - xi[0]) # noqa: E501 + x = ma.concatenate(((xmin,), x, (xmin + 360,))) + z = ma.concatenate((zq, z, zq), axis=1) + # Extend coordinate edges to seam. Size possibly augmented by 1. + elif x.size - 1 == z.shape[1]: + if x[0] != xmin: + x = ma.append(xmin, x) + x[-1] = xmin + 360 + z = ma.concatenate((z[:, -1:], z), axis=1) + else: + raise ValueError('Unexpected shapes of coordinates or data arrays.') + return x, y, z diff --git a/proplot/internals/labels.py b/proplot/internals/labels.py new file mode 100644 index 000000000..cbe4dbe66 --- /dev/null +++ b/proplot/internals/labels.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +""" +Utilities related to matplotlib text labels. +""" +import matplotlib.patheffects as mpatheffects +import matplotlib.text as mtext + +from . import ic # noqa: F401 + + +def _transfer_label(src, dest): + """ + Transfer the input text object properties and content to the destination + text object. Then clear the input object text. + """ + text = src.get_text() + dest.set_color(src.get_color()) # not a font property + dest.set_fontproperties(src.get_fontproperties()) # size, weight, etc. + if not text.strip(): # WARNING: must test strip() (see _align_axis_labels()) + return + dest.set_text(text) + src.set_text('') + + +def _update_label(text, props=None, **kwargs): + """ + Add a monkey patch for ``Text.update`` with pseudo "border" and "bbox" + properties without wrapping the entire class. This facillitates inset titles. + """ + props = props or {} + props = props.copy() # shallow copy + props.update(kwargs) + + # Update border + border = props.pop('border', None) + bordercolor = props.pop('bordercolor', 'w') + borderinvert = props.pop('borderinvert', False) + borderwidth = props.pop('borderwidth', 2) + borderstyle = props.pop('borderstyle', 'miter') + if border: + facecolor, bgcolor = text.get_color(), bordercolor + if borderinvert: + facecolor, bgcolor = bgcolor, facecolor + kw = { + 'linewidth': borderwidth, + 'foreground': bgcolor, + 'joinstyle': borderstyle, + } + text.set_color(facecolor) + text.set_path_effects( + [mpatheffects.Stroke(**kw), mpatheffects.Normal()], + ) + elif border is False: + text.set_path_effects(None) + + # Update bounding box + # NOTE: We use '_title_pad' and '_title_above' for both titles and a-b-c + # labels because always want to keep them aligned. + # NOTE: For some reason using pad / 10 results in perfect alignment for + # med-large labels. Tried scaling to be font size relative but never works. + pad = text.axes._title_pad / 10 # default pad + bbox = props.pop('bbox', None) + bboxcolor = props.pop('bboxcolor', 'w') + bboxstyle = props.pop('bboxstyle', 'round') + bboxalpha = props.pop('bboxalpha', 0.5) + bboxpad = props.pop('bboxpad', None) + bboxpad = pad if bboxpad is None else bboxpad + if bbox is None: + pass + elif isinstance(bbox, dict): # *native* matplotlib usage + props['bbox'] = bbox + elif not bbox: + props['bbox'] = None # disable the bbox + else: + props['bbox'] = { + 'edgecolor': 'black', + 'facecolor': bboxcolor, + 'boxstyle': bboxstyle, + 'alpha': bboxalpha, + 'pad': bboxpad, + } + return mtext.Text.update(text, props) diff --git a/proplot/internals/rcsetup.py b/proplot/internals/rcsetup.py new file mode 100644 index 000000000..105f219f3 --- /dev/null +++ b/proplot/internals/rcsetup.py @@ -0,0 +1,2079 @@ +#!/usr/bin/env python3 +""" +Utilities for global configuration. +""" +import functools +import re +from collections.abc import MutableMapping +from numbers import Integral, Real + +import matplotlib.rcsetup as msetup +import numpy as np +from cycler import Cycler +from matplotlib import RcParams +from matplotlib import rcParamsDefault as _rc_matplotlib_native +from matplotlib.colors import Colormap +from matplotlib.font_manager import font_scalings +from matplotlib.fontconfig_pattern import parse_fontconfig_pattern + +from . import ic # noqa: F401 +from . import warnings +from .versions import _version_mpl + +# Regex for "probable" unregistered named colors. Try to retain warning message for +# colors that were most likely a failed literal string evaluation during startup. +REGEX_NAMED_COLOR = re.compile(r'\A[a-zA-Z0-9:_ -]*\Z') + +# Configurable validation settings +# NOTE: These are set to True inside __init__.py +# NOTE: We really cannot delay creation of 'rc' until after registration because +# colormap creation depends on rc['cmap.lut'] and rc['cmap.listedthresh']. +# And anyway to revoke that dependence would require other uglier kludges. +VALIDATE_REGISTERED_CMAPS = False +VALIDATE_REGISTERED_COLORS = False + +# Initial synced properties +# NOTE: Important that LINEWIDTH is less than matplotlib default of 0.8. +# In general want axes lines to look about as thick as text. +# NOTE: Important that default values are equivalent to the *validated* values +# used in the RcParams dictionaries. Otherwise _user_settings() detects changes. +# NOTE: We *could* just leave some settings empty and leave it up to Configurator +# to sync them when proplot is imported... but also sync them here so that we can +# simply compare any Configurator state to these dictionaries and use save() to +# save only the settings changed by the user. +BLACK = 'black' +CYCLE = 'colorblind' +CMAPCYC = 'twilight' +CMAPDIV = 'BuRd' +CMAPSEQ = 'Fire' +CMAPCAT = 'colorblind10' +DIVERGING = 'div' +FRAMEALPHA = 0.8 # legend and colorbar +FONTNAME = 'sans-serif' +FONTSIZE = 9.0 +GRIDALPHA = 0.1 +GRIDBELOW = 'line' +GRIDPAD = 3.0 +GRIDRATIO = 0.5 # differentiated from major by half size reduction +GRIDSTYLE = '-' +LABELPAD = 4.0 # default is 4.0, previously was 3.0 +LARGESIZE = 'med-large' +LINEWIDTH = 0.6 +MARGIN = 0.05 +MATHTEXT = False +SMALLSIZE = 'medium' +TICKDIR = 'out' +TICKLEN = 4.0 +TICKLENRATIO = 0.5 # very noticeable length reduction +TICKMINOR = True +TICKPAD = 2.0 +TICKWIDTHRATIO = 0.8 # very slight width reduction +TITLEPAD = 5.0 # default is 6.0, previously was 3.0 +WHITE = 'white' +ZLINES = 2 # default zorder for lines +ZPATCHES = 1 + +# Preset legend locations and aliases +LEGEND_LOCS = { + 'fill': 'fill', + 'inset': 'best', + 'i': 'best', + 0: 'best', + 1: 'upper right', + 2: 'upper left', + 3: 'lower left', + 4: 'lower right', + 5: 'center left', + 6: 'center right', + 7: 'lower center', + 8: 'upper center', + 9: 'center', + 'l': 'left', + 'r': 'right', + 'b': 'bottom', + 't': 'top', + 'c': 'center', + 'ur': 'upper right', + 'ul': 'upper left', + 'll': 'lower left', + 'lr': 'lower right', + 'cr': 'center right', + 'cl': 'center left', + 'uc': 'upper center', + 'lc': 'lower center', +} +for _loc in tuple(LEGEND_LOCS.values()): + if _loc not in LEGEND_LOCS: + LEGEND_LOCS[_loc] = _loc # identity assignments +TEXT_LOCS = { + key: val for key, val in LEGEND_LOCS.items() if val in ( + 'left', 'center', 'right', + 'upper left', 'upper center', 'upper right', + 'lower left', 'lower center', 'lower right', + ) +} +COLORBAR_LOCS = { + key: val for key, val in LEGEND_LOCS.items() if val in ( + 'fill', 'best', + 'left', 'right', 'top', 'bottom', + 'upper left', 'upper right', 'lower left', 'lower right', + ) +} +PANEL_LOCS = { + key: val for key, val in LEGEND_LOCS.items() if val in ( + 'left', 'right', 'top', 'bottom' + ) +} +ALIGN_LOCS = { + key: val for key, val in LEGEND_LOCS.items() if isinstance(key, str) and val in ( + 'left', 'right', 'top', 'bottom', 'center', + ) +} + +# Matplotlib setting categories +EM_KEYS = ( # em-width units + 'legend.borderpad', + 'legend.labelspacing', + 'legend.handlelength', + 'legend.handleheight', + 'legend.handletextpad', + 'legend.borderaxespad', + 'legend.columnspacing', +) +PT_KEYS = ( + 'font.size', # special case + 'xtick.major.size', + 'xtick.minor.size', + 'ytick.major.size', + 'ytick.minor.size', + 'xtick.major.pad', + 'xtick.minor.pad', + 'ytick.major.pad', + 'ytick.minor.pad', + 'xtick.major.width', + 'xtick.minor.width', + 'ytick.major.width', + 'ytick.minor.width', + 'axes.labelpad', + 'axes.titlepad', + 'axes.linewidth', + 'grid.linewidth', + 'patch.linewidth', + 'hatch.linewidth', + 'lines.linewidth', + 'contour.linewidth', +) +FONT_KEYS = set() # dynamically add to this below + + +def _get_default_param(key): + """ + Get the default parameter from one of three places. This is used for + the :rc: role when compiling docs and when saving proplotrc files. + """ + sentinel = object() + for dict_ in ( + _rc_proplot_default, + _rc_matplotlib_default, # imposed defaults + _rc_matplotlib_native, # native defaults + ): + value = dict_.get(key, sentinel) + if value is not sentinel: + return value + raise KeyError(f'Invalid key {key!r}.') + + +def _validate_abc(value): + """ + Validate a-b-c setting. + """ + try: + if np.iterable(value): + return all(map(_validate_bool, value)) + else: + return _validate_bool(value) + except ValueError: + pass + if isinstance(value, str): + if 'a' in value.lower(): + return value + else: + if all(isinstance(_, str) for _ in value): + return tuple(value) + raise ValueError( + "A-b-c setting must be string containing 'a' or 'A' or sequence of strings." + ) + + +def _validate_belongs(*options): + """ + Return a validator ensuring the item belongs in the list. + """ + def _validate_belongs(value): # noqa: E306 + for opt in options: + if isinstance(value, str) and isinstance(opt, str): + if value.lower() == opt.lower(): # noqa: E501 + return opt + elif value is True or value is False or value is None: + if value is opt: + return opt + elif value == opt: + return opt + raise ValueError( + f'Invalid value {value!r}. Options are: ' + + ', '.join(map(repr, options)) + + '.' + ) + return _validate_belongs + + +def _validate_cmap(subtype): + """ + Validate the colormap or cycle. Possibly skip name registration check + and assign the colormap name rather than a colormap instance. + """ + def _validate_cmap(value): + name = value + if isinstance(value, str): + if VALIDATE_REGISTERED_CMAPS: + from ..colors import _get_cmap_subtype + _get_cmap_subtype(name, subtype) # may trigger useful error message + return name + elif isinstance(value, Colormap): + name = getattr(value, 'name', None) + if isinstance(name, str): + from ..colors import _cmap_database # avoid circular imports + _cmap_database[name] = value + return name + raise ValueError(f'Invalid colormap or color cycle name {name!r}.') + return _validate_cmap + + +def _validate_color(value, alternative=None): + """ + Validate the color. Possibly skip name registration check. + """ + if alternative and isinstance(value, str) and value.lower() == alternative: + return value + try: + return msetup.validate_color(value) + except ValueError: + if ( + VALIDATE_REGISTERED_COLORS + or not isinstance(value, str) + or not REGEX_NAMED_COLOR.match(value) + ): + raise ValueError(f'{value!r} is not a valid color arg.') from None + return value + except Exception as error: + raise error + + +def _validate_fontprops(s): + """ + Parse font property with support for ``'regular'`` placeholder. + """ + b = s.startswith('regular') + if b: + s = s.replace('regular', 'sans', 1) + parse_fontconfig_pattern(s) + if b: + s = s.replace('sans', 'regular', 1) + return s + + +def _validate_fontsize(value): + """ + Validate font size with new scalings and permitting other units. + """ + if value is None and None in font_scalings: # has it always been this way? + return + if isinstance(value, str): + value = value.lower() + if value in font_scalings: + return value + try: + return _validate_pt(value) # note None is also a valid font size! + except ValueError: + pass + raise ValueError( + f'Invalid font size {value!r}. Can be points or one of the ' + 'preset scalings: ' + ', '.join(map(repr, font_scalings)) + '.' + ) + + +def _validate_labels(labels, lon=True): + """ + Convert labels argument to length-4 boolean array. + """ + if labels is None: + return [None] * 4 + which = 'lon' if lon else 'lat' + if isinstance(labels, str): + labels = (labels,) + array = np.atleast_1d(labels).tolist() + if all(isinstance(_, str) for _ in array): + bool_ = [False] * 4 + opts = ('left', 'right', 'bottom', 'top') + for string in array: + if string in opts: + string = string[0] + elif set(string) - set('lrbt'): + raise ValueError( + f'Invalid {which}label string {string!r}. Must be one of ' + + ', '.join(map(repr, opts)) + + " or a string of single-letter characters like 'lr'." + ) + for char in string: + bool_['lrbt'.index(char)] = True + array = bool_ + if len(array) == 1: + array.append(False) # default is to label bottom or left + if len(array) == 2: + if lon: + array = [False, False, *array] + else: + array = [*array, False, False] + if len(array) != 4 or any(isinstance(_, str) for _ in array): + raise ValueError(f'Invalid {which}label spec: {labels}.') + return array + + +def _validate_or_none(validator): + """ + Allow none otherwise pass to the input validator. + """ + @functools.wraps(validator) + def _validate_or_none(value): + if value is None: + return + if isinstance(value, str) and value.lower() == 'none': + return + return validator(value) + _validate_or_none.__name__ = validator.__name__ + '_or_none' + return _validate_or_none + + +def _validate_rotation(value): + """ + Valid rotation arguments. + """ + if isinstance(value, str) and value.lower() in ('horizontal', 'vertical'): + return value + return _validate_float(value) + + +def _validate_units(dest): + """ + Validate the input using the units function. + """ + def _validate_units(value): + if isinstance(value, str): + from ..utils import units # avoid circular imports + value = units(value, dest) # validation happens here + return _validate_float(value) + return _validate_units + + +def _rst_table(): + """ + Return the setting names and descriptions in an RST-style table. + """ + # Initial stuff + colspace = 2 # spaces between each column + descrips = tuple(descrip for (_, _, descrip) in _rc_proplot_table.values()) + keylen = len(max((*_rc_proplot_table, 'Key'), key=len)) + 4 # literal backticks + vallen = len(max((*descrips, 'Description'), key=len)) + divider = '=' * keylen + ' ' * colspace + '=' * vallen + '\n' + header = 'Key' + ' ' * (keylen - 3 + colspace) + 'Description\n' + + # Build table + string = divider + header + divider + for key, (_, _, descrip) in _rc_proplot_table.items(): + spaces = ' ' * (keylen - (len(key) + 4) + colspace) + string += f'``{key}``{spaces}{descrip}\n' + + string = string + divider + return '.. rst-class:: proplot-rctable\n\n' + string.strip() + + +def _to_string(value): + """ + Translate setting to a string suitable for saving. + """ + # NOTE: Never safe hex strings with leading '#'. In both matplotlibrc + # and proplotrc this will be read as comment character. + if value is None or isinstance(value, (str, bool, Integral)): + value = str(value) + if value[:1] == '#': # i.e. a HEX string + value = value[1:] + elif isinstance(value, Real): + value = str(round(value, 6)) # truncate decimals + elif isinstance(value, Cycler): + value = repr(value) # special case! + elif isinstance(value, (list, tuple, np.ndarray)): + value = ', '.join(map(_to_string, value)) # sexy recursion + else: + value = None + return value + + +def _yaml_table(rcdict, comment=True, description=False): + """ + Return the settings as a nicely tabulated YAML-style table. + """ + prefix = '# ' if comment else '' + data = [] + for key, args in rcdict.items(): + # Optionally append description + includes_descrip = isinstance(args, tuple) and len(args) == 3 + if not description: + descrip = '' + value = args[0] if includes_descrip else args + elif includes_descrip: + value, validator, descrip = args + descrip = '# ' + descrip # skip the validator + else: + raise ValueError(f'Unexpected input {key}={args!r}.') + + # Translate object to string + value = _to_string(value) + if value is not None: + data.append((key, value, descrip)) + else: + warnings._warn_proplot( + f'Failed to write rc setting {key} = {value!r}. Must be None, bool, ' + 'string, int, float, a list or tuple thereof, or a property cycler.' + ) + + # Generate string + string = '' + keylen = len(max(rcdict, key=len)) + vallen = len(max((tup[1] for tup in data), key=len)) + for key, value, descrip in data: + space1 = ' ' * (keylen - len(key) + 1) + space2 = ' ' * (vallen - len(value) + 2) if descrip else '' + string += f'{prefix}{key}:{space1}{value}{space2}{descrip}\n' + + return string.strip() + + +class _RcParams(MutableMapping, dict): + """ + A simple dictionary with locked inputs and validated assignments. + """ + # NOTE: By omitting __delitem__ in MutableMapping we effectively + # disable mutability. Also disables deleting items with pop(). + def __init__(self, source, validate): + self._validate = validate + for key, value in source.items(): + self.__setitem__(key, value) # trigger validation + + def __repr__(self): + return RcParams.__repr__(self) + + def __str__(self): + return RcParams.__repr__(self) + + def __len__(self): + return dict.__len__(self) + + def __iter__(self): + # NOTE: Proplot doesn't add deprecated args to dictionary so + # we don't have to suppress warning messages here. + yield from sorted(dict.__iter__(self)) + + def __getitem__(self, key): + key, _ = self._check_key(key) + return dict.__getitem__(self, key) + + def __setitem__(self, key, value): + key, value = self._check_key(key, value) + if key not in self._validate: + raise KeyError(f'Invalid rc key {key!r}.') + try: + value = self._validate[key](value) + except (ValueError, TypeError) as error: + raise ValueError(f'Key {key}: {error}') from None + if key is not None: + dict.__setitem__(self, key, value) + + @staticmethod + def _check_key(key, value=None): + # NOTE: If we assigned from the Configurator then the deprecated key will + # still propagate to the same 'children' as the new key. + # NOTE: This also translates values for special cases of renamed keys. + # Currently the special cases are 'basemap' and 'cartopy.autoextent'. + if key in _rc_renamed: + key_new, version = _rc_renamed[key] + warnings._warn_proplot( + f'The rc setting {key!r} was deprecated in version {version} and may be ' # noqa: E501 + f'removed in {warnings._next_release()}. Please use {key_new!r} instead.' # noqa: E501 + ) + if key == 'basemap': # special case + value = ('cartopy', 'basemap')[int(bool(value))] + if key == 'cartopy.autoextent': + value = ('globe', 'auto')[int(bool(value))] + key = key_new + if key in _rc_removed: + info, version = _rc_removed[key] + raise KeyError( + f'The rc setting {key!r} was removed in version {version}.' + + (info and ' ' + info) + ) + return key, value + + def copy(self): + source = {key: dict.__getitem__(self, key) for key in self} + return _RcParams(source, self._validate) + + +# Borrow validators from matplotlib and construct some new ones +# WARNING: Instead of validate_fontweight matplotlib used validate_string +# until version 3.1.2. So use that as backup here. +# WARNING: We create custom 'or none' validators since their +# availability seems less consistent across matplotlib versions. +_validate_pt = _validate_units('pt') +_validate_em = _validate_units('em') +_validate_in = _validate_units('in') +_validate_bool = msetup.validate_bool +_validate_int = msetup.validate_int +_validate_float = msetup.validate_float +_validate_string = msetup.validate_string +_validate_fontname = msetup.validate_stringlist # same as 'font.family' +_validate_fontweight = getattr(msetup, 'validate_fontweight', _validate_string) + +# Special style validators +# See: https://matplotlib.org/stable/api/_as_gen/matplotlib.patches.FancyBboxPatch.html +_validate_boxstyle = _validate_belongs( + 'square', 'circle', 'round', 'round4', 'sawtooth', 'roundtooth', +) +if hasattr(msetup, '_validate_linestyle'): # fancy validation including dashes + _validate_linestyle = msetup._validate_linestyle +else: # no dashes allowed then but no big deal + _validate_linestyle = _validate_belongs( + '-', ':', '--', '-.', 'solid', 'dashed', 'dashdot', 'dotted', 'none', ' ', '', + ) + +# Patch existing matplotlib validators. +# NOTE: validate_fontsizelist is unused in recent matplotlib versions and +# validate_colorlist is only used with prop cycle eval (which we don't care about) +font_scalings['med'] = 1.0 # consistent shorthand +font_scalings['med-small'] = 0.9 # add scaling +font_scalings['med-large'] = 1.1 # add scaling +if not hasattr(RcParams, 'validate'): # not mission critical so skip + warnings._warn_proplot('Failed to update matplotlib rcParams validators.') +else: + _validate = RcParams.validate + _validate['image.cmap'] = _validate_cmap('continuous') + _validate['legend.loc'] = _validate_belongs(*LEGEND_LOCS) + for _key, _validator in _validate.items(): + if _validator is getattr(msetup, 'validate_fontsize', None): # should exist + FONT_KEYS.add(_key) + _validate[_key] = _validate_fontsize + if _validator is getattr(msetup, 'validate_fontsize_None', None): + FONT_KEYS.add(_key) + _validate[_key] = _validate_or_none(_validate_fontsize) + if _validator is getattr(msetup, 'validate_font_properties', None): + _validate[_key] = _validate_fontprops + if _validator is getattr(msetup, 'validate_color', None): # should exist + _validate[_key] = _validate_color + if _validator is getattr(msetup, 'validate_color_or_auto', None): + _validate[_key] = functools.partial(_validate_color, alternative='auto') + if _validator is getattr(msetup, 'validate_color_or_inherit', None): + _validate[_key] = functools.partial(_validate_color, alternative='inherit') + for _keys, _validator_replace in ((EM_KEYS, _validate_em), (PT_KEYS, _validate_pt)): + for _key in _keys: + _validator = _validate.get(_key, None) + if _validator is None: + continue + if _validator is msetup.validate_float: + _validate[_key] = _validator_replace + if _validator is getattr(msetup, 'validate_float_or_None'): + _validate[_key] = _validate_or_none(_validator_replace) + + +# Proplot overrides of matplotlib default style +# WARNING: Critical to include every parameter here that can be changed by a +# "meta" setting so that _get_default_param returns the value imposed by *proplot* +# and so that "changed" settings detected by Configurator.save are correct. +_rc_matplotlib_default = { + 'axes.axisbelow': GRIDBELOW, + 'axes.formatter.use_mathtext': MATHTEXT, + 'axes.grid': True, # enable lightweight transparent grid by default + 'axes.grid.which': 'major', + 'axes.edgecolor': BLACK, + 'axes.labelcolor': BLACK, + 'axes.labelpad': LABELPAD, # more compact + 'axes.labelsize': SMALLSIZE, + 'axes.labelweight': 'normal', + 'axes.linewidth': LINEWIDTH, + 'axes.titlepad': TITLEPAD, # more compact + 'axes.titlesize': LARGESIZE, + 'axes.titleweight': 'normal', + 'axes.xmargin': MARGIN, + 'axes.ymargin': MARGIN, + 'errorbar.capsize': 3.0, + 'figure.autolayout': False, + 'figure.figsize': (4.0, 4.0), # for interactife backends + 'figure.dpi': 100, + 'figure.facecolor': '#f4f4f4', # similar to MATLAB interface + 'figure.titlesize': LARGESIZE, + 'figure.titleweight': 'bold', # differentiate from axes titles + 'font.serif': [ + 'TeX Gyre Schola', # Century lookalike + 'TeX Gyre Bonum', # Bookman lookalike + 'TeX Gyre Termes', # Times New Roman lookalike + 'TeX Gyre Pagella', # Palatino lookalike + 'DejaVu Serif', + 'Bitstream Vera Serif', + 'Computer Modern Roman', + 'Bookman', + 'Century Schoolbook L', + 'Charter', + 'ITC Bookman', + 'New Century Schoolbook', + 'Nimbus Roman No9 L', + 'Noto Serif', + 'Palatino', + 'Source Serif Pro', + 'Times New Roman', + 'Times', + 'Utopia', + 'serif', + ], + 'font.sans-serif': [ + 'TeX Gyre Heros', # Helvetica lookalike + 'DejaVu Sans', + 'Bitstream Vera Sans', + 'Computer Modern Sans Serif', + 'Arial', + 'Avenir', + 'Fira Math', + 'Fira Sans', + 'Frutiger', + 'Geneva', + 'Gill Sans', + 'Helvetica', + 'Lucid', + 'Lucida Grande', + 'Myriad Pro', + 'Noto Sans', + 'Roboto', + 'Source Sans Pro', + 'Tahoma', + 'Trebuchet MS', + 'Ubuntu', + 'Univers', + 'Verdana', + 'sans-serif', + ], + 'font.cursive': [ + 'TeX Gyre Chorus', # Chancery lookalike + 'Apple Chancery', + 'Felipa', + 'Sand', + 'Script MT', + 'Textile', + 'Zapf Chancery', + 'cursive', + ], + 'font.fantasy': [ + 'TeX Gyre Adventor', # Avant Garde lookalike + 'Avant Garde', + 'Charcoal', + 'Chicago', + 'Comic Sans MS', + 'Futura', + 'Humor Sans', + 'Impact', + 'Optima', + 'Western', + 'xkcd', + 'fantasy', + ], + 'font.monospace': [ + 'TeX Gyre Cursor', # Courier lookalike + 'DejaVu Sans Mono', + 'Bitstream Vera Sans Mono', + 'Computer Modern Typewriter', + 'Andale Mono', + 'Courier New', + 'Courier', + 'Fixed', + 'Nimbus Mono L', + 'Terminal', + 'monospace', + ], + 'font.family': FONTNAME, + 'font.size': FONTSIZE, + 'grid.alpha': GRIDALPHA, # lightweight unobtrusive gridlines + 'grid.color': BLACK, # lightweight unobtrusive gridlines + 'grid.linestyle': GRIDSTYLE, + 'grid.linewidth': LINEWIDTH, + 'hatch.color': BLACK, + 'hatch.linewidth': LINEWIDTH, + 'image.cmap': CMAPSEQ, + 'lines.linestyle': '-', + 'lines.linewidth': 1.5, + 'lines.markersize': 6.0, + 'legend.borderaxespad': 0, # i.e. flush against edge + 'legend.borderpad': 0.5, # a bit more roomy + 'legend.columnspacing': 1.5, # a bit more compact (see handletextpad) + 'legend.edgecolor': BLACK, + 'legend.facecolor': WHITE, + 'legend.fancybox': False, # i.e. BboxStyle 'square' not 'round' + 'legend.fontsize': SMALLSIZE, + 'legend.framealpha': FRAMEALPHA, + 'legend.handleheight': 1.0, # default is 0.7 + 'legend.handlelength': 2.0, # default is 2.0 + 'legend.handletextpad': 0.5, # a bit more compact (see columnspacing) + 'mathtext.default': 'it', + 'mathtext.fontset': 'custom', + 'mathtext.bf': 'regular:bold', # custom settings implemented above + 'mathtext.cal': 'cursive', + 'mathtext.it': 'regular:italic', + 'mathtext.rm': 'regular', + 'mathtext.sf': 'regular', + 'mathtext.tt': 'monospace', + 'patch.linewidth': LINEWIDTH, + 'savefig.bbox': None, # do not use 'tight' + 'savefig.directory': '', # use the working directory + 'savefig.dpi': 1000, # use academic journal recommendation + 'savefig.facecolor': WHITE, # use white instead of 'auto' + 'savefig.format': 'pdf', # use vector graphics + 'savefig.transparent': False, + 'xtick.color': BLACK, + 'xtick.direction': TICKDIR, + 'xtick.labelsize': SMALLSIZE, + 'xtick.major.pad': TICKPAD, + 'xtick.major.size': TICKLEN, + 'xtick.major.width': LINEWIDTH, + 'xtick.minor.pad': TICKPAD, + 'xtick.minor.size': TICKLEN * TICKLENRATIO, + 'xtick.minor.width': LINEWIDTH * TICKWIDTHRATIO, + 'xtick.minor.visible': TICKMINOR, + 'ytick.color': BLACK, + 'ytick.direction': TICKDIR, + 'ytick.labelsize': SMALLSIZE, + 'ytick.major.pad': TICKPAD, + 'ytick.major.size': TICKLEN, + 'ytick.major.width': LINEWIDTH, + 'ytick.minor.pad': TICKPAD, + 'ytick.minor.size': TICKLEN * TICKLENRATIO, + 'ytick.minor.width': LINEWIDTH * TICKWIDTHRATIO, + 'ytick.minor.visible': TICKMINOR, +} +if 'mathtext.fallback' in _rc_matplotlib_native: + _rc_matplotlib_default['mathtext.fallback'] = 'stixsans' + +# Proplot pseudo-setting defaults, validators, and descriptions +# NOTE: Cannot have different a-b-c and title paddings because they are both controlled +# by matplotlib's _title_offset_trans transform and want to keep them aligned anyway. +_addendum_rotation = " Must be 'vertical', 'horizontal', or a float indicating degrees." +_addendum_em = ' Interpreted by `~proplot.utils.units`. Numeric units are em-widths.' +_addendum_in = ' Interpreted by `~proplot.utils.units`. Numeric units are inches.' +_addendum_pt = ' Interpreted by `~proplot.utils.units`. Numeric units are points.' +_addendum_font = ( + ' Must be a :ref:`relative font size ` or unit string ' + 'interpreted by `~proplot.utils.units`. Numeric units are points.' +) +_rc_proplot_table = { + # Stylesheet + 'style': ( + None, + _validate_or_none(_validate_string), + 'The default matplotlib `stylesheet ' + '`__ ' # noqa: E501 + 'name. If ``None``, a custom proplot style is used. ' + "If ``'default'``, the default matplotlib style is used." + ), + + # A-b-c labels + 'abc': ( + False, + _validate_abc, + 'If ``False`` then a-b-c labels are disabled. If ``True`` the default label ' + 'style ``a`` is used. If string this indicates the style and must contain the ' + "character ``a`` or ``A``, for example ``'a.'`` or ``'(A)'``." + ), + 'abc.border': ( + True, + _validate_bool, + 'Whether to draw a white border around a-b-c labels ' + 'when :rcraw:`abc.loc` is inside the axes.' + ), + 'abc.borderwidth': ( + 1.5, + _validate_pt, + 'Width of the white border around a-b-c labels.' + ), + 'abc.bbox': ( + False, + _validate_bool, + 'Whether to draw semi-transparent bounding boxes around a-b-c labels ' + 'when :rcraw:`abc.loc` is inside the axes.' + ), + 'abc.bboxcolor': ( + WHITE, + _validate_color, + 'a-b-c label bounding box color.' + ), + 'abc.bboxstyle': ( + 'square', + _validate_boxstyle, + 'a-b-c label bounding box style.' + ), + 'abc.bboxalpha': ( + 0.5, + _validate_float, + 'a-b-c label bounding box opacity.' + ), + 'abc.bboxpad': ( + None, + _validate_or_none(_validate_pt), + 'Padding for the a-b-c label bounding box. By default this is scaled ' + 'to make the box flush against the subplot edge.' + _addendum_pt + ), + 'abc.color': ( + BLACK, + _validate_color, + 'a-b-c label color.' + ), + 'abc.loc': ( + 'left', # left side above the axes + _validate_belongs(*TEXT_LOCS), + 'a-b-c label position. ' + 'For options see the :ref:`location table `.' + ), + 'abc.size': ( + LARGESIZE, + _validate_fontsize, + 'a-b-c label font size.' + _addendum_font + ), + 'abc.titlepad': ( + LABELPAD, + _validate_pt, + 'Padding separating the title and a-b-c label when in the same location.' + + _addendum_pt + ), + 'abc.weight': ( + 'bold', + _validate_fontweight, + 'a-b-c label font weight.' + ), + + # Autoformatting + 'autoformat': ( + True, + _validate_bool, + 'Whether to automatically apply labels from `pandas.Series`, ' + '`pandas.DataFrame`, and `xarray.DataArray` objects passed to ' + 'plotting functions. See also :rcraw:`unitformat`.' + ), + + # Axes additions + 'axes.alpha': ( + None, + _validate_or_none(_validate_float), + 'Opacity of the background axes patch.' + ), + 'axes.inbounds': ( + True, + _validate_bool, + 'Whether to exclude out-of-bounds data when determining the default *y* (*x*) ' + 'axis limits and the *x* (*y*) axis limits have been locked.' + ), + 'axes.margin': ( + MARGIN, + _validate_float, + 'The fractional *x* and *y* axis margins when limits are unset.' + ), + + # Country borders + 'borders': ( + False, + _validate_bool, + 'Toggles country border lines on and off.' + ), + 'borders.alpha': ( + None, + _validate_or_none(_validate_float), + 'Opacity for country border lines.', + ), + 'borders.color': ( + BLACK, + _validate_color, + 'Line color for country border lines.' + ), + 'borders.linewidth': ( + LINEWIDTH, + _validate_pt, + 'Line width for country border lines.' + ), + 'borders.zorder': ( + ZLINES, + _validate_float, + 'Z-order for country border lines.' + ), + + # Bottom subplot labels + 'bottomlabel.color': ( + BLACK, + _validate_color, + 'Font color for column labels on the bottom of the figure.' + ), + 'bottomlabel.pad': ( + TITLEPAD, + _validate_pt, + 'Padding between axes content and column labels on the bottom of the figure.' + + _addendum_pt + ), + 'bottomlabel.rotation': ( + 'horizontal', + _validate_rotation, + 'Rotation for column labels at the bottom of the figure.' + _addendum_rotation + ), + 'bottomlabel.size': ( + LARGESIZE, + _validate_fontsize, + 'Font size for column labels on the bottom of the figure.' + _addendum_font + ), + 'bottomlabel.weight': ( + 'bold', + _validate_fontweight, + 'Font weight for column labels on the bottom of the figure.' + ), + + # Coastlines + 'coast': ( + False, + _validate_bool, + 'Toggles coastline lines on and off.' + ), + 'coast.alpha': ( + None, + _validate_or_none(_validate_float), + 'Opacity for coast lines', + ), + 'coast.color': ( + BLACK, + _validate_color, + 'Line color for coast lines.' + ), + 'coast.linewidth': ( + LINEWIDTH, + _validate_pt, + 'Line width for coast lines.' + ), + 'coast.zorder': ( + ZLINES, + _validate_float, + 'Z-order for coast lines.' + ), + + # Colorbars + 'colorbar.edgecolor': ( + BLACK, + _validate_color, + 'Color for the inset colorbar frame edge.' + ), + 'colorbar.extend': ( + 1.3, + _validate_em, + 'Length of rectangular or triangular "extensions" for panel colorbars.' + + _addendum_em + ), + 'colorbar.fancybox': ( + False, + _validate_bool, + 'Whether to use a "fancy" round bounding box for inset colorbar frames.' + ), + 'colorbar.framealpha': ( + FRAMEALPHA, + _validate_float, + 'Opacity for inset colorbar frames.' + ), + 'colorbar.facecolor': ( + WHITE, + _validate_color, + 'Color for the inset colorbar frame.' + ), + 'colorbar.frameon': ( + True, + _validate_bool, + 'Whether to draw a frame behind inset colorbars.' + ), + 'colorbar.grid': ( + False, + _validate_bool, + 'Whether to draw borders between each level of the colorbar.' + ), + 'colorbar.insetextend': ( + 0.9, + _validate_em, + 'Length of rectangular or triangular "extensions" for inset colorbars.' + + _addendum_em + ), + 'colorbar.insetlength': ( + 8, + _validate_em, + 'Length of inset colorbars.' + _addendum_em + ), + 'colorbar.insetpad': ( + 0.7, + _validate_em, + 'Padding between axes edge and inset colorbars.' + _addendum_em + ), + 'colorbar.insetwidth': ( + 1.2, + _validate_em, + 'Width of inset colorbars.' + _addendum_em + ), + 'colorbar.length': ( + 1, + _validate_em, + 'Length of outer colorbars.' + ), + 'colorbar.loc': ( + 'right', + _validate_belongs(*COLORBAR_LOCS), + 'Inset colorbar location. ' + 'For options see the :ref:`location table `.' + ), + 'colorbar.width': ( + 0.2, + _validate_in, + 'Width of outer colorbars.' + _addendum_in + ), + 'colorbar.rasterized': ( + False, + _validate_bool, + 'Whether to use rasterization for colorbar solids.' + ), + 'colorbar.shadow': ( + False, + _validate_bool, + 'Whether to add a shadow underneath inset colorbar frames.' + ), + + # Color cycle additions + 'cycle': ( + CYCLE, + _validate_cmap('discrete'), + 'Name of the color cycle assigned to :rcraw:`axes.prop_cycle`.' + ), + + # Colormap additions + 'cmap': ( + CMAPSEQ, + _validate_cmap('continuous'), + 'Alias for :rcraw:`cmap.sequential` and :rcraw:`image.cmap`.' + ), + 'cmap.autodiverging': ( + True, + _validate_bool, + 'Whether to automatically apply a diverging colormap and ' + 'normalizer based on the data.' + ), + 'cmap.qualitative': ( + CMAPCAT, + _validate_cmap('discrete'), + 'Default colormap for qualitative datasets.' + ), + 'cmap.cyclic': ( + CMAPCYC, + _validate_cmap('continuous'), + 'Default colormap for cyclic datasets.' + ), + 'cmap.discrete': ( + None, + _validate_or_none(_validate_bool), + 'If ``True``, `~proplot.colors.DiscreteNorm` is used for every colormap plot. ' + 'If ``False``, it is never used. If ``None``, it is used for all plot types ' + 'except `imshow`, `matshow`, `spy`, `hexbin`, and `hist2d`.' + ), + 'cmap.diverging': ( + CMAPDIV, + _validate_cmap('continuous'), + 'Default colormap for diverging datasets.' + ), + 'cmap.inbounds': ( + True, + _validate_bool, + 'If ``True`` and the *x* and *y* axis limits are fixed, only in-bounds data ' + 'is considered when determining the default colormap `vmin` and `vmax`.' + ), + 'cmap.levels': ( + 11, + _validate_int, + 'Default number of `~proplot.colors.DiscreteNorm` levels for plotting ' + 'commands that use colormaps.' + ), + 'cmap.listedthresh': ( + 64, + _validate_int, + 'Native `~matplotlib.colors.ListedColormap`\\ s with more colors than ' + 'this are converted to `~proplot.colors.ContinuousColormap` rather than ' + '`~proplot.colors.DiscreteColormap`. This helps translate continuous ' + 'colormaps from external projects.' + ), + 'cmap.lut': ( + 256, + _validate_int, + 'Number of colors in the colormap lookup table. ' + 'Alias for :rcraw:`image.lut`.' + ), + 'cmap.robust': ( + False, + _validate_bool, + 'If ``True``, the default colormap `vmin` and `vmax` are chosen using the ' + '2nd to 98th percentiles rather than the minimum and maximum.' + ), + 'cmap.sequential': ( + CMAPSEQ, + _validate_cmap('continuous'), + 'Default colormap for sequential datasets. Alias for :rcraw:`image.cmap`.' + ), + + # Special setting + 'edgefix': ( + True, + _validate_bool, + 'Whether to fix issues with "white lines" appearing between patches ' + 'in saved vector graphics and with vector graphic backends. Applies ' + 'to colorbar levels and bar, area, pcolor, and contour plots.' + ), + + # Font settings + 'font.name': ( + FONTNAME, + _validate_fontname, + 'Alias for :rcraw:`font.family`.' + ), + 'font.small': ( + SMALLSIZE, + _validate_fontsize, + 'Alias for :rcraw:`font.smallsize`.' + ), + 'font.smallsize': ( + SMALLSIZE, + _validate_fontsize, + 'Meta setting that changes the label-like sizes ``axes.labelsize``, ' + '``legend.fontsize``, ``tick.labelsize``, and ``grid.labelsize``. Default is ' + "``'medium'`` (equivalent to :rcraw:`font.size`)." + _addendum_font + ), + 'font.large': ( + LARGESIZE, + _validate_fontsize, + 'Alias for :rcraw:`font.largesize`.' + ), + 'font.largesize': ( + LARGESIZE, + _validate_fontsize, + 'Meta setting that changes the title-like sizes ``abc.size``, ``title.size``, ' + '``suptitle.size``, ``leftlabel.size``, ``rightlabel.size``, etc. Default is ' + "``'med-large'`` (i.e. 1.1 times :rcraw:`font.size`)." + _addendum_font + ), + + # Formatter settings + 'formatter.timerotation': ( + 'vertical', + _validate_rotation, + 'Rotation for *x* axis datetime tick labels.' + _addendum_rotation + ), + 'formatter.zerotrim': ( + True, + _validate_bool, + 'Whether to trim trailing decimal zeros on tick labels.' + ), + 'formatter.limits': ( + [-5, 6], # must be list or else validated + _validate['axes.formatter.limits'], + 'Alias for :rcraw:`axes.formatter.limits`.' + ), + 'formatter.min_exponent': ( + 0, + _validate['axes.formatter.min_exponent'], + 'Alias for :rcraw:`axes.formatter.min_exponent`.' + ), + 'formatter.offset_threshold': ( + 4, + _validate['axes.formatter.offset_threshold'], + 'Alias for :rcraw:`axes.formatter.offset_threshold`.' + ), + 'formatter.use_locale': ( + False, + _validate_bool, + 'Alias for :rcraw:`axes.formatter.use_locale`.' + ), + 'formatter.use_mathtext': ( + MATHTEXT, + _validate_bool, + 'Alias for :rcraw:`axes.formatter.use_mathtext`.' + ), + 'formatter.use_offset': ( + True, + _validate_bool, + 'Alias for :rcraw:`axes.formatter.useOffset`.' + ), + + # Geographic axes settings + 'geo.backend': ( + 'cartopy', + _validate_belongs('cartopy', 'basemap'), + 'The backend used for `~proplot.axes.GeoAxes`. Must be ' + "either 'cartopy' or 'basemap'." + ), + 'geo.extent': ( + 'globe', + _validate_belongs('globe', 'auto'), + "If ``'globe'``, the extent of cartopy `~proplot.axes.GeoAxes` is always " + "global. If ``'auto'``, the extent is automatically adjusted based on " + "plotted content. Default is ``'globe'``." + ), + 'geo.round': ( + True, + _validate_bool, + "If ``True`` (the default), polar `~proplot.axes.GeoAxes` like ``'npstere'`` " + "and ``'spstere'`` are bounded with circles rather than squares." + ), + + + # Gridlines + # NOTE: Here 'grid' and 'gridminor' or *not* aliases for native 'axes.grid' and + # invented 'axes.gridminor' because native 'axes.grid' controls both major *and* + # minor gridlines. Must handle it independently from these settings. + 'grid': ( + True, + _validate_bool, + 'Toggle major gridlines on and off.' + ), + 'grid.below': ( + GRIDBELOW, # like axes.axisbelow + _validate_belongs(False, 'line', True), + 'Alias for :rcraw:`axes.axisbelow`. If ``True``, draw gridlines below ' + "everything. If ``True``, draw them above everything. If ``'line'``, " + 'draw them above patches but below lines and markers.' + ), + 'grid.checkoverlap': ( + True, + _validate_bool, + 'Whether to have cartopy automatically check for and remove overlapping ' + '`~proplot.axes.GeoAxes` gridline labels.' + ), + 'grid.dmslabels': ( + True, + _validate_bool, + 'Whether to use degrees-minutes-seconds rather than decimals for ' + 'cartopy `~proplot.axes.GeoAxes` gridlines.' + ), + 'grid.geolabels': ( + True, + _validate_bool, + "Whether to include the ``'geo'`` spine in cartopy >= 0.20 when otherwise " + 'toggling left, right, bottom, or top `~proplot.axes.GeoAxes` gridline labels.' + ), + 'grid.inlinelabels': ( + False, + _validate_bool, + 'Whether to add inline labels for cartopy `~proplot.axes.GeoAxes` gridlines.' + ), + 'grid.labels': ( + False, + _validate_bool, + 'Whether to add outer labels for `~proplot.axes.GeoAxes` gridlines.' + ), + 'grid.labelcolor': ( + BLACK, + _validate_color, + 'Font color for `~proplot.axes.GeoAxes` gridline labels.' + ), + 'grid.labelpad': ( + GRIDPAD, + _validate_pt, + 'Padding between the map boundary and cartopy `~proplot.axes.GeoAxes` ' + 'gridline labels.' + _addendum_pt + ), + 'grid.labelsize': ( + SMALLSIZE, + _validate_fontsize, + 'Font size for `~proplot.axes.GeoAxes` gridline labels.' + _addendum_font + ), + 'grid.labelweight': ( + 'normal', + _validate_fontweight, + 'Font weight for `~proplot.axes.GeoAxes` gridline labels.' + ), + 'grid.nsteps': ( + 250, + _validate_int, + 'Number of points used to draw cartopy `~proplot.axes.GeoAxes` gridlines.' + ), + 'grid.pad': ( + GRIDPAD, + _validate_pt, + 'Alias for :rcraw:`grid.labelpad`.' + ), + 'grid.rotatelabels': ( + False, # False limits projections where labels are available + _validate_bool, + 'Whether to rotate cartopy `~proplot.axes.GeoAxes` gridline labels.' + ), + 'grid.style': ( + '-', + _validate_linestyle, + 'Major gridline style. Alias for :rcraw:`grid.linestyle`.' + ), + 'grid.width': ( + LINEWIDTH, + _validate_pt, + 'Major gridline width. Alias for :rcraw:`grid.linewidth`.' + ), + 'grid.widthratio': ( + GRIDRATIO, + _validate_float, + 'Ratio of minor gridline width to major gridline width.' + ), + + # Minor gridlines + 'gridminor': ( + False, + _validate_bool, + 'Toggle minor gridlines on and off.' + ), + 'gridminor.alpha': ( + GRIDALPHA, + _validate_float, + 'Minor gridline opacity.' + ), + 'gridminor.color': ( + BLACK, + _validate_color, + 'Minor gridline color.' + ), + 'gridminor.linestyle': ( + GRIDSTYLE, + _validate_linestyle, + 'Minor gridline style.' + ), + 'gridminor.linewidth': ( + GRIDRATIO * LINEWIDTH, + _validate_pt, + 'Minor gridline width.' + ), + 'gridminor.style': ( + GRIDSTYLE, + _validate_linestyle, + 'Minor gridline style. Alias for :rcraw:`gridminor.linestyle`.' + ), + 'gridminor.width': ( + GRIDRATIO * LINEWIDTH, + _validate_pt, + 'Minor gridline width. Alias for :rcraw:`gridminor.linewidth`.' + ), + + # Backend stuff + 'inlineformat': ( + 'retina', + _validate_belongs('svg', 'pdf', 'retina', 'png', 'jpeg'), + 'The inline backend figure format. Valid formats include ' + "``'svg'``, ``'pdf'``, ``'retina'``, ``'png'``, and ``jpeg``." + ), + + # Inner borders + 'innerborders': ( + False, + _validate_bool, + 'Toggles internal political border lines (e.g. states and provinces) ' + 'on and off.' + ), + 'innerborders.alpha': ( + None, + _validate_or_none(_validate_float), + 'Opacity for internal political border lines', + ), + 'innerborders.color': ( + BLACK, + _validate_color, + 'Line color for internal political border lines.' + ), + 'innerborders.linewidth': ( + LINEWIDTH, + _validate_pt, + 'Line width for internal political border lines.' + ), + 'innerborders.zorder': ( + ZLINES, + _validate_float, + 'Z-order for internal political border lines.' + ), + + # Axis label settings + 'label.color': ( + BLACK, + _validate_color, + 'Alias for :rcraw:`axes.labelcolor`.' + ), + 'label.pad': ( + LABELPAD, + _validate_pt, + 'Alias for :rcraw:`axes.labelpad`.' + + _addendum_pt + ), + 'label.size': ( + SMALLSIZE, + _validate_fontsize, + 'Alias for :rcraw:`axes.labelsize`.' + _addendum_font + ), + 'label.weight': ( + 'normal', + _validate_fontweight, + 'Alias for :rcraw:`axes.labelweight`.' + ), + + # Lake patches + 'lakes': ( + False, + _validate_bool, + 'Toggles lake patches on and off.' + ), + 'lakes.alpha': ( + None, + _validate_or_none(_validate_float), + 'Opacity for lake patches', + ), + 'lakes.color': ( + WHITE, + _validate_color, + 'Face color for lake patches.' + ), + 'lakes.zorder': ( + ZPATCHES, + _validate_float, + 'Z-order for lake patches.' + ), + + # Land patches + 'land': ( + False, + _validate_bool, + 'Toggles land patches on and off.' + ), + 'land.alpha': ( + None, + _validate_or_none(_validate_float), + 'Opacity for land patches', + ), + 'land.color': ( + BLACK, + _validate_color, + 'Face color for land patches.' + ), + 'land.zorder': ( + ZPATCHES, + _validate_float, + 'Z-order for land patches.' + ), + + # Left subplot labels + 'leftlabel.color': ( + BLACK, + _validate_color, + 'Font color for row labels on the left-hand side.' + ), + 'leftlabel.pad': ( + TITLEPAD, + _validate_pt, + 'Padding between axes content and row labels on the left-hand side.' + + _addendum_pt + ), + 'leftlabel.rotation': ( + 'vertical', + _validate_rotation, + 'Rotation for row labels on the left-hand side.' + _addendum_rotation + ), + 'leftlabel.size': ( + LARGESIZE, + _validate_fontsize, + 'Font size for row labels on the left-hand side.' + _addendum_font + ), + 'leftlabel.weight': ( + 'bold', + _validate_fontweight, + 'Font weight for row labels on the left-hand side.' + ), + + # Meta settings + 'margin': ( + MARGIN, + _validate_float, + 'The fractional *x* and *y* axis data margins when limits are unset. ' + 'Alias for :rcraw:`axes.margin`.' + ), + 'meta.edgecolor': ( + BLACK, + _validate_color, + 'Color of axis spines, tick marks, tick labels, and labels.' + ), + 'meta.color': ( + BLACK, + _validate_color, + 'Color of axis spines, tick marks, tick labels, and labels. ' + 'Alias for :rcraw:`meta.edgecolor`.' + ), + 'meta.linewidth': ( + LINEWIDTH, + _validate_pt, + 'Thickness of axis spines and major tick lines.' + ), + 'meta.width': ( + LINEWIDTH, + _validate_pt, + 'Thickness of axis spines and major tick lines. ' + 'Alias for :rcraw:`meta.linewidth`.' + ), + + # For negative positive patches + 'negcolor': ( + 'blue7', + _validate_color, + 'Color for negative bars and shaded areas when using ``negpos=True``. ' + 'See also :rcraw:`poscolor`.' + ), + 'poscolor': ( + 'red7', + _validate_color, + 'Color for positive bars and shaded areas when using ``negpos=True``. ' + 'See also :rcraw:`negcolor`.' + ), + + # Ocean patches + 'ocean': ( + False, + _validate_bool, + 'Toggles ocean patches on and off.' + ), + 'ocean.alpha': ( + None, + _validate_or_none(_validate_float), + 'Opacity for ocean patches', + ), + 'ocean.color': ( + WHITE, + _validate_color, + 'Face color for ocean patches.' + ), + 'ocean.zorder': ( + ZPATCHES, + _validate_float, + 'Z-order for ocean patches.' + ), + + # Geographic resolution + 'reso': ( + 'lo', + _validate_belongs('lo', 'med', 'hi', 'x-hi', 'xx-hi'), + 'Resolution for `~proplot.axes.GeoAxes` geographic features. ' + "Must be one of ``'lo'``, ``'med'``, ``'hi'``, ``'x-hi'``, or ``'xx-hi'``." + ), + + # Right subplot labels + 'rightlabel.color': ( + BLACK, + _validate_color, + 'Font color for row labels on the right-hand side.' + ), + 'rightlabel.pad': ( + TITLEPAD, + _validate_pt, + 'Padding between axes content and row labels on the right-hand side.' + + _addendum_pt + ), + 'rightlabel.rotation': ( + 'vertical', + _validate_rotation, + 'Rotation for row labels on the right-hand side.' + _addendum_rotation + ), + 'rightlabel.size': ( + LARGESIZE, + _validate_fontsize, + 'Font size for row labels on the right-hand side.' + _addendum_font + ), + 'rightlabel.weight': ( + 'bold', + _validate_fontweight, + 'Font weight for row labels on the right-hand side.' + ), + + # River lines + 'rivers': ( + False, + _validate_bool, + 'Toggles river lines on and off.' + ), + 'rivers.alpha': ( + None, + _validate_or_none(_validate_float), + 'Opacity for river lines.', + ), + 'rivers.color': ( + BLACK, + _validate_color, + 'Line color for river lines.' + ), + 'rivers.linewidth': ( + LINEWIDTH, + _validate_pt, + 'Line width for river lines.' + ), + 'rivers.zorder': ( + ZLINES, + _validate_float, + 'Z-order for river lines.' + ), + + # Subplots settings + 'subplots.align': ( + False, + _validate_bool, + 'Whether to align axis labels during draw. See `aligning labels ' + '`__.' # noqa: E501 + ), + 'subplots.equalspace': ( + False, + _validate_bool, + 'Whether to make the tight layout algorithm assign the same space for ' + 'every row and the same space for every column.' + ), + 'subplots.groupspace': ( + True, + _validate_bool, + 'Whether to make the tight layout algorithm consider space between only ' + 'adjacent subplot "groups" rather than every subplot in the row or column.' + ), + 'subplots.innerpad': ( + 1, + _validate_em, + 'Padding between adjacent subplots.' + _addendum_em + ), + 'subplots.outerpad': ( + 0.5, + _validate_em, + 'Padding around figure edge.' + _addendum_em + ), + 'subplots.panelpad': ( + 0.5, + _validate_em, + 'Padding between subplots and panels, and between stacked panels.' + + _addendum_em + ), + 'subplots.panelwidth': ( + 0.5, + _validate_in, + 'Width of side panels.' + _addendum_in + ), + 'subplots.refwidth': ( + 2.5, + _validate_in, + 'Default width of the reference subplot.' + _addendum_in + ), + 'subplots.share': ( + True, + _validate_belongs(0, 1, 2, 3, 4, False, 'labels', 'limits', True, 'all'), + 'The axis sharing level, one of ``0``, ``1``, ``2``, or ``3``, or the ' + "more intuitive aliases ``False``, ``'labels'``, ``'limits'``, or ``True``. " + 'See `~proplot.figure.Figure` for details.' + ), + 'subplots.span': ( + True, + _validate_bool, + 'Toggles spanning axis labels. See `~proplot.ui.subplots` for details.' + ), + 'subplots.tight': ( + True, + _validate_bool, + 'Whether to auto-adjust the subplot spaces and figure margins.' + ), + + # Super title settings + 'suptitle.color': ( + BLACK, + _validate_color, + 'Figure title color.' + ), + 'suptitle.pad': ( + TITLEPAD, + _validate_pt, + 'Padding between axes content and the figure super title.' + _addendum_pt + ), + 'suptitle.size': ( + LARGESIZE, + _validate_fontsize, + 'Figure title font size.' + _addendum_font + ), + 'suptitle.weight': ( + 'bold', + _validate_fontweight, + 'Figure title font weight.' + ), + + # Tick settings + 'tick.color': ( + BLACK, + _validate_color, + 'Major and minor tick color.' + ), + 'tick.dir': ( + TICKDIR, + _validate_belongs('in', 'out', 'inout'), + 'Major and minor tick direction. Must be one of ' + "``'out'``, ``'in'``, or ``'inout'``." + ), + 'tick.labelcolor': ( + BLACK, + _validate_color, + 'Axis tick label color.' + ), + 'tick.labelpad': ( + TICKPAD, + _validate_pt, + 'Padding between ticks and tick labels.' + _addendum_pt + ), + 'tick.labelsize': ( + SMALLSIZE, + _validate_fontsize, + 'Axis tick label font size.' + _addendum_font + ), + 'tick.labelweight': ( + 'normal', + _validate_fontweight, + 'Axis tick label font weight.' + ), + 'tick.len': ( + TICKLEN, + _validate_pt, + 'Length of major ticks in points.' + ), + 'tick.lenratio': ( + TICKLENRATIO, + _validate_float, + 'Ratio of minor tickline length to major tickline length.' + ), + 'tick.linewidth': ( + LINEWIDTH, + _validate_pt, + 'Major tickline width.' + ), + 'tick.minor': ( + TICKMINOR, + _validate_bool, + 'Toggles minor ticks on and off.', + ), + 'tick.pad': ( + TICKPAD, + _validate_pt, + 'Alias for :rcraw:`tick.labelpad`.' + ), + 'tick.width': ( + LINEWIDTH, + _validate_pt, + 'Major tickline width. Alias for :rcraw:`tick.linewidth`.' + ), + 'tick.widthratio': ( + TICKWIDTHRATIO, + _validate_float, + 'Ratio of minor tickline width to major tickline width.' + ), + + # Title settings + 'title.above': ( + True, + _validate_belongs(False, True, 'panels'), + 'Whether to move outer titles and a-b-c labels above panels, colorbars, or ' + "legends that are above the axes. If the string 'panels' then text is only " + 'redirected above axes panels. Otherwise should be boolean.' + ), + 'title.border': ( + True, + _validate_bool, + 'Whether to draw a white border around titles ' + 'when :rcraw:`title.loc` is inside the axes.' + ), + 'title.borderwidth': ( + 1.5, + _validate_pt, + 'Width of the border around titles.' + ), + 'title.bbox': ( + False, + _validate_bool, + 'Whether to draw semi-transparent bounding boxes around titles ' + 'when :rcraw:`title.loc` is inside the axes.' + ), + 'title.bboxcolor': ( + WHITE, + _validate_color, + 'Axes title bounding box color.' + ), + 'title.bboxstyle': ( + 'square', + _validate_boxstyle, + 'Axes title bounding box style.' + ), + 'title.bboxalpha': ( + 0.5, + _validate_float, + 'Axes title bounding box opacity.' + ), + 'title.bboxpad': ( + None, + _validate_or_none(_validate_pt), + 'Padding for the title bounding box. By default this is scaled ' + 'to make the box flush against the axes edge.' + _addendum_pt + ), + 'title.color': ( + BLACK, + _validate_color, + 'Axes title color. Alias for :rcraw:`axes.titlecolor`.' + ), + 'title.loc': ( + 'center', + _validate_belongs(*TEXT_LOCS), + 'Title position. For options see the :ref:`location table `.' + ), + 'title.pad': ( + TITLEPAD, + _validate_pt, + 'Padding between the axes edge and the inner and outer titles and ' + 'a-b-c labels. Alias for :rcraw:`axes.titlepad`.' + _addendum_pt + ), + 'title.size': ( + LARGESIZE, + _validate_fontsize, + 'Axes title font size. Alias for :rcraw:`axes.titlesize`.' + _addendum_font + ), + 'title.weight': ( + 'normal', + _validate_fontweight, + 'Axes title font weight. Alias for :rcraw:`axes.titleweight`.' + ), + + # Top subplot label settings + 'toplabel.color': ( + BLACK, + _validate_color, + 'Font color for column labels on the top of the figure.' + ), + 'toplabel.pad': ( + TITLEPAD, + _validate_pt, + 'Padding between axes content and column labels on the top of the figure.' + + _addendum_pt + ), + 'toplabel.rotation': ( + 'horizontal', + _validate_rotation, + 'Rotation for column labels at the top of the figure.' + _addendum_rotation + ), + 'toplabel.size': ( + LARGESIZE, + _validate_fontsize, + 'Font size for column labels on the top of the figure.' + _addendum_font + ), + 'toplabel.weight': ( + 'bold', + _validate_fontweight, + 'Font weight for column labels on the top of the figure.' + ), + + # Unit formatting + 'unitformat': ( + 'L', + _validate_string, + 'The format string used to format `pint.Quantity` default unit labels ' + 'using ``format(units, unitformat)``. See also :rcraw:`autoformat`.' + ), +} + +# Child settings. Changing the parent changes all the children, but +# changing any of the children does not change the parent. +_rc_children = { + 'font.smallsize': ( # the 'small' fonts + 'tick.labelsize', 'xtick.labelsize', 'ytick.labelsize', + 'axes.labelsize', 'legend.fontsize', 'grid.labelsize' + ), + 'font.largesize': ( # the 'large' fonts + 'abc.size', 'figure.titlesize', 'suptitle.size', 'axes.titlesize', 'title.size', + 'leftlabel.size', 'toplabel.size', 'rightlabel.size', 'bottomlabel.size' + ), + 'meta.color': ( # change the 'color' of an axes + 'axes.edgecolor', 'axes.labelcolor', 'legend.edgecolor', 'colorbar.edgecolor', + 'tick.labelcolor', 'hatch.color', 'xtick.color', 'ytick.color' + ), + 'meta.width': ( # change the tick and frame line width + 'axes.linewidth', 'tick.width', 'tick.linewidth', 'xtick.major.width', + 'ytick.major.width', 'grid.width', 'grid.linewidth', + ), + 'axes.margin': ('axes.xmargin', 'axes.ymargin'), + 'grid.color': ('gridminor.color', 'grid.labelcolor'), + 'grid.alpha': ('gridminor.alpha',), + 'grid.linewidth': ('gridminor.linewidth',), + 'grid.linestyle': ('gridminor.linestyle',), + 'tick.color': ('xtick.color', 'ytick.color'), + 'tick.dir': ('xtick.direction', 'ytick.direction'), + 'tick.len': ('xtick.major.size', 'ytick.major.size'), + 'tick.minor': ('xtick.minor.visible', 'ytick.minor.visible'), + 'tick.pad': ('xtick.major.pad', 'xtick.minor.pad', 'ytick.major.pad', 'ytick.minor.pad'), # noqa: E501 + 'tick.width': ('xtick.major.width', 'ytick.major.width'), + 'tick.labelsize': ('xtick.labelsize', 'ytick.labelsize'), +} + +# Recently added settings. Update these only if the version is recent enough +# NOTE: We don't make 'title.color' a child of 'axes.titlecolor' because +# the latter can take on the value 'auto' and can't handle that right now. +if _version_mpl >= '3.2': + _rc_matplotlib_default['axes.titlecolor'] = BLACK + _rc_children['title.color'] = ('axes.titlecolor',) +if _version_mpl >= '3.4': + _rc_matplotlib_default['xtick.labelcolor'] = BLACK + _rc_matplotlib_default['ytick.labelcolor'] = BLACK + _rc_children['tick.labelcolor'] = ('xtick.labelcolor', 'ytick.labelcolor') + _rc_children['grid.labelcolor'] = ('xtick.labelcolor', 'ytick.labelcolor') + _rc_children['meta.color'] += ('xtick.labelcolor', 'ytick.labelcolor') + +# Setting synonyms. Changing one setting changes the other. Also account for existing +# children. Most of these are aliased due to proplot settings overlapping with +# existing matplotlib settings. +_rc_synonyms = ( + ('cmap', 'image.cmap', 'cmap.sequential'), + ('cmap.lut', 'image.lut'), + ('font.name', 'font.family'), + ('font.small', 'font.smallsize'), + ('font.large', 'font.largesize'), + ('formatter.limits', 'axes.formatter.limits'), + ('formatter.use_locale', 'axes.formatter.use_locale'), + ('formatter.use_mathtext', 'axes.formatter.use_mathtext'), + ('formatter.min_exponent', 'axes.formatter.min_exponent'), + ('formatter.use_offset', 'axes.formatter.useoffset'), + ('formatter.offset_threshold', 'axes.formatter.offset_threshold'), + ('grid.below', 'axes.axisbelow'), + ('grid.labelpad', 'grid.pad'), + ('grid.linewidth', 'grid.width'), + ('grid.linestyle', 'grid.style'), + ('gridminor.linewidth', 'gridminor.width'), + ('gridminor.linestyle', 'gridminor.style'), + ('label.color', 'axes.labelcolor'), + ('label.pad', 'axes.labelpad'), + ('label.size', 'axes.labelsize'), + ('label.weight', 'axes.labelweight'), + ('margin', 'axes.margin'), + ('meta.width', 'meta.linewidth'), + ('meta.color', 'meta.edgecolor'), + ('tick.labelpad', 'tick.pad'), + ('tick.labelsize', 'grid.labelsize'), + ('tick.labelcolor', 'grid.labelcolor'), + ('tick.labelweight', 'grid.labelweight'), + ('tick.linewidth', 'tick.width'), + ('title.pad', 'axes.titlepad'), + ('title.size', 'axes.titlesize'), + ('title.weight', 'axes.titleweight'), +) +for _keys in _rc_synonyms: + for _key in _keys: + _set = {_ for k in _keys for _ in {k, *_rc_children.get(k, ())}} - {_key} + _rc_children[_key] = tuple(sorted(_set)) + +# Previously removed settings. +# NOTE: Initial idea was to defer deprecation warnings in Configurator to the +# subsequent RcParams indexing. However this turned out be complicated, because +# would have to detect deprecated keys in _get_item_dicts blocks, and need to +# validate values before e.g. applying 'tick.lenratio'. So the renamed parameters +# do not have to be added as _rc_children, since Configurator translates before +# retrieving the list of children in _get_item_dicts. +_rc_removed = { # {key: (alternative, version)} dictionary + 'rgbcycle': ('', '0.6.0'), # no alternative, we no longer offer this feature + 'geogrid.latmax': ('Please use ax.format(latmax=N) instead.', '0.6.0'), + 'geogrid.latstep': ('Please use ax.format(latlines=N) instead.', '0.6.0'), + 'geogrid.lonstep': ('Please use ax.format(lonlines=N) instead.', '0.6.0'), + 'gridminor.latstep': ('Please use ax.format(latminorlines=N) instead.', '0.6.0'), + 'gridminor.lonstep': ('Please use ax.format(lonminorlines=N) instead.', '0.6.0'), +} +_rc_renamed = { # {old_key: (new_key, version)} dictionary + 'abc.format': ('abc', '0.5.0'), + 'align': ('subplots.align', '0.6.0'), + 'axes.facealpha': ('axes.alpha', '0.6.0'), + 'geoaxes.edgecolor': ('axes.edgecolor', '0.6.0'), + 'geoaxes.facealpha': ('axes.alpha', '0.6.0'), + 'geoaxes.facecolor': ('axes.facecolor', '0.6.0'), + 'geoaxes.linewidth': ('axes.linewidth', '0.6.0'), + 'geogrid.alpha': ('grid.alpha', '0.6.0'), + 'geogrid.color': ('grid.color', '0.6.0'), + 'geogrid.labels': ('grid.labels', '0.6.0'), + 'geogrid.labelpad': ('grid.pad', '0.6.0'), + 'geogrid.labelsize': ('grid.labelsize', '0.6.0'), + 'geogrid.linestyle': ('grid.linestyle', '0.6.0'), + 'geogrid.linewidth': ('grid.linewidth', '0.6.0'), + 'share': ('subplots.share', '0.6.0'), + 'small': ('font.smallsize', '0.6.0'), + 'large': ('font.largesize', '0.6.0'), + 'span': ('subplots.span', '0.6.0'), + 'tight': ('subplots.tight', '0.6.0'), + 'axes.formatter.timerotation': ('formatter.timerotation', '0.6.0'), + 'axes.formatter.zerotrim': ('formatter.zerotrim', '0.6.0'), + 'abovetop': ('title.above', '0.7.0'), + 'subplots.pad': ('subplots.outerpad', '0.7.0'), + 'subplots.axpad': ('subplots.innerpad', '0.7.0'), + 'subplots.axwidth': ('subplots.refwidth', '0.7.0'), + 'text.labelsize': ('font.smallsize', '0.8.0'), + 'text.titlesize': ('font.largesize', '0.8.0'), + 'alpha': ('axes.alpha', '0.8.0'), + 'facecolor': ('axes.facecolor', '0.8.0'), + 'edgecolor': ('meta.color', '0.8.0'), + 'color': ('meta.color', '0.8.0'), + 'linewidth': ('meta.width', '0.8.0'), + 'lut': ('cmap.lut', '0.8.0'), + 'image.levels': ('cmap.levels', '0.8.0'), + 'image.inbounds': ('cmap.inbounds', '0.8.0'), + 'image.discrete': ('cmap.discrete', '0.8.0'), + 'image.edgefix': ('edgefix', '0.8.0'), + 'tick.ratio': ('tick.widthratio', '0.8.0'), + 'grid.ratio': ('grid.widthratio', '0.8.0'), + 'abc.style': ('abc', '0.8.0'), + 'grid.loninline': ('grid.inlinelabels', '0.8.0'), + 'grid.latinline': ('grid.inlinelabels', '0.8.0'), + 'cmap.edgefix': ('edgefix', '0.9.0'), + 'basemap': ('geo.backend', '0.10.0'), + 'inlinefmt': ('inlineformat', '0.10.0'), + 'cartopy.circular': ('geo.round', '0.10.0'), + 'cartopy.autoextent': ('geo.extent', '0.10.0'), + 'colorbar.rasterize': ('colorbar.rasterized', '0.10.0'), +} + +# Validate the default settings dictionaries using a custom proplot _RcParams +# and the original matplotlib RcParams. Also surreptitiously add proplot +# font settings to the font keys list (beoolean below always evalutes to True) +# font keys list whlie initializing. +_rc_proplot_default = { + key: value for key, (value, _, _) in _rc_proplot_table.items() +} +_rc_proplot_validate = { + key: validator for key, (_, validator, _) in _rc_proplot_table.items() + if not (validator is _validate_fontsize and FONT_KEYS.add(key)) +} +_rc_proplot_default = _RcParams(_rc_proplot_default, _rc_proplot_validate) +_rc_matplotlib_default = RcParams(_rc_matplotlib_default) + +# Important joint matplotlib proplot constants +# NOTE: The 'nodots' dictionary should include removed and renamed settings +_rc_categories = { + '.'.join(name.split('.')[:i + 1]) + for dict_ in (_rc_proplot_default, _rc_matplotlib_native) + for name in dict_ + for i in range(len(name.split('.')) - 1) +} +_rc_nodots = { + name.replace('.', ''): name + for dict_ in (_rc_proplot_default, _rc_matplotlib_native, _rc_renamed, _rc_removed) + for name in dict_.keys() +} diff --git a/proplot/internals/versions.py b/proplot/internals/versions.py new file mode 100644 index 000000000..dfeec28d1 --- /dev/null +++ b/proplot/internals/versions.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +""" +Utilities for handling dependencies and version changes. +""" +from . import ic # noqa: F401 +from . import warnings + + +class _version(list): + """ + Casual parser for ``major.minor`` style version strings. We do not want to + add a 'packaging' dependency and only care about major and minor tags. + """ + def __str__(self): + return self._version + + def __repr__(self): + return f'version({self._version})' + + def __init__(self, version): + try: + major, minor, *_ = version.split('.') + major, minor = int(major or 0), int(minor or 0) + except Exception: + warnings._warn_proplot(f'Unexpected version {version!r}. Using 0.0.0.') + major = minor = 0 + self._version = f'{major}.{minor}' + super().__init__((major, minor)) # then use builtin python list sorting + + def __eq__(self, other): + return super().__eq__(_version(other)) + + def __ne__(self, other): + return super().__ne__(_version(other)) + + def __gt__(self, other): + return super().__gt__(_version(other)) + + def __lt__(self, other): + return super().__lt__(_version(other)) + + def __ge__(self, other): + return super().__ge__(_version(other)) + + def __le__(self, other): + return super().__le__(_version(other)) + + +# Matplotlib version +import matplotlib # isort:skip +_version_mpl = _version(matplotlib.__version__) + +# Cartopy version +try: + import cartopy +except ImportError: + _version_cartopy = _version('0.0.0') +else: + _version_cartopy = _version(cartopy.__version__) diff --git a/proplot/internals/warnings.py b/proplot/internals/warnings.py new file mode 100644 index 000000000..577800460 --- /dev/null +++ b/proplot/internals/warnings.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +""" +Utilities for internal warnings and deprecations. +""" +import functools +import re +import sys +import warnings + +from . import ic # noqa: F401 + +# Internal modules omitted from warning message +REGEX_INTERNAL = re.compile(r'\A(matplotlib|mpl_toolkits|proplot)\.') + +# Trivial warning class meant only to communicate the source of the warning +ProplotWarning = type('ProplotWarning', (UserWarning,), {}) + +# Add due to overwriting the module name +catch_warnings = warnings.catch_warnings +simplefilter = warnings.simplefilter + + +def _next_release(): + """ + Message indicating the next major release. + """ + from .. import __version__ + try: + num = int(__version__[0]) + 1 + except TypeError: + string = 'the next major release' + else: + which = 'first' if num == 1 else 'next' + string = f'the {which} major release (version {num}.0.0)' + return string + + +def _warn_proplot(message): + """ + Emit a `ProplotWarning` and show the stack level outside of matplotlib and + proplot. This is adapted from matplotlib's warning system. + """ + frame = sys._getframe() + stacklevel = 1 + while frame is not None: + if not REGEX_INTERNAL.match(frame.f_globals.get('__name__', '')): + break # this is the first external frame + frame = frame.f_back + stacklevel += 1 + warnings.warn(message, ProplotWarning, stacklevel=stacklevel) + + +def _rename_objs(version, **kwargs): + """ + Emit a basic deprecation warning after renaming function(s), method(s), or + class(es). Each key should be an old name, and each argument should be the new + object to point to. Do not document the deprecated object(s) to discourage use. + """ + objs = [] + for old_name, new_obj in kwargs.items(): + new_name = new_obj.__name__ + message = ( + f'{old_name!r} was deprecated in version {version} and may be ' + f'removed in {_next_release()}. Please use {new_name!r} instead.' + ) + if isinstance(new_obj, type): + class _deprecated_class(new_obj): + def __init__(self, *args, new_obj=new_obj, message=message, **kwargs): + _warn_proplot(message) + super().__init__(*args, **kwargs) + _deprecated_class.__name__ = old_name + objs.append(_deprecated_class) + elif callable(new_obj): + def _deprecated_function(*args, new_obj=new_obj, message=message, **kwargs): + _warn_proplot(message) + return new_obj(*args, **kwargs) + _deprecated_function.__name__ = old_name + objs.append(_deprecated_function) + else: + raise ValueError(f'Invalid deprecated object replacement {new_obj!r}.') + if len(objs) == 1: + return objs[0] + else: + return tuple(objs) + + +def _rename_kwargs(version, **kwargs_rename): + """ + Emit a basic deprecation warning after removing or renaming keyword argument(s). + Each key should be an old keyword, and each argument should be the new keyword + or *instructions* for what to use instead. + """ + def _decorator(func_orig): + @functools.wraps(func_orig) + def _deprecate_kwargs_wrapper(*args, **kwargs): + for key_old, key_new in kwargs_rename.items(): + if key_old not in kwargs: + continue + value = kwargs.pop(key_old) + if key_new.isidentifier(): + # Rename argument + kwargs[key_new] = value + elif '{}' in key_new: + # Nice warning message, but user's desired behavior fails + key_new = key_new.format(value) + _warn_proplot( + f'Keyword {key_old!r} was deprecated in version {version} and may ' + f'be removed in {_next_release()}. Please use {key_new!r} instead.' + ) + return func_orig(*args, **kwargs) + return _deprecate_kwargs_wrapper + return _decorator diff --git a/proplot/proj.py b/proplot/proj.py new file mode 100644 index 000000000..a7a33636c --- /dev/null +++ b/proplot/proj.py @@ -0,0 +1,318 @@ +#!/usr/bin/env python3 +""" +Additional cartopy projection classes. +""" +import warnings + +from .internals import ic # noqa: F401 +from .internals import docstring + +try: + from cartopy.crs import ( # stereo projections needed in geo.py + AzimuthalEquidistant, + Gnomonic, + LambertAzimuthalEqualArea, + NorthPolarStereo, + SouthPolarStereo, + _WarpedRectangularProjection, + ) +except ModuleNotFoundError: + AzimuthalEquidistant = Gnomonic = LambertAzimuthalEqualArea = object + _WarpedRectangularProjection = NorthPolarStereo = SouthPolarStereo = object + +__all__ = [ + 'Aitoff', + 'Hammer', + 'KavrayskiyVII', + 'WinkelTripel', + 'NorthPolarAzimuthalEquidistant', + 'SouthPolarAzimuthalEquidistant', + 'NorthPolarGnomonic', + 'SouthPolarGnomonic', + 'NorthPolarLambertAzimuthalEqualArea', + 'SouthPolarLambertAzimuthalEqualArea', +] + + +_reso_docstring = """ +The projection resolution. +""" +_init_docstring = """ +Parameters +---------- +central_longitude : float, default: 0 + The central meridian longitude in degrees. +false_easting: float, default: 0 + X offset from planar origin in metres. +false_northing: float, default: 0 + Y offset from planar origin in metres. +globe : `~cartopy.crs.Globe`, optional + If omitted, a default globe is created. +""" +docstring._snippet_manager['proj.reso'] = _reso_docstring +docstring._snippet_manager['proj.init'] = _init_docstring + + +class Aitoff(_WarpedRectangularProjection): + """ + The `Aitoff `__ projection. + """ + #: Registered projection name. + name = 'aitoff' + + @docstring._snippet_manager + def __init__( + self, central_longitude=0, globe=None, + false_easting=None, false_northing=None + ): + """ + %(proj.init)s + """ + from cartopy.crs import WGS84_SEMIMAJOR_AXIS, Globe + if globe is None: + globe = Globe(semimajor_axis=WGS84_SEMIMAJOR_AXIS, ellipse=None) + + a = globe.semimajor_axis or WGS84_SEMIMAJOR_AXIS + b = globe.semiminor_axis or a + if b != a or globe.ellipse is not None: + warnings.warn( + f'The {self.name!r} projection does not handle elliptical globes.' + ) + + proj4_params = {'proj': 'aitoff', 'lon_0': central_longitude} + super().__init__( + proj4_params, central_longitude, + false_easting=false_easting, + false_northing=false_northing, + globe=globe + ) + + @docstring._snippet_manager + @property + def threshold(self): # how finely to interpolate line data, etc. + """ + %(proj.reso)s + """ + return 1e5 + + +class Hammer(_WarpedRectangularProjection): + """ + The `Hammer `__ projection. + """ + #: Registered projection name. + name = 'hammer' + + @docstring._snippet_manager + def __init__( + self, central_longitude=0, globe=None, + false_easting=None, false_northing=None + ): + """ + %(proj.init)s + """ + from cartopy.crs import WGS84_SEMIMAJOR_AXIS, Globe + if globe is None: + globe = Globe(semimajor_axis=WGS84_SEMIMAJOR_AXIS, ellipse=None) + + a = globe.semimajor_axis or WGS84_SEMIMAJOR_AXIS + b = globe.semiminor_axis or a + if b != a or globe.ellipse is not None: + warnings.warn( + f'The {self.name!r} projection does not handle elliptical globes.' + ) + + proj4_params = {'proj': 'hammer', 'lon_0': central_longitude} + super().__init__( + proj4_params, central_longitude, + false_easting=false_easting, + false_northing=false_northing, + globe=globe + ) + + @docstring._snippet_manager + @property + def threshold(self): # how finely to interpolate line data, etc. + """ + %(proj.reso)s + """ + return 1e5 + + +class KavrayskiyVII(_WarpedRectangularProjection): + """ + The `Kavrayskiy VII \ +`__ projection. + """ + #: Registered projection name. + name = 'kavrayskiyVII' + + @docstring._snippet_manager + def __init__( + self, central_longitude=0, globe=None, + false_easting=None, false_northing=None + ): + """ + %(proj.init)s + """ + from cartopy.crs import WGS84_SEMIMAJOR_AXIS, Globe + if globe is None: + globe = Globe(semimajor_axis=WGS84_SEMIMAJOR_AXIS, ellipse=None) + + a = globe.semimajor_axis or WGS84_SEMIMAJOR_AXIS + b = globe.semiminor_axis or a + if b != a or globe.ellipse is not None: + warnings.warn( + f'The {self.name!r} projection does not handle elliptical globes.' + ) + + proj4_params = {'proj': 'kav7', 'lon_0': central_longitude} + super().__init__( + proj4_params, central_longitude, + false_easting=false_easting, + false_northing=false_northing, + globe=globe + ) + + @docstring._snippet_manager + @property + def threshold(self): + """ + %(proj.reso)s + """ + return 1e5 + + +class WinkelTripel(_WarpedRectangularProjection): + """ + The `Winkel tripel (Winkel III) \ +`__ projection. + """ + #: Registered projection name. + name = 'winkeltripel' + + @docstring._snippet_manager + def __init__( + self, central_longitude=0, globe=None, + false_easting=None, false_northing=None + ): + """ + %(proj.init)s + """ + from cartopy.crs import WGS84_SEMIMAJOR_AXIS, Globe + if globe is None: + globe = Globe(semimajor_axis=WGS84_SEMIMAJOR_AXIS, ellipse=None) + + a = globe.semimajor_axis or WGS84_SEMIMAJOR_AXIS + b = globe.semiminor_axis or a + if b != a or globe.ellipse is not None: + warnings.warn( + f'The {self.name!r} projection does not handle ' + 'elliptical globes.' + ) + + proj4_params = {'proj': 'wintri', 'lon_0': central_longitude} + super().__init__( + proj4_params, central_longitude, + false_easting=false_easting, + false_northing=false_northing, + globe=globe + ) + + @docstring._snippet_manager + @property + def threshold(self): + """ + %(proj.reso)s + """ + return 1e5 + + +class NorthPolarAzimuthalEquidistant(AzimuthalEquidistant): + """ + Analogous to `~cartopy.crs.NorthPolarStereo`. + """ + @docstring._snippet_manager + def __init__(self, central_longitude=0.0, globe=None): + """ + %(proj.init)s + """ + super().__init__( + central_latitude=90, + central_longitude=central_longitude, globe=globe + ) + + +class SouthPolarAzimuthalEquidistant(AzimuthalEquidistant): + """ + Analogous to `~cartopy.crs.SouthPolarStereo`. + """ + @docstring._snippet_manager + def __init__(self, central_longitude=0.0, globe=None): + """ + %(proj.init)s + """ + super().__init__( + central_latitude=-90, + central_longitude=central_longitude, globe=globe + ) + + +class NorthPolarLambertAzimuthalEqualArea(LambertAzimuthalEqualArea): + """ + Analogous to `~cartopy.crs.NorthPolarStereo`. + """ + @docstring._snippet_manager + def __init__(self, central_longitude=0.0, globe=None): + """ + %(proj.init)s + """ + super().__init__( + central_latitude=90, + central_longitude=central_longitude, globe=globe + ) + + +class SouthPolarLambertAzimuthalEqualArea(LambertAzimuthalEqualArea): + """ + Analogous to `~cartopy.crs.SouthPolarStereo`. + """ + @docstring._snippet_manager + def __init__(self, central_longitude=0.0, globe=None): + """ + %(proj.init)s + """ + super().__init__( + central_latitude=-90, + central_longitude=central_longitude, globe=globe + ) + + +class NorthPolarGnomonic(Gnomonic): + """ + Analogous to `~cartopy.crs.NorthPolarStereo`. + """ + @docstring._snippet_manager + def __init__(self, central_longitude=0.0, globe=None): + """ + %(proj.init)s + """ + super().__init__( + central_latitude=90, + central_longitude=central_longitude, globe=globe + ) + + +class SouthPolarGnomonic(Gnomonic): + """ + Analogous to `~cartopy.crs.SouthPolarStereo`. + """ + @docstring._snippet_manager + def __init__(self, central_longitude=0.0, globe=None): + """ + %(proj.init)s + """ + super().__init__( + central_latitude=-90, + central_longitude=central_longitude, globe=globe + ) diff --git a/proplot/projs.py b/proplot/projs.py deleted file mode 100644 index c05d71115..000000000 --- a/proplot/projs.py +++ /dev/null @@ -1,534 +0,0 @@ -#!/usr/bin/env python3 -""" -New cartopy projection classes and a projection constructor function -for generating `~mpl_toolkits.basemap.Basemap` and cartopy -`~cartopy.crs.Projection` classes. -""" -from .utils import _warn_proplot -try: # use this for debugging instead of print()! - from icecream import ic -except ImportError: # graceful fallback if IceCream isn't installed - ic = lambda *a: None if not a else (a[0] if len(a) == 1 else a) # noqa -try: - from mpl_toolkits.basemap import Basemap -except ImportError: - Basemap = object -try: - from cartopy.crs import ( - CRS, _WarpedRectangularProjection, - LambertAzimuthalEqualArea, AzimuthalEquidistant, Gnomonic - ) -except ModuleNotFoundError: - CRS = object - _WarpedRectangularProjection = object - LambertAzimuthalEqualArea = object - AzimuthalEquidistant = object - Gnomonic = object - -__all__ = [ - 'Proj', - 'basemap_kwargs', 'cartopy_names', - 'Aitoff', 'Hammer', 'KavrayskiyVII', - 'NorthPolarAzimuthalEquidistant', - 'NorthPolarGnomonic', - 'NorthPolarLambertAzimuthalEqualArea', - 'SouthPolarAzimuthalEquidistant', - 'SouthPolarGnomonic', - 'SouthPolarLambertAzimuthalEqualArea', - 'WinkelTripel', -] - -_reso_doc = """Projection resolution.""" -_proj_doc = """ -Parameters ----------- -central_longitude : float, optional - The longitude of the central meridian in degrees. Default is 0. -false_easting: float, optional - X offset from planar origin in metres. Defaults to 0. -false_northing: float, optional - Y offset from planar origin in metres. Defaults to 0. -globe : `~cartopy.crs.Globe`, optional - If omitted, a default globe is created. -""" - - -def Proj(name, basemap=False, **kwargs): - """ - Returns a `~mpl_toolkits.basemap.Basemap` or `cartopy.crs.Projection` - instance, used to interpret the `proj` and `proj_kw` arguments when - passed to `~proplot.subplots.subplots`. - - Parameters - ---------- - name : str, `~mpl_toolkits.basemap.Basemap`, or `cartopy.crs.Projection` - The projection name or projection class instance. If the latter, it - is simply returned. If the former, it must correspond to one of the - `PROJ `__ projection name shorthands, like in - basemap. - - The following table lists the valid projection name shorthands, their - full names (with links to the relevant `PROJ documentation \ -`__), - and whether they are available in the cartopy and basemap packages. - - (added) indicates a projection class that ProPlot has "added" - to cartopy using the cartopy API. - - ============= ============================================================================================ ========= ======= - Key Name Cartopy Basemap - ============= ============================================================================================ ========= ======= - ``'aea'`` `Albers Equal Area `__ ✓ ✓ - ``'aeqd'`` `Azimuthal Equidistant `__ ✓ ✓ - ``'aitoff'`` `Aitoff `__ ✓ (added) ✗ - ``'cass'`` `Cassini-Soldner `__ ✗ ✓ - ``'cea'`` `Cylindrical Equal Area `__ ✗ ✓ - ``'cyl'`` `Cylindrical Equidistant `__ ✓ ✓ - ``'eck1'`` `Eckert I `__ ✓ ✗ - ``'eck2'`` `Eckert II `__ ✓ ✗ - ``'eck3'`` `Eckert III `__ ✓ ✗ - ``'eck4'`` `Eckert IV `__ ✓ ✓ - ``'eck5'`` `Eckert V `__ ✓ ✗ - ``'eck6'`` `Eckert VI `__ ✓ ✗ - ``'eqdc'`` `Equidistant Conic `__ ✓ ✓ - ``'eqc'`` `Cylindrical Equidistant `__ ✓ ✓ - ``'eqearth'`` `Equal Earth `__ ✓ ✗ - ``'europp'`` Euro PP (Europe) ✓ ✗ - ``'gall'`` `Gall Stereographic Cylindrical `__ ✗ ✓ - ``'geos'`` `Geostationary `__ ✓ ✓ - ``'gnom'`` `Gnomonic `__ ✓ ✓ - ``'hammer'`` `Hammer `__ ✓ (added) ✓ - ``'igh'`` `Interrupted Goode Homolosine `__ ✓ ✗ - ``'kav7'`` `Kavrayskiy VII `__ ✓ (added) ✓ - ``'laea'`` `Lambert Azimuthal Equal Area `__ ✓ ✓ - ``'lcc'`` `Lambert Conformal `__ ✓ ✓ - ``'lcyl'`` Lambert Cylindrical ✓ ✗ - ``'mbtfpq'`` `McBryde-Thomas Flat-Polar Quartic `__ ✗ ✓ - ``'merc'`` `Mercator `__ ✓ ✓ - ``'mill'`` `Miller Cylindrical `__ ✓ ✓ - ``'moll'`` `Mollweide `__ ✓ ✓ - ``'npaeqd'`` North-Polar Azimuthal Equidistant ✓ (added) ✓ - ``'npgnom'`` North-Polar Gnomonic ✓ (added) ✗ - ``'nplaea'`` North-Polar Lambert Azimuthal ✓ (added) ✓ - ``'npstere'`` North-Polar Stereographic ✓ ✓ - ``'nsper'`` `Near-Sided Perspective `__ ✓ ✓ - ``'osni'`` OSNI (Ireland) ✓ ✗ - ``'osgb'`` OSGB (UK) ✓ ✗ - ``'omerc'`` `Oblique Mercator `__ ✗ ✓ - ``'ortho'`` `Orthographic `__ ✓ ✓ - ``'pcarree'`` `Cylindrical Equidistant `__ ✓ ✓ - ``'poly'`` `Polyconic `__ ✗ ✓ - ``'rotpole'`` Rotated Pole ✓ ✓ - ``'sinu'`` `Sinusoidal `__ ✓ ✓ - ``'spaeqd'`` South-Polar Azimuthal Equidistant ✓ (added) ✓ - ``'spgnom'`` South-Polar Gnomonic ✓ (added) ✗ - ``'splaea'`` South-Polar Lambert Azimuthal ✓ (added) ✓ - ``'spstere'`` South-Polar Stereographic ✓ ✓ - ``'stere'`` `Stereographic `__ ✓ ✓ - ``'tmerc'`` `Transverse Mercator `__ ✓ ✓ - ``'utm'`` `Universal Transverse Mercator `__ ✓ ✗ - ``'vandg'`` `van der Grinten `__ ✗ ✓ - ``'wintri'`` `Winkel tripel `__ ✓ (added) ✗ - ============= ============================================================================================ ========= ======= - - basemap : bool, optional - Whether to use the basemap package as opposed to the cartopy package. - Default is ``False``. - **kwargs - Passed to the `~mpl_toolkits.basemap.Basemap` or - cartopy `~cartopy.crs.Projection` class. - - Returns - ------- - proj : `~mpl_toolkits.basemap.Basemap` or `~cartopy.crs.Projection` - The projection instance. - - See also - -------- - `~proplot.axes.GeoAxes`, `~proplot.axes.BasemapAxes` - """ # noqa - # Class instances - if ((CRS is not object and isinstance(name, CRS)) - or (Basemap is not object and isinstance(name, Basemap))): - proj = name - elif not isinstance(name, str): - raise ValueError( - f'Unexpected Proj() argument {name!r}. ' - 'Must be name, mpl_toolkits.basemap.Basemap instance, ' - 'or cartopy.crs.CRS instance.' - ) - # Basemap - elif basemap: - import mpl_toolkits.basemap as mbasemap - name = BASEMAP_TRANSLATE.get(name, name) - kwproj = basemap_kwargs.get(name, {}) - kwproj.update(kwargs) - kwproj.setdefault('fix_aspect', True) - if name[:2] in ('np', 'sp'): - kwproj.setdefault('round', True) - # Fix non-conda installed basemap issue: - # https://github.com/matplotlib/basemap/issues/361 - if name == 'geos': - kwproj.setdefault('rsphere', (6378137.00, 6356752.3142)) - reso = kwproj.pop('resolution', None) or kwproj.pop( - 'reso', None) or 'c' - proj = mbasemap.Basemap(projection=name, resolution=reso, **kwproj) - # Cartopy - else: - import cartopy.crs as _ # noqa - kwargs = {CARTOPY_CRS_TRANSLATE.get( - key, key): value for key, value in kwargs.items()} - crs = cartopy_names.get(name, None) - if name == 'geos': # fix common mistake - kwargs.pop('central_latitude', None) - if 'boundinglat' in kwargs: - raise ValueError( - f'"boundinglat" must be passed to the ax.format() command ' - 'for cartopy axes.' - ) - if crs is None: - raise ValueError( - f'Unknown projection {name!r}. Options are: ' - + ', '.join(map(repr, cartopy_names.keys())) + '.' - ) - proj = crs(**kwargs) - return proj - - -class Aitoff(_WarpedRectangularProjection): - """ - The `Aitoff `__ - projection. - """ - #: Registered projection name. - name = 'aitoff' - - def __init__(self, central_longitude=0, globe=None, - false_easting=None, false_northing=None): - from cartopy._crs import Globe - from cartopy.crs import WGS84_SEMIMAJOR_AXIS - if globe is None: - globe = Globe(semimajor_axis=WGS84_SEMIMAJOR_AXIS, ellipse=None) - - a = globe.semimajor_axis or WGS84_SEMIMAJOR_AXIS - b = globe.semiminor_axis or a - if b != a or globe.ellipse is not None: - _warn_proplot( - f'The {self.name!r} projection does not handle ' - 'elliptical globes.' - ) - - proj4_params = {'proj': 'aitoff', 'lon_0': central_longitude} - super().__init__( - proj4_params, central_longitude, - false_easting=false_easting, - false_northing=false_northing, - globe=globe - ) - - @property - def threshold(self): # how finely to interpolate line data, etc. - return 1e5 - - __init__.__doc__ = _proj_doc - threshold.__doc__ = _reso_doc - - -class Hammer(_WarpedRectangularProjection): - """ - The `Hammer `__ - projection. - """ - #: Registered projection name. - name = 'hammer' - - def __init__(self, central_longitude=0, globe=None, - false_easting=None, false_northing=None): - from cartopy._crs import Globe - from cartopy.crs import WGS84_SEMIMAJOR_AXIS - if globe is None: - globe = Globe(semimajor_axis=WGS84_SEMIMAJOR_AXIS, ellipse=None) - - a = globe.semimajor_axis or WGS84_SEMIMAJOR_AXIS - b = globe.semiminor_axis or a - if b != a or globe.ellipse is not None: - _warn_proplot( - f'The {self.name!r} projection does not handle ' - 'elliptical globes.' - ) - - proj4_params = {'proj': 'hammer', 'lon_0': central_longitude} - super().__init__( - proj4_params, central_longitude, - false_easting=false_easting, - false_northing=false_northing, - globe=globe - ) - - @property - def threshold(self): # how finely to interpolate line data, etc. - return 1e5 - - __init__.__doc__ = _proj_doc - threshold.__doc__ = _reso_doc - - -class KavrayskiyVII(_WarpedRectangularProjection): - """ - The `Kavrayskiy VII \ -`__ projection. - """ - #: Registered projection name. - name = 'kavrayskiyVII' - - def __init__(self, central_longitude=0, globe=None, - false_easting=None, false_northing=None): - from cartopy._crs import Globe - from cartopy.crs import WGS84_SEMIMAJOR_AXIS - if globe is None: - globe = Globe(semimajor_axis=WGS84_SEMIMAJOR_AXIS, ellipse=None) - - a = globe.semimajor_axis or WGS84_SEMIMAJOR_AXIS - b = globe.semiminor_axis or a - if b != a or globe.ellipse is not None: - _warn_proplot( - f'The {self.name!r} projection does not handle ' - 'elliptical globes.' - ) - - proj4_params = {'proj': 'kav7', 'lon_0': central_longitude} - super().__init__(proj4_params, central_longitude, - false_easting=false_easting, - false_northing=false_northing, - globe=globe) - - @property - def threshold(self): - return 1e5 - - __init__.__doc__ = _proj_doc - threshold.__doc__ = _reso_doc - - -class WinkelTripel(_WarpedRectangularProjection): - """ - The `Winkel tripel (Winkel III) \ -`__ projection. - """ - #: Registered projection name. - name = 'winkeltripel' - - def __init__(self, central_longitude=0, globe=None, - false_easting=None, false_northing=None): - from cartopy._crs import Globe - from cartopy.crs import WGS84_SEMIMAJOR_AXIS - if globe is None: - globe = Globe(semimajor_axis=WGS84_SEMIMAJOR_AXIS, ellipse=None) - - a = globe.semimajor_axis or WGS84_SEMIMAJOR_AXIS - b = globe.semiminor_axis or a - if b != a or globe.ellipse is not None: - _warn_proplot( - f'The {self.name!r} projection does not handle ' - 'elliptical globes.' - ) - - proj4_params = {'proj': 'wintri', 'lon_0': central_longitude} - super().__init__(proj4_params, central_longitude, - false_easting=false_easting, - false_northing=false_northing, - globe=globe) - - @property - def threshold(self): - return 1e5 - - __init__.__doc__ = _proj_doc - threshold.__doc__ = _reso_doc - - -class NorthPolarAzimuthalEquidistant(AzimuthalEquidistant): - """ - Analogous to `~cartopy.crs.NorthPolarStereo`. - """ - def __init__(self, central_longitude=0.0, globe=None): - super().__init__( - central_latitude=90, - central_longitude=central_longitude, globe=globe - ) - __init__.__doc__ = _proj_doc - - -class SouthPolarAzimuthalEquidistant(AzimuthalEquidistant): - """ - Analogous to `~cartopy.crs.SouthPolarStereo`. - """ - def __init__(self, central_longitude=0.0, globe=None): - super().__init__( - central_latitude=-90, - central_longitude=central_longitude, globe=globe - ) - __init__.__doc__ = _proj_doc - - -class NorthPolarLambertAzimuthalEqualArea(LambertAzimuthalEqualArea): - """ - Analogous to `~cartopy.crs.NorthPolarStereo`. - """ - def __init__(self, central_longitude=0.0, globe=None): - super().__init__( - central_latitude=90, - central_longitude=central_longitude, globe=globe - ) - __init__.__doc__ = _proj_doc - - -class SouthPolarLambertAzimuthalEqualArea(LambertAzimuthalEqualArea): - """ - Analogous to `~cartopy.crs.SouthPolarStereo`. - """ - def __init__(self, central_longitude=0.0, globe=None): - super().__init__( - central_latitude=-90, - central_longitude=central_longitude, globe=globe - ) - __init__.__doc__ = _proj_doc - - -class NorthPolarGnomonic(Gnomonic): - """ - Analogous to `~cartopy.crs.SouthPolarStereo`. - """ - def __init__(self, central_longitude=0.0, globe=None): - super().__init__( - central_latitude=90, - central_longitude=central_longitude, globe=globe - ) - __init__.__doc__ = _proj_doc - - -class SouthPolarGnomonic(Gnomonic): - """ - Analogous to `~cartopy.crs.SouthPolarStereo`. - """ - def __init__(self, central_longitude=0.0, globe=None): - super().__init__( - central_latitude=-90, - central_longitude=central_longitude, globe=globe - ) - __init__.__doc__ = _proj_doc - - -# Hidden constants -BASEMAP_TRANSLATE = { - 'eqc': 'cyl', - 'pcarree': 'cyl', -} -CARTOPY_CRS_TRANSLATE = { # add to this - 'lat_0': 'central_latitude', - 'lon_0': 'central_longitude', - 'lat_min': 'min_latitude', - 'lat_max': 'max_latitude', -} - -#: Default keyword args for `~mpl_toolkits.basemap.Basemap` projections. -#: `~mpl_toolkits.basemap` will raise an error if you don't provide them, -#: so ProPlot imposes some sensible default behavior. -basemap_kwargs = { - 'eck4': {'lon_0': 0}, - 'geos': {'lon_0': 0}, - 'hammer': {'lon_0': 0}, - 'moll': {'lon_0': 0}, - 'kav7': {'lon_0': 0}, - 'sinu': {'lon_0': 0}, - 'vandg': {'lon_0': 0}, - 'mbtfpq': {'lon_0': 0}, - 'robin': {'lon_0': 0}, - 'ortho': {'lon_0': 0, 'lat_0': 0}, - 'nsper': {'lon_0': 0, 'lat_0': 0}, - 'aea': {'lon_0': 0, 'lat_0': 90, 'width': 15000e3, 'height': 15000e3}, - 'eqdc': {'lon_0': 0, 'lat_0': 90, 'width': 15000e3, 'height': 15000e3}, - 'cass': {'lon_0': 0, 'lat_0': 90, 'width': 15000e3, 'height': 15000e3}, - 'gnom': {'lon_0': 0, 'lat_0': 90, 'width': 15000e3, 'height': 15000e3}, - 'lcc': {'lon_0': 0, 'lat_0': 90, 'width': 10000e3, 'height': 10000e3}, - 'poly': {'lon_0': 0, 'lat_0': 0, 'width': 10000e3, 'height': 10000e3}, - 'npaeqd': {'lon_0': 0, 'boundinglat': 10}, - 'nplaea': {'lon_0': 0, 'boundinglat': 10}, - 'npstere': {'lon_0': 0, 'boundinglat': 10}, - 'spaeqd': {'lon_0': 0, 'boundinglat': -10}, - 'splaea': {'lon_0': 0, 'boundinglat': -10}, - 'spstere': {'lon_0': 0, 'boundinglat': -10}, - 'tmerc': {'lon_0': 0, 'lat_0': 0, 'width': 10000e3, 'height': 10000e3}, - 'merc': {'llcrnrlat': -80, 'urcrnrlat': 84, - 'llcrnrlon': -180, 'urcrnrlon': 180}, - 'omerc': {'lat_0': 0, 'lon_0': 0, 'lat_1': -10, 'lat_2': 10, - 'lon_1': 0, 'lon_2': 0, 'width': 10000e3, 'height': 10000e3}, -} - -#: Mapping of "projection names" to cartopy `~cartopy.crs.Projection` classes. -cartopy_names = {} -if CRS is not object: - # Custom ones, these are always present - import cartopy.crs as ccrs # verify package is available - cartopy_names = { # interpret string, create cartopy projection - 'aitoff': Aitoff, - 'hammer': Hammer, - 'kav7': KavrayskiyVII, - 'wintri': WinkelTripel, - 'npgnom': NorthPolarGnomonic, - 'spgnom': SouthPolarGnomonic, - 'npaeqd': NorthPolarAzimuthalEquidistant, - 'spaeqd': SouthPolarAzimuthalEquidistant, - 'nplaea': NorthPolarLambertAzimuthalEqualArea, - 'splaea': SouthPolarLambertAzimuthalEqualArea, - } - # Builtin ones. Some of these are unavailable in older versions, so - # we just print warning in that case. - _unavail = [] - for _name, _class in { # interpret string, create cartopy projection - 'aea': 'AlbersEqualArea', - 'aeqd': 'AzimuthalEquidistant', - 'cyl': 'PlateCarree', # only basemap name not matching PROJ - 'eck1': 'EckertI', - 'eck2': 'EckertII', - 'eck3': 'EckertIII', - 'eck4': 'EckertIV', - 'eck5': 'EckertV', - 'eck6': 'EckertVI', - 'eqc': 'PlateCarree', # actual PROJ name - 'eqdc': 'EquidistantConic', - 'eqearth': 'EqualEarth', # better looking Robinson; not in basemap - 'euro': 'EuroPP', # Europe; not in basemap or PROJ - 'geos': 'Geostationary', - 'gnom': 'Gnomonic', - 'igh': 'InterruptedGoodeHomolosine', # not in basemap - 'laea': 'LambertAzimuthalEqualArea', - 'lcc': 'LambertConformal', - 'lcyl': 'LambertCylindrical', # not in basemap or PROJ - 'merc': 'Mercator', - 'mill': 'Miller', - 'moll': 'Mollweide', - 'npstere': 'NorthPolarStereo', # np/sp stuff not in PROJ - 'nsper': 'NearsidePerspective', - 'ortho': 'Orthographic', - 'osgb': 'OSGB', # UK; not in basemap or PROJ - 'osni': 'OSNI', # Ireland; not in basemap or PROJ - 'pcarree': 'PlateCarree', # common alternate name - 'robin': 'Robinson', - 'rotpole': 'RotatedPole', - 'sinu': 'Sinusoidal', - 'spstere': 'SouthPolarStereo', - 'stere': 'Stereographic', - 'tmerc': 'TransverseMercator', - 'utm': 'UTM', # not in basemap - }.items(): - _class = getattr(ccrs, _class, None) - if _class is None: - _unavail.append(_name) - continue - cartopy_names[_name] = _class - if _unavail: - _warn_proplot( - f'Cartopy projection(s) {", ".join(map(repr, _unavail))} are ' - f'unavailable. Consider updating to cartopy >= 0.17.0.' - ) diff --git a/proplot/rctools.py b/proplot/rctools.py deleted file mode 100644 index 567a5d319..000000000 --- a/proplot/rctools.py +++ /dev/null @@ -1,1205 +0,0 @@ -#!/usr/bin/env python3 -""" -Utilities for configuring matplotlib and ProPlot global settings. -See :ref:`Configuring proplot` for details. -""" -# NOTE: Make sure to add to docs/configuration.rst when updating or adding -# new settings! Much of this script was adapted from seaborn; see: -# https://github.com/mwaskom/seaborn/blob/master/seaborn/rcmod.py -import re -import os -import numpy as np -import cycler -import matplotlib.colors as mcolors -import matplotlib.cm as mcm -from numbers import Number -from matplotlib import style, rcParams -try: # use this for debugging instead of print()! - from icecream import ic -except ImportError: # graceful fallback if IceCream isn't installed - ic = lambda *a: None if not a else (a[0] if len(a) == 1 else a) # noqa -try: - from IPython import get_ipython -except ImportError: - def get_ipython(): - return -from .utils import _warn_proplot, _counter, units - -# Disable mathtext "missing glyph" warnings -import matplotlib.mathtext # noqa -import logging -logger = logging.getLogger('matplotlib.mathtext') -logger.setLevel(logging.ERROR) # suppress warnings! - -__all__ = [ - 'rc', 'rc_configurator', 'inline_backend_fmt', -] - -# Dictionaries used to track custom proplot settings -rcParamsShort = {} -rcParamsLong = {} - -# Dictionaries containing default settings -defaultParamsShort = { - 'abc': False, - 'align': False, - 'alpha': 1, - 'borders': False, - 'cmap': 'fire', - 'coast': False, - 'color': 'k', - 'cycle': 'colorblind', - 'facecolor': 'w', - 'fontname': 'sans-serif', - 'inlinefmt': 'retina', - 'geogrid': True, - 'grid': True, - 'gridminor': False, - 'gridratio': 0.5, - 'innerborders': False, - 'lakes': False, - 'land': False, - 'large': 10, - 'linewidth': 0.6, - 'lut': 256, - 'margin': 0.0, - 'ocean': False, - 'reso': 'lo', - 'rgbcycle': False, - 'rivers': False, - 'share': 3, - 'small': 9, - 'span': True, - 'tickdir': 'out', - 'ticklen': 4.0, - 'ticklenratio': 0.5, - 'tickminor': True, - 'tickpad': 2.0, - 'tickratio': 0.8, - 'tight': True, -} -defaultParamsLong = { - 'abc.border': True, - 'abc.borderwidth': 1.5, - 'abc.color': 'k', - 'abc.loc': 'l', # left side above the axes - 'abc.size': None, # = large - 'abc.style': 'a', - 'abc.weight': 'bold', - 'axes.facealpha': None, # if empty, depends on 'savefig.transparent' - 'axes.formatter.timerotation': 90, - 'axes.formatter.zerotrim': True, - 'axes.geogrid': True, - 'axes.gridminor': True, - 'borders.color': 'k', - 'borders.linewidth': 0.6, - 'bottomlabel.color': 'k', - 'bottomlabel.size': None, # = large - 'bottomlabel.weight': 'bold', - 'coast.color': 'k', - 'coast.linewidth': 0.6, - 'colorbar.extend': '1.3em', - 'colorbar.framealpha': 0.8, - 'colorbar.frameon': True, - 'colorbar.grid': False, - 'colorbar.insetextend': '1em', - 'colorbar.insetlength': '8em', - 'colorbar.insetpad': '0.5em', - 'colorbar.insetwidth': '1.2em', - 'colorbar.length': 1, - 'colorbar.loc': 'right', - 'colorbar.width': '1.5em', - 'geoaxes.edgecolor': None, # = color - 'geoaxes.facealpha': None, # = alpha - 'geoaxes.facecolor': None, # = facecolor - 'geoaxes.linewidth': None, # = linewidth - 'geogrid.alpha': 0.5, - 'geogrid.color': 'k', - 'geogrid.labels': False, - 'geogrid.labelsize': None, # = small - 'geogrid.latmax': 90, - 'geogrid.latstep': 20, - 'geogrid.linestyle': ':', - 'geogrid.linewidth': 1.0, - 'geogrid.lonstep': 30, - 'gridminor.alpha': None, # = grid.alpha - 'gridminor.color': None, # = grid.color - 'gridminor.linestyle': None, # = grid.linewidth - 'gridminor.linewidth': None, # = grid.linewidth x gridratio - 'image.edgefix': True, - 'image.levels': 11, - 'innerborders.color': 'k', - 'innerborders.linewidth': 0.6, - 'lakes.color': 'w', - 'land.color': 'k', - 'leftlabel.color': 'k', - 'leftlabel.size': None, # = large - 'leftlabel.weight': 'bold', - 'ocean.color': 'w', - 'rightlabel.color': 'k', - 'rightlabel.size': None, # = large - 'rightlabel.weight': 'bold', - 'rivers.color': 'k', - 'rivers.linewidth': 0.6, - 'subplots.axpad': '1em', - 'subplots.axwidth': '18em', - 'subplots.pad': '0.5em', - 'subplots.panelpad': '0.5em', - 'subplots.panelwidth': '4em', - 'suptitle.color': 'k', - 'suptitle.size': None, # = large - 'suptitle.weight': 'bold', - 'tick.labelcolor': None, # = color - 'tick.labelsize': None, # = small - 'tick.labelweight': 'normal', - 'title.border': True, - 'title.borderwidth': 1.5, - 'title.color': 'k', - 'title.loc': 'c', # centered above the axes - 'title.pad': 3.0, # copy - 'title.size': None, # = large - 'title.weight': 'normal', - 'toplabel.color': 'k', - 'toplabel.size': None, # = large - 'toplabel.weight': 'bold', -} -defaultParams = { - 'axes.grid': True, - 'axes.labelpad': 3.0, - 'axes.titlepad': 3.0, - 'axes.titleweight': 'normal', - 'axes.xmargin': 0.0, - 'axes.ymargin': 0.0, - 'figure.autolayout': False, - 'figure.facecolor': '#f2f2f2', - 'figure.max_open_warning': 0, - 'figure.titleweight': 'bold', - 'font.serif': ( - 'TeX Gyre Schola', # Century lookalike - 'TeX Gyre Bonum', # Bookman lookalike - 'TeX Gyre Termes', # Times New Roman lookalike - 'TeX Gyre Pagella', # Palatino lookalike - 'DejaVu Serif', - 'Bitstream Vera Serif', - 'Computer Modern Roman', - 'Bookman', - 'Century Schoolbook L', - 'Charter', - 'ITC Bookman', - 'New Century Schoolbook', - 'Nimbus Roman No9 L', - 'Palatino', - 'Times New Roman', - 'Times', - 'Utopia', - 'serif' - ), - 'font.sans-serif': ( - 'TeX Gyre Heros', # Helvetica lookalike - 'DejaVu Sans', - 'Bitstream Vera Sans', - 'Computer Modern Sans Serif', - 'Arial', - 'Avenir', - 'Fira Math', - 'Frutiger', - 'Geneva', - 'Gill Sans', - 'Helvetica', - 'Lucid', - 'Lucida Grande', - 'Myriad Pro', - 'Noto Sans', - 'Roboto', - 'Source Sans Pro', - 'Tahoma', - 'Trebuchet MS', - 'Ubuntu', - 'Univers', - 'Verdana', - 'sans-serif' - ), - 'font.monospace': ( - 'TeX Gyre Cursor', # Courier lookalike - 'DejaVu Sans Mono', - 'Bitstream Vera Sans Mono', - 'Computer Modern Typewriter', - 'Andale Mono', - 'Courier New', - 'Courier', - 'Fixed', - 'Nimbus Mono L', - 'Terminal', - 'monospace' - ), - 'font.cursive': ( - 'TeX Gyre Chorus', # Chancery lookalike - 'Apple Chancery', - 'Felipa', - 'Sand', - 'Script MT', - 'Textile', - 'Zapf Chancery', - 'cursive' - ), - 'font.fantasy': ( - 'TeX Gyre Adventor', # Avant Garde lookalike - 'Avant Garde', - 'Charcoal', - 'Chicago', - 'Comic Sans MS', - 'Futura', - 'Humor Sans', - 'Impact', - 'Optima', - 'Western', - 'xkcd', - 'fantasy' - ), - 'grid.alpha': 0.1, - 'grid.color': 'k', - 'grid.linestyle': '-', - 'grid.linewidth': 0.6, - 'hatch.color': 'k', - 'hatch.linewidth': 0.6, - 'legend.borderaxespad': 0, - 'legend.borderpad': 0.5, - 'legend.columnspacing': 1.0, - 'legend.fancybox': False, - 'legend.framealpha': 0.8, - 'legend.frameon': True, - 'legend.handlelength': 1.5, - 'legend.handletextpad': 0.5, - 'legend.labelspacing': 0.5, - 'lines.linewidth': 1.3, - 'lines.markersize': 3.0, - 'mathtext.fontset': 'custom', - 'mathtext.default': 'regular', - 'savefig.bbox': 'standard', - 'savefig.directory': '', - 'savefig.dpi': 300, - 'savefig.facecolor': 'white', - 'savefig.format': 'pdf', - 'savefig.pad_inches': 0.0, - 'savefig.transparent': True, - 'text.usetex': False, - 'xtick.minor.visible': True, - 'ytick.minor.visible': True, -} - -# "Global" settings and the lower-level settings they change -_rc_children = { - 'cmap': ( - 'image.cmap', - ), - 'lut': ( - 'image.lut', - ), - 'alpha': ( # this is a custom setting - 'axes.facealpha', 'geoaxes.facealpha', - ), - 'facecolor': ( - 'axes.facecolor', 'geoaxes.facecolor' - ), - 'fontname': ( - 'font.family', - ), - 'color': ( # change the 'color' of an axes - 'axes.edgecolor', 'geoaxes.edgecolor', 'axes.labelcolor', - 'tick.labelcolor', 'hatch.color', 'xtick.color', 'ytick.color' - ), - 'small': ( # the 'small' fonts - 'font.size', 'tick.labelsize', 'xtick.labelsize', 'ytick.labelsize', - 'axes.labelsize', 'legend.fontsize', 'geogrid.labelsize' - ), - 'large': ( # the 'large' fonts - 'abc.size', 'figure.titlesize', - 'axes.titlesize', 'suptitle.size', 'title.size', - 'leftlabel.size', 'toplabel.size', - 'rightlabel.size', 'bottomlabel.size' - ), - 'linewidth': ( - 'axes.linewidth', 'geoaxes.linewidth', 'hatch.linewidth', - 'xtick.major.width', 'ytick.major.width' - ), - 'margin': ( - 'axes.xmargin', 'axes.ymargin' - ), - 'grid': ( - 'axes.grid', - ), - 'gridminor': ( - 'axes.gridminor', - ), - 'geogrid': ( - 'axes.geogrid', - ), - 'ticklen': ( - 'xtick.major.size', 'ytick.major.size' - ), - 'tickdir': ( - 'xtick.direction', 'ytick.direction' - ), - 'labelpad': ( - 'axes.labelpad', - ), - 'titlepad': ( - 'axes.titlepad', - ), - 'tickpad': ( - 'xtick.major.pad', 'xtick.minor.pad', - 'ytick.major.pad', 'ytick.minor.pad' - ), - 'grid.color': ( - 'gridminor.color', - ), - 'grid.linewidth': ( - 'gridminor.linewidth', - ), - 'grid.linestyle': ( - 'gridminor.linestyle', - ), - 'grid.alpha': ( - 'gridminor.alpha', - ), -} - -# Mapping of settings without "dots" to their full names. This lets us pass -# all settings as kwargs, e.g. ax.format(landcolor='b') instead of the much -# more verbose ax.format(rc_kw={'land.color':'b'}). -# WARNING: rcParamsShort has to be in here because Axes.format() only checks -# _rc_nodots to filter out the rc kwargs! -_rc_nodots = { - name.replace('.', ''): name - for names in (defaultParamsShort, defaultParamsLong, rcParams) - for name in names.keys() -} - -# Category names, used for returning dicts of subcategory properties -_rc_categories = { - *( - re.sub(r'\.[^.]*$', '', name) - for names in (defaultParamsLong, rcParams) - for name in names.keys() - ), - *( - re.sub(r'\..*$', '', name) - for names in (defaultParamsLong, rcParams) - for name in names.keys() - ) -} - - -def _get_config_paths(): - """ - Return a list of configuration file paths in reverse order of - precedence. - """ - # Local configuration - idir = os.getcwd() - paths = [] - while idir: # not empty string - ipath = os.path.join(idir, '.proplotrc') - if os.path.exists(ipath): - paths.append(ipath) - ndir = os.path.dirname(idir) - if ndir == idir: # root - break - idir = ndir - paths = paths[::-1] # sort from decreasing to increasing importantce - # Home configuration - ipath = os.path.join(os.path.expanduser('~'), '.proplotrc') - if os.path.exists(ipath) and ipath not in paths: - paths.insert(0, ipath) - return paths - - -def _get_synced_params(key, value): - """ - Return dictionaries for updating the `rcParamsShort`, `rcParamsLong`, - and `rcParams` properties associated with this key. - """ - kw = {} # builtin properties that global setting applies to - kw_long = {} # custom properties that global setting applies to - kw_short = {} # short name properties - - # Skip full name keys - key = _sanitize_key(key) - if '.' in key: - pass - - # Backend - elif key == 'inlinefmt': - inline_backend_fmt(value) - - # Cycler - elif key in ('cycle', 'rgbcycle'): - if key == 'rgbcycle': - cycle, rgbcycle = rcParamsShort['cycle'], value - else: - cycle, rgbcycle = value, rcParamsShort['rgbcycle'] - try: - colors = mcm.cmap_d[cycle].colors - except (KeyError, AttributeError): - cycles = sorted( - name for name, cmap in mcm.cmap_d.items() - if isinstance(cmap, mcolors.ListedColormap) - ) - raise ValueError( - f'Invalid cycle name {cycle!r}. Options are: ' - + ', '.join(map(repr, cycles)) + '.' - ) - if rgbcycle and cycle.lower() == 'colorblind': - regcolors = colors + [(0.1, 0.1, 0.1)] - elif mcolors.to_rgb('r') != (1.0, 0.0, 0.0): # reset - regcolors = [ - (0.0, 0.0, 1.0), - (1.0, 0.0, 0.0), - (0.0, 1.0, 0.0), - (0.75, 0.75, 0.0), - (0.75, 0.75, 0.0), - (0.0, 0.75, 0.75), - (0.0, 0.0, 0.0) - ] - else: - regcolors = [] # no reset necessary - for code, color in zip('brgmyck', regcolors): - rgb = mcolors.to_rgb(color) - mcolors.colorConverter.colors[code] = rgb - mcolors.colorConverter.cache[code] = rgb - kw['patch.facecolor'] = colors[0] - kw['axes.prop_cycle'] = cycler.cycler('color', colors) - - # Zero linewidth almost always means zero tick length - elif key == 'linewidth' and _to_points(key, value) == 0: - _, ikw_long, ikw = _get_synced_params('ticklen', 0) - kw.update(ikw) - kw_long.update(ikw_long) - - # Tick length/major-minor tick length ratio - elif key in ('ticklen', 'ticklenratio'): - if key == 'ticklen': - ticklen = _to_points(key, value) - ratio = rcParamsShort['ticklenratio'] - else: - ticklen = rcParamsShort['ticklen'] - ratio = value - kw['xtick.minor.size'] = ticklen * ratio - kw['ytick.minor.size'] = ticklen * ratio - - # Spine width/major-minor tick width ratio - elif key in ('linewidth', 'tickratio'): - if key == 'linewidth': - tickwidth = _to_points(key, value) - ratio = rcParamsShort['tickratio'] - else: - tickwidth = rcParamsShort['linewidth'] - ratio = value - kw['xtick.minor.width'] = tickwidth * ratio - kw['ytick.minor.width'] = tickwidth * ratio - - # Gridline width - elif key in ('grid.linewidth', 'gridratio'): - if key == 'grid.linewidth': - gridwidth = _to_points(key, value) - ratio = rcParamsShort['gridratio'] - else: - gridwidth = rcParams['grid.linewidth'] - ratio = value - kw_long['gridminor.linewidth'] = gridwidth * ratio - - # Gridline toggling, complicated because of the clunky way this is - # implemented in matplotlib. There should be a gridminor setting! - elif key in ('grid', 'gridminor'): - ovalue = rcParams['axes.grid'] - owhich = rcParams['axes.grid.which'] - # Instruction is to turn off gridlines - if not value: - # Gridlines are already off, or they are on for the particular - # ones that we want to turn off. Instruct to turn both off. - if not ovalue or (key == 'grid' and owhich == 'major') or ( - key == 'gridminor' and owhich == 'minor'): - which = 'both' # disable both sides - # Gridlines are currently on for major and minor ticks, so we - # instruct to turn on gridlines for the one we *don't* want off - elif owhich == 'both': # and ovalue is True, as we already tested - # if gridminor=False, enable major, and vice versa - value = True - which = 'major' if key == 'gridminor' else 'minor' - # Gridlines are on for the ones that we *didn't* instruct to turn - # off, and off for the ones we do want to turn off. This just - # re-asserts the ones that are already on. - else: - value = True - which = owhich - # Instruction is to turn on gridlines - else: - # Gridlines are already both on, or they are off only for the ones - # that we want to turn on. Turn on gridlines for both. - if owhich == 'both' or (key == 'grid' and owhich == 'minor') or ( - key == 'gridminor' and owhich == 'major'): - which = 'both' - # Gridlines are off for both, or off for the ones that we - # don't want to turn on. We can just turn on these ones. - else: - which = owhich - kw['axes.grid'] = value - kw['axes.grid.which'] = which - - # Update setting in dictionary, detect invalid keys - value = _to_points(key, value) - if key in rcParamsShort: - kw_short[key] = value - elif key in rcParamsLong: - kw_long[key] = value - elif key in rcParams: - kw[key] = value - else: - raise KeyError(f'Invalid key {key!r}.') - - # Update linked settings - for name in _rc_children.get(key, ()): - if name in rcParamsLong: - kw_long[name] = value - else: - kw[name] = value - return kw_short, kw_long, kw - - -def _sanitize_key(key): - """ - Ensure string and convert keys with omitted dots. - """ - if not isinstance(key, str): - raise KeyError(f'Invalid key {key!r}. Must be string.') - if '.' not in key and key not in rcParamsShort: # speedup - key = _rc_nodots.get(key, key) - return key.lower() - - -def _to_points(key, value): - """ - Convert certain rc keys to the units "points". - """ - # TODO: Incorporate into more sophisticated validation system - # See: https://matplotlib.org/users/customizing.html, all props matching - # the below strings use the units 'points', except custom categories! - if ( - isinstance(value, str) - and key.split('.')[0] not in ('colorbar', 'subplots') - and re.match('^.*(width|space|size|pad|len|small|large)$', key) - ): - value = units(value, 'pt') - return value - - -def _update_from_file(file): - """ - Apply updates from a file. This is largely copied from matplotlib. - - Parameters - ---------- - file : str - The path. - """ - cnt = 0 - file = os.path.expanduser(file) - added = set() - with open(file, 'r') as fd: - for line in fd: - # Read file - cnt += 1 - stripped = line.split('#', 1)[0].strip() - if not stripped: - continue - pair = stripped.split(':', 1) - if len(pair) != 2: - _warn_proplot( - f'Illegal line #{cnt} in file {file!r}:\n{line!r}"' - ) - continue - key, value = pair - key = key.strip() - value = value.strip() - if key in added: - _warn_proplot( - f'Duplicate key {key!r} on line #{cnt} in file {file!r}.' - ) - added.add(key) - - # *Very primitive* type conversion system. Just check proplot - # settings (they are all simple/scalar) and leave rcParams alone. - # TODO: Add built-in validation by making special RcParamsLong - # and RcParamsShort classes just like matplotlib RcParams - if key in rcParamsShort or key in rcParamsLong: - if not value: - value = None # older proplot versions supported this - elif value in ('True', 'False', 'None'): - value = eval(value) # rare case where eval is o.k. - else: - try: - # int-float distinction does not matter in python3 - value = float(value) - except ValueError: - pass - - # Add to dictionaries - try: - rc_short, rc_long, rc = _get_synced_params(key, value) - except KeyError: - _warn_proplot( - f'Invalid key {key!r} on line #{cnt} in file {file!r}.' - ) - else: - rcParamsShort.update(rc_short) - rcParamsLong.update(rc_long) - rcParams.update(rc) - - -def _write_defaults(filename, comment=True): - """ - Save a file to the specified path containing the default `rc` settings. - - Parameters - ---------- - filename : str - The path. - comment : bool, optional - Whether to "comment out" each setting. - """ - def _tabulate(rcdict): - string = '' - prefix = '# ' if comment else '' - maxlen = max(map(len, rcdict)) - NoneType = type(None) - for key, value in rcdict.items(): - if isinstance(value, cycler.Cycler): # special case! - value = repr(value) - elif isinstance(value, (str, Number, NoneType)): - value = str(value) - elif isinstance(value, (list, tuple)) and all( - isinstance(val, (str, Number)) for val in value - ): - value = ', '.join(str(val) for val in value) - else: - raise ValueError( - f'Failed to write rc setting {key} = {value!r}. ' - 'Must be string, number, or list or tuple thereof, ' - 'or None or a cycler.' - ) - space = ' ' * (maxlen - len(key) + 1) - string += f'{prefix}{key}:{space}{value}\n' - return string.strip() - - # Fill empty defaultParamsLong values with rcDefaultParamsShort - # They are only allowed to be None in the *default dictionaries* because - # they are immediately overwritten. However if users try to set them as - # None in a .proplotrc file, may trigger error down the line. - rc_parents = { - child: parent - for parent, children in _rc_children.items() - for child in children - } - defaultParamsLong_filled = defaultParamsLong.copy() - for key, value in defaultParamsLong.items(): - if value is None: - try: - parent = rc_parents[key] - except KeyError: - raise RuntimeError( - f'rcParamsLong param {key!r} has default value of None ' - 'but has no rcParmsShort parent!' - ) - if parent in defaultParamsShort: - value = defaultParamsShort[parent] - elif parent in defaultParams: - value = defaultParams[parent] - else: - value = rcParams[parent] - defaultParamsLong_filled[key] = value - - with open(filename, 'w') as f: - f.write(f""" -#--------------------------------------------------------------------- -# Use this file to change the default proplot and matplotlib settings -# The syntax is mostly the same as for matplotlibrc files -# For descriptions of each setting see: -# https://proplot.readthedocs.io/en/latest/configuration.html -# https://matplotlib.org/3.1.1/tutorials/introductory/customizing.html -#--------------------------------------------------------------------- -# ProPlot short name settings -{_tabulate(defaultParamsShort)} - -# ProPlot long name settings -{_tabulate(defaultParamsLong_filled)} - -# Matplotlib settings -{_tabulate(defaultParams)} -""".strip()) - - -class rc_configurator(object): - """ - Magical abstract class for managing matplotlib - `rcParams `__ - and additional ProPlot :ref:`rcParamsLong` and :ref:`rcParamsShort` - settings. This loads the default ProPlot settings and the - user ``.proplotrc`` overrides. See :ref:`Configuring proplot` for details. - """ - def __contains__(self, key): - return key in rcParamsShort or key in rcParamsLong or key in rcParams - - def __iter__(self): - for key in sorted((*rcParamsShort, *rcParamsLong, *rcParams)): - yield key - - def __repr__(self): - rcdict = type('rc', (dict,), {})(rcParamsShort) - string = type(rcParams).__repr__(rcdict) - indent = ' ' * 4 # indent is rc({ - return string.strip( - '})') + f'\n{indent}... (rcParams) ...\n{indent}}})' - - def __str__(self): # encapsulate params in temporary class - rcdict = type('rc', (dict,), {})(rcParamsShort) - string = type(rcParams).__str__(rcdict) - return string + '\n... (rcParams) ...' - - @_counter # about 0.05s - def __init__(self, local=True): - """ - Parameters - ---------- - local : bool, optional - Whether to load overrides from local and user ``.proplotrc`` - file(s). Default is ``True``. - """ - # Remove context objects - object.__setattr__(self, '_context', []) - - # Set default style - # NOTE: Previously, style.use would trigger first pyplot import because - # rcParams.__getitem__['backend'] imports pyplot.switch_backend() so it - # can determine the default backend. - style.use('default') - - # Update from defaults - rcParams.update(defaultParams) - rcParamsLong.clear() - rcParamsLong.update(defaultParamsLong) - rcParamsShort.clear() - rcParamsShort.update(defaultParamsShort) - for rcdict in (rcParamsShort, rcParamsLong): - for key, value in rcdict.items(): - _, rc_long, rc = _get_synced_params(key, value) - rcParamsLong.update(rc_long) - rcParams.update(rc) - - # Update from files - if not local: - return - for i, file in enumerate(_get_config_paths()): - if not os.path.exists(file): - continue - _update_from_file(file) - - def __enter__(self): - """ - Apply settings from the most recent context block. - """ - if not self._context: - raise RuntimeError( - f'rc object must be initialized with rc.context().' - ) - *_, kwargs, cache, restore = self._context[-1] - - def _update(rcdict, newdict): - for key, value in newdict.items(): - restore[key] = rcdict[key] - rcdict[key] = cache[key] = value - for key, value in kwargs.items(): - rc_short, rc_long, rc = _get_synced_params(key, value) - _update(rcParamsShort, rc_short) - _update(rcParamsLong, rc_long) - _update(rcParams, rc) - - def __exit__(self, *args): # noqa: U100 - """ - Restore settings from the most recent context block. - """ - if not self._context: - raise RuntimeError( - f'rc object must be initialized with rc.context().' - ) - *_, restore = self._context[-1] - for key, value in restore.items(): - rc_short, rc_long, rc = _get_synced_params(key, value) - rcParamsShort.update(rc_short) - rcParamsLong.update(rc_long) - rcParams.update(rc) - del self._context[-1] - - def __delitem__(self, item): # noqa: 100 - """ - Raise an error. This enforces pseudo-immutability. - """ - raise RuntimeError('rc settings cannot be deleted.') - - def __delattr__(self, item): # noqa: 100 - """ - Raise an error. This enforces pseudo-immutability. - """ - raise RuntimeError('rc settings cannot be deleted.') - - def __getattr__(self, attr): - """ - Pass the attribute to `~rc_configurator.__getitem__` and return - the result. - """ - if attr[:1] == '_': - return super().__getattr__(attr) - else: - return self[attr] - - def __getitem__(self, key): - """ - Return an `rcParams `__, - :ref:`rcParamsLong`, or :ref:`rcParamsShort` setting. - """ - key = _sanitize_key(key) - for kw in (rcParamsShort, rcParamsLong, rcParams): - try: - return kw[key] - except KeyError: - continue - raise KeyError(f'Invalid setting name {key!r}.') - - def __setattr__(self, attr, value): - """ - Pass the attribute and value to `~rc_configurator.__setitem__`. - """ - self[attr] = value - - def __setitem__(self, key, value): - """ - Modify an `rcParams `__, - :ref:`rcParamsLong`, and :ref:`rcParamsShort` setting(s). - """ - rc_short, rc_long, rc = _get_synced_params(key, value) - rcParamsShort.update(rc_short) - rcParamsLong.update(rc_long) - rcParams.update(rc) - - def _get_item(self, key, mode=None): - """ - As with `~rc_configurator.__getitem__` but the search is limited - based on the context mode and ``None`` is returned if the key is not - found in the dictionaries. - """ - if mode is None: - mode = min((context[0] for context in self._context), default=0) - caches = (context[2] for context in self._context) - if mode == 0: - rcdicts = (*caches, rcParamsShort, rcParamsLong, rcParams) - elif mode == 1: - rcdicts = (*caches, rcParamsShort, rcParamsLong) # custom only! - elif mode == 2: - rcdicts = (*caches,) - else: - raise KeyError(f'Invalid caching mode {mode!r}.') - for rcdict in rcdicts: - if not rcdict: - continue - try: - return rcdict[key] - except KeyError: - continue - if mode == 0: - raise KeyError(f'Invalid setting name {key!r}.') - else: - return - - def category(self, cat, *, trimcat=True, context=False): - """ - Return a dictionary of settings beginning with the substring - ``cat + '.'``. - - Parameters - ---------- - cat : str, optional - The `rc` setting category. - trimcat : bool, optional - Whether to trim ``cat`` from the key names in the output - dictionary. Default is ``True``. - context : bool, optional - If ``True``, then each category setting that is not found in the - context mode dictionaries is omitted from the output dictionary. - See `~rc_configurator.context`. - """ - if cat not in _rc_categories: - raise ValueError( - f'Invalid rc category {cat!r}. Valid categories are ' - ', '.join(map(repr, _rc_categories)) + '.' - ) - kw = {} - mode = 0 if not context else None - for rcdict in (rcParamsLong, rcParams): - for key in rcdict: - if not re.match(fr'\A{cat}\.[^.]+\Z', key): - continue - value = self._get_item(key, mode) - if value is None: - continue - if trimcat: - key = re.sub(fr'\A{cat}\.', '', key) - kw[key] = value - return kw - - def context(self, *args, mode=0, **kwargs): - """ - Temporarily modify the rc settings in a "with as" block. - - This is used by ProPlot internally but may also be useful for power - users. It was invented to prevent successive calls to - `~proplot.axes.Axes.format` from constantly looking up and - re-applying unchanged settings. Testing showed that these gratuitous - `rcParams `__ - lookups and artist updates increased runtime by seconds, even for - relatively simple plots. It also resulted in overwriting previous - rc changes with the default values upon subsequent calls to - `~proplot.axes.Axes.format`. - - Parameters - ---------- - *args - Dictionaries of `rc` names and values. - **kwargs - `rc` names and values passed as keyword arguments. If the - name has dots, simply omit them. - - Other parameters - ---------------- - mode : {0,1,2}, optional - The context mode. Dictates the behavior of `~rc_configurator.get`, - `~rc_configurator.fill`, and `~rc_configurator.category` within a - "with as" block when called with ``context=True``. The options are - as follows. - - 0. All settings (`rcParams \ -`__, - :ref:`rcParamsLong`, and :ref:`rcParamsShort`) are returned, - whether or not `~rc_configurator.context` has changed them. - 1. Unchanged `rcParams \ -`__ - return ``None``. :ref:`rcParamsLong` and :ref:`rcParamsShort` - are returned whether or not `~rc_configurator.context` has - changed them. This is used in the `~proplot.axes.Axes.__init__` - call to `~proplot.axes.Axes.format`. When a lookup returns - ``None``, `~proplot.axes.Axes.format` does not apply it. - 2. All unchanged settings return ``None``. This is used during user - calls to `~proplot.axes.Axes.format`. - - Example - ------- - The below applies settings to axes in a specific figure using - `~rc_configurator.context`. - - >>> import proplot as plot - >>> with plot.rc.context(linewidth=2, ticklen=5): - ... f, ax = plot.subplots() - ... ax.plot(data) - - By contrast, the below applies settings to a specific axes using - `~proplot.axes.Axes.format`. - - >>> import proplot as plot - >>> f, ax = plot.subplots() - >>> ax.format(linewidth=2, ticklen=5) - - """ - if mode not in range(3): - raise ValueError(f'Invalid mode {mode!r}.') - for arg in args: - if not isinstance(arg, dict): - raise ValueError('Non-dictionary argument {arg!r}.') - kwargs.update(arg) - self._context.append((mode, kwargs, {}, {})) - return self - - def dict(self): - """ - Return a raw dictionary of all settings. - """ - output = {} - for key in sorted((*rcParamsShort, *rcParamsLong, *rcParams)): - output[key] = self[key] - return output - - def get(self, key, *, context=False): - """ - Return a single setting. - - Parameters - ---------- - key : str - The setting name. - context : bool, optional - If ``True``, then ``None`` is returned if the setting is not found - in the context mode dictionaries. See `~rc_configurator.context`. - """ - mode = 0 if not context else None - return self._get_item(key, mode) - - def fill(self, props, *, context=False): - """ - Return a dictionary filled with settings whose names match the - string values in the input dictionary. - - Parameters - ---------- - props : dict-like - Dictionary whose values are `rc` setting names. - context : bool, optional - If ``True``, then each setting that is not found in the - context mode dictionaries is omitted from the output dictionary. - See `~rc_configurator.context`. - """ - kw = {} - mode = 0 if not context else None - for key, value in props.items(): - item = self._get_item(value, mode) - if item is not None: - kw[key] = item - return kw - - def items(self): - """ - Return an iterator that loops over all setting names and values. - Same as `dict.items`. - """ - for key in self: - yield key, self[key] - - def keys(self): - """ - Return an iterator that loops over all setting names. - Same as `dict.items`. - """ - for key in self: - yield key - - def update(self, *args, **kwargs): - """ - Update several settings at once with a dictionary and/or - keyword arguments. - - Parameters - ---------- - *args : str, dict, or (str, dict), optional - A dictionary containing `rc` keys and values. You can also - pass a "category" name as the first argument, in which case all - settings are prepended with ``'category.'``. For example, - ``rc.update('axes', labelsize=20, titlesize=20)`` changes the - :rcraw:`axes.labelsize` and :rcraw:`axes.titlesize` properties. - **kwargs, optional - `rc` keys and values passed as keyword arguments. If the - name has dots, simply omit them. - """ - # Parse args - kw = {} - prefix = '' - if len(args) > 2: - raise ValueError( - f'rc.update() accepts 1-2 arguments, got {len(args)}. Usage ' - 'is rc.update(kw), rc.update(category, kw), ' - 'rc.update(**kwargs), or rc.update(category, **kwargs).' - ) - elif len(args) == 2: - prefix = args[0] - kw = args[1] - elif len(args) == 1: - if isinstance(args[0], str): - prefix = args[0] - else: - kw = args[0] - # Apply settings - if prefix: - prefix = prefix + '.' - kw.update(kwargs) - for key, value in kw.items(): - self[prefix + key] = value - - def reset(self, **kwargs): - """ - Reset the configurator to its initial state. - - Parameters - ---------- - **kwargs - Passed to `rc_configurator`. - """ - self.__init__(**kwargs) - - def values(self): - """ - Return an iterator that loops over all setting values. - Same as `dict.values`. - """ - for key in self: - yield self[key] - - -def inline_backend_fmt(fmt=None): - """ - Set up the `ipython inline backend \ -`__ - format and ensure that inline figures always look the same as saved - figures. This runs the following ipython magic commands: - - .. code-block:: ipython - - %config InlineBackend.figure_formats = rc['inlinefmt'] - %config InlineBackend.rc = {} # never override the rc settings - %config InlineBackend.close_figures = True # memory issues - %config InlineBackend.print_figure_kwargs = {'bbox_inches': None} \ -# not 'tight', because proplot has its own tight layout algorithm - - When the inline backend is inactive or unavailable, this has no effect. - - Parameters - ---------- - fmt : str or list of str, optional - The inline backend file format(s). Default is :rc:`inlinefmt`. - Valid formats include ``'jpg'``, ``'png'``, ``'svg'``, ``'pdf'``, - and ``'retina'``. - """ # noqa - # Note if inline backend is unavailable this will fail silently - ipython = get_ipython() - if ipython is None: - return - fmt = fmt or rcParamsShort['inlinefmt'] - if isinstance(fmt, str): - fmt = [fmt] - elif np.iterable(fmt): - fmt = list(fmt) - else: - raise ValueError( - f'Invalid inline backend format {fmt!r}. ' - 'Must be string or list thereof.' - ) - ipython.magic('config InlineBackend.figure_formats = ' + repr(fmt)) - ipython.magic('config InlineBackend.rc = {}') # no notebook overrides - ipython.magic('config InlineBackend.close_figures = True') # memory - ipython.magic( # use ProPlot tight layout instead - 'config InlineBackend.print_figure_kwargs = {"bbox_inches": None}' - ) - - -# Write defaults -_user_rc_file = os.path.join(os.path.expanduser('~'), '.proplotrc') -if not os.path.exists(_user_rc_file): - _write_defaults(_user_rc_file) - -#: Instance of `rc_configurator`. This is used to change global settings. -#: See :ref:`Configuring proplot` for details. -rc = rc_configurator() diff --git a/proplot/scale.py b/proplot/scale.py new file mode 100644 index 000000000..7e5a0ef6e --- /dev/null +++ b/proplot/scale.py @@ -0,0 +1,945 @@ +#!/usr/bin/env python3 +""" +Various axis `~matplotlib.scale.ScaleBase` classes. +""" +import copy + +import matplotlib.scale as mscale +import matplotlib.ticker as mticker +import matplotlib.transforms as mtransforms +import numpy as np +import numpy.ma as ma + +from . import ticker as pticker +from .internals import ic # noqa: F401 +from .internals import _not_none, _version_mpl, warnings + +__all__ = [ + 'CutoffScale', + 'ExpScale', + 'FuncScale', + 'InverseScale', + 'LinearScale', + 'LogitScale', + 'LogScale', + 'MercatorLatitudeScale', + 'PowerScale', + 'SineLatitudeScale', + 'SymmetricalLogScale', +] + + +def _parse_logscale_args(*keys, **kwargs): + """ + Parse arguments for `LogScale` and `SymmetricalLogScale` that + inexplicably require ``x`` and ``y`` suffixes by default. Also + change the default `linthresh` to ``1``. + """ + # NOTE: Scale classes ignore unused arguments with warnings, but matplotlib 3.3 + # version changes the keyword args. Since we can't do a try except clause, only + # way to avoid warnings with 3.3 upgrade is to test version string. + kwsuffix = '' if _version_mpl >= '3.3' else 'x' + for key in keys: + # Remove duplicates + opts = { + key: kwargs.pop(key, None), + key + 'x': kwargs.pop(key + 'x', None), + key + 'y': kwargs.pop(key + 'y', None), + } + value = _not_none(**opts) # issues warning if multiple values passed + + # Apply defaults and adjust + # NOTE: If linthresh is *exactly* on a power of the base, can end + # up with additional log-locator step inside the threshold, e.g. major + # ticks on -10, -1, -0.1, 0.1, 1, 10 for linthresh of 1. Adding slight + # offset to *desired* linthresh prevents this. + if key == 'subs': + if value is None: + value = np.arange(1, 10) + if key == 'linthresh': + if value is None: + value = 1 + power = np.log10(value) + if power % 1 == 0: # exact power of 10 + value = value + 10 ** (power - 10) + if value is not None: # dummy axis_name is 'x' + kwargs[key + kwsuffix] = value + + return kwargs + + +class _Scale(object): + """ + Mix-in class that standardizes the behavior of + `~matplotlib.scale.ScaleBase.set_default_locators_and_formatters` + and `~matplotlib.scale.ScaleBase.get_transform`. Also overrides + `__init__` so you no longer have to instantiate scales with an + `~matplotlib.axis.Axis` instance. + """ + def __init__(self, *args, **kwargs): + # Pass a dummy axis to the superclass + axis = type('Axis', (object,), {'axis_name': 'x'})() + super().__init__(axis, *args, **kwargs) + self._default_major_locator = mticker.AutoLocator() + self._default_minor_locator = mticker.AutoMinorLocator() + self._default_major_formatter = pticker.AutoFormatter() + self._default_minor_formatter = mticker.NullFormatter() + + def set_default_locators_and_formatters(self, axis, only_if_default=False): + """ + Apply all locators and formatters defined as attributes on + initialization and define defaults for all scales. + + Parameters + ---------- + axis : `~matplotlib.axis.Axis` + The axis. + only_if_default : bool, optional + Whether to refrain from updating the locators and formatters if the + axis is currently using non-default versions. Useful if we want to + avoid overwriting user customization when the scale is changed. + """ + # TODO: Always use only_if_default=True? Used only for dual axes right now + # NOTE: We set isDefault_minloc to True when simply toggling minor ticks + # on and off with CartesianAxes format command. + from .config import rc + if not only_if_default or axis.isDefault_majloc: + locator = copy.copy(self._default_major_locator) + axis.set_major_locator(locator) + axis.isDefault_majloc = True + if not only_if_default or axis.isDefault_minloc: + x = axis.axis_name if axis.axis_name in 'xy' else 'x' + if rc[x + 'tick.minor.visible']: + locator = copy.copy(self._default_minor_locator) + else: + locator = mticker.NullLocator() + axis.set_minor_locator(locator) + axis.isDefault_minloc = True + if not only_if_default or axis.isDefault_majfmt: + formatter = copy.copy(self._default_major_formatter) + axis.set_major_formatter(formatter) + axis.isDefault_majfmt = True + if not only_if_default or axis.isDefault_minfmt: + formatter = copy.copy(self._default_minor_formatter) + axis.set_minor_formatter(formatter) + axis.isDefault_minfmt = True + + def get_transform(self): + """ + Return the scale transform. + """ + return self._transform + + +class LinearScale(_Scale, mscale.LinearScale): + """ + As with `~matplotlib.scale.LinearScale` but with + `~proplot.ticker.AutoFormatter` as the default major formatter. + """ + #: The registered scale name + name = 'linear' + + def __init__(self, **kwargs): + """ + See also + -------- + proplot.constructor.Scale + """ + super().__init__(**kwargs) + self._transform = mtransforms.IdentityTransform() + + +class LogitScale(_Scale, mscale.LogitScale): + """ + As with `~matplotlib.scale.LogitScale` but with `~proplot.ticker.AutoFormatter` + as the default major formatter. + """ + #: The registered scale name + name = 'logit' + + def __init__(self, **kwargs): + """ + Parameters + ---------- + nonpos : {'mask', 'clip'} + Values outside of (0, 1) can be masked as invalid, or clipped to a + number very close to 0 or 1. + + See also + -------- + proplot.constructor.Scale + """ + super().__init__(**kwargs) + # self._default_major_formatter = mticker.LogitFormatter() + self._default_major_locator = mticker.LogitLocator() + self._default_minor_locator = mticker.LogitLocator(minor=True) + + +class LogScale(_Scale, mscale.LogScale): + """ + As with `~matplotlib.scale.LogScale` but with `~proplot.ticker.AutoFormatter` + as the default major formatter. ``x`` and ``y`` versions of each keyword + argument are no longer required. + """ + #: The registered scale name + name = 'log' + + def __init__(self, **kwargs): + """ + Parameters + ---------- + base : float, default: 10 + The base of the logarithm. + nonpos : {'mask', 'clip'}, optional + Non-positive values in *x* or *y* can be masked as + invalid, or clipped to a very small positive number. + subs : sequence of int, default: ``[1 2 3 4 5 6 7 8 9]`` + Default *minor* tick locations are on these multiples of each power + of the base. For example, ``subs=(1, 2, 5)`` draws ticks on 1, 2, + 5, 10, 20, 50, 100, 200, 500, etc. + basex, basey, nonposx, nonposy, subsx, subsy + Aliases for the above keywords. These used to be conditional + on the *name* of the axis. + + See also + -------- + proplot.constructor.Scale + """ + keys = ('base', 'nonpos', 'subs') + super().__init__(**_parse_logscale_args(*keys, **kwargs)) + self._default_major_locator = mticker.LogLocator(self.base) + self._default_minor_locator = mticker.LogLocator(self.base, self.subs) + + +class SymmetricalLogScale(_Scale, mscale.SymmetricalLogScale): + """ + As with `~matplotlib.scale.SymmetricalLogScale` but with + `~proplot.ticker.AutoFormatter` as the default major formatter. + ``x`` and ``y`` versions of each keyword argument are no longer + required. + """ + #: The registered scale name + name = 'symlog' + + def __init__(self, **kwargs): + """ + Parameters + ---------- + base : float, default: 10 + The base of the logarithm. + linthresh : float, default: 1 + Defines the range ``(-linthresh, linthresh)``, within which the plot + is linear. This avoids having the plot go to infinity around zero. + linscale : float, default: 1 + This allows the linear range ``(-linthresh, linthresh)`` to be + stretched relative to the logarithmic range. Its value is the + number of decades to use for each half of the linear range. For + example, when `linscale` is ``1`` (the default), the space used + for the positive and negative halves of the linear range will be + equal to one decade in the logarithmic range. + subs : sequence of int, default: ``[1 2 3 4 5 6 7 8 9]`` + Default *minor* tick locations are on these multiples of each power + of the base. For example, ``subs=(1, 2, 5)`` draws ticks on 1, 2, + 5, 10, 20, 50, 100, 200, 500, etc. + basex, basey, linthreshx, linthreshy, linscalex, linscaley, subsx, subsy + Aliases for the above keywords. These keywords used to be + conditional on the name of the axis. + + See also + -------- + proplot.constructor.Scale + """ + keys = ('base', 'linthresh', 'linscale', 'subs') + super().__init__(**_parse_logscale_args(*keys, **kwargs)) + transform = self.get_transform() + self._default_major_locator = mticker.SymmetricalLogLocator(transform) + self._default_minor_locator = mticker.SymmetricalLogLocator(transform, self.subs) # noqa: E501 + + +class FuncScale(_Scale, mscale.ScaleBase): + """ + Axis scale composed of arbitrary forward and inverse transformations. + """ + #: The registered scale name + name = 'function' + + def __init__(self, transform=None, invert=False, parent_scale=None, **kwargs): + """ + Parameters + ---------- + transform : callable, 2-tuple of callable, or scale-spec + The transform used to translate units from the parent axis to + the secondary axis. Input can be as follows: + + * A single `linear `__ or + `involutory `__ + function that accepts a number and returns some transformation of + that number. For example, to convert Kelvin to Celsius, use + ``ax.dualx(lambda x: x - 273.15)``. To convert kilometers to + meters, use ``ax.dualx(lambda x: x * 1e3)``. + * A 2-tuple of arbitrary functions. This should only be used if your + functions are non-linear and non-involutory. The second function must + be the inverse of the first. For example, to apply the square, use + ``ax.dualx((lambda x: x ** 2, lambda x: x ** 0.5))``. + * A scale specification passed to the `~proplot.constructor.Scale` + constructor function. The transform and default locators and formatters + are borrowed from the resulting `~matplotlib.scale.ScaleBase` instance. + For example, to apply the inverse, use ``ax.dualx('inverse')``. + To apply the base-10 exponential, use ``ax.dualx(('exp', 10))``. + + invert : bool, optional + If ``True``, the forward and inverse functions are *swapped*. + Used when drawing dual axes. + parent_scale : `~matplotlib.scale.ScaleBase`, default: `LinearScale` + The axis scale of the "parent" axis. Its forward transform + is applied to the `FuncTransform`. + major_locator, minor_locator : locator-spec, optional + The default major and minor locator. Passed to the + `~proplot.constructor.Locator` constructor function. By default, these are + the same as the default locators on the input transform. If the input + transform was not an axis scale, these are borrowed from `parent_scale`. + major_formatter, minor_formatter : formatter-spec, optional + The default major and minor formatter. Passed to the + `~proplot.constructor.Formatter` constructor function. By default, these are + the same as the default formatters on the input transform. If the input + transform was not an axis scale, these are borrowed from `parent_scale`. + + See also + -------- + proplot.constructor.Scale + proplot.axes.CartesianAxes.dualx + proplot.axes.CartesianAxes.dualy + """ + # Parse input args + # NOTE: Permit *arbitrary* parent axis scales and infer default locators and + # formatters from the input scale (if it was passed) or the parent scale. Use + # case for latter is e.g. logarithmic scale with linear transformation. + if 'functions' in kwargs: # matplotlib compatibility (critical for >= 3.5) + functions = kwargs.pop('functions', None) + if transform is None: + transform = functions + else: + warnings._warn_proplot("Ignoring keyword argument 'functions'.") + from .constructor import Formatter, Locator, Scale + super().__init__() + if callable(transform): + forward, inverse, inherit_scale = transform, transform, None + elif np.iterable(transform) and len(transform) == 2 and all(map(callable, transform)): # noqa: E501 + forward, inverse, inherit_scale = *transform, None + else: + try: + inherit_scale = Scale(transform) + except ValueError: + raise ValueError( + 'Expected a function, 2-tuple of forward and inverse functions, ' + f'or an axis scale specification. Got {transform!r}.' + ) + transform = inherit_scale.get_transform() + forward, inverse = transform.transform, transform.inverted().transform + + # Create the transform + # NOTE: Linear scale is always identity transform (no-op). + # NOTE: Must transform parent scale cutoff arguments as well. Use inverse + # function because we are converting from some *other* axis to this one. + if invert: # used for dualx and dualy + forward, inverse = inverse, forward + parent_scale = _not_none(parent_scale, LinearScale()) + if not isinstance(parent_scale, mscale.ScaleBase): + raise ValueError(f'Parent scale must be ScaleBase. Got {parent_scale!r}.') + if isinstance(parent_scale, CutoffScale): + args = list(parent_scale.args) # mutable copy + args[::2] = (inverse(arg) for arg in args[::2]) # transform cutoffs + parent_scale = CutoffScale(*args) + if isinstance(parent_scale, mscale.SymmetricalLogScale): + keys = ('base', 'linthresh', 'linscale', 'subs') + kwsym = {key: getattr(parent_scale, key) for key in keys} + kwsym['linthresh'] = inverse(kwsym['linthresh']) + parent_scale = SymmetricalLogScale(**kwsym) + self.functions = (forward, inverse) + self._transform = parent_scale.get_transform() + FuncTransform(forward, inverse) + + # Apply default locators and formatters + # NOTE: We pass these through contructor functions + scale = inherit_scale or parent_scale + for which in ('major', 'minor'): + for type_, parser in (('locator', Locator), ('formatter', Formatter)): + key = which + '_' + type_ + attr = '_default_' + key + ticker = kwargs.pop(key, None) + if ticker is None: + ticker = getattr(scale, attr, None) + if ticker is None: # e.g. someone used a matplotlib scale + continue # revert to defaults + ticker = parser(ticker) + setattr(self, attr, copy.copy(ticker)) + if kwargs: + raise TypeError(f'FuncScale got unexpected arguments: {kwargs}') + + +class FuncTransform(mtransforms.Transform): + input_dims = 1 + output_dims = 1 + is_separable = True + has_inverse = True + + def __init__(self, forward, inverse): + super().__init__() + if callable(forward) and callable(inverse): + self._forward = forward + self._inverse = inverse + else: + raise ValueError('arguments to FuncTransform must be functions') + + def inverted(self): + return FuncTransform(self._inverse, self._forward) + + def transform_non_affine(self, values): + with np.errstate(divide='ignore', invalid='ignore'): + return self._forward(values) + + +class PowerScale(_Scale, mscale.ScaleBase): + r""" + "Power scale" that performs the transformation + + .. math:: + + x^{c} + + """ + #: The registered scale name + name = 'power' + + def __init__(self, power=1, inverse=False): + """ + Parameters + ---------- + power : float, optional + The power :math:`c` to which :math:`x` is raised. + inverse : bool, optional + If ``True`` this performs the inverse operation :math:`x^{1/c}`. + """ + super().__init__() + if not inverse: + self._transform = PowerTransform(power) + else: + self._transform = InvertedPowerTransform(power) + + def limit_range_for_scale(self, vmin, vmax, minpos): + """ + Return the range *vmin* and *vmax* limited to positive numbers. + """ + if not np.isfinite(minpos): + minpos = 1e-300 + return ( + minpos if vmin <= 0 else vmin, + minpos if vmax <= 0 else vmax, + ) + + +class PowerTransform(mtransforms.Transform): + input_dims = 1 + output_dims = 1 + has_inverse = True + is_separable = True + + def __init__(self, power): + super().__init__() + self._power = power + + def inverted(self): + return InvertedPowerTransform(self._power) + + def transform_non_affine(self, a): + with np.errstate(divide='ignore', invalid='ignore'): + return np.power(a, self._power) + + +class InvertedPowerTransform(mtransforms.Transform): + input_dims = 1 + output_dims = 1 + has_inverse = True + is_separable = True + + def __init__(self, power): + super().__init__() + self._power = power + + def inverted(self): + return PowerTransform(self._power) + + def transform_non_affine(self, a): + with np.errstate(divide='ignore', invalid='ignore'): + return np.power(a, 1 / self._power) + + +class ExpScale(_Scale, mscale.ScaleBase): + r""" + "Exponential scale" that performs either of two transformations. When + `inverse` is ``False`` (the default), performs the transformation + + .. math:: + + Ca^{bx} + + where the constants :math:`a`, :math:`b`, and :math:`C` are set by the + input (see below). When `inverse` is ``True``, this performs the inverse + transformation + + .. math:: + + (\log_a(x) - \log_a(C))/b + + which in appearance is equivalent to `LogScale` since it is just a linear + transformation of the logarithm. + """ + #: The registered scale name + name = 'exp' + + def __init__(self, a=np.e, b=1, c=1, inverse=False): + """ + Parameters + ---------- + a : float, optional + The base of the exponential, i.e. the :math:`a` in :math:`Ca^{bx}`. + b : float, optional + The scale for the exponent, i.e. the :math:`b` in :math:`Ca^{bx}`. + c : float, optional + The coefficient of the exponential, i.e. the :math:`C` in :math:`Ca^{bx}`. + inverse : bool, optional + If ``True``, the "forward" direction performs the inverse operation. + + See also + -------- + proplot.constructor.Scale + """ + super().__init__() + if not inverse: + self._transform = ExpTransform(a, b, c) + else: + self._transform = InvertedExpTransform(a, b, c) + + def limit_range_for_scale(self, vmin, vmax, minpos): + """ + Return the range *vmin* and *vmax* limited to positive numbers. + """ + if not np.isfinite(minpos): + minpos = 1e-300 + return ( + minpos if vmin <= 0 else vmin, + minpos if vmax <= 0 else vmax, + ) + + +class ExpTransform(mtransforms.Transform): + input_dims = 1 + output_dims = 1 + has_inverse = True + is_separable = True + + def __init__(self, a, b, c): + super().__init__() + self._a = a + self._b = b + self._c = c + + def inverted(self): + return InvertedExpTransform(self._a, self._b, self._c) + + def transform_non_affine(self, a): + with np.errstate(divide='ignore', invalid='ignore'): + return self._c * np.power(self._a, self._b * np.array(a)) + + +class InvertedExpTransform(mtransforms.Transform): + input_dims = 1 + output_dims = 1 + has_inverse = True + is_separable = True + + def __init__(self, a, b, c): + super().__init__() + self._a = a + self._b = b + self._c = c + + def inverted(self): + return ExpTransform(self._a, self._b, self._c) + + def transform_non_affine(self, a): + with np.errstate(divide='ignore', invalid='ignore'): + return np.log(a / self._c) / (self._b * np.log(self._a)) + + +class MercatorLatitudeScale(_Scale, mscale.ScaleBase): + """ + Axis scale that is linear in the `Mercator projection latitude \ +`__. Adapted from `this example \ +`__. + The scale function is as follows: + + .. math:: + + y = \\ln(\\tan(\\pi x \\,/\\, 180) + \\sec(\\pi x \\,/\\, 180)) + + The inverse scale function is as follows: + + .. math:: + + x = 180\\,\\arctan(\\sinh(y)) \\,/\\, \\pi + + """ + #: The registered scale name + name = 'mercator' + + def __init__(self, thresh=85.0): + """ + Parameters + ---------- + thresh : float, optional + Threshold between 0 and 90, used to constrain axis limits + between ``-thresh`` and ``+thresh``. + + See also + -------- + proplot.constructor.Scale + """ + super().__init__() + if thresh >= 90: + raise ValueError("Mercator scale 'thresh' must be <= 90.") + self._thresh = thresh + self._transform = MercatorLatitudeTransform(thresh) + self._default_major_formatter = pticker.AutoFormatter(suffix='\N{DEGREE SIGN}') + + def limit_range_for_scale(self, vmin, vmax, minpos): # noqa: U100 + """ + Return the range *vmin* and *vmax* limited to within +/-90 degrees + (exclusive). + """ + return max(vmin, -self._thresh), min(vmax, self._thresh) + + +class MercatorLatitudeTransform(mtransforms.Transform): + input_dims = 1 + output_dims = 1 + is_separable = True + has_inverse = True + + def __init__(self, thresh): + super().__init__() + self._thresh = thresh + + def inverted(self): + return InvertedMercatorLatitudeTransform(self._thresh) + + def transform_non_affine(self, a): + # NOTE: Critical to truncate valid range inside transform *and* + # in limit_range_for_scale or get weird duplicate tick labels. This + # is not necessary for positive-only scales because it is harder to + # run up right against the scale boundaries. + with np.errstate(divide='ignore', invalid='ignore'): + m = ma.masked_where((a <= -90) | (a >= 90), a) + if m.mask.any(): + m = np.deg2rad(m) + return ma.log(ma.abs(ma.tan(m) + 1 / ma.cos(m))) + else: + a = np.deg2rad(a) + return np.log(np.abs(np.tan(a) + 1 / np.cos(a))) + + +class InvertedMercatorLatitudeTransform(mtransforms.Transform): + input_dims = 1 + output_dims = 1 + is_separable = True + has_inverse = True + + def __init__(self, thresh): + super().__init__() + self._thresh = thresh + + def inverted(self): + return MercatorLatitudeTransform(self._thresh) + + def transform_non_affine(self, a): + with np.errstate(divide='ignore', invalid='ignore'): + return np.rad2deg(np.arctan2(1, np.sinh(a))) + + +class SineLatitudeScale(_Scale, mscale.ScaleBase): + r""" + Axis scale that is linear in the sine transformation of *x*. The axis + limits are constrained to fall between ``-90`` and ``+90`` degrees. + The scale function is as follows: + + .. math:: + + y = \sin(\pi x/180) + + The inverse scale function is as follows: + + .. math:: + + x = 180\arcsin(y)/\pi + """ + #: The registered scale name + name = 'sine' + + def __init__(self): + """ + See also + -------- + proplot.constructor.Scale + """ + super().__init__() + self._transform = SineLatitudeTransform() + self._default_major_formatter = pticker.AutoFormatter(suffix='\N{DEGREE SIGN}') + + def limit_range_for_scale(self, vmin, vmax, minpos): # noqa: U100 + """ + Return the range *vmin* and *vmax* limited to within +/-90 degrees + (inclusive). + """ + return max(vmin, -90), min(vmax, 90) + + +class SineLatitudeTransform(mtransforms.Transform): + input_dims = 1 + output_dims = 1 + is_separable = True + has_inverse = True + + def __init__(self): + super().__init__() + + def inverted(self): + return InvertedSineLatitudeTransform() + + def transform_non_affine(self, a): + # NOTE: Critical to truncate valid range inside transform *and* + # in limit_range_for_scale or get weird duplicate tick labels. This + # is not necessary for positive-only scales because it is harder to + # run up right against the scale boundaries. + with np.errstate(divide='ignore', invalid='ignore'): + m = ma.masked_where((a < -90) | (a > 90), a) + if m.mask.any(): + return ma.sin(np.deg2rad(m)) + else: + return np.sin(np.deg2rad(a)) + + +class InvertedSineLatitudeTransform(mtransforms.Transform): + input_dims = 1 + output_dims = 1 + is_separable = True + has_inverse = True + + def __init__(self): + super().__init__() + + def inverted(self): + return SineLatitudeTransform() + + def transform_non_affine(self, a): + with np.errstate(divide='ignore', invalid='ignore'): + return np.rad2deg(np.arcsin(a)) + + +class CutoffScale(_Scale, mscale.ScaleBase): + """ + Axis scale composed of arbitrary piecewise linear transformations. + The axis can undergo discrete jumps, "accelerations", or "decelerations" + between successive thresholds. + """ + #: The registered scale name + name = 'cutoff' + + def __init__(self, *args): + """ + Parameters + ---------- + *args : thresh_1, scale_1, ..., thresh_N, [scale_N], optional + Sequence of "thresholds" and "scales". If the final scale is + omitted (i.e. you passed an odd number of arguments) it is set + to ``1``. Each ``scale_i`` in the sequence can be interpreted + as follows: + + * If ``scale_i < 1``, the axis is decelerated from ``thresh_i`` to + ``thresh_i+1``. For ``scale_N``, the axis is decelerated + everywhere above ``thresh_N``. + * If ``scale_i > 1``, the axis is accelerated from ``thresh_i`` to + ``thresh_i+1``. For ``scale_N``, the axis is accelerated + everywhere above ``thresh_N``. + * If ``scale_i == numpy.inf``, the axis *discretely jumps* from + ``thresh_i`` to ``thresh_i+1``. The final scale ``scale_N`` + *cannot* be ``numpy.inf``. + + See also + -------- + proplot.constructor.Scale + + Example + ------- + >>> import proplot as pplt + >>> import numpy as np + >>> scale = pplt.CutoffScale(10, 0.5) # move slower above 10 + >>> scale = pplt.CutoffScale(10, 2, 20) # move faster between 10 and 20 + >>> scale = pplt.CutoffScale(10, np.inf, 20) # jump from 10 to 20 + """ + # NOTE: See https://stackoverflow.com/a/5669301/4970632 + super().__init__() + args = list(args) + if len(args) % 2 == 1: + args.append(1) + self.args = args + self.threshs = args[::2] + self.scales = args[1::2] + self._transform = CutoffTransform(self.threshs, self.scales) + + +class CutoffTransform(mtransforms.Transform): + input_dims = 1 + output_dims = 1 + has_inverse = True + is_separable = True + + def __init__(self, threshs, scales, zero_dists=None): + # The zero_dists array is used to fill in distances where scales and + # threshold steps are zero. Used for inverting discrete transorms. + super().__init__() + dists = np.diff(threshs) + scales = np.asarray(scales) + threshs = np.asarray(threshs) + if len(scales) != len(threshs): + raise ValueError(f'Got {len(threshs)} but {len(scales)} scales.') + if any(scales < 0): + raise ValueError('Scales must be non negative.') + if scales[-1] in (0, np.inf): + raise ValueError('Final scale must be finite.') + if any(dists < 0): + raise ValueError('Thresholds must be monotonically increasing.') + if any((dists == 0) | (scales == 0)): + if zero_dists is None: + raise ValueError('Keyword zero_dists is required for discrete steps.') + if any((dists == 0) != (scales == 0)): + raise ValueError('Input scales disagree with discrete step locations.') + self._scales = scales + self._threshs = threshs + with np.errstate(divide='ignore', invalid='ignore'): + dists = np.concatenate((threshs[:1], dists / scales[:-1])) + if zero_dists is not None: + dists[scales[:-1] == 0] = zero_dists + self._dists = dists + + def inverted(self): + # Use same algorithm for inversion! + threshs = np.cumsum(self._dists) # thresholds in transformed space + with np.errstate(divide='ignore', invalid='ignore'): + scales = 1.0 / self._scales # new scales are inverse + zero_dists = np.diff(self._threshs)[scales[:-1] == 0] + return CutoffTransform(threshs, scales, zero_dists=zero_dists) + + def transform_non_affine(self, a): + # Cannot do list comprehension because this method sometimes + # received non-1D arrays + dists = self._dists + scales = self._scales + threshs = self._threshs + aa = np.array(a) # copy + with np.errstate(divide='ignore', invalid='ignore'): + for i, ai in np.ndenumerate(a): + j = np.searchsorted(threshs, ai) + if j > 0: + aa[i] = dists[:j].sum() + (ai - threshs[j - 1]) / scales[j - 1] + return aa + + +class InverseScale(_Scale, mscale.ScaleBase): + r""" + Axis scale that is linear in the *inverse* of *x*. The forward and inverse + scale functions are as follows: + + .. math:: + + y = x^{-1} + + """ + #: The registered scale name + name = 'inverse' + + def __init__(self): + """ + See also + -------- + proplot.constructor.Scale + """ + super().__init__() + self._transform = InverseTransform() + self._default_major_locator = mticker.LogLocator(10) + self._default_minor_locator = mticker.LogLocator(10, np.arange(1, 10)) + + def limit_range_for_scale(self, vmin, vmax, minpos): + """ + Return the range *vmin* and *vmax* limited to positive numbers. + """ + # Unlike log-scale, we can't just warp the space between + # the axis limits -- have to actually change axis limits. Also this + # scale will invert and swap the limits you provide. + if not np.isfinite(minpos): + minpos = 1e-300 + return ( + minpos if vmin <= 0 else vmin, + minpos if vmax <= 0 else vmax, + ) + + +class InverseTransform(mtransforms.Transform): + # Create transform object + input_dims = 1 + output_dims = 1 + is_separable = True + has_inverse = True + + def __init__(self): + super().__init__() + + def inverted(self): + return InverseTransform() + + def transform_non_affine(self, a): + with np.errstate(divide='ignore', invalid='ignore'): + return 1.0 / a + + +def _scale_factory(scale, axis, *args, **kwargs): # noqa: U100 + """ + Generate an axis scale. + + Parameters + ---------- + scale : str or `~matplotlib.scale.ScaleBase` + The axis scale name or scale instance. + axis : `~matplotlib.axis.Axis` + The axis instance. + *args, **kwargs + Passed to `~matplotlib.scale.ScaleBase` if `scale` is a string. + """ + mapping = mscale._scale_mapping + if isinstance(scale, mscale.ScaleBase): + if args or kwargs: + warnings._warn_proplot(f'Ignoring args {args} and keyword args {kwargs}.') + return scale # do nothing + else: + scale = scale.lower() + if scale not in mapping: + raise ValueError( + f'Unknown axis scale {scale!r}. Options are ' + + ', '.join(map(repr, mapping)) + '.' + ) + return mapping[scale](*args, **kwargs) + + +# Monkey patch matplotlib scale factory with version that accepts ScaleBase instances. +# This lets set_xscale and set_yscale accept axis scales returned by Scale constructor +# and makes things constistent with the other constructor functions. +if mscale.scale_factory is not _scale_factory: + mscale.scale_factory = _scale_factory diff --git a/proplot/styletools.py b/proplot/styletools.py deleted file mode 100644 index 391e590b5..000000000 --- a/proplot/styletools.py +++ /dev/null @@ -1,3949 +0,0 @@ -#!/usr/bin/env python3 -""" -Tools for registering and visualizing colormaps, color cycles, color string -names, and fonts. New colormap classes, new colormap normalizer -classes, and new constructor functions for generating instances of these -classes. Related utilities for manipulating colors. See -:ref:`Colormaps`, :ref:`Color cycles`, and :ref:`Colors and fonts` -for details. -""" -# Potential bottleneck, loading all this stuff? *No*. Try using @timer on -# register functions, turns out worst is colormap one at 0.1 seconds. -import os -import re -import json -import glob -import cycler -from xml.etree import ElementTree -from numbers import Number, Integral -from matplotlib import rcParams -import numpy as np -import numpy.ma as ma -import matplotlib.colors as mcolors -import matplotlib.cm as mcm -from .utils import _warn_proplot, _notNone, _timer -from .external import hsluv -try: # use this for debugging instead of print()! - from icecream import ic -except ImportError: # graceful fallback if IceCream isn't installed - ic = lambda *a: None if not a else (a[0] if len(a) == 1 else a) # noqa - -__all__ = [ - 'BinNorm', 'CmapDict', 'ColorDict', - 'LinearSegmentedNorm', - 'LinearSegmentedColormap', - 'ListedColormap', - 'MidpointNorm', 'PerceptuallyUniformColormap', - 'cmaps', 'colors', 'cycles', 'fonts', - 'make_mapping_array', - 'register_cmaps', 'register_colors', 'register_cycles', 'register_fonts', - 'saturate', 'shade', 'show_cmaps', 'show_channels', - 'show_colors', 'show_colorspaces', 'show_cycles', 'show_fonts', - 'to_rgb', 'to_xyz', - 'Colormap', 'Colors', 'Cycle', 'Norm', -] - -# Colormap stuff -CYCLES_TABLE = { - 'Matplotlib defaults': ( - 'default', 'classic', - ), - 'Matplotlib stylesheets': ( - 'colorblind', 'colorblind10', 'ggplot', 'bmh', 'solarized', '538', - ), - 'ColorBrewer2.0 qualitative': ( - 'Accent', 'Dark2', - 'Paired', 'Pastel1', 'Pastel2', - 'Set1', 'Set2', 'Set3', - ), - 'Other qualitative': ( - 'FlatUI', 'Qual1', 'Qual2', - ), -} -CMAPS_TABLE = { - # Assorted origin, but these belong together - 'Grayscale': ( - 'Grays', 'Mono', 'GrayC', 'GrayCycle', - ), - # Builtin - 'Matplotlib sequential': ( - 'viridis', 'plasma', 'inferno', 'magma', 'cividis', - ), - 'Matplotlib cyclic': ( - 'twilight', - ), - # Seaborn - 'Seaborn sequential': ( - 'Rocket', 'Mako', - ), - 'Seaborn diverging': ( - 'IceFire', 'Vlag', - ), - # PerceptuallyUniformColormap - 'ProPlot sequential': ( - 'Fire', - 'Stellar', - 'Boreal', - 'Marine', - 'Dusk', - 'Glacial', - 'Sunrise', - 'Sunset', - ), - 'ProPlot diverging': ( - 'Div', 'NegPos', 'DryWet', - ), - # Nice diverging maps - 'Other diverging': ( - 'ColdHot', 'CoolWarm', 'BR', - ), - # cmOcean - 'cmOcean sequential': ( - 'Oxy', 'Thermal', 'Dense', 'Ice', 'Haline', - 'Deep', 'Algae', 'Tempo', 'Speed', 'Turbid', 'Solar', 'Matter', - 'Amp', - ), - 'cmOcean diverging': ( - 'Balance', 'Delta', 'Curl', - ), - 'cmOcean cyclic': ( - 'Phase', - ), - # Fabio Crameri - 'Scientific colour maps sequential': ( - 'batlow', 'oleron', - 'devon', 'davos', 'oslo', 'lapaz', 'acton', - 'lajolla', 'bilbao', 'tokyo', 'turku', 'bamako', 'nuuk', - 'hawaii', 'buda', 'imola', - ), - 'Scientific colour maps diverging': ( - 'roma', 'broc', 'cork', 'vik', 'berlin', 'lisbon', 'tofino', - ), - 'Scientific colour maps cyclic': ( - 'romaO', 'brocO', 'corkO', 'vikO', - ), - # ColorBrewer - 'ColorBrewer2.0 sequential': ( - 'Purples', 'Blues', 'Greens', 'Oranges', 'Reds', - 'YlOrBr', 'YlOrRd', 'OrRd', 'PuRd', 'RdPu', 'BuPu', - 'PuBu', 'PuBuGn', 'BuGn', 'GnBu', 'YlGnBu', 'YlGn' - ), - 'ColorBrewer2.0 diverging': ( - 'Spectral', 'PiYG', 'PRGn', 'BrBG', 'PuOr', 'RdGY', - 'RdBu', 'RdYlBu', 'RdYlGn', - ), - # SciVisColor - 'SciVisColor blues': ( - 'Blue0', 'Blue1', 'Blue2', 'Blue3', 'Blue4', 'Blue5', - 'Blue6', 'Blue7', 'Blue8', 'Blue9', 'Blue10', 'Blue11', - ), - 'SciVisColor greens': ( - 'Green1', 'Green2', 'Green3', 'Green4', 'Green5', - 'Green6', 'Green7', 'Green8', - ), - 'SciVisColor oranges': ( - 'Orange1', 'Orange2', 'Orange3', 'Orange4', 'Orange5', - 'Orange6', 'Orange7', 'Orange8', - ), - 'SciVisColor browns': ( - 'Brown1', 'Brown2', 'Brown3', 'Brown4', 'Brown5', - 'Brown6', 'Brown7', 'Brown8', 'Brown9', - ), - 'SciVisColor reds and purples': ( - 'RedPurple1', 'RedPurple2', 'RedPurple3', 'RedPurple4', - 'RedPurple5', 'RedPurple6', 'RedPurple7', 'RedPurple8', - ), - # Builtin maps that will be deleted; categories are taken from comments in - # matplotlib source code. Some of these are really bad, some are segmented - # maps when the should be color cycles, and some are just uninspiring. - 'MATLAB': ( - 'bone', 'cool', 'copper', 'autumn', 'flag', 'prism', - 'jet', 'hsv', 'hot', 'spring', 'summer', 'winter', 'pink', 'gray', - ), - 'GNUplot': ( - 'gnuplot', 'gnuplot2', 'ocean', 'afmhot', 'rainbow', - ), - 'GIST': ( - 'gist_earth', 'gist_gray', 'gist_heat', 'gist_ncar', - 'gist_rainbow', 'gist_stern', 'gist_yarg', - ), - 'Other': ( - 'binary', 'bwr', 'brg', # appear to be custom matplotlib - 'cubehelix', 'Wistia', 'CMRmap', # individually released - 'seismic', 'terrain', 'nipy_spectral', # origin ambiguous - 'tab10', 'tab20', 'tab20b', 'tab20c', # merged colormap cycles - ) -} -CMAPS_DIVERGING = tuple( - (key1.lower(), key2.lower()) for key1, key2 in ( - ('PiYG', 'GYPi'), - ('PRGn', 'GnRP'), - ('BrBG', 'GBBr'), - ('PuOr', 'OrPu'), - ('RdGy', 'GyRd'), - ('RdBu', 'BuRd'), - ('RdYlBu', 'BuYlRd'), - ('RdYlGn', 'GnYlRd'), - ('BR', 'RB'), - ('CoolWarm', 'WarmCool'), - ('ColdHot', 'HotCold'), - ('NegPos', 'PosNeg'), - ('DryWet', 'WetDry') - )) - -# Named color filter props -COLORS_SPACE = 'hcl' # color "distincness" is defined with this space -COLORS_THRESH = 0.10 # bigger number equals fewer colors -COLORS_TRANSLATIONS = tuple((re.compile(regex), sub) for regex, sub in ( - ('/', ' '), - ('\'s', ''), - (r'\s?majesty', ''), # purple mountains majesty is too long - ('reddish', 'red'), # remove 'ish' - ('purplish', 'purple'), - ('bluish', 'blue'), - (r'ish\b', ''), - ('grey', 'gray'), - ('pinky', 'pink'), - ('greeny', 'green'), - ('bluey', 'blue'), - ('purply', 'purple'), - ('purpley', 'purple'), - ('yellowy', 'yellow'), - ('robin egg', 'robins egg'), - ('egg blue', 'egg'), - ('bluegray', 'blue gray'), - ('grayblue', 'gray blue'), - ('lightblue', 'light blue'), -)) # prevent registering similar-sounding names -COLORS_IGNORE = re.compile('(' + '|'.join(( - 'shit', 'poop', 'poo', 'pee', 'piss', 'puke', 'vomit', 'snot', - 'booger', 'bile', 'diarrhea', -)) + ')') # filter these out, let's try to be professional here... -COLORS_INCLUDE = ( - 'charcoal', 'sky blue', 'eggshell', 'sea blue', 'coral', 'aqua', - 'tomato red', 'brick red', 'crimson', - 'red orange', 'yellow orange', 'yellow green', 'blue green', - 'blue violet', 'red violet', -) # common names that should always be included -COLORS_OPEN = ( - 'red', 'pink', 'grape', 'violet', - 'indigo', 'blue', 'cyan', 'teal', - 'green', 'lime', 'yellow', 'orange', 'gray' -) -COLORS_BASE = { - 'blue': (0, 0, 1), - 'green': (0, 0.5, 0), - 'red': (1, 0, 0), - 'cyan': (0, 0.75, 0.75), - 'magenta': (0.75, 0, 0.75), - 'yellow': (0.75, 0.75, 0), - 'black': (0, 0, 0), - 'white': (1, 1, 1), -} - - -def _get_channel(color, channel, space='hcl'): - """ - Get the hue, saturation, or luminance channel value from the input color. - The color name `color` can optionally be a string with the format - ``'color+x'`` or ``'color-x'``, where `x` specifies the offset from the - channel value. - - Parameters - ---------- - color : color-spec - The color. Sanitized with `to_rgb`. - channel : {'hue', 'chroma', 'saturation', 'luminance'} - The HCL channel to be retrieved. - space : {'hcl', 'hpl', 'hsl', 'hsv', 'rgb'}, optional - The colorspace for the corresponding channel value. - - Returns - ------- - value : float - The channel value. - """ - # Interpret channel - if callable(color) or isinstance(color, Number): - return color - if channel == 'hue': - channel = 0 - elif channel in ('chroma', 'saturation'): - channel = 1 - elif channel == 'luminance': - channel = 2 - else: - raise ValueError(f'Unknown channel {channel!r}.') - # Interpret string or RGB tuple - offset = 0 - if isinstance(color, str): - match = re.search('([-+][0-9.]+)$', color) - if match: - offset = float(match.group(0)) - color = color[:match.start()] - return offset + to_xyz(color, space)[channel] - - -def shade(color, scale=1): - """ - Scale the luminance channel of the input color. - - Parameters - ---------- - color : color-spec - The color. Sanitized with `to_rgb`. - scale : float, optoinal - The luminance channel is multiplied by this value. - - Returns - ------- - color - The new RGB tuple. - """ - *color, alpha = to_rgb(color, alpha=True) - color = [*hsluv.rgb_to_hsl(*color)] - # multiply luminance by this value - color[2] = max(0, min(color[2] * scale, 100)) - color = [*hsluv.hsl_to_rgb(*color)] - return (*color, alpha) - - -def saturate(color, scale=0.5): - """ - Scale the saturation channel of the input color. - - Parameters - ---------- - color : color-spec - The color. Sanitized with `to_rgb`. - scale : float, optoinal - The HCL saturation channel is multiplied by this value. - - Returns - ------- - color - The new RGB tuple. - """ - *color, alpha = to_rgb(color, alpha=True) - color = [*hsluv.rgb_to_hsl(*color)] - # multiply luminance by this value - color[1] = max(0, min(color[1] * scale, 100)) - color = [*hsluv.hsl_to_rgb(*color)] - return (*color, alpha) - - -def to_rgb(color, space='rgb', cycle=None, alpha=False): - """ - Translate the color in *any* format and from *any* colorspace to an RGB - tuple. This is a generalization of `matplotlib.colors.to_rgb` and the - inverse of `to_xyz`. - - Parameters - ---------- - color : str or length-3 list - The color specification. Can be a tuple of channel values for the - `space` colorspace, a hex string, a registered color name, a cycle - color, or a colormap color (see `ColorDict`). - - If `space` is ``'rgb'``, this is a tuple of RGB values, and any - channels are larger than ``2``, the channels are assumed to be on - a ``0`` to ``255`` scale and are therefore divided by ``255``. - space : {'rgb', 'hsv', 'hsl', 'hpl', 'hcl'}, optional - The colorspace for the input channel values. Ignored unless `color` is - an container of numbers. - cycle : str or list, optional - The registered color cycle name used to interpret colors that - look like ``'C0'``, ``'C1'``, etc. Default is :rc:`cycle`. - alpha : bool, optional - Whether to preserve the opacity channel, if it exists. Default - is ``False``. - - Returns - ------- - color - The RGB tuple. - """ - # Convert color cycle strings - if isinstance(color, str) and re.match('^C[0-9]$', color): - if isinstance(cycle, str): - try: - cycle = mcm.cmap_d[cycle].colors - except (KeyError, AttributeError): - cycles = sorted( - name for name, cmap in mcm.cmap_d.items() - if isinstance(cmap, ListedColormap) - ) - raise ValueError( - f'Invalid cycle {cycle!r}. Options are: ' - + ', '.join(map(repr, cycles)) + '.' - ) - elif cycle is None: - cycle = rcParams['axes.prop_cycle'].by_key() - if 'color' not in cycle: - cycle = ['k'] - else: - cycle = cycle['color'] - else: - raise ValueError(f'Invalid cycle {cycle!r}.') - color = cycle[int(color[-1]) % len(cycle)] - - # Translate RGB strings and (cmap,index) tuples - opacity = 1 - if isinstance(color, str) or (np.iterable(color) and len(color) == 2): - try: - *color, opacity = mcolors.to_rgba(color) # ensure is valid color - except (ValueError, TypeError): - raise ValueError(f'Invalid RGB argument {color!r}.') - - # Pull out alpha channel - if len(color) == 4: - *color, opacity = color - elif len(color) != 3: - raise ValueError(f'Invalid RGB argument {color!r}.') - - # Translate arbitrary colorspaces - if space == 'rgb': - try: - if any(c > 2 for c in color): - color = [c / 255 for c in color] # scale to within 0-1 - color = tuple(color) - except (ValueError, TypeError): - raise ValueError(f'Invalid RGB argument {color!r}.') - elif space == 'hsv': - color = hsluv.hsl_to_rgb(*color) - elif space == 'hpl': - color = hsluv.hpluv_to_rgb(*color) - elif space == 'hsl': - color = hsluv.hsluv_to_rgb(*color) - elif space == 'hcl': - color = hsluv.hcl_to_rgb(*color) - else: - raise ValueError('Invalid color {color!r} for colorspace {space!r}.') - - # Return RGB or RGBA - if alpha: - return (*color, opacity) - else: - return color - - -def to_xyz(color, space='hcl', alpha=False): - """ - Translate color in *any* format to a tuple of channel values in *any* - colorspace. This is the inverse of `to_rgb`. - - Parameters - ---------- - color : color-spec - The color. Sanitized with `to_rgb`. - space : {'hcl', 'hpl', 'hsl', 'hsv', 'rgb'}, optional - The colorspace for the output channel values. - alpha : bool, optional - Whether to preserve the opacity channel, if it exists. Default - is ``False``. - - Returns - ------- - color - Tuple of colorspace `space` channel values. - """ - # Run tuple conversions - # NOTE: Don't pass color tuple, because we may want to permit - # out-of-bounds RGB values to invert conversion - *color, opacity = to_rgb(color, alpha=True) - if space == 'rgb': - pass - elif space == 'hsv': - color = hsluv.rgb_to_hsl(*color) # rgb_to_hsv would also work - elif space == 'hpl': - color = hsluv.rgb_to_hpluv(*color) - elif space == 'hsl': - color = hsluv.rgb_to_hsluv(*color) - elif space == 'hcl': - color = hsluv.rgb_to_hcl(*color) - else: - raise ValueError(f'Invalid colorspace {space}.') - if alpha: - return (*color, opacity) - else: - return color - - -def _clip_colors(colors, clip=True, gray=0.2): - """ - Clip impossible colors rendered in an HSL-to-RGB colorspace conversion. - Used by `PerceptuallyUniformColormap`. If `mask` is ``True``, impossible - colors are masked out. - - Parameters - ---------- - colors : list of length-3 tuples - The RGB colors. - clip : bool, optional - If `clip` is ``True`` (the default), RGB channel values >1 are clipped - to 1. Otherwise, the color is masked out as gray. - gray : float, optional - The identical RGB channel values (gray color) to be used if `mask` - is ``True``. - """ - # Clip colors - colors = np.array(colors) - over = (colors > 1) - under = (colors < 0) - if clip: - colors[under] = 0 - colors[over] = 1 - else: - colors[(under | over)] = gray - # Message - # NOTE: Never print warning because happens when using builtin maps - # message = 'Clipped' if clip else 'Invalid' - # for i,name in enumerate('rgb'): - # if under[:,i].any(): - # _warn_proplot(f'{message} {name!r} channel ( < 0).') - # if over[:,i].any(): - # _warn_proplot(f'{message} {name!r} channel ( > 1).') - return colors - - -def _make_segmentdata_array(values, coords=None, ratios=None): - """ - Return a segmentdata array or callable given the input colors - and coordinates. - - Parameters - ---------- - values : list of float - The channel values. - coords : list of float, optional - The segment coordinates. - ratios : list of float, optional - The relative length of each segment transition. - """ - # Allow callables - if callable(values): - return values - values = np.atleast_1d(values) - if len(values) == 1: - value = values[0] - return [(0, value, value), (1, value, value)] - - # Get coordinates - if not np.iterable(values): - raise TypeError('Colors must be iterable, got {values!r}.') - if coords is not None: - coords = np.atleast_1d(coords) - if ratios is not None: - _warn_proplot( - f'Segment coordinates were provided, ignoring ' - f'ratios={ratios!r}.' - ) - if len(coords) != len(values) or coords[0] != 0 or coords[-1] != 1: - raise ValueError( - f'Coordinates must range from 0 to 1, got {coords!r}.' - ) - elif ratios is not None: - coords = np.atleast_1d(ratios) - if len(coords) != len(values) - 1: - raise ValueError( - f'Need {len(values)-1} ratios for {len(values)} colors, ' - f'but got {len(ratios)} ratios.' - ) - coords = np.concatenate(([0], np.cumsum(coords))) - coords = coords / np.max(coords) # normalize to 0-1 - else: - coords = np.linspace(0, 1, len(values)) - - # Build segmentdata array - array = [] - for c, value in zip(coords, values): - array.append((c, value, value)) - return array - - -def make_mapping_array(N, data, gamma=1.0, inverse=False): - r""" - Similar to `~matplotlib.colors.makeMappingArray` but permits - *circular* hue gradations along 0-360, disables clipping of - out-of-bounds channel values, and uses fancier "gamma" scaling. - - Parameters - ---------- - N : int - Number of points in the colormap lookup table. - data : 2D array-like - List of :math:`(x, y_0, y_1)` tuples specifying the channel jump (from - :math:`y_0` to :math:`y_1`) and the :math:`x` coordinate of that - transition (ranges between 0 and 1). - See `~matplotlib.colors.LinearSegmentedColormap` for details. - gamma : float or list of float, optional - To obtain channel values between coordinates :math:`x_i` and - :math:`x_{i+1}` in rows :math:`i` and :math:`i+1` of `data`, - we use the formula: - - .. math:: - - y = y_{1,i} + w_i^{\gamma_i}*(y_{0,i+1} - y_{1,i}) - - where :math:`\gamma_i` corresponds to `gamma` and the weight - :math:`w_i` ranges from 0 to 1 between rows ``i`` and ``i+1``. - If `gamma` is float, it applies to every transition. Otherwise, - its length must equal ``data.shape[0]-1``. - - This is like the `gamma` used with matplotlib's - `~matplotlib.colors.makeMappingArray`, except it controls the - weighting for transitions *between* each segment data coordinate rather - than the coordinates themselves. This makes more sense for - `PerceptuallyUniformColormap`\\ s because they usually consist of just - one linear transition for *sequential* colormaps and two linear - transitions for *diverging* colormaps -- and in the latter case, it - is often desirable to modify both "halves" of the colormap in the - same way. - inverse : bool, optional - If ``True``, :math:`w_i^{\gamma_i}` is replaced with - :math:`1 - (1 - w_i)^{\gamma_i}` -- that is, when `gamma` is greater - than 1, this weights colors toward *higher* channel values instead - of lower channel values. - - This is implemented in case we want to apply *equal* "gamma scaling" - to different HSL channels in different directions. Usually, this - is done to weight low data values with higher luminance *and* lower - saturation, thereby emphasizing "extreme" data values with stronger - colors. - """ - # Allow for *callable* instead of linearly interpolating between segments - gammas = np.atleast_1d(gamma) - if (gammas < 0.01).any() or (gammas > 10).any(): - raise ValueError('Gamma can only be in range [0.01,10].') - if callable(data): - if len(gammas) > 1: - raise ValueError( - 'Only one gamma allowed for functional segmentdata.') - x = np.linspace(0, 1, N)**gamma - lut = np.array(data(x), dtype=float) - return lut - - # Get array - data = np.array(data) - shape = data.shape - if len(shape) != 2 or shape[1] != 3: - raise ValueError('Data must be nx3 format.') - if len(gammas) != 1 and len(gammas) != shape[0] - 1: - raise ValueError( - f'Need {shape[0]-1} gammas for {shape[0]}-level mapping array, ' - f'but got {len(gamma)}.' - ) - if len(gammas) == 1: - gammas = np.repeat(gammas, shape[:1]) - - # Get indices - x = data[:, 0] - y0 = data[:, 1] - y1 = data[:, 2] - if x[0] != 0.0 or x[-1] != 1.0: - raise ValueError( - 'Data mapping points must start with x=0 and end with x=1.' - ) - if (np.diff(x) < 0).any(): - raise ValueError( - 'Data mapping points must have x in increasing order.' - ) - x = x * (N - 1) - - # Get distances from the segmentdata entry to the *left* for each requested - # level, excluding ends at (0,1), which must exactly match segmentdata ends - xq = (N - 1) * np.linspace(0, 1, N) - # where xq[i] must be inserted so it is larger than x[ind[i]-1] but - # smaller than x[ind[i]] - ind = np.searchsorted(x, xq)[1:-1] - distance = (xq[1:-1] - x[ind - 1]) / (x[ind] - x[ind - 1]) - - # Scale distances in each segment by input gamma - # The ui are starting-points, the ci are counts from that point - # over which segment applies (i.e. where to apply the gamma), the relevant - # 'segment' is to the *left* of index returned by searchsorted - _, uind, cind = np.unique(ind, return_index=True, return_counts=True) - for ui, ci in zip(uind, cind): # length should be N-1 - # the relevant segment is to *left* of this number - gamma = gammas[ind[ui] - 1] - if gamma == 1: - continue - ireverse = False - if ci > 1: # i.e. more than 1 color in this 'segment' - # by default want to weight toward a *lower* channel value - ireverse = ((y0[ind[ui]] - y1[ind[ui] - 1]) < 0) - if inverse: - ireverse = (not ireverse) - if ireverse: - distance[ui:ui + ci] = 1 - (1 - distance[ui:ui + ci])**gamma - else: - distance[ui:ui + ci] **= gamma - - # Perform successive linear interpolations all rolled up into one equation - lut = np.zeros((N,), float) - lut[1:-1] = distance * (y0[ind] - y1[ind - 1]) + y1[ind - 1] - lut[0] = y1[0] - lut[-1] = y0[-1] - return lut - - -_from_file_docstring = """ -Valid file extensions are as follows: - -================== ===================================================================================================================================================================================================================== -Extension Description -================== ===================================================================================================================================================================================================================== -``.hex`` List of HEX strings in any format (comma-separated, separate lines, with double quotes... anything goes). -``.xml`` XML files with ```` tags specifying ``x``, ``r``, ``g``, ``b``, and (optionally) ``o`` parameters, where ``x`` is the coordinate and the rest are the red, blue, green, and opacity channel values. -``.rgb``, ``.txt`` 3-4 column table of red, blue, green, and (optionally) opacity channel values, delimited by commas or spaces. If values larger than 1 are detected, they are assumed to be on the 0-255 scale and are divided by 255. -================== ===================================================================================================================================================================================================================== - -Parameters ----------- -path : str - The file path. -warn_on_failure : bool, optional - If ``True``, issue a warning when loading fails rather than - raising an error. -""" # noqa - - -class _Colormap(object): - """ - Mixin class used to add some helper methods. - """ - def _get_data(self, ext, alpha=True): - """ - Return a string containing the colormap colors for saving. - - Parameters - ---------- - ext : {'hex', 'txt', 'rgb'} - The filename extension. - alpha : bool, optional - Whether to include an opacity column. - """ - # Get lookup table colors and filter out bad ones - if not self._isinit: - self._init() - colors = self._lut[:-3, :] - # Get data string - if ext == 'hex': - data = ', '.join(mcolors.to_hex(color) for color in colors) - elif ext in ('txt', 'rgb'): - rgb = mcolors.to_rgba if alpha else mcolors.to_rgb - data = [rgb(color) for color in colors] - data = '\n'.join( - ' '.join(f'{num:0.6f}' for num in line) for line in data - ) - else: - raise ValueError( - f'Invalid extension {ext!r}. Options are: ' - "'hex', 'txt', 'rgb', 'rgba'." - ) - return data - - def _parse_path(self, path, dirname='.', ext=''): - """ - Parse the user input path. - - Parameters - ---------- - dirname : str, optional - The default directory. - ext : str, optional - The default extension. - """ - path = os.path.expanduser(path or '') - dirname = os.path.expanduser(dirname or '') - if not path or os.path.isdir(path): - path = os.path.join(path or dirname, self.name) # default name - dirname, basename = os.path.split(path) # default to current directory - path = os.path.join(dirname or '.', basename) - if not os.path.splitext(path)[-1]: - path = path + '.' + ext # default file extension - return path - - -class LinearSegmentedColormap(mcolors.LinearSegmentedColormap, _Colormap): - r""" - New base class for all `~matplotlib.colors.LinearSegmentedColormap`\ s. - """ - def __str__(self): - return type(self).__name__ + f'(name={self.name!r})' - - def __repr__(self): - string = f" 'name': {self.name!r},\n" - if hasattr(self, '_space'): - string += f" 'space': {self._space!r},\n" - if hasattr(self, '_cyclic'): - string += f" 'cyclic': {self._cyclic!r},\n" - for key, data in self._segmentdata.items(): - if callable(data): - string += f' {key!r}: ,\n' - else: - string += (f' {key!r}: [{data[0][2]:.3f}, ' - f'..., {data[-1][1]:.3f}],\n') - return type(self).__name__ + '({\n' + string + '})' - - def __init__(self, *args, cyclic=False, alpha=None, **kwargs): - """ - Parameters - ---------- - cyclic : bool, optional - Whether the colormap is cyclic. If ``True``, this changes how the - leftmost and rightmost color levels are selected, and `extend` can - only be ``'neither'`` (a warning will be issued otherwise). - alpha : float, optional - The opacity for the entire colormap. Overrides the input - segment data. - *args, **kwargs - Passed to `~matplotlib.colors.LinearSegmentedColormap`. - """ - super().__init__(*args, **kwargs) - self._cyclic = cyclic - if alpha is not None: - self.set_alpha(alpha) - - def concatenate(self, *args, ratios=1, name=None, N=None, **kwargs): - """ - Return the concatenation of this colormap with the - input colormaps. - - Parameters - ---------- - *args - Instances of `LinearSegmentedColormap`. - ratios : list of float, optional - Relative extent of each component colormap in the merged colormap. - Length must equal ``len(args) + 1``. - - For example, ``cmap1.concatenate(cmap2, ratios=[2,1])`` generates - a colormap with the left two-thrids containing colors from - ``cmap1`` and the right one-third containing colors from ``cmap2``. - name : str, optional - The colormap name. Default is - ``'_'.join(cmap.name for cmap in args)``. - N : int, optional - The number of points in the colormap lookup table. - Default is :rc:`image.lut` times ``len(args)``. - **kwargs - Passed to `LinearSegmentedColormap.updated` - or `PerceptuallyUniformColormap.updated`. - - Returns - ------- - `LinearSegmentedColormap` - The colormap. - """ - # Try making a simple copy - if not args: - raise ValueError( - f'Got zero positional args, you must provide at least one.' - ) - if not all(isinstance(cmap, type(self)) for cmap in args): - raise ValueError( - f'Colormaps {cmap.name + ": " + repr(cmap) for cmap in args} ' - f'must all belong to the same class.' - ) - cmaps = (self, *args) - spaces = {cmap.name: getattr(cmap, '_space', None) for cmap in cmaps} - if len({*spaces.values(), }) > 1: - raise ValueError( - 'Cannot merge PerceptuallyUniformColormaps that use ' - 'different colorspaces: ' - + ', '.join(map(repr, spaces)) + '.' - ) - N = N or len(cmaps) * rcParams['image.lut'] - if name is None: - name = '_'.join(cmap.name for cmap in cmaps) - - # Combine the segmentdata, and use the y1/y2 slots at merge points so - # we never interpolate between end colors of different colormaps - segmentdata = {} - ratios = ratios or 1 - if isinstance(ratios, Number): - ratios = [1] * len(cmaps) - ratios = np.array(ratios) / np.sum(ratios) - x0 = np.concatenate([[0], np.cumsum(ratios)]) # coordinates for edges - xw = x0[1:] - x0[:-1] # widths between edges - for key in self._segmentdata.keys(): - # Callable segments - # WARNING: If just reference a global 'funcs' list from inside the - # 'data' function it can get overwritten in this loop. Must - # embed 'funcs' into the definition using a keyword argument. - callable_ = [callable(cmap._segmentdata[key]) for cmap in cmaps] - if all(callable_): # expand range from x-to-w to 0-1 - funcs = [cmap._segmentdata[key] for cmap in cmaps] - - def xyy(ix, funcs=funcs): - ix = np.atleast_1d(ix) - kx = np.empty(ix.shape) - for j, jx in enumerate(ix.flat): - idx = max(np.searchsorted(x0, jx) - 1, 0) - kx.flat[j] = funcs[idx]((jx - x0[idx]) / xw[idx]) - return kx - # Concatenate segment arrays and make the transition at the - # seam instant so we *never interpolate* between end colors - # of different maps. - elif not any(callable_): - datas = [] - for x, w, cmap in zip(x0[:-1], xw, cmaps): - xyy = np.array(cmap._segmentdata[key]) - xyy[:, 0] = x + w * xyy[:, 0] - datas.append(xyy) - for i in range(len(datas) - 1): - datas[i][-1, 2] = datas[i + 1][0, 2] - datas[i + 1] = datas[i + 1][1:, :] - xyy = np.concatenate(datas, axis=0) - xyy[:, 0] = xyy[:, 0] / xyy[:, 0].max(axis=0) # fix fp errors - else: - raise ValueError( - 'Mixed callable and non-callable colormap values.' - ) - segmentdata[key] = xyy - # Handle gamma values - if key == 'saturation': - ikey = 'gamma1' - elif key == 'luminance': - ikey = 'gamma2' - else: - continue - if ikey in kwargs: - continue - gamma = [] - for cmap in cmaps: - igamma = getattr(cmap, '_' + ikey) - if not np.iterable(igamma): - if all(callable_): - igamma = [igamma] - else: - igamma = (len(cmap._segmentdata[key]) - 1) * [igamma] - gamma.extend(igamma) - if all(callable_): - if any(igamma != gamma[0] for igamma in gamma[1:]): - _warn_proplot( - 'Cannot use multiple segment gammas when ' - 'concatenating callable segments. Using the first ' - f'gamma of {gamma[0]}.' - ) - gamma = gamma[0] - kwargs[ikey] = gamma - - # Return copy - return self.updated(name=name, segmentdata=segmentdata, **kwargs) - - def punched(self, cut=None, name=None, **kwargs): - """ - Return a version of the colormap with the center "punched out". - This is great for making the transition from "negative" to "positive" - in a diverging colormap more distinct. - - Parameters - ---------- - cut : float, optional - The proportion to cut from the center of the colormap. - For example, ``center=0.1`` cuts the central 10%. - name : str, optional - The name of the new colormap. Default is - ``self.name + '_punched'``. - **kwargs - Passed to `LinearSegmentedColormap.updated` - or `PerceptuallyUniformColormap.updated`. - - Returns - ------- - `LinearSegmentedColormap` - The colormap. - """ - cut = _notNone(cut, 0) - if cut == 0: - return self - if name is None: - name = self.name + '_punched' - - # Decompose cut into two truncations followed by concatenation - left_center = 0.5 - cut / 2 - right_center = 0.5 + cut / 2 - cmap_left = self.truncated(0, left_center) - cmap_right = self.truncated(right_center, 1) - return cmap_left.concatenate(cmap_right, name=name, **kwargs) - - def reversed(self, name=None, **kwargs): - """ - Return a reversed copy of the colormap, as in - `~matplotlib.colors.LinearSegmentedColormap`. - - Parameters - ---------- - name : str, optional - The new colormap name. Default is ``self.name + '_r'``. - **kwargs - Passed to `LinearSegmentedColormap.updated` - or `PerceptuallyUniformColormap.updated`. - """ - if name is None: - name = self.name + '_r' - - def factory(dat): - def func_r(x): - return dat(1.0 - x) - return func_r - segmentdata = {key: - factory(data) if callable(data) else - [(1.0 - x, y1, y0) for x, y0, y1 in reversed(data)] - for key, data in self._segmentdata.items()} - for key in ('gamma1', 'gamma2'): - if key in kwargs: - continue - gamma = getattr(self, '_' + key, None) - if gamma is not None and np.iterable(gamma): - kwargs[key] = gamma[::-1] - return self.updated(name, segmentdata, **kwargs) - - def save(self, path=None, alpha=True): - """ - Save the colormap data to a file. - - Parameters - ---------- - path : str, optional - The output filename. If not provided, the colormap - is saved under ``~/.proplot/cmaps/name.json`` where ``name`` - is the colormap name. Valid extensions are described in - the below table. - - ===================== ========================================================== - Extension Description - ===================== ========================================================== - ``.json`` (default) JSON database of the channel segment data. - ``.hex`` Comma-delimited list of HEX strings. - ``.rgb``, ``.txt`` 3-4 column table of channel values. - ===================== ========================================================== - - alpha : bool, optional - Whether to include an opacity column for ``.rgb`` - and ``.txt`` files. - """ # noqa - dirname = os.path.join('~', '.proplot', 'cmaps') - filename = self._parse_path(path, dirname, 'json') - - # Save channel segment data in json file - _, ext = os.path.splitext(filename) - if ext[1:] == 'json': - # Sanitize segmentdata values - # Convert np.float to builtin float, np.array to list of lists, - # and callable to list of lists. We tried encoding func.__code__ - # with base64 and marshal instead, but when cmap.concatenate() - # embeds functions as keyword arguments, this seems to make it - # *impossible* to load back up the function with FunctionType - # (error message: arg 5 (closure) must be tuple). Instead use - # this brute force workaround. - data = {} - for key, value in self._segmentdata.items(): - if callable(value): - x = np.linspace(0, 1, 256) # just save the transitions - y = np.array([value(_) for _ in x]).squeeze() - value = np.vstack((x, y, y)).T - data[key] = np.asarray(value).astype(float).tolist() - # Add critical attributes to the dictionary - keys = () - if isinstance(self, PerceptuallyUniformColormap): - keys = ('cyclic', 'gamma1', 'gamma2', 'space') - elif isinstance(self, LinearSegmentedColormap): - keys = ('cyclic', 'gamma') - for key in keys: - data[key] = getattr(self, '_' + key) - with open(filename, 'w') as file: - json.dump(data, file, indent=4) - - # Save lookup table colors - else: - data = self._get_data(ext[1:], alpha=alpha) - with open(filename, 'w') as f: - f.write(data) - print(f'Saved colormap to {filename!r}.') - - def set_alpha(self, alpha): - """ - Set the opacity for the entire colormap. - - Parameters - ---------- - alpha : float - The opacity. - """ - self._segmentdata['alpha'] = [(0, alpha, alpha), (1, alpha, alpha)] - self._isinit = False - - def set_cyclic(self, b): - """ - Set whether this colormap is "cyclic". See `LinearSegmentedColormap` - for details. - """ - self._cyclic = bool(b) - self._isinit = False - - def shifted(self, shift=None, name=None, **kwargs): - """ - Return a cyclicaly shifted version of the colormap. If the colormap - cyclic property is set to ``False`` a warning will be raised. - - Parameters - ---------- - shift : float, optional - The number of degrees to shift, out of 360 degrees. If ``None``, - the original colormap is returned. - name : str, optional - The name of the new colormap. Default is - ``self.name + '_shifted'``. - **kwargs - Passed to `LinearSegmentedColormap.updated` - or `PerceptuallyUniformColormap.updated`. - """ - shift = ((shift or 0) / 360) % 1 - if shift == 0: - return self - if name is None: - name = self.name + '_shifted' - if not self._cyclic: - _warn_proplot( - f'Shifting non-cyclic colormap {self.name!r}. ' - f'Use cmap.set_cyclic(True) or Colormap(..., cyclic=True) to ' - 'suppress this warning.' - ) - self._cyclic = True - - # Decompose shift into two truncations followed by concatenation - cmap_left = self.truncated(shift, 1) - cmap_right = self.truncated(0, shift) - return cmap_left.concatenate( - cmap_right, ratios=(1 - shift, shift), name=name, **kwargs - ) - - def truncated(self, left=None, right=None, name=None, **kwargs): - """ - Return a truncated version of the colormap. - - Parameters - ---------- - left : float, optional - The colormap index for the new "leftmost" color. Must fall between - ``0`` and ``1``. For example, - ``left=0.1`` cuts the leftmost 10%% of the colors. - right : float, optional - The colormap index for the new "rightmost" color. Must fall between - ``0`` and ``1``. For example, - ``right=0.9`` cuts the leftmost 10%% of the colors. - name : str, optional - The name of the new colormap. Default is - ``self.name + '_truncated'``. - **kwargs - Passed to `LinearSegmentedColormap.updated` - or `PerceptuallyUniformColormap.updated`. - """ - # Bail out - left = max(_notNone(left, 0), 0) - right = min(_notNone(right, 1), 1) - if left == 0 and right == 1: - return self - if name is None: - name = self.name + '_truncated' - - # Resample the segmentdata arrays - segmentdata = {} - for key, xyy in self._segmentdata.items(): - # Callable array - # WARNING: If just reference a global 'xyy' callable from inside - # the lambda function it gets overwritten in the loop! Must embed - # the old callable in the new one as a default keyword arg. - if callable(xyy): - def xyy(x, func=xyy): - return func(left + x * (right - left)) - # Slice - # l is the first point where x > 0 or x > left, should be >= 1 - # r is the last point where r < 1 or r < right - else: - xyy = np.asarray(xyy) - x = xyy[:, 0] - l = np.searchsorted(x, left) # first x value > left # noqa - r = np.searchsorted(x, right) - 1 # last x value < right - xc = xyy[l:r + 1, :].copy() - xl = xyy[l - 1, 1:] + (left - x[l - 1]) * ( - (xyy[l, 1:] - xyy[l - 1, 1:]) / (x[l] - x[l - 1]) - ) - xr = xyy[r, 1:] + (right - x[r]) * ( - (xyy[r + 1, 1:] - xyy[r, 1:]) / (x[r + 1] - x[r]) - ) - xyy = np.vstack(((left, *xl), xc, (right, *xr))) - xyy[:, 0] = (xyy[:, 0] - left) / (right - left) - segmentdata[key] = xyy - # Retain the corresponding gamma *segments* - if key == 'saturation': - ikey = 'gamma1' - elif key == 'luminance': - ikey = 'gamma2' - else: - continue - if ikey in kwargs: - continue - gamma = getattr(self, '_' + ikey) - if np.iterable(gamma): - if callable(xyy): - if any(igamma != gamma[0] for igamma in gamma[1:]): - _warn_proplot( - 'Cannot use multiple segment gammas when ' - 'truncating colormap. Using the first gamma ' - f'of {gamma[0]}.' - ) - gamma = gamma[0] - else: - gamma = gamma[l - 1:r + 1] - kwargs[ikey] = gamma - return self.updated(name, segmentdata, **kwargs) - - def updated( - self, name=None, segmentdata=None, N=None, *, - alpha=None, gamma=None, cyclic=None - ): - """ - Return a new colormap, with relevant properties copied from this one - if they were not provided as keyword arguments. - - Parameters - ---------- - name : str - The colormap name. Default is ``self.name + '_updated'``. - segmentdata, N, alpha, gamma, cyclic : optional - See `LinearSegmentedColormap`. If not provided, - these are copied from the current colormap. - """ - if name is None: - name = self.name + '_updated' - if segmentdata is None: - segmentdata = self._segmentdata - if gamma is None: - gamma = self._gamma - if cyclic is None: - cyclic = self._cyclic - if N is None: - N = self.N - cmap = LinearSegmentedColormap( - name, segmentdata, N, - alpha=alpha, gamma=gamma, cyclic=cyclic - ) - cmap._rgba_bad = self._rgba_bad - cmap._rgba_under = self._rgba_under - cmap._rgba_over = self._rgba_over - return cmap - - @staticmethod - def from_file(path, warn_on_failure=False): - """Load colormap from a file.""" - return _from_file(path, listed=False, warn_on_failure=warn_on_failure) - - @staticmethod - def from_list(name, colors, ratios=None, **kwargs): - """ - Make a `LinearSegmentedColormap` from a list of colors. - - Parameters - ---------- - name : str - The colormap name. - colors : list of color-spec or (float, color-spec) tuples, optional - If list of RGB[A] tuples or color strings, the colormap transitions - evenly from ``colors[0]`` at the left-hand side to - ``colors[-1]`` at the right-hand side. - - If list of (float, color-spec) tuples, the float values are the - coordinate of each transition and must range from 0 to 1. This - can be used to divide the colormap range unevenly. - ratios : list of float, optional - Relative extents of each color transition. Must have length - ``len(colors) - 1``. Larger numbers indicate a slower - transition, smaller numbers indicate a faster transition. - - Other parameters - ---------------- - **kwargs - Passed to `LinearSegmentedColormap`. - - Returns - ------- - `LinearSegmentedColormap` - The colormap. - """ - # Get coordinates - coords = None - if not np.iterable(colors): - raise ValueError(f'Colors must be iterable, got colors={colors!r}') - if (np.iterable(colors[0]) and len(colors[0]) == 2 - and not isinstance(colors[0], str)): - coords, colors = zip(*colors) - colors = [to_rgb(color, alpha=True) for color in colors] - - # Build segmentdata - keys = ('red', 'green', 'blue', 'alpha') - cdict = {} - for key, values in zip(keys, zip(*colors)): - cdict[key] = _make_segmentdata_array(values, coords, ratios) - return LinearSegmentedColormap(name, cdict, **kwargs) - - # Fix docstrings - # NOTE: Docstrings cannot be programatically altered in place e.g. - # with f-strings. Can only be modified a posteriori. - from_file.__func__.__doc__ += _from_file_docstring - - -class ListedColormap(mcolors.ListedColormap, _Colormap): - r""" - New base class for all `~matplotlib.colors.ListedColormap`\ s. - """ - def __str__(self): - return f'ListedColormap(name={self.name!r})' - - def __repr__(self): - return ( - 'ListedColormap({\n' - f" 'name': {self.name!r},\n" - f" 'colors': {[mcolors.to_hex(color) for color in self.colors]},\n" - '})') - - def __init__(self, *args, alpha=None, **kwargs): - """ - Parameters - ---------- - alpha : float, optional - The opacity for the entire colormap. Overrides the input - colors. - *args, **kwargs - Passed to `~matplotlib.colors.ListedColormap`. - """ - super().__init__(*args, **kwargs) - if alpha is not None: - self.set_alpha(alpha) - - def concatenate(self, *args, name=None, N=None, **kwargs): - """ - Append arbitrary colormaps onto this colormap. - - Parameters - ---------- - *args - Instances of `ListedColormap`. - name : str, optional - The colormap name. Default is - ``'_'.join(cmap.name for cmap in args)``. - N : int, optional - The number of colors in the colormap lookup table. Default is - the number of colors in the concatenated lists. - **kwargs - Passed to `~ListedColormap.updated`. - """ - if not args: - raise ValueError( - f'Got zero positional args, you must provide at least one.' - ) - if not all(isinstance(cmap, type(self)) for cmap in args): - raise ValueError( - f'Input arguments {args} must all be ListedColormap.' - ) - cmaps = (self, *args) - if name is None: - name = '_'.join(cmap.name for cmap in cmaps) - colors = [color for cmap in cmaps for color in cmap.colors] - return self.updated(colors, name, N or len(colors), **kwargs) - - def save(self, path=None, alpha=True): - """ - Save the colormap data to a file. - - Parameters - ---------- - path : str, optional - The output filename. If not provided, the colormap - is saved under ``~/.proplot/cycles/name.hex`` where ``name`` - is the colormap name. Valid extensions are described in - the below table. - - ===================== ========================================================== - Extension Description - ===================== ========================================================== - ``.hex`` (default) Comma-delimited list of HEX strings. - ``.rgb``, ``.txt`` 3-4 column table of channel values. - ===================== ========================================================== - - alpha : bool, optional - Whether to include an opacity column for ``.rgb`` - and ``.txt`` files. - """ # noqa - dirname = os.path.join('~', '.proplot', 'cmaps') - filename = self._parse_path(path, dirname, 'hex') - - # Save lookup table colors - _, ext = os.path.splitext(filename) - data = self._get_data(ext[1:], alpha=alpha) - with open(filename, 'w') as f: - f.write(data) - print(f'Saved colormap to {filename!r}.') - - def set_alpha(self, alpha): - """ - Set the opacity for the entire colormap. - - Parameters - ---------- - alpha : float - The opacity. - """ - colors = [list(mcolors.to_rgba(color)) for color in self.colors] - for color in colors: - color[3] = alpha - self.colors = colors - self._init() - - def shifted(self, shift=None, name=None): - """ - Return a cyclically shifted version of the colormap. - - Parameters - ---------- - shift : float, optional - The number of places to shift, between ``-self.N`` and ``self.N``. - If ``None``, the original colormap is returned. - name : str, optional - The new colormap name. Default is ``self.name + '_shifted'``. - """ - if not shift: - return self - if name is None: - name = self.name + '_shifted' - shift = shift % len(self.colors) - colors = [*self.colors] # ensure list - colors = colors[shift:] + colors[:shift] - return self.updated(colors, name, len(colors)) - - def truncated(self, left=None, right=None, name=None): - """ - Return a truncated version of the colormap. - - Parameters - ---------- - left : float, optional - The colormap index for the new "leftmost" color. Must fall between - ``0`` and ``self.N``. For example, - ``left=2`` deletes the two first colors. - right : float, optional - The colormap index for the new "rightmost" color. Must fall between - ``0`` and ``self.N``. For example, - ``right=4`` deletes colors after the fourth color. - name : str, optional - The new colormap name. Default is ``self.name + '_truncated'``. - """ - if left is None and right is None: - return self - if name is None: - name = self.name + '_truncated' - colors = self.colors[left:right] - return self.updated(colors, name, len(colors)) - - def updated(self, colors=None, name=None, N=None, *, alpha=None): - """ - Return a new colormap with relevant properties copied from this one - if they were not provided as keyword arguments. - - Parameters - ---------- - name : str - The colormap name. Default is ``self.name + '_updated'``. - colors, N, alpha : optional - See `ListedColormap`. If not provided, - these are copied from the current colormap. - """ - if name is None: - name = self.name + '_updated' - if colors is None: - colors = self.colors - if N is None: - N = self.N - cmap = ListedColormap(colors, name, N, alpha=alpha) - cmap._rgba_bad = self._rgba_bad - cmap._rgba_under = self._rgba_under - cmap._rgba_over = self._rgba_over - return cmap - - @staticmethod - def from_file(path, warn_on_failure=False): - """ - Load color cycle from a file. - """ - return _from_file(path, listed=True, warn_on_failure=warn_on_failure) - - # Fix docstrings - # NOTE: Docstrings cannot be programatically altered in place e.g. - # with f-strings. Can only be modified a posteriori. - from_file.__func__.__doc__ += _from_file_docstring - - -class PerceptuallyUniformColormap(LinearSegmentedColormap, _Colormap): - """ - Similar to `~matplotlib.colors.LinearSegmentedColormap`, but instead - of varying the RGB channels, we vary hue, saturation, and luminance in - either the HCL colorspace or the HSL or HPL scalings of HCL. - """ - def __init__( - self, name, segmentdata, N=None, space=None, clip=True, - gamma=None, gamma1=None, gamma2=None, - **kwargs - ): - """ - Parameters - ---------- - name : str - The colormap name. - segmentdata : dict-like - Mapping containing the keys ``'hue'``, ``'saturation'``, and - ``'luminance'``. The key values can be callable functions that - return channel values given a colormap index, or lists containing - any of the following channel specifiers: - - 1. Numbers, within the range 0-360 for hue and 0-100 for - saturation and luminance. - 2. Color string names or hex tags, in which case the channel - value for that color is looked up. - - See `~matplotlib.colors.LinearSegmentedColormap` for a more - detailed explanation. - N : int, optional - Number of points in the colormap lookup table. - Default is :rc:`image.lut`. - space : {'hsl', 'hpl', 'hcl'}, optional - The hue, saturation, luminance-style colorspace to use for - interpreting the channels. See - `this page `__ for a description. - clip : bool, optional - Whether to "clip" impossible colors, i.e. truncate HCL colors - with RGB channels with values >1, or mask them out as gray. - cyclic : bool, optional - Whether the colormap is cyclic. If ``True``, this changes how the - leftmost and rightmost color levels are selected, and `extend` can - only be ``'neither'`` (a warning will be issued otherwise). - gamma : float, optional - Sets `gamma1` and `gamma2` to this identical value. - gamma1 : float, optional - If >1, makes low saturation colors more prominent. If <1, - makes high saturation colors more prominent. Similar to the - `HCLWizard `_ option. - See `make_mapping_array` for details. - gamma2 : float, optional - If >1, makes high luminance colors more prominent. If <1, - makes low luminance colors more prominent. Similar to the - `HCLWizard `_ option. - See `make_mapping_array` for details. - **kwargs - Passed to `LinearSegmentedColormap`. - - Example - ------- - The following generates a `PerceptuallyUniformColormap` from a - `segmentdata` dictionary that uses color names for the hue data, - instead of channel values between ``0`` and ``360``. - - >>> import proplot as plot - >>> data = { - ... 'hue': [[0, 'red', 'red'], [1, 'blue', 'blue']], - ... 'saturation': [[0, 100, 100], [1, 100, 100]], - ... 'luminance': [[0, 100, 100], [1, 20, 20]], - ... } - >>> cmap = plot.PerceptuallyUniformColormap(data) - - """ - # Checks - space = _notNone(space, 'hsl').lower() - if space not in ('rgb', 'hsv', 'hpl', 'hsl', 'hcl'): - raise ValueError(f'Unknown colorspace {space!r}.') - keys = {*segmentdata.keys()} - target = {'hue', 'saturation', 'luminance', 'alpha'} - if not keys <= target: - raise ValueError( - f'Invalid segmentdata dictionary with keys {keys!r}.' - ) - # Convert color strings to channel values - for key, array in segmentdata.items(): - if callable(array): # permit callable - continue - for i, xyy in enumerate(array): - xyy = list(xyy) # make copy! - for j, y in enumerate(xyy[1:]): # modify the y values - xyy[j + 1] = _get_channel(y, key, space) - segmentdata[key][i] = xyy - # Initialize - N = N or rcParams['image.lut'] - super().__init__(name, segmentdata, N, gamma=1.0, **kwargs) - # Custom properties - self._gamma1 = _notNone(gamma1, gamma, 1.0) - self._gamma2 = _notNone(gamma2, gamma, 1.0) - self._space = space - self._clip = clip - - def _init(self): - """ - As with `~matplotlib.colors.LinearSegmentedColormap`, but convert - each value in the lookup table from ``self._space`` to RGB. - """ - # First generate the lookup table - channels = ('hue', 'saturation', 'luminance') - # gamma weights *low chroma* and *high luminance* - inverses = (False, False, True) - gammas = (1.0, self._gamma1, self._gamma2) - self._lut_hsl = np.ones((self.N + 3, 4), float) # fill - for i, (channel, gamma, inverse) in enumerate( - zip(channels, gammas, inverses)): - self._lut_hsl[:-3, i] = make_mapping_array( - self.N, self._segmentdata[channel], gamma, inverse) - if 'alpha' in self._segmentdata: - self._lut_hsl[:-3, 3] = make_mapping_array( - self.N, self._segmentdata['alpha']) - self._lut_hsl[:-3, 0] %= 360 - # Make hues circular, set extremes i.e. copy HSL values - self._lut = self._lut_hsl.copy() - self._set_extremes() # generally just used end values in segmentdata - self._isinit = True - # Now convert values to RGB and clip colors - for i in range(self.N + 3): - self._lut[i, :3] = to_rgb(self._lut[i, :3], self._space) - self._lut[:, :3] = _clip_colors(self._lut[:, :3], self._clip) - - @staticmethod - def from_color(name, color, fade=None, space='hsl', **kwargs): - """ - Return a monochromatic "sequential" colormap that blends from white - or near-white to the input color. - - Parameters - ---------- - name : str, optional - The colormap name. - color : color-spec - RGB tuple, hex string, or named color string. - fade : float or color-spec, optional - If float, this is the luminance channel strength on the left-hand - side of the colormap (default is ``100``), and the saturation - channel is held constant throughout the colormap. - - If RGB tuple, hex string, or named color string, the luminance and - saturation (but *not* the hue) from this color are used for the - left-hand side of the colormap. - space : {'hsl', 'hpl', 'hcl'}, optional - The colorspace in which the luminance is varied. - - Other parameters - ---------------- - **kwargs - Passed to `PerceptuallyUniformColormap.from_hsl`. - - Returns - ------- - `PerceptuallyUniformColormap` - The colormap. - """ - hue, saturation, luminance, alpha = to_xyz(color, space, alpha=True) - if fade is None: - fade = 100 - if isinstance(fade, Number): - saturation_fade, luminance_fade = saturation, fade - else: - _, saturation_fade, luminance_fade = to_xyz(fade, space) - return PerceptuallyUniformColormap.from_hsl( - name, hue=hue, alpha=alpha, space=space, - saturation=(saturation_fade, saturation), - luminance=(luminance_fade, luminance), - **kwargs) - - @staticmethod - def from_hsl( - name, hue=0, saturation=100, luminance=(100, 20), alpha=None, - ratios=None, **kwargs - ): - """ - Make a `~PerceptuallyUniformColormap` by specifying the hue, - saturation, and luminance transitions individually. - - Parameters - ---------- - name : str, optional - The colormap name. - hue : float, str, or list thereof, optional - Hue channel value or list of values. Values can be - any of the following. - - 1. Numbers, within the range 0-360 for hue and 0-100 for - saturation and luminance. - 2. Color string names or hex strings, in which case the channel - value for that color is looked up. - - If scalar, the hue does not change across the colormap. - saturation, luminance, alpha : float, str, or list thereof, optional - As with `hue`, but for the saturation, luminance, and alpha - (opacity) channels, respectively. - ratios : list of float, optional - Relative extents of each color transition. Must have length - ``len(colors) - 1``. Larger numbers indicate a slower - transition, smaller numbers indicate a faster transition. - - For example, ``luminance=[100,50,0]`` with ``ratios=[2,1]`` - results in a colormap with the transition from luminance ``100`` - to ``50`` taking *twice as long* as the transition from luminance - ``50`` to ``0``. - - Other parameters - ---------------- - **kwargs - Passed to `PerceptuallyUniformColormap`. - - Returns - ------- - `PerceptuallyUniformColormap` - The colormap. - """ - cdict = {} - alpha = _notNone(alpha, 1.0) - for key, channel in zip( - ('hue', 'saturation', 'luminance', 'alpha'), - (hue, saturation, luminance, alpha) - ): - cdict[key] = _make_segmentdata_array(channel, ratios=ratios) - return PerceptuallyUniformColormap(name, cdict, **kwargs) - - @staticmethod - def from_list(name, colors, ratios=None, **kwargs): - """ - Make a `PerceptuallyUniformColormap` from a list of colors. - - Parameters - ---------- - name : str - The colormap name. - colors : list of color-spec or (float, color-spec) tuples, optional - If list of RGB[A] tuples or color strings, the colormap transitions - evenly from ``colors[0]`` at the left-hand side to - ``colors[-1]`` at the right-hand side. - - If list of (float, color-spec) tuples, the float values are the - coordinate of each transition and must range from 0 to 1. This - can be used to divide the colormap range unevenly. - ratios : list of float, optional - Relative extents of each color transition. Must have length - ``len(colors) - 1``. Larger numbers indicate a slower - transition, smaller numbers indicate a faster transition. - - For example, ``red=[1,0.5,0]`` with ``ratios=[2,1]`` - results in a colormap with the transition from red ``1`` - to ``0.5`` taking *twice as long* as the transition from red - ``0.5`` to ``0``. - - Other parameters - ---------------- - **kwargs - Passed to `PerceptuallyUniformColormap`. - - Returns - ------- - `PerceptuallyUniformColormap` - The colormap. - """ - # Get coordinates - coords = None - space = kwargs.get('space', 'hsl') # use the builtin default - if not np.iterable(colors): - raise ValueError(f'Colors must be iterable, got colors={colors!r}') - if (np.iterable(colors[0]) and len(colors[0]) == 2 - and not isinstance(colors[0], str)): - coords, colors = zip(*colors) - colors = [to_xyz(color, space, alpha=True) for color in colors] - - # Build segmentdata - keys = ('hue', 'saturation', 'luminance', 'alpha') - cdict = {} - for key, values in zip(keys, zip(*colors)): - cdict[key] = _make_segmentdata_array(values, coords, ratios) - return PerceptuallyUniformColormap(name, cdict, **kwargs) - - def set_gamma(self, gamma=None, gamma1=None, gamma2=None): - """ - Modify the gamma value(s) and refresh the lookup table. - - Parameters - ---------- - gamma : float, optional - Sets `gamma1` and `gamma2` to this identical value. - gamma1 : float, optional - If >1, makes low saturation colors more prominent. If <1, - makes high saturation colors more prominent. Similar to the - `HCLWizard `_ option. - See `make_mapping_array` for details. - gamma2 : float, optional - If >1, makes high luminance colors more prominent. If <1, - makes low luminance colors more prominent. Similar to the - `HCLWizard `_ option. - See `make_mapping_array` for details. - """ - gamma1 = _notNone(gamma1, gamma) - gamma2 = _notNone(gamma2, gamma) - if gamma1 is not None: - self._gamma1 = gamma1 - if gamma2 is not None: - self._gamma2 = gamma2 - self._init() - - def updated( - self, name=None, segmentdata=None, N=None, *, - alpha=None, gamma=None, cyclic=None, - clip=None, gamma1=None, gamma2=None, space=None - ): - """ - Return a new colormap with relevant properties copied from this one - if they were not provided as keyword arguments. - - Parameters - ---------- - name : str - The colormap name. Default is ``self.name + '_updated'``. - segmentdata, N, alpha, clip, cyclic, gamma, gamma1, gamma2, space : \ -optional - See `PerceptuallyUniformColormap`. If not provided, - these are copied from the current colormap. - """ - if name is None: - name = self.name + '_updated' - if segmentdata is None: - segmentdata = self._segmentdata - if space is None: - space = self._space - if clip is None: - clip = self._clip - if gamma is not None: - gamma1 = gamma2 = gamma - if gamma1 is None: - gamma1 = self._gamma1 - if gamma2 is None: - gamma2 = self._gamma2 - if cyclic is None: - cyclic = self._cyclic - if N is None: - N = self.N - cmap = PerceptuallyUniformColormap( - name, segmentdata, N, - alpha=alpha, clip=clip, cyclic=cyclic, - gamma1=gamma1, gamma2=gamma2, space=space) - cmap._rgba_bad = self._rgba_bad - cmap._rgba_under = self._rgba_under - cmap._rgba_over = self._rgba_over - return cmap - - -class CmapDict(dict): - """ - Dictionary subclass used to replace the `matplotlib.cm.cmap_d` - colormap dictionary. See `~CmapDict.__getitem__` and - `~CmapDict.__setitem__` for details. - """ - def __init__(self, kwargs): - """ - Parameters - ---------- - kwargs : dict-like - The source dictionary. - """ - for key, value in kwargs.items(): - if not isinstance(key, str): - raise KeyError(f'Invalid key {key}. Must be string.') - self.__setitem__(key, value, sort=False) - try: - for record in (cmaps, cycles): - record[:] = sorted(record) - except NameError: - pass - - def __delitem__(self, key): - """ - Delete the item from the list records. - """ - super().__delitem__(self, key) - try: - for record in (cmaps, cycles): - try: - record.remove(key) - except ValueError: - pass - except NameError: - pass - - def __getitem__(self, key): - """ - Retrieve the colormap associated with the sanitized key name. The - key name is case insensitive. - - * If the key ends in ``'_r'``, the result of ``cmap.reversed()`` is - returned for the colormap registered under the name ``key[:-2]``. - * If it ends in ``'_shifted'``, the result of ``cmap.shifted(180)`` is - returned for the colormap registered under the name ``cmap[:-8]``. - * Reversed diverging colormaps can be requested with their "reversed" - name -- for example, ``'BuRd'`` is equivalent to ``'RdBu_r'``. - """ - key = self._sanitize_key(key, mirror=True) - shift = (key[-8:] == '_shifted') - if shift: - key = key[:-8] - reverse = (key[-2:] == '_r') - if reverse: - key = key[:-2] - value = super().__getitem__(key) # may raise keyerror - if shift: - if hasattr(value, 'shifted'): - value = value.shifted(180) - else: - raise KeyError( - f'Item of type {type(value).__name__!r} ' - 'does not have shifted() method.' - ) - if reverse: - if hasattr(value, 'reversed'): - value = value.reversed() - else: - raise KeyError( - f'Item of type {type(value).__name__!r} ' - 'does not have reversed() method.' - ) - return value - - def __setitem__(self, key, item, sort=True): - """ - Store the colormap under its lowercase name. If the colormap is - a matplotlib `~matplotlib.colors.ListedColormap` or - `~matplotlib.colors.LinearSegmentedColormap`, it is converted to the - ProPlot `ListedColormap` or `LinearSegmentedColormap` subclass. - """ - if isinstance(item, (ListedColormap, LinearSegmentedColormap)): - pass - elif isinstance(item, mcolors.LinearSegmentedColormap): - item = LinearSegmentedColormap( - item.name, item._segmentdata, item.N, item._gamma) - elif isinstance(item, mcolors.ListedColormap): - item = ListedColormap( - item.colors, item.name, item.N) - elif item is None: - return - else: - raise ValueError( - f'Invalid colormap {item}. Must be instance of ' - 'matplotlib.colors.ListedColormap or ' - 'matplotlib.colors.LinearSegmentedColormap.' - ) - key = self._sanitize_key(key, mirror=False) - try: - record = cycles if isinstance(item, ListedColormap) else cmaps - record.append(key) - if sort: - record[:] = sorted(record) - except NameError: - pass - return super().__setitem__(key, item) - - def __contains__(self, item): - """ - Test for membership using the sanitized colormap name. - """ - try: # by default __contains__ ignores __getitem__ overrides - self.__getitem__(item) - return True - except KeyError: - return False - - def _sanitize_key(self, key, mirror=True): - """ - Return the sanitized colormap name. - """ - if not isinstance(key, str): - raise KeyError(f'Invalid key {key!r}. Key must be a string.') - key = key.lower() - reverse = False - if key[-2:] == '_r': - key = key[:-2] - reverse = True - if mirror and not super().__contains__(key): # search for mirrored key - key_mirror = key - for pair in CMAPS_DIVERGING: - try: - idx = pair.index(key) - key_mirror = pair[1 - idx] - except (ValueError, KeyError): - continue - if super().__contains__(key_mirror): - reverse = (not reverse) - key = key_mirror - if reverse: - key = key + '_r' - return key - - def get(self, key, *args): - """ - Retrieve the sanitized colormap name. - """ - key = self._sanitize_key(key, mirror=True) - return super().get(key, *args) - - def pop(self, key, *args): - """ - Pop the sanitized colormap name. - """ - key = self._sanitize_key(key, mirror=True) - try: - for record in (cmaps, cycles): - try: - record.remove(key) - except ValueError: - pass - except NameError: - pass - return super().pop(key, *args) - - def update(self, *args, **kwargs): - """ - Update the dictionary with sanitized colormap names. - """ - if len(args) == 1: - kwargs.update(args[0]) - elif len(args) > 1: - raise TypeError( - f'update() expected at most 1 arguments, got {len(args)}.' - ) - for key, value in kwargs.items(): - self[key] = value - - -class _ColorMappingOverride(mcolors._ColorMapping): - """ - Mapping whose cache attribute is a `ColorDict` dictionary. - """ - def __init__(self, mapping): - super().__init__(mapping) - self.cache = ColorDict({}) - - -class ColorDict(dict): - """ - This class overrides the builtin matplotlib color cache, allowing - users to draw colors from *named colormaps and color cycles* for any - plotting command that accepts a `color` keyword arg. - See `~ColorDict.__getitem__` for details. - """ - def __getitem__(self, key): - """ - Allows user to select colors from arbitrary named colormaps and - color cycles. - - * For a smooth colormap, usage is e.g. ``color=('Blues', 0.8)``. The - number is the colormap index, and must be between 0 and 1. - * For a color cycle, usage is e.g. ``color=('colorblind', 2)``. The - number is the list index. - - These examples work with any - matplotlib command that accepts a `color` keyword arg. - """ - # Matplotlib 'color' args are passed to to_rgba, which tries to read - # directly from cache and if that fails, sanitizes input, which - # raises error on receiving (colormap, idx) tuple. So we *have* to - # override cache instead of color dict itself. - rgb, alpha = key - if (not isinstance(rgb, str) and np.iterable(rgb) and len(rgb) == 2 - and isinstance(rgb[1], Number) and isinstance(rgb[0], str)): - try: - cmap = mcm.cmap_d[rgb[0]] - except (TypeError, KeyError): - pass - else: - if isinstance(cmap, ListedColormap): - if not 0 <= rgb[1] < len(cmap.colors): - raise ValueError( - f'Color cycle sample for {rgb[0]!r} cycle must be ' - f'between 0 and {len(cmap.colors)-1}, ' - f'got {rgb[1]}.' - ) - # draw color from the list of colors, using index - rgb = cmap.colors[rgb[1]] - else: - if not 0 <= rgb[1] <= 1: - raise ValueError( - f'Colormap sample for {rgb[0]!r} colormap must be ' - f'between 0 and 1, got {rgb[1]}.' - ) - # interpolate color from colormap, using key in range 0-1 - rgb = cmap(rgb[1]) - rgba = mcolors.to_rgba(rgb, alpha) - return rgba - return super().__getitem__((rgb, alpha)) - - -def Colors(*args, **kwargs): - """ - Pass all arguments to `Cycle` and return the list of colors from - the resulting `~cycler.Cycler` object. - """ - cycle = Cycle(*args, **kwargs) - return [dict_['color'] for dict_ in cycle] - - -def Colormap( - *args, name=None, listmode='perceptual', fade=None, cycle=None, - shift=None, cut=None, left=None, right=None, reverse=False, - save=False, save_kw=None, **kwargs -): - """ - Generate or retrieve colormaps and optionally merge and manipulate - them in a variety of ways. Used to interpret the `cmap` and `cmap_kw` - arguments when passed to any plotting method wrapped by - `~proplot.wrappers.cmap_changer`. - - Parameters - ---------- - *args : colormap-spec - Positional arguments that individually generate colormaps. If more than - one argument is passed, the resulting colormaps are merged. Arguments - are interpreted as follows. - - * If `~matplotlib.colors.Colormap` or a registered colormap name, the - colormap is simply returned. - * If a filename string with valid extension, the colormap data will - be loaded. See `register_cmaps` and `register_cycles`. - * If RGB tuple or color string, a `PerceptuallyUniformColormap` is - generated with `~PerceptuallyUniformColormap.from_color`. If the - string ends in ``'_r'``, the monochromatic map will be *reversed*, - i.e. will go from dark to light instead of light to dark. - * If list of RGB tuples or color strings, a - `PerceptuallyUniformColormap` is generated with - `~PerceptuallyUniformColormap.from_list`. - * If dictionary containing the keys ``'hue'``, ``'saturation'``, and - ``'luminance'``, a `PerceptuallyUniformColormap` is generated with - `~PerceptuallyUniformColormap.from_hsl`. - - name : str, optional - Name under which the final colormap is registered. It can then be - reused by passing ``cmap='name'`` to plotting functions like - `~matplotlib.axes.Axes.contourf`. - fade : float, optional - The maximum luminosity used when generating colormaps with - `PerceptuallyUniformColormap.from_color`. Default is ``100`` when - calling `Colormap` directly, and ``90`` when `Colormap` is called by - `Cycle` (this prevents having pure white in the color cycle). - - For example, ``plot.Colormap('blue', fade=80)`` generates a blue - colormap that fades to a pale blue with 80% luminance. - cycle : str or list of color-spec, optional - The registered cycle name or a list of colors used to interpret cycle - color strings like ``'C0'`` and ``'C2'`` when generating colormaps - with `PerceptuallyUniformColormap.from_color`. Default is colors - from the currently active property cycler. - - For example, ``plot.Colormap('C0', 'C1', 'C2', cycle='538')`` - generates a colormap using colors from the ``'538'`` color cycle. - listmode : {'perceptual', 'linear', 'listed'}, optional - Controls how colormaps are generated when you input list(s) of colors. - If ``'perceptual'``, a `PerceptuallyUniformColormap` is generated with - `PerceptuallyUniformColormap.from_list`. If ``'linear'``, - a `~matplotlib.colors.LinearSegmentedColormap` is generated with - `~matplotlib.colors.LinearSegmentedColormap.from_list`. If - ``'listed'``, the `~matplotlib.colors.ListedColormap` is generated. - - Default is ``'perceptual'`` when calling `Colormap` directly, and - ``'listed'`` when `Colormap` is called by `Cycle`. - cut : float, optional - Passed to `LinearSegmentedColormap.punched`. - This applies to the final *merged* colormap. - left, right : float, optional - Passed to `LinearSegmentedColormap.truncated` or - `ListedColormap.truncated`. These apply to *each colormap* - individually. - reverse : bool, optional - Passed to `LinearSegmentedColormap.reversed` or - `ListedColormap.reversed`. This applies to *each colormap* - individually. - shift : float, optional - Passed to `LinearSegmentedColormap.shifted` or - `ListedColormap.shifted`. This applies to the final *merged* colormap. - save : bool, optional - Whether to call the colormap save method, i.e. - `LinearSegmentedColormap.save` or - `ListedColormap.save`. - save_kw : dict-like, optional - Ignored if `save` is ``False``. Passed to the colormap save method, - i.e. `LinearSegmentedColormap.save` or - `ListedColormap.save`. - **kwargs - Passed to `LinearSegmentedColormap.concatenate` or - `ListedColormap.concatenate`. Each of these functions accepts - arbitrary colormap settings. - - Returns - ------- - `~matplotlib.colors.Colormap` - A `~matplotlib.colors.LinearSegmentedColormap` or - `~matplotlib.colors.ListedColormap` instance. - """ - # Initial stuff - # TODO: Play with using "qualitative" colormaps in realistic examples, - # how to make colormaps cyclic. - if not args: - raise ValueError( - f'Colormap() requires at least one positional argument.' - ) - if listmode not in ('listed', 'linear', 'perceptual'): - raise ValueError( - f'Invalid listmode={listmode!r}. Options are: ' - "'listed', 'linear', 'perceptual'." - ) - tmp = '_no_name' - cmaps = [] - for i, cmap in enumerate(args): - # Load registered colormaps and maps on file - # TODO: Document how 'listmode' also affects loaded files - if isinstance(cmap, str): - if '.' in cmap: - if listmode == 'listed': - cmap = ListedColormap.from_file(cmap) - else: - cmap = LinearSegmentedColormap.from_file(cmap) - else: - try: - cmap = mcm.cmap_d[cmap] - except KeyError: - pass - # Convert matplotlib colormaps to subclasses - if isinstance(cmap, (ListedColormap, LinearSegmentedColormap)): - pass - elif isinstance(cmap, mcolors.LinearSegmentedColormap): - cmap = LinearSegmentedColormap( - cmap.name, cmap._segmentdata, cmap.N, cmap._gamma) - elif isinstance(cmap, mcolors.ListedColormap): - cmap = ListedColormap( - cmap.colors, cmap.name, cmap.N) - # Dictionary of hue/sat/luminance values or 2-tuples representing - # linear transition - elif isinstance(cmap, dict): - cmap = PerceptuallyUniformColormap.from_hsl(tmp, **cmap) - # List of color tuples or color strings, i.e. iterable of iterables - elif (not isinstance(cmap, str) and np.iterable(cmap) - and all(np.iterable(color) for color in cmap)): - try: - colors = [to_rgb(color, cycle=cycle, alpha=True) - for color in cmap] - except (ValueError, TypeError): - raise ValueError(f'Invalid color(s) in list {cmap!r}.') - if listmode == 'listed': - cmap = ListedColormap(colors, tmp) - elif listmode == 'linear': - cmap = LinearSegmentedColormap.from_list(tmp, colors) - else: - cmap = PerceptuallyUniformColormap.from_list(tmp, colors) - # Monochrome colormap from input color - else: - ireverse = (isinstance(cmap, str) and cmap[-2:] == '_r') - if ireverse: - cmap = cmap[:-2] - try: - color = to_rgb(cmap, cycle=cycle, alpha=True) - except (ValueError, TypeError): - msg = f'Invalid cmap, cycle, or color {cmap!r}.' - if isinstance(cmap, str): - msg += ( - f'\nValid cmap and cycle names: ' - + ', '.join(map(repr, sorted(mcm.cmap_d))) + '.' - f'\nValid color names: ' - + ', '.join(map(repr, sorted( - mcolors.colorConverter.colors)) - ) + '.' - ) - raise ValueError(msg) - cmap = PerceptuallyUniformColormap.from_color(tmp, color, fade) - if ireverse: - cmap = cmap.reversed() - - # Cut the edges and/or reverse the map - if left is not None or right is not None: - cmap = cmap.truncated(left, right) - if reverse: - cmap = cmap.reversed() - cmaps.append(cmap) - - # Merge the result of this arbitrary user input - if len(cmaps) > 1: # more than one map? - cmap = cmaps[0].concatenate(*cmaps[1:], **kwargs) - elif kwargs: # modify any props? - cmap = cmaps[0].updated(**kwargs) - - # Cut the center and roate the colormap - if cut is not None: - cmap = cmap.punched(cut) - if shift is not None: - cmap = cmap.shifted(shift) - - # Initialize - if not cmap._isinit: - cmap._init() - - # Register and save the colormap - if name is None: - name = cmap.name # may have been modified by e.g. .shifted() - else: - cmap.name = name - mcm.cmap_d[name] = cmap - if save: - save_kw = save_kw or {} - cmap.save(**save_kw) - return cmap - - -def Cycle( - *args, N=None, name=None, - marker=None, alpha=None, dashes=None, linestyle=None, linewidth=None, - markersize=None, markeredgewidth=None, - markeredgecolor=None, markerfacecolor=None, - save=False, save_kw=None, **kwargs -): - """ - Generate and merge `~cycler.Cycler` instances in a variety of ways. - Used to interpret the `cycle` and `cycle_kw` arguments when passed to - any plotting method wrapped by - `~proplot.wrappers.cycle_changer`. - - If you just want a list of colors instead of a `~cycler.Cycler` instance, - use the `colors` function. If you want a `~cycler.Cycler` instance that - imposes black as the default color and cycles through properties like - ``linestyle`` instead, call this function without any positional arguments. - - Parameters - ---------- - *args : colormap-spec or cycle-spec, optional - Positional arguments control the *colors* in the `~cycler.Cycler` - object. If more than one argument is passed, the resulting cycles are - merged. Arguments are interpreted as follows. - - * If `~cycler.Cycler`, nothing more - is done. - * If list of RGB tuples or color strings, these - colors are used. - * If `~matplotlib.colors.ListedColormap`, colors from the ``colors`` - attribute are used. - * If string color cycle name, that `~matplotlib.colors.ListedColormap` - is looked up and its ``colors`` attribute is used. See `cycles`. - * Otherwise, the argument is passed to `Colormap`, and colors - from the resulting `~matplotlib.colors.LinearSegmentedColormap` - are used. See the `N` argument. - - If the last positional argument is numeric, it is used for the `N` - keyword argument. - N : float or list of float, optional - For `~matplotlib.colors.ListedColormap`\ s, this is the number of - colors to select. For example, ``Cycle('538', 4)`` returns the first 4 - colors of the ``'538'`` color cycle. - - For `~matplotlib.colors.LinearSegmentedColormap`\ s, this is either - a *list of sample coordinates* used to draw colors from the map, or an - *integer number of colors* to draw. If the latter, the sample - coordinates are ``np.linspace(0, 1, samples)``. For example, - ``Cycle('Reds', 5)`` divides the ``'Reds'`` colormap into five evenly - spaced colors. - name : str, optional - Name of the resulting `~matplotlib.colors.ListedColormap` used to - register the color cycle. Default name is ``'no_name'``. - marker, alpha, dashes, linestyle, linewidth, markersize, markeredgewidth, markeredgecolor, markerfacecolor : list of specs, optional - Lists of `~matplotlib.lines.Line2D` properties that can be added to - the `~cycler.Cycler` instance. If the lists have unequal length, they - will be filled to match the length of the longest list. See - `~matplotlib.axes.Axes.set_prop_cycle` for more info on cyclers. - Also see the `line style reference \ -`__, - the `marker reference \ -`__, - and the `custom dashes reference \ -`__. - save : bool, optional - Whether to save the `ListedColormap` associated with this cycle. - See `ListedColormap.save`. - save_kw : dict-like, optional - Ignored if `save` is ``False``. Passed to `ListedColormap.save` - for the `ListedColormap` associated with this cycle. - **kwargs - Passed to `Colormap` when the input is not already a `~cycler.Cycler` - instance. - - Returns - ------- - `~cycler.Cycler` - A cycler instance that can be passed to - `~matplotlib.axes.Axes.set_prop_cycle`. - """ # noqa - # Add properties - props = {} - nprops = 0 - for key, value in ( - ('marker', marker), - ('alpha', alpha), - ('dashes', dashes), - ('linestyle', linestyle), - ('linewidth', linewidth), - ('markersize', markersize), - ('markeredgewidth', markeredgewidth), - ('markeredgecolor', markeredgecolor), - ('markerfacecolor', markerfacecolor), - ): - if value is not None: - if isinstance(value, str) or not np.iterable(value): - raise ValueError( - f'Invalid {key!r} property {value!r}. ' - f'Must be list or tuple of properties.' - ) - nprops = max(nprops, len(value)) - props[key] = [*value] # ensure mutable list - # If args is non-empty, means we want color cycle; otherwise is black - if not args: - props['color'] = ['k'] # ensures property cycler is non empty - if kwargs: - _warn_proplot(f'Ignoring Cycle() keyword arg(s) {kwargs}.') - # Merge cycler objects - elif all(isinstance(arg, cycler.Cycler) for arg in args): - if kwargs: - _warn_proplot(f'Ignoring Cycle() keyword arg(s) {kwargs}.') - if len(args) == 1: - return args[0] - else: - props = {} - for arg in args: - for key, value in arg.by_key(): - if key not in props: - props[key] = [] - props[key].extend([*value]) - return cycler.cycler(**props) - # Build and register a ListedColormap - else: - # Collect samples - if args and isinstance(args[-1], Number): - # means we want to sample existing colormaps or cycles - args, N = args[:-1], args[-1] - kwargs.setdefault('fade', 90) - kwargs.setdefault('listmode', 'listed') - cmap = Colormap(*args, **kwargs) # the cmap object itself - if isinstance(cmap, ListedColormap): - colors = cmap.colors[:N] # if N is None, does nothing - else: - N = _notNone(N, 10) - if isinstance(N, Integral): - x = np.linspace(0, 1, N) # from edge to edge - elif np.iterable(N) and all( - isinstance(item, Number) for item in N): - x = np.array(N) - else: - raise ValueError(f'Invalid samples {N!r}.') - N = len(x) - colors = cmap(x) - - # Register and save the samples as a ListedColormap - name = name or '_no_name' - cmap = ListedColormap(colors, name=name, N=N) - mcm.cmap_d[name] = cmap - if save: - save_kw = save_kw or {} - cmap.save(**save_kw) - - # Add to property dict - nprops = max(nprops, len(colors)) - props['color'] = [ - tuple(color) if not isinstance(color, str) else color - for color in cmap.colors - ] # save the tupled version! - - # Build cycler, make sure lengths are the same - for key, value in props.items(): - if len(value) < nprops: - value[:] = [ - value[i % len(value)] for i in range(nprops) - ] # make loop double back - cycle = cycler.cycler(**props) - cycle.name = name - return cycle - - -def Norm(norm, levels=None, **kwargs): - """ - Return an arbitrary `~matplotlib.colors.Normalize` instance. - Used to interpret the `norm` and `norm_kw` arguments when passed to any - plotting method wrapped by `~proplot.wrappers.cmap_changer`. - - Parameters - ---------- - norm : str or `~matplotlib.colors.Normalize` - Key name for the normalizer. The recognized normalizer key names - are as follows. - - =============================== =============================== - Key(s) Class - =============================== =============================== - ``'midpoint'``, ``'zero'`` `MidpointNorm` - ``'segmented'``, ``'segments'`` `LinearSegmentedNorm` - ``'null'``, ``'none'`` `~matplotlib.colors.NoNorm` - ``'linear'`` `~matplotlib.colors.Normalize` - ``'log'`` `~matplotlib.colors.LogNorm` - ``'power'`` `~matplotlib.colors.PowerNorm` - ``'symlog'`` `~matplotlib.colors.SymLogNorm` - =============================== =============================== - - levels : array-like, optional - Level *edges*, passed to `LinearSegmentedNorm` or used to determine - the `vmin` and `vmax` arguments for `MidpointNorm`. - **kwargs - Passed to the `~matplotlib.colors.Normalize` initializer. - See `this tutorial \ -`__ - for more info. - - Returns - ------- - `~matplotlib.colors.Normalize` - A `~matplotlib.colors.Normalize` instance. - """ - if isinstance(norm, mcolors.Normalize): - return norm - if isinstance(norm, str): - # Get class - norm_out = normalizers.get(norm, None) - if norm_out is None: - raise ValueError( - f'Unknown normalizer {norm!r}. Options are: ' - + ', '.join(map(repr, normalizers.keys())) + '.' - ) - # Instantiate class - if norm_out is LinearSegmentedNorm: - if not np.iterable(levels): - raise ValueError( - f'Need levels for normalizer {norm!r}. ' - f'Received levels={levels!r}.' - ) - kwargs.update({'levels': levels}) - norm_out = norm_out(**kwargs) # initialize - else: - raise ValueError(f'Unknown norm {norm_out!r}.') - return norm_out - - -class BinNorm(mcolors.BoundaryNorm): - """ - This normalizer is used for all colormap plots. It can be thought of as a - "meta-normalizer": It first scales the data according to any - arbitrary `~matplotlib.colors.Normalize` class, then maps the normalized - values ranging from 0-1 into **discrete** levels. - - Consider input levels of ``[0, 3, 6, 9, 12, 15]``. The algorithm is - as follows. - - 1. `levels` are normalized according to the input normalizer `norm`. - If it is ``None``, they are not changed. Possible normalizers include - `~matplotlib.colors.LogNorm`, which makes color transitions linear in - the logarithm of the value, or `LinearSegmentedNorm`, which makes - color transitions linear in the *index* of the level array. - 2. Possible colormap coordinates, corresponding to bins delimited by the - normalized `levels` array, are calculated. In this case, the bin - centers are simply ``[1.5, 4.5, 7.5, 10.5, 13.5]``, which gives us - normalized colormap coordinates of ``[0, 0.25, 0.5, 0.75, 1]``. - 3. Out-of-bounds coordinates are added. These depend on the value of the - `extend` keyword argument. For `extend` equal to ``'neither'``, - the coordinates including out-of-bounds values are - ``[0, 0, 0.25, 0.5, 0.75, 1, 1]`` -- out-of-bounds values have the same - color as the nearest in-bounds values. For `extend` equal to ``'both'``, - the bins are ``[0, 0.16, 0.33, 0.5, 0.66, 0.83, 1]`` -- - out-of-bounds values are given distinct colors. This makes sure your - colorbar always shows the *full range of colors* in the colormap. - 4. Whenever `BinNorm.__call__` is invoked, the input value normalized by - `norm` is compared against the normalized `levels` array. Its bin index - is determined with `numpy.searchsorted`, and its corresponding - colormap coordinate is selected using this index. - - """ - # See this post: https://stackoverflow.com/a/48614231/4970632 - # WARNING: Must be child of BoundaryNorm. Many methods in ColorBarBase - # test for class membership, crucially including _process_values(), which - # if it doesn't detect BoundaryNorm will try to use BinNorm.inverse(). - def __init__( - self, levels, norm=None, clip=False, - step=1.0, extend=None, - ): - """ - Parameters - ---------- - levels : list of float - The discrete data levels. - norm : `~matplotlib.colors.Normalize`, optional - The normalizer used to transform `levels` and all data passed - to `BinNorm.__call__` *before* discretization. - step : float, optional - The intensity of the transition to out-of-bounds color, as a - faction of the *average* step between in-bounds colors. - Default is ``1``. - extend : {'neither', 'both', 'min', 'max'}, optional - Which direction colors will be extended. No matter the `extend` - option, `BinNorm` ensures colors always extend through the - extreme end colors. - clip : bool, optional - Whether to clip values falling outside of the level bins. This - only has an effect on lower colors when extend is - ``'min'`` or ``'both'``, and on upper colors when extend is - ``'max'`` or ``'both'``. - - Note - ---- - If you are using a diverging colormap with ``extend='max'`` or - ``extend='min'``, the center will get messed up. But that is very - strange usage anyway... so please just don't do that :) - """ - # NOTE: This must be a subclass BoundaryNorm, so ColorbarBase will - # detect it... even though we completely override it. - # Check input levels - levels = np.atleast_1d(levels) - diffs = np.sign(np.diff(levels)) - self._descending = False - if levels.ndim != 1: - raise ValueError('Levels must be 1-dimensional.') - elif levels.size < 2: - raise ValueError('Need at least two levels.') - elif all(diffs == -1): - self._descending = True - levels = levels[::-1] - elif not all(diffs == 1): - raise ValueError( - f'Levels {levels!r} must be monotonically increasing.' - ) - - # Check input extend - extend = extend or 'neither' - extends = ('both', 'min', 'max', 'neither') - if extend not in extends: - raise ValueError( - f'Unknown extend option {extend!r}. Options are: ' - + ', '.join(map(repr, extends)) + '.' - ) - - # Check input normalizer - if not norm: - norm = mcolors.Normalize() - elif not isinstance(norm, mcolors.Normalize): - raise ValueError( - 'Normalizer must be matplotlib.colors.Normalize, ' - f'got {type(norm)}.' - ) - elif isinstance(norm, mcolors.BoundaryNorm): - raise ValueError( - f'Normalizer cannot be an instance of ' - 'matplotlib.colors.BoundaryNorm.' - ) - - # Normalize the level boundaries and get color coordinates - # corresponding to each bin. - x_b = norm(levels) - if isinstance(x_b, ma.core.MaskedArray): - x_b = x_b.filled(np.nan) - mask = np.isfinite(x_b) - if mask.sum() < 2: - raise ValueError( - f'Normalizer {norm!r} converted {levels!r} to {x_b!r}, ' - 'but we need at least *2* valid levels to continue.' - ) - x_b = x_b[mask] - x_m = (x_b[1:] + x_b[:-1]) / 2 # get level centers after norm scaling - y = (x_m - x_m.min()) / (x_m.max() - x_m.min()) - - # Get extra 2 color coordinates for out-of-bounds colors - # For *same* out-of-bounds colors, looks like [0, 0, ..., 1, 1] - # For *unique* out-of-bounds colors, looks like [0, X, ..., 1 - X, 1] - offset = 0 - scale = 1 - if extend == 'max': - scale = 1 - step / y.size - elif extend == 'min': - offset = step / y.size - scale = 1 - offset - elif extend == 'both': - offset = step / (y.size + 1) - scale = 1 - 2 * offset - y = np.concatenate(([0], offset + scale * y, [1])) - - # Builtin properties - # NOTE: With extend='min' the minimimum in-bounds and out-of-bounds - # colors are the same so clip=True will have no effect. Same goes - # for extend='max' with maximum colors. - self.N = levels.size - self.clip = clip - self.boundaries = levels - self.vmin = levels.min() - self.vmax = levels.max() - - # Extra properties - # WARNING: For some reason must clip manually for LogNorm, or - # end up with unpredictable fill value, weird "out-of-bounds" colors - self._norm = norm - self._x_b = x_b - self._y = y - self._bmin = self.vmin + (levels[1] - levels[0]) / 2 - self._bmax = self.vmax - (levels[-1] - levels[-2]) / 2 - if isinstance(norm, mcolors.LogNorm): - self._clip_norm = (5e-249, None) - else: - self._clip_norm = None - - def __call__(self, value, clip=None): - """ - Normalize data values to 0-1. - - Parameters - ---------- - value : numeric - The data to be normalized. - clip : bool, optional - Whether to clip values falling outside of the level bins. - Default is ``self.clip``. - """ - # Follow example of LinearSegmentedNorm, but perform no interpolation, - # just use searchsorted to bin the data. - clip_norm = self._clip_norm - if clip_norm: # special extra clipping due to normalizer - value = np.clip(value, *clip_norm) - if clip is None: # builtin clipping - clip = self.clip - if clip: # note that np.clip can handle masked arrays - value = np.clip(value, self._bmin, self._bmax) - xq = self._norm(value) - yq = self._y[np.searchsorted(self._x_b, xq)] - if self._descending: - yq = 1 - yq - mask = ma.getmaskarray(xq) - return ma.array(yq, mask=mask) - - def inverse(self, value): # noqa: U100 - """ - Raise an error. Inversion after discretization is impossible. - """ - raise ValueError('BinNorm is not invertible.') - - -class LinearSegmentedNorm(mcolors.Normalize): - """ - This is the default normalizer paired with `BinNorm` whenever `levels` - are non-linearly spaced. The normalized value is linear with respect to - its average index in the `levels` vector, allowing uniform color - transitions across arbitrarily spaced monotonically increasing values. - Can be explicitly used by passing ``norm='segmented'`` to any command - accepting ``cmap``. - """ - def __init__(self, levels, vmin=None, vmax=None, clip=False): - """ - Parameters - ---------- - levels : list of float - The discrete data levels. - vmin, vmax : None - Ignored. `vmin` and `vmax` are set to the minimum and - maximum of `levels`. - clip : bool, optional - Whether to clip values falling outside of the minimum and - maximum levels. - """ - levels = np.atleast_1d(levels) - diffs = np.sign(np.diff(levels)) - y = np.linspace(0, 1, len(levels)) - self._descending = False - if levels.ndim != 1: - raise ValueError('Levels must be 1-dimensional.') - elif levels.size < 2: - raise ValueError('Need at least two levels.') - elif all(diffs == -1): - self._descending = True - levels = levels[::-1] - y = y[::-1] - elif not all(diffs == 1): - raise ValueError( - f'Levels {levels!r} must be monotonically increasing.' - ) - vmin, vmax = levels.min(), levels.max() - super().__init__(vmin, vmax, clip=clip) # second level superclass - self._x = levels - self._y = y - - def __call__(self, value, clip=None): - """ - Normalize the data values to 0-1. Inverse - of `~LinearSegmentedNorm.inverse`. - - Parameters - ---------- - value : numeric - The data to be normalized. - clip : bool, optional - Whether to clip values falling outside of the minimum and - maximum levels. Default is ``self.clip``. - """ - # Follow example of make_mapping_array for efficient, vectorized - # linear interpolation across multiple segments. - # * Normal test puts values at a[i] if a[i-1] < v <= a[i]; for - # left-most data, satisfy a[0] <= v <= a[1] - # * searchsorted gives where xq[i] must be inserted so it is larger - # than x[ind[i]-1] but smaller than x[ind[i]] - if clip is None: # builtin clipping - clip = self.clip - if clip: # note that np.clip can handle masked arrays - value = np.clip(value, self.vmin, self.vmax) - x = self._x # from arbitrarily spaced monotonic levels - y = self._y # to linear range 0-1 - xq = np.atleast_1d(value) - idx = np.searchsorted(x, xq) - idx[idx == 0] = 1 - idx[idx == len(x)] = len(x) - 1 - distance = (xq - x[idx - 1]) / (x[idx] - x[idx - 1]) - yq = distance * (y[idx] - y[idx - 1]) + y[idx - 1] - if self._descending: - yq = 1 - yq - mask = ma.getmaskarray(xq) - return ma.array(yq, mask=mask) - - def inverse(self, value): - """ - Inverse operation of `~LinearSegmentedNorm.__call__`. - - Parameters - ---------- - value : numeric - The data to be un-normalized. - """ - x = self._x - y = self._y - yq = np.atleast_1d(value) - idx = np.searchsorted(y, yq) - idx[idx == 0] = 1 - idx[idx == len(y)] = len(y) - 1 - distance = (yq - y[idx - 1]) / (y[idx] - y[idx - 1]) - xq = distance * (x[idx] - x[idx - 1]) + x[idx - 1] - mask = ma.getmaskarray(yq) - return ma.array(xq, mask=mask) - - -class MidpointNorm(mcolors.Normalize): - """ - Ensures a "midpoint" always lies at the central colormap color. - Can be used by passing ``norm='midpoint'`` to any command accepting - ``cmap``. - """ - def __init__(self, midpoint=0, vmin=-1, vmax=1, clip=None): - """ - Parameters - ---------- - midpoint : float, optional - The midpoint, i.e. the data value corresponding to the position - in the middle of the colormap. The default is ``0``. - vmin, vmax : float, optional - The minimum and maximum data values. The defaults are ``-1`` - and ``1``, respectively. - clip : bool, optional - Whether to clip values falling outside of `vmin` and `vmax`. - """ - # Bigger numbers are too one-sided - super().__init__(vmin, vmax, clip) - self._midpoint = midpoint - if self.vmin >= self._midpoint or self.vmax <= self._midpoint: - raise ValueError( - f'Midpoint {self._midpoint} outside of vmin {self.vmin} ' - f'and vmax {self.vmax}.' - ) - - def __call__(self, value, clip=None): - """ - Normalize data values to 0-1. - - Parameters - ---------- - value : numeric - The data to be normalized. - clip : bool, optional - Whether to clip values falling outside of `vmin` and `vmax`. - Default is ``self.clip``. - """ - # Get middle point in normalized coords - if clip is None: # builtin clipping - clip = self.clip - if clip: # note that np.clip can handle masked arrays - value = np.clip(value, self.vmin, self.vmax) - x = np.array([self.vmin, self._midpoint, self.vmax]) - y = np.array([0, 0.5, 1]) - xq = np.atleast_1d(value) - idx = np.searchsorted(x, xq) - idx[idx == 0] = 1 # get normed value <0 - idx[idx == len(x)] = len(x) - 1 # get normed value >0 - distance = (xq - x[idx - 1]) / (x[idx] - x[idx - 1]) - yq = distance * (y[idx] - y[idx - 1]) + y[idx - 1] - mask = ma.getmaskarray(xq) - return ma.array(yq, mask=mask) - - def inverse(self, value): - """ - Inverse operation of `~MidpointNorm.__call__`. - - Parameters - ---------- - value : numeric - The data to be un-normalized. - """ - # Invert the above - # x, y = [self.vmin, self._midpoint, self.vmax], [0, 0.5, 1] - # return ma.masked_array(np.interp(yq, y, x)) - # Performs inverse operation of __call__ - x = np.array([self.vmin, self._midpoint, self.vmax]) - y = np.array([0, 0.5, 1]) - yq = np.atleast_1d(value) - ind = np.searchsorted(y, yq) - ind[ind == 0] = 1 - ind[ind == len(y)] = len(y) - 1 - distance = (yq - y[ind - 1]) / (y[ind] - y[ind - 1]) - xq = distance * (x[ind] - x[ind - 1]) + x[ind - 1] - mask = ma.getmaskarray(yq) - return ma.array(xq, mask=mask) - - -def _get_data_paths(dirname): - """ - Return data folder paths in reverse order of precedence. - """ - # When loading colormaps, cycles, and colors, files in the latter - # directories overwrite files in the former directories. When loading - # fonts, the resulting paths need to be *reversed*. - return [ - os.path.join(os.path.dirname(__file__), dirname), - os.path.join(os.path.expanduser('~'), '.proplot', dirname) - ] - - -def _from_file(filename, listed=False, warn_on_failure=False): - """ - Read generalized colormap and color cycle files. - """ - filename = os.path.expanduser(filename) - if os.path.isdir(filename): # no warning - return - - # Warn if loading failed during `register_cmaps` or `register_cycles` - # but raise error if user tries to load a file. - def _warn_or_raise(msg, error=RuntimeError): - if warn_on_failure: - _warn_proplot(msg) - else: - raise error(msg) - - # Directly read segmentdata json file - # NOTE: This is special case! Immediately return name and cmap - if not os.path.exists(filename): - _warn_or_raise(f'File {filename!r} not found.', FileNotFoundError) - return - N = rcParams['image.lut'] - name, ext = os.path.splitext(os.path.basename(filename)) - ext = ext[1:] - cmap = None - if ext == 'json': - try: - with open(filename, 'r') as f: - data = json.load(f) - except json.JSONDecodeError: - _warn_or_raise( - f'Failed to load {filename!r}.', json.JSONDecodeError - ) - return - kw = {} - for key in ('cyclic', 'gamma', 'gamma1', 'gamma2', 'space'): - kw[key] = data.pop(key, None) - if 'red' in data: - cmap = LinearSegmentedColormap(name, data, N=N) - else: - cmap = PerceptuallyUniformColormap(name, data, N=N, **kw) - if name[-2:] == '_r': - cmap = cmap.reversed(name[:-2]) - - # Read .rgb and .rgba files - elif ext in ('txt', 'rgb'): - # Load - # NOTE: This appears to be biggest import time bottleneck! Increases - # time from 0.05s to 0.2s, with numpy loadtxt or with this regex thing. - delim = re.compile(r'[,\s]+') - data = [ - delim.split(line.strip()) - for line in open(filename).readlines() - if line.strip() and line.strip()[0] != '#' - ] - try: - data = [[float(num) for num in line] for line in data] - except ValueError: - _warn_or_raise( - f'Failed to load {filename!r}. Expected a table of comma ' - 'or space-separated values.' - ) - return - # Build x-coordinates and standardize shape - data = np.array(data) - if data.shape[1] not in (3, 4): - _warn_or_raise( - f'Failed to load {filename!r}. Got {data.shape[1]} columns, ' - f'but expected 3 or 4.' - ) - return - if ext[0] != 'x': # i.e. no x-coordinates specified explicitly - x = np.linspace(0, 1, data.shape[0]) - else: - x, data = data[:, 0], data[:, 1:] - - # Load XML files created with scivizcolor - # Adapted from script found here: - # https://sciviscolor.org/matlab-matplotlib-pv44/ - elif ext == 'xml': - try: - doc = ElementTree.parse(filename) - except ElementTree.ParseError: - _warn_or_raise( - f'Failed to load {filename!r}. Parsing error.', - ElementTree.ParseError - ) - return - x, data = [], [] - for s in doc.getroot().findall('.//Point'): - # Verify keys - if any(key not in s.attrib for key in 'xrgb'): - _warn_or_raise( - f'Failed to load {filename!r}. Missing an x, r, g, or b ' - 'specification inside one or more tags.' - ) - return - # Get data - color = [] - for key in 'rgbao': # o for opacity - if key not in s.attrib: - continue - color.append(float(s.attrib[key])) - x.append(float(s.attrib['x'])) - data.append(color) - # Convert to array - if not all( - len(data[0]) == len(color) and len(color) in (3, 4) - for color in data - ): - _warn_or_raise( - f'Failed to load {filename!r}. Unexpected number of channels ' - 'or mixed channels across tags.' - ) - return - - # Read hex strings - elif ext == 'hex': - # Read arbitrary format - string = open(filename).read() # into single string - data = re.findall('#[0-9a-fA-F]{6}', string) # list of strings - if len(data) < 2: - _warn_or_raise( - f'Failed to load {filename!r}. Hex strings not found.' - ) - return - # Convert to array - x = np.linspace(0, 1, len(data)) - data = [to_rgb(color) for color in data] - else: - _warn_or_raise( - f'Colormap or cycle file {filename!r} has unknown extension.' - ) - return - - # Standardize and reverse if necessary to cmap - # TODO: Document the fact that filenames ending in _r return a reversed - # version of the colormap stored in that file. - if not cmap: - x, data = np.array(x), np.array(data) - # for some reason, some aren't in 0-1 range - x = (x - x.min()) / (x.max() - x.min()) - if (data > 2).any(): # from 0-255 to 0-1 - data = data / 255 - if name[-2:] == '_r': - name = name[:-2] - data = data[::-1, :] - x = 1 - x[::-1] - if listed: - cmap = ListedColormap(data, name, N=len(data)) - else: - data = [(x, color) for x, color in zip(x, data)] - cmap = LinearSegmentedColormap.from_list(name, data, N=N) - - # Return colormap or data - return cmap - - -@_timer -def register_cmaps(): - """ - Register colormaps packaged with ProPlot or saved to the - ``~/.proplot/cmaps`` folder. This is called on import. Maps are registered - according to their filenames -- for example, ``name.xyz`` will be - registered as ``'name'``. - - For a table of valid extensions, see `LinearSegmentedColormap.from_file`. - To visualize the registered colormaps, use `show_cmaps`. - """ - for i, path in enumerate(_get_data_paths('cmaps')): - for filename in sorted(glob.glob(os.path.join(path, '*'))): - cmap = LinearSegmentedColormap.from_file( - filename, warn_on_failure=True - ) - if not cmap: - continue - if i == 0 and cmap.name.lower() in ( - 'phase', 'graycycle', 'romao', 'broco', 'corko', 'viko', - ): - cmap._cyclic = True - mcm.cmap_d[cmap.name] = cmap - - -@_timer -def register_cycles(): - """ - Register color cycles packaged with ProPlot or saved to the - ``~/.proplot/cycles`` folder. This is called on import. Cycles are - registered according to their filenames -- for example, ``name.hex`` will - be registered under the name ``'name'`` as a - `~matplotlib.colors.ListedColormap` map (see `Cycle` for details). - - For a table of valid extensions, see `ListedColormap.from_file`. - To visualize the registered colormaps, use `show_cmaps`. - """ - for path in _get_data_paths('cycles'): - for filename in sorted(glob.glob(os.path.join(path, '*'))): - cmap = ListedColormap.from_file( - filename, warn_on_failure=True - ) - if not cmap: - continue - if isinstance(cmap, LinearSegmentedColormap): - cmap = ListedColormap(colors(cmap), name=cmap.name) - mcm.cmap_d[cmap.name] = cmap - cycles.append(cmap.name) - - -@_timer -def register_colors(nmax=np.inf): - """ - Add color names packaged with ProPlot or saved to the ``~/.proplot/colors`` - folder. ProPlot loads the crowd-sourced XKCD color - name database, Crayola crayon color database, and any user input - files, then filters them to be "perceptually distinct" in the HCL - colorspace. Files must just have one line per color in the format - ``name : hex``. Whitespace is ignored. - - This is called on import. Use `show_colors` to generate a table of the - resulting colors. - """ - # Reset native colors dictionary and add some default groups - # Add in CSS4 so no surprises for user, but we will not encourage this - # usage and will omit CSS4 colors from the demo table. - colors.clear() - base = {} - base.update(mcolors.BASE_COLORS) - base.update(COLORS_BASE) # full names - mcolors.colorConverter.colors.clear() # clean out! - mcolors.colorConverter.cache.clear() # clean out! - for name, dict_ in (('base', base), ('css', mcolors.CSS4_COLORS)): - mcolors.colorConverter.colors.update(dict_) - colors[name] = sorted(dict_) - - # Load colors from file and get their HCL values - dicts = {} - seen = {*base} # never overwrite base names, e.g. 'blue' and 'b'! - hcls = [] - data = [] - for i, path in enumerate(_get_data_paths('colors')): - if i == 0: - paths = [ # be explicit because categories matter! - os.path.join(path, base) - for base in ('xkcd.txt', 'crayola.txt', 'opencolor.txt') - ] - else: - paths = sorted(glob.glob(os.path.join(path, '*.txt'))) - for file in paths: - cat, _ = os.path.splitext(os.path.basename(file)) - with open(file, 'r') as f: - cnt = 0 - hex = re.compile( - r'\A#(?:[0-9a-fA-F]{3}){1,2}\Z' # ?: prevents capture - ) - pairs = [] - for line in f.readlines(): - cnt += 1 - stripped = line.strip() - if not stripped or stripped[0] == '#': - continue - pair = tuple(item.strip() for item in line.split(':')) - if len(pair) != 2 or not hex.match(pair[1]): - _warn_proplot( - f'Illegal line #{cnt} in file {file!r}:\n' - f'{line!r}\n' - f'Lines must be formatted as "name: hexcolor".' - ) - continue - pairs.append(pair) - - # Categories for which we add *all* colors - if cat == 'opencolor' or i == 1: - dict_ = {name: color for name, color in pairs} - mcolors.colorConverter.colors.update(dict_) - colors[cat] = sorted(dict_) - continue - - # Filter remaining colors to *unique* colors - j = 0 - if cat not in dicts: - dicts[cat] = {} - for name, color in pairs: # is list of name, color tuples - j += 1 - if j > nmax: # e.g. for xkcd colors - break - for regex, sub in COLORS_TRANSLATIONS: - name = regex.sub(sub, name) - if name in seen or COLORS_IGNORE.search(name): - continue - seen.add(name) - hcls.append(to_xyz(color, space=COLORS_SPACE)) - data.append((cat, name, color)) # category name pair - - # Remove colors that are 'too similar' by rounding to the nearest n units - # WARNING: Unique axis argument requires numpy version >=1.13 - hcls = np.array(hcls) - if hcls.size > 0: - hcls = hcls / np.array([360, 100, 100]) - hcls = np.round(hcls / COLORS_THRESH).astype(np.int64) - _, idxs, _ = np.unique( - hcls, return_index=True, return_counts=True, axis=0) - for idx, (cat, name, color) in enumerate(data): - if name in COLORS_INCLUDE or idx in idxs: - dicts[cat][name] = color - for cat, dict_ in dicts.items(): - mcolors.colorConverter.colors.update(dict_) - colors[cat] = sorted(dict_) - - -@_timer -def register_fonts(): - """ - Add fonts packaged with ProPlot or saved to the ``~/.proplot/fonts`` - folder, if they are not already added. Detects ``.ttf`` and ``.otf`` files - -- see `this link \ -`__ - for a guide on converting various other font file types to ``.ttf`` and - ``.otf`` for use with matplotlib. - """ - # Add proplot path to TTFLIST and rebuild cache *only if necessary* - # * Nice gallery of sans-serif fonts: - # https://www.lifewire.com/classic-sans-serif-fonts-clean-appearance-1077406 # noqa - # * Sources for downloading more fonts: - # https://fonts.google.com/?category=Sans+Serif - # https://www.cufonfonts.com - # WARNING: If you include a font file with an unrecognized style, - # matplotlib may use that font instead of the 'normal' one! Valid styles: - # 'ultralight', 'light', 'normal', 'regular', 'book', 'medium', 'roman', - # 'semibold', 'demibold', 'demi', 'bold', 'heavy', 'extra bold', 'black' - # https://matplotlib.org/api/font_manager_api.html - # For macOS the only fonts with 'Thin' in one of the .ttf file names - # are Helvetica Neue and .SF NS Display Condensed. Never try to use these! - paths = ':'.join(_get_data_paths('fonts')[::-1]) # user paths come first - if 'TTFPATH' not in os.environ: - os.environ['TTFPATH'] = paths - elif paths not in os.environ['TTFPATH']: - os.environ['TTFPATH'] += (':' + paths) - - # Detect user-input .ttc fonts - import matplotlib.font_manager as mfonts - fnames_proplot = {*mfonts.findSystemFonts(paths.split(':'))} - fnames_proplot_ttc = { - file for file in fnames_proplot if os.path.splitext(file)[1] == '.ttc' - } - if fnames_proplot_ttc: - _warn_proplot( - 'Ignoring the following .ttc fonts because they cannot be ' - 'saved into PDF or EPS files (see matplotlib issue #3135): ' - + ', '.join(map(repr, sorted(fnames_proplot_ttc))) - + '. Please consider expanding them into separate .ttf files.' - ) - - # Rebuild font cache only if necessary! Can be >50% of total import time! - fnames_all = {font.fname for font in mfonts.fontManager.ttflist} - fnames_proplot -= fnames_proplot_ttc - if not fnames_all >= fnames_proplot: - _warn_proplot('Rebuilding font cache.') - if hasattr(mfonts.fontManager, 'addfont'): - for fname in fnames_proplot: - mfonts.fontManager.addfont(fname) - mfonts.json_dump(mfonts.fontManager, mfonts._fmcache) - else: - mfonts._rebuild() - - # Remove ttc files *after* rebuild - mfonts.fontManager.ttflist = [ - font for font in mfonts.fontManager.ttflist - if os.path.splitext(font.fname)[1] != '.ttc' - ] - - # Populate font name lists, with proplot fonts *first* - fonts_proplot = sorted({ - font.name for font in mfonts.fontManager.ttflist - if any(path in font.fname for path in paths.split(':')) - }) - fonts_system = sorted({ - font.name for font in mfonts.fontManager.ttflist - if not any(path in font.fname for path in paths.split(':')) - }) - fonts[:] = [*fonts_proplot, *fonts_system] - - -def _draw_bars( - names, *, source, unknown='User', length=4.0, width=0.2, N=None -): - """ - Draw colorbars for "colormaps" and "color cycles". This is called by - `show_cycles` and `show_cmaps`. - """ - # Categorize the input names - cmapdict = {} - names_all = list(map(str.lower, names)) - names_known = list(map(str.lower, sum(map(list, source.values()), []))) - names_unknown = [name for name in names_all if name not in names_known] - if unknown and names_unknown: - cmapdict[unknown] = names_unknown - for cat, names in source.items(): - names_cat = [name for name in names if name.lower() in names_all] - if names_cat: - cmapdict[cat] = names_cat - - # Draw figure - from . import subplots - naxs = len(cmapdict) + sum(map(len, cmapdict.values())) - fig, axs = subplots( - nrows=naxs, axwidth=length, axheight=width, - share=0, hspace=0.03, - ) - iax = -1 - nheads = nbars = 0 # for deciding which axes to plot in - for cat, names in cmapdict.items(): - nheads += 1 - for imap, name in enumerate(names): - iax += 1 - if imap + nheads + nbars > naxs: - break - ax = axs[iax] - if imap == 0: # allocate this axes for title - iax += 1 - ax.set_visible(False) - ax = axs[iax] - cmap = mcm.cmap_d[name] - if N is not None: - cmap = cmap.updated(N=N) - ax.colorbar( # TODO: support this in public API - cmap, loc='_fill', - orientation='horizontal', locator='null', linewidth=0 - ) - ax.text( - 0 - (rcParams['axes.labelpad'] / 72) / length, 0.45, name, - ha='right', va='center', transform='axes', - ) - if imap == 0: - ax.set_title(cat) - nbars += len(names) - return fig - - -def show_channels( - *args, N=100, rgb=True, saturation=True, minhue=0, - maxsat=500, width=100, axwidth=1.7 -): - """ - Show how arbitrary colormap(s) vary with respect to the hue, chroma, - luminance, HSL saturation, and HPL saturation channels, and optionally - the red, blue and green channels. Adapted from `this example \ -`__. - - Parameters - ---------- - *args : colormap-spec, optional - Positional arguments are colormap names or objects. Default is - :rc:`image.cmap`. - N : int, optional - The number of markers to draw for each colormap. - rgb : bool, optional - Whether to also show the red, green, and blue channels in the bottom - row. Default is ``True``. - saturation : bool, optional - Whether to show the HSL and HPL saturation channels alongside the - raw chroma. - minhue : float, optional - The minimum hue. This lets you rotate the hue plot cyclically. - maxsat : float, optional - The maximum saturation. Use this to truncate large saturation values. - width : int, optional - The width of each colormap line in points. - axwidth : int or str, optional - The width of each subplot. Passed to `~proplot.subplots.subplots`. - - Returns - ------- - `~proplot.subplots.Figure` - The figure. - """ - # Figure and plot - from . import subplots - if not args: - raise ValueError(f'At least one positional argument required.') - array = [[1, 1, 2, 2, 3, 3]] - labels = ('Hue', 'Chroma', 'Luminance') - if saturation: - array += [[0, 4, 4, 5, 5, 0]] - labels += ('HSL saturation', 'HPL saturation') - if rgb: - array += [np.array([4, 4, 5, 5, 6, 6]) + 2 * int(saturation)] - labels += ('Red', 'Green', 'Blue') - fig, axs = subplots( - array=array, span=False, share=1, - axwidth=axwidth, axpad='1em', - ) - # Iterate through colormaps - mc, ms, mp = 0, 0, 0 - cmaps = [] - for cmap in args: - # Get colormap and avoid registering new names - name = cmap if isinstance(cmap, str) else getattr(cmap, 'name', None) - cmap = Colormap(cmap, N=N) # arbitrary cmap argument - if name is not None: - cmap.name = name - cmap._init() - cmaps.append(cmap) - # Get clipped RGB table - x = np.linspace(0, 1, N) - lut = cmap._lut[:-3, :3].copy() - rgb_data = lut.T # 3 by N - hcl_data = np.array([to_xyz(color, space='hcl') - for color in lut]).T # 3 by N - hsl_data = [to_xyz(color, space='hsl')[1] for color in lut] - hpl_data = [to_xyz(color, space='hpl')[1] for color in lut] - # Plot channels - # If rgb is False, the zip will just truncate the other iterables - data = (*hcl_data,) - if saturation: - data += (hsl_data, hpl_data) - if rgb: - data += (*rgb_data,) - for ax, y, label in zip(axs, data, labels): - ylim, ylocator = None, None - if label in ('Red', 'Green', 'Blue'): - ylim = (0, 1) - ylocator = 0.2 - elif label == 'Luminance': - ylim = (0, 100) - ylocator = 20 - elif label == 'Hue': - ylim = (minhue, minhue + 360) - ylocator = 90 - y = y - 720 - for _ in range(3): # rotate up to 1080 degrees - y[y < minhue] += 360 - else: - if label == 'Chroma': - mc = max(min(max(mc, max(y)), maxsat), 100) - m = mc - elif 'HSL' in label: - ms = max(min(max(ms, max(y)), maxsat), 100) - m = ms - else: - mp = max(min(max(mp, max(y)), maxsat), 100) - m = mp - ylim = (0, m) - ylocator = ('maxn', 5) - ax.scatter(x, y, c=x, cmap=cmap, s=width, linewidths=0) - ax.format(title=label, ylim=ylim, ylocator=ylocator) - # Formatting - suptitle = ', '.join(repr(cmap.name) for cmap in cmaps[:-1]) + ( - ', and ' if len(cmaps) > 2 else ' and ' if len(cmaps) == 2 else ' ' - ) + f'{repr(cmaps[-1].name)} colormap' + ('s' if len(cmaps) > 1 else '') - axs.format( - xlocator=0.25, xformatter='null', - suptitle=f'{suptitle} by channel', ylim=None, ytickminor=False, - ) - # Colorbar on the bottom - for cmap in cmaps: - fig.colorbar(cmap, - loc='b', span=(2, 5), - locator='null', label=cmap.name, labelweight='bold') - return fig - - -def show_colorspaces(luminance=None, saturation=None, hue=None, axwidth=2): - """ - Generate hue-saturation, hue-luminance, and luminance-saturation - cross-sections for the HCL, HSL, and HPL colorspaces. - - Parameters - ---------- - luminance : float, optional - If passed, saturation-hue cross-sections are drawn for - this luminance. Must be between ``0` and ``100``. Default is ``50``. - saturation : float, optional - If passed, luminance-hue cross-sections are drawn for this - saturation. Must be between ``0` and ``100``. - hue : float, optional - If passed, luminance-saturation cross-sections - are drawn for this hue. Must be between ``0` and ``360``. - axwidth : str or float, optional - Average width of each subplot. Units are interpreted by - `~proplot.utils.units`. - - Returns - ------- - `~proplot.subplots.Figure` - The figure. - """ - # Get colorspace properties - hues = np.linspace(0, 360, 361) - sats = np.linspace(0, 120, 120) - lums = np.linspace(0, 99.99, 101) - if luminance is None and saturation is None and hue is None: - luminance = 50 - if luminance is not None: - hsl = np.concatenate(( - np.repeat(hues[:, None], len(sats), axis=1)[..., None], - np.repeat(sats[None, :], len(hues), axis=0)[..., None], - np.ones((len(hues), len(sats)))[..., None] * luminance, - ), axis=2) - suptitle = f'Hue-saturation cross-section for luminance {luminance}' - xlabel, ylabel = 'hue', 'saturation' - xloc, yloc = 60, 20 - elif saturation is not None: - hsl = np.concatenate(( - np.repeat(hues[:, None], len(lums), axis=1)[..., None], - np.ones((len(hues), len(lums)))[..., None] * saturation, - np.repeat(lums[None, :], len(hues), axis=0)[..., None], - ), axis=2) - suptitle = f'Hue-luminance cross-section for saturation {saturation}' - xlabel, ylabel = 'hue', 'luminance' - xloc, yloc = 60, 20 - elif hue is not None: - hsl = np.concatenate(( - np.ones((len(lums), len(sats)))[..., None] * hue, - np.repeat(sats[None, :], len(lums), axis=0)[..., None], - np.repeat(lums[:, None], len(sats), axis=1)[..., None], - ), axis=2) - suptitle = 'Luminance-saturation cross-section' - xlabel, ylabel = 'luminance', 'saturation' - xloc, yloc = 20, 20 - - # Make figure, with black indicating invalid values - # Note we invert the x-y ordering for imshow - from . import subplots - fig, axs = subplots( - ncols=3, share=0, axwidth=axwidth, aspect=1, axpad=0.05 - ) - for ax, space in zip(axs, ('hcl', 'hsl', 'hpl')): - rgba = np.ones((*hsl.shape[:2][::-1], 4)) # RGBA - for j in range(hsl.shape[0]): - for k in range(hsl.shape[1]): - rgb_jk = to_rgb(hsl[j, k, :].flat, space) - if not all(0 <= c <= 1 for c in rgb_jk): - rgba[k, j, 3] = 0 # black cell - else: - rgba[k, j, :3] = rgb_jk - ax.imshow(rgba, origin='lower', aspect='auto') - ax.format( - xlabel=xlabel, ylabel=ylabel, suptitle=suptitle, - grid=False, xtickminor=False, ytickminor=False, - xlocator=xloc, ylocator=yloc, facecolor='k', - title=space.upper(), titleweight='bold' - ) - return fig - - -def show_colors(nhues=17, minsat=20): - """ - Generate tables of the registered color names. Adapted from - `this example `__. - - Parameters - ---------- - nhues : int, optional - The number of breaks between hues for grouping "like colors" in the - color table. - minsat : float, optional - The threshold saturation, between ``0`` and ``100``, for designating - "gray colors" in the color table. - - Returns - ------- - figs : list of `~proplot.subplots.Figure` - The figure. - """ - # Test used to "categories" colors - breakpoints = np.linspace(0, 360, nhues) - def _color_filter(i, hcl): # noqa: E306 - gray = hcl[1] <= minsat - if i == 0: - return gray - color = breakpoints[i - 1] <= hcl[0] < breakpoints[i] - if i == nhues - 1: - color = color or color == breakpoints[i] # endpoint inclusive - return not gray and color - - # Draw figures for different groups of colors - figs = [] - from . import subplots - for cats in ( - ('opencolor',), - tuple(name for name in colors if name not in ('css', 'opencolor')) - ): - # Dictionary of colors for that category - data = {} - for cat in cats: - for color in colors[cat]: - data[color] = mcolors.colorConverter.colors[color] - - # Group colors together by discrete range of hue, then sort by value - # For opencolors this is not necessary - if cats == ('opencolor',): - wscale = 0.5 - swatch = 1.5 - nrows, ncols = 10, len(COLORS_OPEN) # rows and columns - names = np.array([ - [name + str(i) for i in range(nrows)] - for name in COLORS_OPEN - ]) - nrows = nrows * 2 - ncols = (ncols + 1) // 2 - names.resize((ncols, nrows)) - - # Get colors in perceptally uniform space, then group based on hue - # thresholds - else: - ncols = 4 - wscale = 0.8 - swatch = 1.2 - hclpairs = [ - (name, to_xyz(color, COLORS_SPACE)) - for name, color in data.items() - ] - hclpairs = [ - sorted( - [pair for pair in hclpairs if _color_filter(i, pair[1])], - key=lambda x: x[1][2] - ) - for i in range(nhues) - ] - names = np.array([ - name for ipairs in hclpairs for name, _ in ipairs - ]) - nrows = len(names) // ncols + 1 - names.resize((ncols, nrows)) - - # Draw swatches as lines - fig, ax = subplots( - width=8 * wscale * (ncols / 4), - height=5 * (nrows / 40), - left=0, right=0, top=0, bottom=0, tight=False - ) - X, Y = fig.get_dpi() * fig.get_size_inches() # size in dots - hsep, wsep = Y / (nrows + 1), X / ncols # height and width in dots - for col, inames in enumerate(names): - for row, name in enumerate(inames): - if not name: - continue - y = Y - hsep * (row + 1) - xi = wsep * (col + 0.05) - xf = wsep * (col + 0.25 * swatch) - yline = y + hsep * 0.1 - xtext = wsep * (col + 0.25 * swatch + 0.03 * swatch) - ax.text(xtext, y, name, ha='left', va='center') - ax.plot( - [xi, xf], [yline, yline], - color=data[name], lw=hsep * 0.6, - solid_capstyle='butt', # do not stick out - ) - - # Apply formatting - ax.format( - xlim=(0, X), ylim=(0, Y), - grid=False, yloc='neither', xloc='neither' - ) - figs.append(fig) - return figs - - -def show_cmaps(*args, **kwargs): - """ - Generate a table of the registered colormaps or the input colormaps - categorized by source. Adapted from `this example \ -`__. - - Parameters - ---------- - *args : colormap-spec, optional - Colormap names or objects. - N : int, optional - The number of levels in each colorbar. Default is - :rc:`image.lut`. - unknown : str, optional - Category name for colormaps that are unknown to ProPlot. The - default is ``'User'``. Set this to ``False`` to hide - unknown colormaps. - length : float or str, optional - The length of the colorbars. Units are interpreted by - `~proplot.utils.units`. - width : float or str, optional - The width of the colorbars. Units are interpreted by - `~proplot.utils.units`. - - Returns - ------- - `~proplot.subplots.Figure` - The figure. - """ - # Have colormaps separated into categories - if args: - names = [Colormap(cmap).name for cmap in args] - else: - names = [ - name for name in mcm.cmap_d.keys() if - isinstance(mcm.cmap_d[name], LinearSegmentedColormap) - ] - - # Return figure of colorbars - kwargs.setdefault('source', CMAPS_TABLE) - return _draw_bars(names, **kwargs) - - -def show_cycles(*args, **kwargs): - """ - Generate a table of registered color cycles or the input color cycles - categorized by source. Adapted from `this example \ -`__. - - Parameters - ---------- - *args : colormap-spec, optional - Cycle names or objects. - unknown : str, optional - Category name for cycles that are unknown to ProPlot. The - default is ``'User'``. Set this to ``False`` to hide - unknown colormaps. - length : float or str, optional - The length of the colorbars. Units are interpreted by - `~proplot.utils.units`. - width : float or str, optional - The width of the colorbars. Units are interpreted by - `~proplot.utils.units`. - - Returns - ------- - `~proplot.subplots.Figure` - The figure. - """ - # Get the list of cycles - if args: - names = [cmap.name for cmap in args] - else: - names = [ - name for name in mcm.cmap_d.keys() if - isinstance(mcm.cmap_d[name], ListedColormap) - ] - - # Return figure of colorbars - kwargs.setdefault('source', CYCLES_TABLE) - return _draw_bars(names, **kwargs) - - -def show_fonts( - *args, family=None, text=None, - size=12, weight='normal', style='normal', stretch='normal', -): - """ - Generate a table of fonts. If a glyph for a particular font is unavailable, - it is replaced with the "¤" dummy character. - - Parameters - ---------- - *args - The font name(s). If none are provided and the `family` keyword - argument was not provided, the *available* :rcraw:`font.sans-serif` - fonts and the fonts in your ``.proplot/fonts`` folder are shown. - family : {'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', \ -'tex-gyre'}, optional - If provided, the *available* fonts in the corresponding families - are shown. The fonts belonging to these families are listed under the - :rcraw:`font.serif`, :rcraw:`font.sans-serif`, :rcraw:`font.monospace`, - :rcraw:`font.cursive`, and :rcraw:`font.fantasy` settings. The special - family ``'tex-gyre'`` draws the `TeX Gyre \ -`__ fonts. - text : str, optional - The sample text. The default sample text includes the Latin letters, - Greek letters, Arabic numerals, and some simple mathematical symbols. - size : float, optional - The font size in points. - weight : weight-spec, optional - The font weight. - style : style-spec, optional - The font style. - stretch : stretch-spec, optional - The font stretch. - """ - from . import subplots - import matplotlib.font_manager as mfonts - if not args and family is None: - # User fonts and sans-serif fonts. Note all proplot sans-serif fonts - # are added to 'font.sans-serif' by default - args = sorted({ - font.name for font in mfonts.fontManager.ttflist - if font.name in rcParams['font.sans-serif'] - or _get_data_paths('fonts')[1] == os.path.dirname(font.fname) - }) - elif family is not None: - options = ( - 'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', - 'tex-gyre', - ) - if family not in options: - raise ValueError( - f'Invalid family {family!r}. Options are: ' - + ', '.join(map(repr, options)) + '.' - ) - if family == 'tex-gyre': - family_fonts = ( - 'TeX Gyre Adventor', - 'TeX Gyre Bonum', - 'TeX Gyre Cursor', - 'TeX Gyre Chorus', - 'TeX Gyre Heros', - 'TeX Gyre Pagella', - 'TeX Gyre Schola', - 'TeX Gyre Termes', - ) - else: - family_fonts = rcParams['font.' + family] - args = ( - *args, *sorted({ - font.name for font in mfonts.fontManager.ttflist - if font.name in family_fonts - }) - ) - - # Text - if text is None: - text = ( - 'the quick brown fox jumps over a lazy dog' '\n' - 'THE QUICK BROWN FOX JUMPS OVER A LAZY DOG' '\n' - '(0) + {1\N{DEGREE SIGN}} \N{MINUS SIGN} [2*] - <3> / 4,0 ' - r'$\geq\gg$ 5.0 $\leq\ll$ ~6 $\times$ 7 ' - r'$\equiv$ 8 $\approx$ 9 $\propto$' '\n' - r'$\alpha\beta$ $\Gamma\gamma$ $\Delta\delta$ ' - r'$\epsilon\zeta\eta$ $\Theta\theta$ $\kappa\mu\nu$ ' - r'$\Lambda\lambda$ $\Pi\pi$ $\xi\rho\tau\chi$ $\Sigma\sigma$ ' - r'$\Phi\phi$ $\Psi\psi$ $\Omega\omega$ !?&#%' - ) - - # Create figure - fig, axs = subplots( - ncols=1, nrows=len(args), space=0, - axwidth=4.5, axheight=1.2 * (text.count('\n') + 2.5) * size / 72, - fallback_to_cm=False - ) - axs.format( - xloc='neither', yloc='neither', - xlocator='null', ylocator='null', alpha=0 - ) - for i, ax in enumerate(axs): - font = args[i] - ax.text( - 0, 0.5, f'{font}:\n{text}', - fontfamily=font, fontsize=size, - stretch=stretch, style=style, weight=weight, - ha='left', va='center' - ) - return fig - - -# Apply custom changes -if 'Greys' in mcm.cmap_d: # 'Murica (and consistency with registered colors) - mcm.cmap_d['Grays'] = mcm.cmap_d.pop('Greys') -if 'Spectral' in mcm.cmap_d: # make spectral go from 'cold' to 'hot' - mcm.cmap_d['Spectral'] = mcm.cmap_d['Spectral'].reversed(name='Spectral') -mcm.cmap_d.pop('twilight_shifted', None) # we auto-generate this -for _name in ('viridis', 'plasma', 'inferno', 'magma', 'cividis', 'twilight'): - _cmap = mcm.cmap_d.get(_name, None) - if _cmap and isinstance(_cmap, mcolors.ListedColormap): - mcm.cmap_d.pop(_name, None) - mcm.cmap_d[_name] = LinearSegmentedColormap.from_list( - _name, _cmap.colors, cyclic=(_name == 'twilight') - ) -for _cat in ('MATLAB', 'GNUplot', 'GIST', 'Other'): - for _name in CMAPS_TABLE[_cat]: - mcm.cmap_d.pop(_name, None) - -# Initialize customization folders -_rc_folder = os.path.join(os.path.expanduser('~'), '.proplot') -if not os.path.isdir(_rc_folder): - os.mkdir(_rc_folder) -for _rc_sub in ('cmaps', 'cycles', 'colors', 'fonts'): - _rc_sub = os.path.join(_rc_folder, _rc_sub) - if not os.path.isdir(_rc_sub): - os.mkdir(_rc_sub) - -# Apply monkey patches to top level modules -if not isinstance(mcm.cmap_d, CmapDict): - _dict = { - key: value for key, value in mcm.cmap_d.items() if key[-2:] != '_r' - } - mcm.cmap_d = CmapDict(_dict) -if not isinstance(mcolors._colors_full_map, _ColorMappingOverride): - _map = _ColorMappingOverride(mcolors._colors_full_map) - mcolors._colors_full_map = _map - mcolors.colorConverter.cache = _map.cache # re-instantiate - mcolors.colorConverter.colors = _map # re-instantiate - -#: List of registered colormap names. -cmaps = [] - -#: List of registered color cycle names. -cycles = [] - -#: Lists of registered color names by category. -colors = {} - -#: Registered font names. -fonts = [] - -# Call driver funcs -register_colors() -register_cmaps() -register_cycles() -register_fonts() - -#: Dictionary of possible normalizers. See `Norm` for a table. -normalizers = { - 'none': mcolors.NoNorm, - 'null': mcolors.NoNorm, - 'zero': MidpointNorm, - 'midpoint': MidpointNorm, - 'segments': LinearSegmentedNorm, - 'segmented': LinearSegmentedNorm, - 'log': mcolors.LogNorm, - 'linear': mcolors.Normalize, - 'power': mcolors.PowerNorm, - 'symlog': mcolors.SymLogNorm, -} diff --git a/proplot/subplots.py b/proplot/subplots.py deleted file mode 100644 index 0e03c31b1..000000000 --- a/proplot/subplots.py +++ /dev/null @@ -1,2542 +0,0 @@ -#!/usr/bin/env python3 -""" -The starting point for creating custom ProPlot figures. Includes -pyplot-inspired functions for creating figures and related classes. -""" -# NOTE: Importing backend causes issues with sphinx, and anyway not sure it's -# always included, so make it optional -import os -import numpy as np -import functools -import inspect -import matplotlib.pyplot as plt -import matplotlib.figure as mfigure -import matplotlib.transforms as mtransforms -import matplotlib.gridspec as mgridspec -import matplotlib.axes as maxes -from numbers import Integral -from .rctools import rc -from .utils import _warn_proplot, _notNone, _counter, _setstate, units # noqa -from . import projs, axes -try: # use this for debugging instead of print()! - from icecream import ic -except ImportError: # graceful fallback if IceCream isn't installed - ic = lambda *a: None if not a else (a[0] if len(a) == 1 else a) # noqa - -__all__ = [ - 'subplot_grid', 'close', 'show', 'subplots', 'Figure', - 'GridSpec', 'SubplotSpec', -] - -# Dimensions of figures for common journals -JOURNAL_SPECS = { - 'aaas1': '5.5cm', - 'aaas2': '12cm', - 'agu1': ('95mm', '115mm'), - 'agu2': ('190mm', '115mm'), - 'agu3': ('95mm', '230mm'), - 'agu4': ('190mm', '230mm'), - 'ams1': 3.2, - 'ams2': 4.5, - 'ams3': 5.5, - 'ams4': 6.5, - 'nat1': '89mm', - 'nat2': '183mm', - 'pnas1': '8.7cm', - 'pnas2': '11.4cm', - 'pnas3': '17.8cm', -} - - -def close(*args, **kwargs): - """ - Pass the input arguments to `matplotlib.pyplot.close`. This is included - so you don't have to import `~matplotlib.pyplot`. - """ - plt.close(*args, **kwargs) - - -def show(): - """ - Call `matplotlib.pyplot.show`. This is included so you don't have to import - `~matplotlib.pyplot`. Note this command should *not be necessary* if you - are working in an iPython session and :rcraw:`matplotlib` is non-empty -- - when you create a new figure, it will be automatically displayed. - """ - plt.show() - - -class subplot_grid(list): - """ - List subclass and pseudo-2d array that is used as a container for the - list of axes returned by `subplots`. See `~subplot_grid.__getattr__` - and `~subplot_grid.__getitem__` for details. - """ - def __init__(self, objs, n=1, order='C'): - """ - Parameters - ---------- - objs : list-like - 1d iterable of `~proplot.axes.Axes` instances. - n : int, optional - The length of the fastest-moving dimension, i.e. the number of - columns when `order` is ``'C'``, and the number of rows when - `order` is ``'F'``. Used to treat lists as pseudo-2d arrays. - order : {'C', 'F'}, optional - Whether 1d indexing returns results in row-major (C-style) or - column-major (Fortran-style) order, respectively. Used to treat - lists as pseudo-2d arrays. - """ - if not all(isinstance(obj, axes.Axes) for obj in objs): - raise ValueError( - f'Axes grid must be filled with Axes instances, got {objs!r}.' - ) - super().__init__(objs) - self._n = n - self._order = order - self._shape = (len(self) // n, n)[::(1 if order == 'C' else -1)] - - def __repr__(self): - return 'subplot_grid([' + ', '.join(str(ax) for ax in self) + '])' - - def __setitem__(self, key, value): # noqa: U100 - """ - Raise an error. This enforces pseudo immutability. - """ - raise LookupError('subplot_grid is immutable.') - - def __getitem__(self, key): - """ - If an integer is passed, the item is returned. If a slice is passed, - a `subplot_grid` of the items is returned. You can also use 2D - indexing, and the corresponding axes in the `subplot_grid` will be - chosen. - - Example - ------- - - >>> import proplot as plot - ... f, axs = plot.subplots(nrows=3, ncols=3, colorbars='b', bstack=2) - ... axs[0] # the subplot in the top-right corner - ... axs[3] # the first subplot in the second row - ... axs[1,2] # the subplot in the second row, third from the left - ... axs[:,0] # the subplots in the first column - - """ - # Allow 2d specification - if isinstance(key, tuple) and len(key) == 1: - key = key[0] - # do not expand single slice to list of integers or we get recursion! - # len() operator uses __getitem__! - if not isinstance(key, tuple): - axlist = isinstance(key, slice) - objs = list.__getitem__(self, key) - elif len(key) == 2: - axlist = any(isinstance(ikey, slice) for ikey in key) - # Expand keys - keys = [] - order = self._order - for i, ikey in enumerate(key): - if (i == 1 and order == 'C') or (i == 0 and order != 'C'): - n = self._n - else: - n = len(self) // self._n - if isinstance(ikey, slice): - start, stop, step = ikey.start, ikey.stop, ikey.step - if start is None: - start = 0 - elif start < 0: - start = n + start - if stop is None: - stop = n - elif stop < 0: - stop = n + stop - if step is None: - step = 1 - ikeys = [*range(start, stop, step)] - else: - if ikey < 0: - ikey = n + ikey - ikeys = [ikey] - keys.append(ikeys) - # Get index pairs and get objects - # Note that in double for loop, right loop varies fastest, so - # e.g. axs[:,:] delvers (0,0), (0,1), ..., (0,N), (1,0), ... - # Remember for order == 'F', subplot_grid was sent a list unfurled - # in column-major order, so we replicate row-major indexing syntax - # by reversing the order of the keys. - objs = [] - if self._order == 'C': - idxs = [key0 * self._n + key1 for key0 in keys[0] - for key1 in keys[1]] - else: - idxs = [key1 * self._n + key0 for key1 in keys[1] - for key0 in keys[0]] - for idx in idxs: - objs.append(list.__getitem__(self, idx)) - if not axlist: # objs will always be length 1 - objs = objs[0] - else: - raise IndexError - - # Return - if axlist: - return subplot_grid(objs) - else: - return objs - - def __getattr__(self, attr): - """ - If the attribute is *callable*, return a dummy function that loops - through each identically named method, calls them in succession, and - returns a tuple of the results. This lets you call arbitrary methods - on multiple axes at once! If the `subplot_grid` has length ``1``, the - single result is returned. If the attribute is *not callable*, - returns a tuple of attributes for every object in the list. - - Example - ------- - - >>> import proplot as plot - ... f, axs = plot.subplots(nrows=2, ncols=2) - ... axs.format(...) # calls "format" on all subplots in the list - ... paxs = axs.panel_axes('r') - ... paxs.format(...) # calls "format" on all panels - - """ - if not self: - raise AttributeError( - f'Invalid attribute {attr!r}, axes grid {self!r} is empty.' - ) - objs = (*(getattr(ax, attr) for ax in self),) # may raise error - - # Objects - if not any(callable(_) for _ in objs): - if len(self) == 1: - return objs[0] - else: - return objs - # Methods - # NOTE: Must manually copy docstring because help() cannot inherit it - elif all(callable(_) for _ in objs): - @functools.wraps(objs[0]) - def _iterator(*args, **kwargs): - ret = [] - for func in objs: - ret.append(func(*args, **kwargs)) - ret = (*ret,) - if len(self) == 1: - return ret[0] - elif all(res is None for res in ret): - return None - elif all(isinstance(res, axes.Axes) for res in ret): - return subplot_grid(ret, n=self._n, order=self._order) - else: - return ret - _iterator.__doc__ = inspect.getdoc(objs[0]) - return _iterator - - # Mixed - raise AttributeError(f'Found mixed types for attribute {attr!r}.') - - @property - def shape(self): - """ - The "shape" of the subplot grid. For complex subplot grids, where - subplots may span contiguous rows and columns, this "shape" may be - incorrect. In such cases, 1d indexing should always be used. - """ - return self._shape - - -class SubplotSpec(mgridspec.SubplotSpec): - """ - Matplotlib `~matplotlib.gridspec.SubplotSpec` subclass that adds - some helpful methods. - """ - def __repr__(self): - nrows, ncols, row1, row2, col1, col2 = self.get_rows_columns() - return f'SubplotSpec({nrows}, {ncols}; {row1}:{row2}, {col1}:{col2})' - - def get_active_geometry(self): - """ - Returns the number of rows, number of columns, and 1d subplot - location indices, ignoring rows and columns allocated for spaces. - """ - nrows, ncols, row1, row2, col1, col2 = self.get_active_rows_columns() - num1 = row1 * ncols + col1 - num2 = row2 * ncols + col2 - return nrows, ncols, num1, num2 - - def get_active_rows_columns(self): - """ - Returns the number of rows, number of columns, first subplot row, - last subplot row, first subplot column, and last subplot column, - ignoring rows and columns allocated for spaces. - """ - gridspec = self.get_gridspec() - nrows, ncols = gridspec.get_geometry() - row1, col1 = divmod(self.num1, ncols) - if self.num2 is not None: - row2, col2 = divmod(self.num2, ncols) - else: - row2 = row1 - col2 = col1 - return ( - nrows // 2, ncols // 2, row1 // 2, row2 // 2, col1 // 2, col2 // 2) - - -class GridSpec(mgridspec.GridSpec): - """ - Matplotlib `~matplotlib.gridspec.GridSpec` subclass that allows for grids - with variable spacing between successive rows and columns of axes. - Accomplishes this by actually drawing ``nrows*2 + 1`` and ``ncols*2 + 1`` - `~matplotlib.gridspec.GridSpec` rows and columns, setting `wspace` - and `hspace` to ``0``, and masking out every other row and column - of the `~matplotlib.gridspec.GridSpec`, so they act as "spaces". - These "spaces" are then allowed to vary in width using the builtin - `width_ratios` and `height_ratios` properties. - """ - def __repr__(self): # do not show width and height ratios - nrows, ncols = self.get_geometry() - return f'GridSpec({nrows}, {ncols})' - - def __init__(self, figure, nrows=1, ncols=1, **kwargs): - """ - Parameters - ---------- - figure : `Figure` - The figure instance filled by this gridspec. Unlike - `~matplotlib.gridspec.GridSpec`, this argument is required. - nrows, ncols : int, optional - The number of rows and columns on the subplot grid. - hspace, wspace : float or list of float - The vertical and horizontal spacing between rows and columns of - subplots, respectively. In `~proplot.subplots.subplots`, ``wspace`` - and ``hspace`` are in physical units. When calling - `GridSpec` directly, values are scaled relative to - the average subplot height or width. - - If float, the spacing is identical between all rows and columns. If - list of float, the length of the lists must equal ``nrows-1`` - and ``ncols-1``, respectively. - height_ratios, width_ratios : list of float - Ratios for the relative heights and widths for rows and columns - of subplots, respectively. For example, ``width_ratios=(1,2)`` - scales a 2-column gridspec so that the second column is twice as - wide as the first column. - left, right, top, bottom : float or str - Passed to `~matplotlib.gridspec.GridSpec`, denotes the margin - positions in figure-relative coordinates. - **kwargs - Passed to `~matplotlib.gridspec.GridSpec`. - """ - self._nrows = nrows * 2 - 1 # used with get_geometry - self._ncols = ncols * 2 - 1 - self._nrows_active = nrows - self._ncols_active = ncols - wratios, hratios, kwargs = self._spaces_as_ratios(**kwargs) - super().__init__( - self._nrows, self._ncols, - hspace=0, wspace=0, # replaced with "hidden" slots - width_ratios=wratios, height_ratios=hratios, - figure=figure, **kwargs - ) - - def __getitem__(self, key): - """ - Magic obfuscation that renders `~matplotlib.gridspec.GridSpec` - rows and columns designated as 'spaces' inaccessible. - """ - nrows, ncols = self.get_geometry() - nrows_active, ncols_active = self.get_active_geometry() - if not isinstance(key, tuple): # usage gridspec[1,2] - num1, num2 = self._normalize(key, nrows_active * ncols_active) - else: - if len(key) == 2: - k1, k2 = key - else: - raise ValueError(f'Invalid index {key!r}.') - num1 = self._normalize(k1, nrows_active) - num2 = self._normalize(k2, ncols_active) - num1, num2 = np.ravel_multi_index((num1, num2), (nrows, ncols)) - num1 = self._positem(num1) - num2 = self._positem(num2) - return SubplotSpec(self, num1, num2) - - @staticmethod - def _positem(size): - """ - Account for negative indices. - """ - if size < 0: - # want -1 to stay -1, -2 becomes -3, etc. - return 2 * (size + 1) - 1 - else: - return size * 2 - - @staticmethod - def _normalize(key, size): - """ - Transform gridspec index into standardized form. - """ - if isinstance(key, slice): - start, stop, _ = key.indices(size) - if stop > start: - return start, stop - 1 - else: - if key < 0: - key += size - if 0 <= key < size: - return key, key - raise IndexError(f'Invalid index: {key} with size {size}.') - - def _spaces_as_ratios( - self, hspace=None, wspace=None, # spacing between axes - height_ratios=None, width_ratios=None, - **kwargs - ): - """ - For keyword arg usage, see `GridSpec`. - """ - # Parse flexible input - nrows, ncols = self.get_active_geometry() - hratios = np.atleast_1d(_notNone(height_ratios, 1)) - wratios = np.atleast_1d(_notNone(width_ratios, 1)) - # this is relative to axes - hspace = np.atleast_1d(_notNone(hspace, np.mean(hratios) * 0.10)) - wspace = np.atleast_1d(_notNone(wspace, np.mean(wratios) * 0.10)) - if len(wspace) == 1: - wspace = np.repeat(wspace, (ncols - 1,)) # note: may be length 0 - if len(hspace) == 1: - hspace = np.repeat(hspace, (nrows - 1,)) - if len(wratios) == 1: - wratios = np.repeat(wratios, (ncols,)) - if len(hratios) == 1: - hratios = np.repeat(hratios, (nrows,)) - - # Verify input ratios and spacings - # Translate height/width spacings, implement as extra columns/rows - if len(hratios) != nrows: - raise ValueError(f'Got {nrows} rows, but {len(hratios)} hratios.') - if len(wratios) != ncols: - raise ValueError( - f'Got {ncols} columns, but {len(wratios)} wratios.' - ) - if len(wspace) != ncols - 1: - raise ValueError( - f'Require {ncols-1} width spacings for {ncols} columns, ' - f'got {len(wspace)}.' - ) - if len(hspace) != nrows - 1: - raise ValueError( - f'Require {nrows-1} height spacings for {nrows} rows, ' - f'got {len(hspace)}.' - ) - - # Assign spacing as ratios - nrows, ncols = self.get_geometry() - wratios_final = [None] * ncols - wratios_final[::2] = [*wratios] - if ncols > 1: - wratios_final[1::2] = [*wspace] - hratios_final = [None] * nrows - hratios_final[::2] = [*hratios] - if nrows > 1: - hratios_final[1::2] = [*hspace] - return wratios_final, hratios_final, kwargs # bring extra kwargs back - - def get_margins(self): - """ - Returns left, bottom, right, top values. Not sure why this method - doesn't already exist on `~matplotlib.gridspec.GridSpec`. - """ - return self.left, self.bottom, self.right, self.top - - def get_hspace(self): - """ - Returns row ratios allocated for spaces. - """ - return self.get_height_ratios()[1::2] - - def get_wspace(self): - """ - Returns column ratios allocated for spaces. - """ - return self.get_width_ratios()[1::2] - - def get_active_height_ratios(self): - """ - Returns height ratios excluding slots allocated for spaces. - """ - return self.get_height_ratios()[::2] - - def get_active_width_ratios(self): - """ - Returns width ratios excluding slots allocated for spaces. - """ - return self.get_width_ratios()[::2] - - def get_active_geometry(self): - """ - Returns the number of active rows and columns, i.e. the rows and - columns that aren't skipped by `~GridSpec.__getitem__`. - """ - return self._nrows_active, self._ncols_active - - def update(self, **kwargs): - """ - Update the gridspec with arbitrary initialization keyword arguments - then *apply* those updates to every figure using this gridspec. - The default `~matplotlib.gridspec.GridSpec.update` tries to update - positions for axes on all active figures -- but this can fail after - successive figure edits if it has been removed from the figure - manager. ProPlot insists one gridspec per figure. - - Parameters - ---------- - **kwargs - Valid initialization keyword arguments. See `GridSpec`. - """ - # Convert spaces to ratios - wratios, hratios, kwargs = self._spaces_as_ratios(**kwargs) - self.set_width_ratios(wratios) - self.set_height_ratios(hratios) - - # Validate args - nrows = kwargs.pop('nrows', None) - ncols = kwargs.pop('ncols', None) - nrows_current, ncols_current = self.get_active_geometry() - if (nrows is not None and nrows != nrows_current) or ( - ncols is not None and ncols != ncols_current): - raise ValueError( - f'Input geometry {(nrows, ncols)} does not match ' - f'current geometry {(nrows_current, ncols_current)}.' - ) - self.left = kwargs.pop('left', None) - self.right = kwargs.pop('right', None) - self.bottom = kwargs.pop('bottom', None) - self.top = kwargs.pop('top', None) - if kwargs: - raise ValueError(f'Unknown keyword arg(s): {kwargs}.') - - # Apply to figure and all axes - fig = self.figure - fig.subplotpars.update(self.left, self.bottom, self.right, self.top) - for ax in fig.axes: - if not isinstance(ax, maxes.SubplotBase): - continue - subplotspec = ax.get_subplotspec().get_topmost_subplotspec() - if subplotspec.get_gridspec() is not self: - continue - ax.update_params() - ax.set_position(ax.figbox) - fig.stale = True - - -def _canvas_preprocess(canvas, method): - """ - Return a pre-processer that can be used to override instance-level - canvas draw_idle() and print_figure() methods. This applies tight layout - and aspect ratio-conserving adjustments and aligns labels. Required so that - the canvas methods instantiate renderers with the correct dimensions. - Note that MacOSX currently `cannot be resized \ -`__. - """ - # NOTE: This is by far the most robust approach. Renderer must be (1) - # initialized with the correct figure size or (2) changed inplace during - # draw, but vector graphic renderers *cannot* be changed inplace. - # Options include (1) monkey patch canvas.get_width_height, overriding - # figure.get_size_inches, and exploit the FigureCanvasAgg.get_renderer() - # implementation (because FigureCanvasAgg queries the bbox directly - # rather than using get_width_height() so requires a workaround), or (2) - # override bbox and bbox_inches as *properties*, but these are really - # complicated, dangerous, and result in unnecessary extra draws. - def _preprocess(self, *args, **kwargs): - fig = self.figure # update even if not stale! needed after saves - if method == 'draw_idle' and ( - self._is_idle_drawing # standard - or getattr(self, '_draw_pending', None) # pyqt5 - ): - # For now we override 'draw' and '_draw' rather than 'draw_idle' - # but may change mind in the future. This breakout condition is - # copied from the matplotlib source. - return - if method == 'print_figure': - # When re-generating inline figures, the tight layout algorithm - # can get figure size *or* spacing wrong unless we force additional - # draw! Seems to have no adverse effects when calling savefig. - self.draw() - if fig._is_preprocessing: - return - with fig._context_preprocessing(): - renderer = fig._get_renderer() # any renderer will do for now - for ax in fig._iter_axes(): - ax._draw_auto_legends_colorbars() # may insert panels - resize = rc['backend'] != 'nbAgg' - if resize: - fig._adjust_aspect() # resizes figure - if fig._auto_tight: - fig._adjust_tight_layout(renderer, resize=resize) - fig._align_axislabels(True) - fig._align_labels(renderer) - fallback = _notNone( - fig._fallback_to_cm, rc['mathtext.fallback_to_cm'] - ) - with rc.context({'mathtext.fallback_to_cm': fallback}): - return getattr(type(self), method)(self, *args, **kwargs) - return _preprocess.__get__(canvas) # ...I don't get it either - - -def _get_panelargs( - side, share=None, width=None, space=None, - filled=False, figure=False -): - """ - Return default properties for new axes and figure panels. - """ - if side not in ('left', 'right', 'bottom', 'top'): - raise ValueError(f'Invalid panel location {side!r}.') - space = space_user = units(space) - if share is None: - share = not filled - if width is None: - if filled: - width = rc['colorbar.width'] - else: - width = rc['subplots.panelwidth'] - width = units(width) - if space is None: - key = 'wspace' if side in ('left', 'right') else 'hspace' - pad = rc['subplots.axpad'] if figure else rc['subplots.panelpad'] - space = _get_space(key, share, pad=pad) - return share, width, space, space_user - - -def _get_space(key, share=0, pad=None): - """ - Return suitable default spacing given a shared axes setting. - """ - if key == 'left': - space = units(_notNone(pad, rc['subplots.pad'])) + ( - rc['ytick.major.size'] + rc['ytick.labelsize'] - + rc['ytick.major.pad'] + rc['axes.labelsize']) / 72 - elif key == 'right': - space = units(_notNone(pad, rc['subplots.pad'])) - elif key == 'bottom': - space = units(_notNone(pad, rc['subplots.pad'])) + ( - rc['xtick.major.size'] + rc['xtick.labelsize'] - + rc['xtick.major.pad'] + rc['axes.labelsize']) / 72 - elif key == 'top': - space = units(_notNone(pad, rc['subplots.pad'])) + ( - rc['axes.titlepad'] + rc['axes.titlesize']) / 72 - elif key == 'wspace': - space = (units(_notNone(pad, rc['subplots.axpad'])) - + rc['ytick.major.size'] / 72) - if share < 3: - space += (rc['ytick.labelsize'] + rc['ytick.major.pad']) / 72 - if share < 1: - space += rc['axes.labelsize'] / 72 - elif key == 'hspace': - space = units(_notNone(pad, rc['subplots.axpad'])) + ( - rc['axes.titlepad'] + rc['axes.titlesize'] - + rc['xtick.major.size']) / 72 - if share < 3: - space += (rc['xtick.labelsize'] + rc['xtick.major.pad']) / 72 - if share < 0: - space += rc['axes.labelsize'] / 72 - else: - raise KeyError(f'Invalid space key {key!r}.') - return space - - -def _subplots_geometry(**kwargs): - """ - Save arguments passed to `subplots`, calculates gridspec settings and - figure size necessary for requested geometry, and returns keyword args - necessary to reconstruct and modify this configuration. Note that - `wspace`, `hspace`, `left`, `right`, `top`, and `bottom` always have fixed - physical units, then we scale figure width, figure height, and width - and height ratios to accommodate spaces. - """ - # Dimensions and geometry - nrows, ncols = kwargs['nrows'], kwargs['ncols'] - aspect, xref, yref = kwargs['aspect'], kwargs['xref'], kwargs['yref'] - width, height = kwargs['width'], kwargs['height'] - axwidth, axheight = kwargs['axwidth'], kwargs['axheight'] - # Gridspec settings - wspace, hspace = kwargs['wspace'], kwargs['hspace'] - wratios, hratios = kwargs['wratios'], kwargs['hratios'] - left, bottom = kwargs['left'], kwargs['bottom'] - right, top = kwargs['right'], kwargs['top'] - # Panel string toggles, lists containing empty strings '' (indicating a - # main axes), or one of 'l', 'r', 'b', 't' (indicating axes panels) or - # 'f' (indicating figure panels) - wpanels, hpanels = kwargs['wpanels'], kwargs['hpanels'] - - # Checks, important now that we modify gridspec geometry - if len(hratios) != nrows: - raise ValueError( - f'Expected {nrows} width ratios for {nrows} rows, ' - f'got {len(hratios)}.' - ) - if len(wratios) != ncols: - raise ValueError( - f'Expected {ncols} width ratios for {ncols} columns, ' - f'got {len(wratios)}.' - ) - if len(hspace) != nrows - 1: - raise ValueError( - f'Expected {nrows - 1} hspaces for {nrows} rows, ' - f'got {len(hspace)}.' - ) - if len(wspace) != ncols - 1: - raise ValueError( - f'Expected {ncols - 1} wspaces for {ncols} columns, ' - f'got {len(wspace)}.' - ) - if len(hpanels) != nrows: - raise ValueError( - f'Expected {nrows} hpanel toggles for {nrows} rows, ' - f'got {len(hpanels)}.' - ) - if len(wpanels) != ncols: - raise ValueError( - f'Expected {ncols} wpanel toggles for {ncols} columns, ' - f'got {len(wpanels)}.' - ) - - # Get indices corresponding to main axes or main axes space slots - idxs_ratios, idxs_space = [], [] - for panels in (hpanels, wpanels): - # Ratio indices - mask = np.array([bool(s) for s in panels]) - ratio_idxs, = np.where(~mask) - idxs_ratios.append(ratio_idxs) - # Space indices - space_idxs = [] - for idx in ratio_idxs[:-1]: # exclude last axes slot - offset = 1 - while panels[idx + offset] not in 'rbf': # main space next to this - offset += 1 - space_idxs.append(idx + offset - 1) - idxs_space.append(space_idxs) - # Separate the panel and axes ratios - hratios_main = [hratios[idx] for idx in idxs_ratios[0]] - wratios_main = [wratios[idx] for idx in idxs_ratios[1]] - hratios_panels = [ - ratio for idx, ratio in enumerate(hratios) - if idx not in idxs_ratios[0] - ] - wratios_panels = [ - ratio for idx, ratio in enumerate(wratios) - if idx not in idxs_ratios[1] - ] - hspace_main = [hspace[idx] for idx in idxs_space[0]] - wspace_main = [wspace[idx] for idx in idxs_space[1]] - # Reduced geometry - nrows_main = len(hratios_main) - ncols_main = len(wratios_main) - - # Get reference properties, account for panel slots in space and ratios - # TODO: Shouldn't panel space be included in these calculations? - (x1, x2), (y1, y2) = xref, yref - dx, dy = x2 - x1 + 1, y2 - y1 + 1 - rwspace = sum(wspace_main[x1:x2]) - rhspace = sum(hspace_main[y1:y2]) - rwratio = ( - ncols_main * sum(wratios_main[x1:x2 + 1])) / (dx * sum(wratios_main)) - rhratio = ( - nrows_main * sum(hratios_main[y1:y2 + 1])) / (dy * sum(hratios_main)) - if rwratio == 0 or rhratio == 0: - raise RuntimeError( - f'Something went wrong, got wratio={rwratio!r} ' - f'and hratio={rhratio!r} for reference axes.' - ) - if np.iterable(aspect): - aspect = aspect[0] / aspect[1] - - # Determine figure and axes dims from input in width or height dimenion. - # For e.g. common use case [[1,1,2,2],[0,3,3,0]], make sure we still scale - # the reference axes like square even though takes two columns of gridspec! - auto_width = (width is None and height is not None) - auto_height = (height is None and width is not None) - if width is None and height is None: # get stuff directly from axes - if axwidth is None and axheight is None: - axwidth = units(rc['subplots.axwidth']) - if axheight is not None: - auto_width = True - axheight_all = (nrows_main * (axheight - rhspace)) / (dy * rhratio) - height = axheight_all + top + bottom + \ - sum(hspace) + sum(hratios_panels) - if axwidth is not None: - auto_height = True - axwidth_all = (ncols_main * (axwidth - rwspace)) / (dx * rwratio) - width = axwidth_all + left + right + \ - sum(wspace) + sum(wratios_panels) - if axwidth is not None and axheight is not None: - auto_width = auto_height = False - else: - if height is not None: - axheight_all = height - top - bottom - \ - sum(hspace) - sum(hratios_panels) - axheight = (axheight_all * dy * rhratio) / nrows_main + rhspace - if width is not None: - axwidth_all = width - left - right - \ - sum(wspace) - sum(wratios_panels) - axwidth = (axwidth_all * dx * rwratio) / ncols_main + rwspace - - # Automatically figure dim that was not specified above - if auto_height: - axheight = axwidth / aspect - axheight_all = (nrows_main * (axheight - rhspace)) / (dy * rhratio) - height = axheight_all + top + bottom + \ - sum(hspace) + sum(hratios_panels) - elif auto_width: - axwidth = axheight * aspect - axwidth_all = (ncols_main * (axwidth - rwspace)) / (dx * rwratio) - width = axwidth_all + left + right + sum(wspace) + sum(wratios_panels) - if axwidth_all < 0: - raise ValueError( - f'Not enough room for axes (would have width {axwidth_all}). ' - 'Try using tight=False, increasing figure width, or decreasing ' - "'left', 'right', or 'wspace' spaces." - ) - if axheight_all < 0: - raise ValueError( - f'Not enough room for axes (would have height {axheight_all}). ' - 'Try using tight=False, increasing figure height, or decreasing ' - "'top', 'bottom', or 'hspace' spaces." - ) - - # Reconstruct the ratios array with physical units for subplot slots - # The panel slots are unchanged because panels have fixed widths - wratios_main = axwidth_all * np.array(wratios_main) / sum(wratios_main) - hratios_main = axheight_all * np.array(hratios_main) / sum(hratios_main) - for idx, ratio in zip(idxs_ratios[0], hratios_main): - hratios[idx] = ratio - for idx, ratio in zip(idxs_ratios[1], wratios_main): - wratios[idx] = ratio - - # Convert margins to figure-relative coordinates - left = left / width - bottom = bottom / height - right = 1 - right / width - top = 1 - top / height - - # Return gridspec keyword args - gridspec_kw = { - 'ncols': ncols, 'nrows': nrows, - 'wspace': wspace, 'hspace': hspace, - 'width_ratios': wratios, 'height_ratios': hratios, - 'left': left, 'bottom': bottom, 'right': right, 'top': top, - } - - return (width, height), gridspec_kw, kwargs - - -class _hidelabels(object): - """ - Hide objects temporarily so they are ignored by the tight bounding box - algorithm. - """ - # NOTE: This will be removed when labels are implemented with AxesStack! - def __init__(self, *args): - self._labels = args - - def __enter__(self): - for label in self._labels: - label.set_visible(False) - - def __exit__(self, *args): # noqa: U100 - for label in self._labels: - label.set_visible(True) - - -class Figure(mfigure.Figure): - """ - The `~matplotlib.figure.Figure` class returned by `subplots`. At - draw-time, an improved tight layout algorithm is employed, and - the space around the figure edge, between subplots, and between - panels is changed to accommodate subplot content. Figure dimensions - may be automatically scaled to preserve subplot aspect ratios. - """ - def __init__( - self, tight=None, - ref=1, pad=None, axpad=None, panelpad=None, includepanels=False, - span=None, spanx=None, spany=None, - align=None, alignx=None, aligny=None, - share=None, sharex=None, sharey=None, - autoformat=True, fallback_to_cm=None, - gridspec_kw=None, subplots_kw=None, subplots_orig_kw=None, - **kwargs - ): - """ - Parameters - ---------- - tight : bool, optional - Toggles automatic tight layout adjustments. Default is :rc:`tight`. - If you manually specified a spacing in the call to `subplots`, it - will be used to override the tight layout spacing. For example, - with ``left=0.1``, the left margin is set to 0.1 inches wide, - while the remaining margin widths are calculated automatically. - ref : int, optional - The reference subplot number. See `subplots` for details. Default - is ``1``. - pad : float or str, optional - Padding around edge of figure. Units are interpreted by - `~proplot.utils.units`. Default is :rc:`subplots.pad`. - axpad : float or str, optional - Padding between subplots in adjacent columns and rows. Units are - interpreted by `~proplot.utils.units`. Default is - :rc:`subplots.axpad`. - panelpad : float or str, optional - Padding between subplots and axes panels, and between "stacked" - panels. Units are interpreted by `~proplot.utils.units`. Default is - :rc:`subplots.panelpad`. - includepanels : bool, optional - Whether to include panels when centering *x* axis labels, - *y* axis labels, and figure "super titles" along the edge of the - subplot grid. Default is ``False``. - sharex, sharey, share : {3, 2, 1, 0}, optional - The "axis sharing level" for the *x* axis, *y* axis, or both axes. - Default is ``3``. This can considerably reduce redundancy in your - figure. Options are as follows. - - 0. No axis sharing. Also sets the default `spanx` and `spany` - values to ``False``. - 1. Only draw *axis label* on the leftmost column (*y*) or - bottommost row (*x*) of subplots. Axis tick labels - still appear on every subplot. - 2. As in 1, but forces the axis limits to be identical. Axis - tick labels still appear on every subplot. - 3. As in 2, but only show the *axis tick labels* on the - leftmost column (*y*) or bottommost row (*x*) of subplots. - - spanx, spany, span : bool or {0, 1}, optional - Toggles "spanning" axis labels for the *x* axis, *y* axis, or both - axes. Default is ``False`` if `sharex`, `sharey`, or `share` are - ``0``, ``True`` otherwise. When ``True``, a single, centered axis - label is used for all axes with bottom and left edges in the same - row or column. This can considerably redundancy in your figure. - - "Spanning" labels integrate with "shared" axes. For example, - for a 3-row, 3-column figure, with ``sharey > 1`` and ``spany=1``, - your figure will have 1 ylabel instead of 9. - alignx, aligny, align : bool or {0, 1}, optional - Default is ``False``. Whether to `align axis labels \ -`__ - for the *x* axis, *y* axis, or both axes. Only has an effect when - `spanx`, `spany`, or `span` are ``False``. - autoformat : bool, optional - Whether to automatically configure *x* axis labels, *y* axis - labels, axis formatters, axes titles, colorbar labels, and legend - labels when a `~pandas.Series`, `~pandas.DataFrame` or - `~xarray.DataArray` with relevant metadata is passed to a plotting - command. - fallback_to_cm : bool, optional - Whether to replace unavailable glyphs with a glyph from Computer - Modern or the "¤" dummy character. See `mathtext \ -`__ - for details. - gridspec_kw, subplots_kw, subplots_orig_kw - Keywords used for initializing the main gridspec, for initializing - the figure, and original spacing keyword args used for initializing - the figure that override tight layout spacing. - - Other parameters - ---------------- - **kwargs - Passed to `matplotlib.figure.Figure`. - - See also - -------- - `~matplotlib.figure.Figure` - """ # noqa - tight_layout = kwargs.pop('tight_layout', None) - constrained_layout = kwargs.pop('constrained_layout', None) - if tight_layout or constrained_layout: - _warn_proplot( - f'Ignoring tight_layout={tight_layout} and ' - f'contrained_layout={constrained_layout}. ProPlot uses its ' - 'own tight layout algorithm, activated by default or with ' - 'tight=True.' - ) - - # Initialize first, because need to provide fully initialized figure - # as argument to gridspec, because matplotlib tight_layout does that - self._authorized_add_subplot = False - self._is_preprocessing = False - self._is_resizing = False - super().__init__(**kwargs) - - # Axes sharing and spanning settings - sharex = _notNone(sharex, share, rc['share']) - sharey = _notNone(sharey, share, rc['share']) - spanx = _notNone(spanx, span, 0 if sharex == 0 else None, rc['span']) - spany = _notNone(spany, span, 0 if sharey == 0 else None, rc['span']) - if spanx and (alignx or align): - _warn_proplot(f'"alignx" has no effect when spanx=True.') - if spany and (aligny or align): - _warn_proplot(f'"aligny" has no effect when spany=True.') - alignx = _notNone(alignx, align, rc['align']) - aligny = _notNone(aligny, align, rc['align']) - self.set_alignx(alignx) - self.set_aligny(aligny) - self.set_sharex(sharex) - self.set_sharey(sharey) - self.set_spanx(spanx) - self.set_spany(spany) - - # Various other attributes - gridspec_kw = gridspec_kw or {} - gridspec = GridSpec(self, **gridspec_kw) - nrows, ncols = gridspec.get_active_geometry() - self._pad = units(_notNone(pad, rc['subplots.pad'])) - self._axpad = units(_notNone(axpad, rc['subplots.axpad'])) - self._panelpad = units(_notNone(panelpad, rc['subplots.panelpad'])) - self._auto_format = autoformat - self._auto_tight = _notNone(tight, rc['tight']) - self._include_panels = includepanels - self._fallback_to_cm = fallback_to_cm - self._ref_num = ref - self._axes_main = [] - self._subplots_orig_kw = subplots_orig_kw - self._subplots_kw = subplots_kw - self._bottom_panels = [] - self._top_panels = [] - self._left_panels = [] - self._right_panels = [] - self._bottom_array = np.empty((0, ncols), dtype=bool) - self._top_array = np.empty((0, ncols), dtype=bool) - self._left_array = np.empty((0, nrows), dtype=bool) - self._right_array = np.empty((0, nrows), dtype=bool) - self._gridspec_main = gridspec - self.suptitle('') # add _suptitle attribute - - @_counter - def _add_axes_panel(self, ax, side, filled=False, **kwargs): - """ - Hidden method that powers `~proplot.axes.panel_axes`. - """ - # Interpret args - # NOTE: Axis sharing not implemented for figure panels, 99% of the - # time this is just used as construct for adding global colorbars and - # legends, really not worth implementing axis sharing - if side not in ('left', 'right', 'bottom', 'top'): - raise ValueError(f'Invalid side {side!r}.') - ax = ax._panel_parent or ax # redirect to main axes - share, width, space, space_orig = _get_panelargs( - side, filled=filled, figure=False, **kwargs - ) - - # Get gridspec and subplotspec indices - subplotspec = ax.get_subplotspec() - *_, row1, row2, col1, col2 = subplotspec.get_active_rows_columns() - pgrid = getattr(ax, '_' + side + '_panels') - offset = len(pgrid) * bool(pgrid) + 1 - if side in ('left', 'right'): - iratio = col1 - offset if side == 'left' else col2 + offset - idx1 = slice(row1, row2 + 1) - idx2 = iratio - else: - iratio = row1 - offset if side == 'top' else row2 + offset - idx1 = iratio - idx2 = slice(col1, col2 + 1) - gridspec_prev = self._gridspec_main - gridspec = self._insert_row_column( - side, iratio, width, space, space_orig, figure=False - ) - if gridspec is not gridspec_prev: - if side == 'top': - idx1 += 1 - elif side == 'left': - idx2 += 1 - - # Draw and setup panel - with self._authorize_add_subplot(): - pax = self.add_subplot( - gridspec[idx1, idx2], - projection='xy', - ) - pgrid.append(pax) - pax._panel_side = side - pax._panel_share = share - pax._panel_parent = ax - - # Axis sharing and axis setup only for non-legend or colorbar axes - if not filled: - ax._share_setup() - axis = pax.yaxis if side in ('left', 'right') else pax.xaxis - getattr(axis, 'tick_' + side)() # set tick and label positions - axis.set_label_position(side) - - return pax - - def _add_figure_panel( - self, side, span=None, row=None, col=None, rows=None, cols=None, - **kwargs - ): - """ - Add a figure panel. Also modifies the panel attribute stored - on the figure to include these panels. - """ - # Interpret args and enforce sensible keyword args - if side not in ('left', 'right', 'bottom', 'top'): - raise ValueError(f'Invalid side {side!r}.') - _, width, space, space_orig = _get_panelargs( - side, filled=True, figure=True, **kwargs - ) - if side in ('left', 'right'): - for key, value in (('col', col), ('cols', cols)): - if value is not None: - raise ValueError( - f'Invalid keyword arg {key!r} for figure panel ' - f'on side {side!r}.' - ) - span = _notNone( - span, row, rows, None, names=('span', 'row', 'rows') - ) - else: - for key, value in (('row', row), ('rows', rows)): - if value is not None: - raise ValueError( - f'Invalid keyword arg {key!r} for figure panel ' - f'on side {side!r}.' - ) - span = _notNone( - span, col, cols, None, names=('span', 'col', 'cols') - ) - - # Get props - subplots_kw = self._subplots_kw - if side in ('left', 'right'): - panels, nacross = subplots_kw['hpanels'], subplots_kw['ncols'] - else: - panels, nacross = subplots_kw['wpanels'], subplots_kw['nrows'] - array = getattr(self, '_' + side + '_array') - npanels, nalong = array.shape - - # Check span array - span = _notNone(span, (1, nalong)) - if not np.iterable(span) or len(span) == 1: - span = 2 * np.atleast_1d(span).tolist() - if len(span) != 2: - raise ValueError(f'Invalid span {span!r}.') - if span[0] < 1 or span[1] > nalong: - raise ValueError( - f'Invalid coordinates in span={span!r}. Coordinates ' - f'must satisfy 1 <= c <= {nalong}.' - ) - start, stop = span[0] - 1, span[1] # zero-indexed - - # See if there is room for panel in current figure panels - # The 'array' is an array of boolean values, where each row corresponds - # to another figure panel, moving toward the outside, and boolean - # True indicates the slot has been filled - iratio = -1 if side in ('left', 'top') else nacross # default values - for i in range(npanels): - if not any(array[i, start:stop]): - array[i, start:stop] = True - if side in ('left', 'top'): # descending moves us closer to 0 - # npanels=1, i=0 --> iratio=0 - # npanels=2, i=0 --> iratio=1 - # npanels=2, i=1 --> iratio=0 - iratio = npanels - 1 - i - else: # descending array moves us closer to nacross-1 - # npanels=1, i=0 --> iratio=nacross-1 - # npanels=2, i=0 --> iratio=nacross-2 - # npanels=2, i=1 --> iratio=nacross-1 - iratio = nacross - (npanels - i) - break - if iratio in (-1, nacross): # add to array - iarray = np.zeros((1, nalong), dtype=bool) - iarray[0, start:stop] = True - array = np.concatenate((array, iarray), axis=0) - setattr(self, '_' + side + '_array', array) - - # Get gridspec and subplotspec indices - idxs, = np.where(np.array(panels) == '') - if len(idxs) != nalong: - raise RuntimeError - if side in ('left', 'right'): - idx1 = slice(idxs[start], idxs[stop - 1] + 1) - idx2 = max(iratio, 0) - else: - idx1 = max(iratio, 0) - idx2 = slice(idxs[start], idxs[stop - 1] + 1) - gridspec = self._insert_row_column( - side, iratio, width, space, space_orig, figure=True - ) - - # Draw and setup panel - with self._authorize_add_subplot(): - pax = self.add_subplot(gridspec[idx1, idx2], projection='xy') - pgrid = getattr(self, '_' + side + '_panels') - pgrid.append(pax) - pax._panel_side = side - pax._panel_share = False - pax._panel_parent = None - return pax - - def _adjust_aspect(self): - """ - Adjust the average aspect ratio used for gridspec calculations. - This fixes grids with identically fixed aspect ratios, e.g. - identically zoomed-in cartopy projections and imshow images. - """ - # Get aspect ratio - if not self._axes_main: - return - ax = self._axes_main[self._ref_num - 1] - curaspect = ax.get_aspect() - if isinstance(curaspect, str): - if curaspect == 'auto': - return - elif curaspect != 'equal': - raise RuntimeError(f'Unknown aspect ratio mode {curaspect!r}.') - - # Compare to current aspect - subplots_kw = self._subplots_kw - xscale, yscale = ax.get_xscale(), ax.get_yscale() - if not isinstance(curaspect, str): - aspect = curaspect - elif xscale == 'linear' and yscale == 'linear': - aspect = 1.0 / ax.get_data_ratio() - elif xscale == 'log' and yscale == 'log': - aspect = 1.0 / ax.get_data_ratio_log() - else: - return # matplotlib should have issued warning - if np.isclose(aspect, subplots_kw['aspect']): - return - - # Apply new aspect - subplots_kw['aspect'] = aspect - figsize, gridspec_kw, _ = _subplots_geometry(**subplots_kw) - self.set_size_inches(figsize, auto=True) - self._gridspec_main.update(**gridspec_kw) - - def _adjust_tight_layout(self, renderer, resize=True): - """ - Apply tight layout scaling that permits flexible figure - dimensions and preserves panel widths and subplot aspect ratios. - """ - # Initial stuff - axs = self._iter_axes() - subplots_kw = self._subplots_kw - subplots_orig_kw = self._subplots_orig_kw # tight layout overrides - if not axs or not subplots_kw or not subplots_orig_kw: - return - - # Temporarily disable spanning labels and get correct - # positions for labels and suptitle - self._align_axislabels(False) - self._align_labels(renderer) - - # Tight box *around* figure - # Get bounds from old bounding box - pad = self._pad - obox = self.bbox_inches # original bbox - bbox = self.get_tightbbox(renderer) - left = bbox.xmin - bottom = bbox.ymin - right = obox.xmax - bbox.xmax - top = obox.ymax - bbox.ymax - - # Apply new bounds, permitting user overrides - # TODO: Account for bounding box NaNs? - for key, offset in zip( - ('left', 'right', 'top', 'bottom'), - (left, right, top, bottom) - ): - previous = subplots_orig_kw[key] - current = subplots_kw[key] - subplots_kw[key] = _notNone(previous, current - offset + pad) - - # Get arrays storing gridspec spacing args - axpad = self._axpad - panelpad = self._panelpad - gridspec = self._gridspec_main - nrows, ncols = gridspec.get_active_geometry() - wspace = subplots_kw['wspace'] - hspace = subplots_kw['hspace'] - wspace_orig = subplots_orig_kw['wspace'] - hspace_orig = subplots_orig_kw['hspace'] - - # Get new subplot spacings, axes panel spacing, figure panel spacing - spaces = [] - for (w, x, y, nacross, ispace, ispace_orig) in zip( - 'wh', 'xy', 'yx', (nrows, ncols), - (wspace, hspace), (wspace_orig, hspace_orig), - ): - # Determine which rows and columns correspond to panels - panels = subplots_kw[w + 'panels'] - jspace = [*ispace] - ralong = np.array([ax._range_gridspec(x) for ax in axs]) - racross = np.array([ax._range_gridspec(y) for ax in axs]) - for i, (space, space_orig) in enumerate(zip(ispace, ispace_orig)): - # Figure out whether this is a normal space, or a - # panel stack space/axes panel space - if ( - panels[i] in ('l', 't') - and panels[i + 1] in ('l', 't', '') - or panels[i] in ('', 'r', 'b') - and panels[i + 1] in ('r', 'b') - or panels[i] == 'f' and panels[i + 1] == 'f' - ): - pad = panelpad - else: - pad = axpad - - # Find axes that abutt aginst this space on each row - groups = [] - # i.e. right/bottom edge abutts against this space - filt1 = ralong[:, 1] == i - # i.e. left/top edge abutts against this space - filt2 = ralong[:, 0] == i + 1 - for j in range(nacross): # e.g. each row - # Get indices - filt = (racross[:, 0] <= j) & (j <= racross[:, 1]) - if sum(filt) < 2: # no interface here - continue - idx1, = np.where(filt & filt1) - idx2, = np.where(filt & filt2) - if idx1.size > 1 or idx2.size > 2: - _warn_proplot('This should never happen.') - continue - elif not idx1.size or not idx2.size: - continue - idx1, idx2 = idx1[0], idx2[0] - # Put these axes into unique groups. Store groups as - # (left axes, right axes) or (bottom axes, top axes) pairs. - ax1, ax2 = axs[idx1], axs[idx2] - if x != 'x': # order bottom-to-top - ax1, ax2 = ax2, ax1 - newgroup = True - for (group1, group2) in groups: - if ax1 in group1 or ax2 in group2: - newgroup = False - group1.add(ax1) - group2.add(ax2) - break - if newgroup: - groups.append([{ax1}, {ax2}]) # form new group - # Get spaces - # Layout is lspace, lspaces[0], rspaces[0], wspace, ... - # so panels spaces are located where i % 3 is 1 or 2 - jspaces = [] - for (group1, group2) in groups: - x1 = max(ax._range_tightbbox(x)[1] for ax in group1) - x2 = min(ax._range_tightbbox(x)[0] for ax in group2) - jspaces.append((x2 - x1) / self.dpi) - if jspaces: - space = max(0, space - min(jspaces) + pad) - space = _notNone(space_orig, space) # user input overwrite - jspace[i] = space - spaces.append(jspace) - - # Update geometry solver kwargs - subplots_kw.update({ - 'wspace': spaces[0], 'hspace': spaces[1], - }) - if not resize: - width, height = self.get_size_inches() - subplots_kw = subplots_kw.copy() - subplots_kw.update(width=width, height=height) - - # Apply new spacing - figsize, gridspec_kw, _ = _subplots_geometry(**subplots_kw) - if resize: - self.set_size_inches(figsize, auto=True) - self._gridspec_main.update(**gridspec_kw) - - def _align_axislabels(self, b=True): - """ - Align spanning *x* and *y* axis labels in the perpendicular - direction and, if `b` is ``True``, the parallel direction. - """ - # TODO: Ensure this is robust to complex panels and shared axes - # NOTE: Need to turn off aligned labels before _adjust_tight_layout - # call, so cannot put this inside Axes draw - tracker = {*()} - for ax in self._axes_main: - if not isinstance(ax, axes.XYAxes): - continue - for x, axis in zip('xy', (ax.xaxis, ax.yaxis)): - side = axis.get_label_position() - span = getattr(self, '_span' + x) - align = getattr(self, '_align' + x) - if side not in ('bottom', 'left') or axis in tracker: - continue - axs = ax._get_side_axes(side) - for _ in range(2): - axs = [getattr(ax, '_share' + x) or ax for ax in axs] - # Align axis label offsets - axises = [getattr(ax, x + 'axis') for ax in axs] - tracker.update(axises) - if span or align: - grp = getattr(self, '_align_' + x + 'label_grp', None) - if grp is not None: - for ax in axs[1:]: - # copied from source code, add to grouper - grp.join(axs[0], ax) - elif align: - _warn_proplot( - f'Aligning *x* and *y* axis labels required ' - f'matplotlib >=3.1.0' - ) - if not span: - continue - # Get spanning label position - c, spanax = self._get_align_coord(side, axs) - spanaxis = getattr(spanax, x + 'axis') - spanlabel = spanaxis.label - if not hasattr(spanlabel, '_orig_transform'): - spanlabel._orig_transform = spanlabel.get_transform() - spanlabel._orig_position = spanlabel.get_position() - if not b: # toggle off, done before tight layout - spanlabel.set_transform(spanlabel._orig_transform) - spanlabel.set_position(spanlabel._orig_position) - for axis in axises: - axis.label.set_visible(True) - else: # toggle on, done after tight layout - if x == 'x': - position = (c, 1) - transform = mtransforms.blended_transform_factory( - self.transFigure, mtransforms.IdentityTransform()) - else: - position = (1, c) - transform = mtransforms.blended_transform_factory( - mtransforms.IdentityTransform(), self.transFigure) - for axis in axises: - axis.label.set_visible((axis is spanaxis)) - spanlabel.update({ - 'position': position, 'transform': transform - }) - - def _align_labels(self, renderer): - """ - Adjust the position of row and column labels, and align figure super - title accounting for figure margins and axes and figure panels. - """ - # Offset using tight bounding boxes - # TODO: Super labels fail with popup backend!! Fix this - # NOTE: Must use get_tightbbox so (1) this will work if tight layout - # mode if off and (2) actually need *two* tight bounding boxes when - # labels are present: 1 not including the labels, used to position - # them, and 1 including the labels, used to determine figure borders - suptitle = self._suptitle - suptitle_on = suptitle.get_text().strip() - width, height = self.get_size_inches() - for side in ('left', 'right', 'bottom', 'top'): - # Get axes and offset the label to relevant panel - if side in ('left', 'right'): - x = 'x' - iter_panels = ('bottom', 'top') - else: - x = 'y' - iter_panels = ('left', 'right') - axs = self._get_align_axes(side) - axs = [ax._reassign_suplabel(side) for ax in axs] - labels = [getattr(ax, '_' + side + '_label') for ax in axs] - coords = [None] * len(axs) - if side == 'top' and suptitle_on: - supaxs = axs - with _hidelabels(*labels): - for i, (ax, label) in enumerate(zip(axs, labels)): - label_on = label.get_text().strip() - if not label_on: - continue - # Get coord from tight bounding box - # Include twin axes and panels along the same side - icoords = [] - for iax in ax._iter_panels(iter_panels): - bbox = iax.get_tightbbox(renderer) - if side == 'left': - jcoords = (bbox.xmin, 0) - elif side == 'right': - jcoords = (bbox.xmax, 0) - elif side == 'top': - jcoords = (0, bbox.ymax) - else: - jcoords = (0, bbox.ymin) - c = self.transFigure.inverted().transform(jcoords) - c = c[0] if side in ('left', 'right') else c[1] - icoords.append(c) - # Offset, and offset a bit extra for left/right labels - # See: - # https://matplotlib.org/api/text_api.html#matplotlib.text.Text.set_linespacing - fontsize = label.get_fontsize() - if side in ('left', 'right'): - scale1, scale2 = 0.6, width - else: - scale1, scale2 = 0.3, height - if side in ('left', 'bottom'): - coords[i] = min(icoords) - ( - scale1 * fontsize / 72 - ) / scale2 - else: - coords[i] = max(icoords) + ( - scale1 * fontsize / 72 - ) / scale2 - # Assign coords - coords = [i for i in coords if i is not None] - if coords: - if side in ('left', 'bottom'): - c = min(coords) - else: - c = max(coords) - for label in labels: - label.update({x: c}) - - # Update super title position - # If no axes on the top row are visible, do not try to align! - if suptitle_on and supaxs: - ys = [] - for ax in supaxs: - bbox = ax.get_tightbbox(renderer) - _, y = self.transFigure.inverted().transform((0, bbox.ymax)) - ys.append(y) - x, _ = self._get_align_coord('top', supaxs) - y = max(ys) + (0.3 * suptitle.get_fontsize() / 72) / height - kw = { - 'x': x, 'y': y, - 'ha': 'center', 'va': 'bottom', - 'transform': self.transFigure - } - suptitle.update(kw) - - def _authorize_add_subplot(self): - """ - Prevent warning message when adding subplots one-by-one. Used - internally. - """ - return _setstate(self, _authorized_add_subplot=True) - - def _context_resizing(self): - """ - Ensure backend calls to `~matplotlib.figure.Figure.set_size_inches` - during pre-processing are not interpreted as *manual* resizing. - """ - return _setstate(self, _is_resizing=True) - - def _context_preprocessing(self): - """ - Prevent re-running pre-processing steps due to draws triggered - by figure resizes during pre-processing. - """ - return _setstate(self, _is_preprocessing=True) - - def _get_align_coord(self, side, axs): - """ - Return the figure coordinate for spanning labels or super titles. - The `x` can be ``'x'`` or ``'y'``. - """ - # Get position in figure relative coordinates - if side in ('left', 'right'): - x = 'y' - iter_panels = ('top', 'bottom') - else: - x = 'x' - iter_panels = ('left', 'right') - if self._include_panels: - axs = [iax for ax in axs for iax in ax._iter_panels(iter_panels)] - ranges = np.array([ax._range_gridspec(x) for ax in axs]) - min_, max_ = ranges[:, 0].min(), ranges[:, 1].max() - axlo = axs[np.where(ranges[:, 0] == min_)[0][0]] - axhi = axs[np.where(ranges[:, 1] == max_)[0][0]] - lobox = axlo.get_subplotspec().get_position(self) - hibox = axhi.get_subplotspec().get_position(self) - if x == 'x': - pos = (lobox.x0 + hibox.x1) / 2 - else: - # 'lo' is actually on top, highest up in gridspec - pos = (lobox.y1 + hibox.y0) / 2 - # Return axis suitable for spanning position - spanax = axs[(np.argmin(ranges[:, 0]) + np.argmax(ranges[:, 1])) // 2] - spanax = spanax._panel_parent or spanax - return pos, spanax - - def _get_align_axes(self, side): - """ - Return the main axes along the left, right, bottom, or top sides - of the figure. - """ - # Initial stuff - idx = 0 if side in ('left', 'top') else 1 - if side in ('left', 'right'): - x, y = 'x', 'y' - else: - x, y = 'y', 'x' - # Get edge index - axs = self._axes_main - if not axs: - return [] - ranges = np.array([ax._range_gridspec(x) for ax in axs]) - min_, max_ = ranges[:, 0].min(), ranges[:, 1].max() - edge = min_ if side in ('left', 'top') else max_ - # Return axes on edge sorted by order of appearance - axs = [ - ax for ax in self._axes_main if ax._range_gridspec(x)[idx] == edge - ] - ranges = [ax._range_gridspec(y)[0] for ax in axs] - return [ax for _, ax in sorted(zip(ranges, axs)) if ax.get_visible()] - - def _get_renderer(self): - """ - Get a renderer at all costs, even if it means generating a brand - new one! Used for updating the figure bounding box when it is accessed - and calculating centered-row legend bounding boxes. This is copied - from tight_layout.py in matplotlib. - """ - if self._cachedRenderer: - renderer = self._cachedRenderer - else: - canvas = self.canvas - if canvas and hasattr(canvas, 'get_renderer'): - renderer = canvas.get_renderer() - else: - from matplotlib.backends.backend_agg import FigureCanvasAgg - canvas = FigureCanvasAgg(self) - renderer = canvas.get_renderer() - return renderer - - def _insert_row_column( - self, side, idx, - ratio, space, space_orig, figure=False, - ): - """ - "Overwrite" the main figure gridspec to make room for a panel. The - `side` is the panel side, the `idx` is the slot you want the panel - to occupy, and the remaining args are the panel widths and spacings. - """ - # Constants and stuff - # Insert spaces to the left of right panels or to the right of - # left panels. And note that since .insert() pushes everything in - # that column to the right, actually must insert 1 slot farther to - # the right when inserting left panels/spaces - if side not in ('left', 'right', 'bottom', 'top'): - raise ValueError(f'Invalid side {side}.') - idx_space = idx - 1 * bool(side in ('bottom', 'right')) - idx_offset = 1 * bool(side in ('top', 'left')) - if side in ('left', 'right'): - w, ncols = 'w', 'ncols' - else: - w, ncols = 'h', 'nrows' - - # Load arrays and test if we need to insert - subplots_kw = self._subplots_kw - subplots_orig_kw = self._subplots_orig_kw - panels = subplots_kw[w + 'panels'] - ratios = subplots_kw[w + 'ratios'] - spaces = subplots_kw[w + 'space'] - spaces_orig = subplots_orig_kw[w + 'space'] - - # Slot already exists - entry = 'f' if figure else side[0] - exists = idx not in (-1, len(panels)) and panels[idx] == entry - if exists: # already exists! - if spaces_orig[idx_space] is None: - spaces_orig[idx_space] = units(space_orig) - spaces[idx_space] = _notNone(spaces_orig[idx_space], space) - # Make room for new panel slot - else: - # Modify basic geometry - idx += idx_offset - idx_space += idx_offset - subplots_kw[ncols] += 1 - # Original space, ratio array, space array, panel toggles - spaces_orig.insert(idx_space, space_orig) - spaces.insert(idx_space, space) - ratios.insert(idx, ratio) - panels.insert(idx, entry) - # Reference ax location array - # TODO: For now do not need to increment, but need to double - # check algorithm for fixing axes aspect! - # ref = subplots_kw[x + 'ref'] - # ref[:] = [val + 1 if val >= idx else val for val in ref] - - # Update figure - figsize, gridspec_kw, _ = _subplots_geometry(**subplots_kw) - self.set_size_inches(figsize, auto=True) - if exists: - gridspec = self._gridspec_main - gridspec.update(**gridspec_kw) - else: - # New gridspec - gridspec = GridSpec(self, **gridspec_kw) - self._gridspec_main = gridspec - # Reassign subplotspecs to all axes and update positions - # May seem inefficient but it literally just assigns a hidden, - # attribute, and the creation time for subpltospecs is tiny - axs = [iax for ax in self._iter_axes() - for iax in (ax, *ax.child_axes)] - for ax in axs: - # Get old index - # NOTE: Endpoints are inclusive, not exclusive! - if not hasattr(ax, 'get_subplotspec'): - continue - if side in ('left', 'right'): - inserts = (None, None, idx, idx) - else: - inserts = (idx, idx, None, None) - subplotspec = ax.get_subplotspec() - igridspec = subplotspec.get_gridspec() - topmost = subplotspec.get_topmost_subplotspec() - # Apply new subplotspec! - _, _, *coords = topmost.get_active_rows_columns() - for i in range(4): - # if inserts[i] is not None and coords[i] >= inserts[i]: - if inserts[i] is not None and coords[i] >= inserts[i]: - coords[i] += 1 - (row1, row2, col1, col2) = coords - subplotspec_new = gridspec[row1:row2 + 1, col1:col2 + 1] - if topmost is subplotspec: - ax.set_subplotspec(subplotspec_new) - elif topmost is igridspec._subplot_spec: - igridspec._subplot_spec = subplotspec_new - else: - raise ValueError( - f'Unexpected GridSpecFromSubplotSpec nesting.' - ) - # Update parent or child position - ax.update_params() - ax.set_position(ax.figbox) - - return gridspec - - def _update_figtitle(self, title, **kwargs): - """ - Assign the figure "super title" and update settings. - """ - if title is not None and self._suptitle.get_text() != title: - self._suptitle.set_text(title) - if kwargs: - self._suptitle.update(kwargs) - - def _update_labels(self, ax, side, labels, **kwargs): - """ - Assign the side labels and update settings. - """ - if side not in ('left', 'right', 'bottom', 'top'): - raise ValueError(f'Invalid label side {side!r}.') - - # Get main axes on the edge - axs = self._get_align_axes(side) - if not axs: - return # occurs if called while adding axes - - # Update label text for axes on the edge - if labels is None or isinstance(labels, str): # common during testing - labels = [labels] * len(axs) - if len(labels) != len(axs): - raise ValueError( - f'Got {len(labels)} {side}labels, but there are ' - f'{len(axs)} axes along that side.' - ) - for ax, label in zip(axs, labels): - obj = getattr(ax, '_' + side + '_label') - if label is not None and obj.get_text() != label: - obj.set_text(label) - if kwargs: - obj.update(kwargs) - - def add_subplot(self, *args, **kwargs): - """ - Issues warning for new users that try to call - `~matplotlib.figure.Figure.add_subplot` manually. - """ - if not self._authorized_add_subplot: - _warn_proplot( - 'Using "fig.add_subplot()" with ProPlot figures may result in ' - 'unexpected behavior. Please use "proplot.subplots()" instead.' - ) - ax = super().add_subplot(*args, **kwargs) - return ax - - def colorbar( - self, *args, - loc='r', width=None, space=None, - row=None, col=None, rows=None, cols=None, span=None, - **kwargs - ): - """ - Draw a colorbar along the left, right, bottom, or top side - of the figure, centered between the leftmost and rightmost (or - topmost and bottommost) main axes. - - Parameters - ---------- - loc : str, optional - The colorbar location. Valid location keys are as follows. - - =========== ===================== - Location Valid keys - =========== ===================== - left edge ``'l'``, ``'left'`` - right edge ``'r'``, ``'right'`` - bottom edge ``'b'``, ``'bottom'`` - top edge ``'t'``, ``'top'`` - =========== ===================== - - row, rows : optional - Aliases for `span` for panels on the left or right side. - col, cols : optional - Aliases for `span` for panels on the top or bottom side. - span : int or (int, int), optional - Describes how the colorbar spans rows and columns of subplots. - For example, ``fig.colorbar(loc='b', col=1)`` draws a colorbar - beneath the leftmost column of subplots, and - ``fig.colorbar(loc='b', cols=(1,2))`` draws a colorbar beneath the - left two columns of subplots. By default, the colorbar will span - all rows and columns. - space : float or str, optional - The space between the main subplot grid and the colorbar, or the - space between successively stacked colorbars. Units are interpreted - by `~proplot.utils.units`. By default, this is determined by - the "tight layout" algorithm, or is :rc:`subplots.panelpad` - if "tight layout" is off. - width : float or str, optional - The colorbar width. Units are interpreted by - `~proplot.utils.units`. Default is :rc:`colorbar.width`. - *args, **kwargs - Passed to `~proplot.axes.Axes.colorbar`. - """ - ax = kwargs.pop('ax', None) - cax = kwargs.pop('cax', None) - # Fill this axes - if cax is not None: - return super().colorbar(*args, cax=cax, **kwargs) - # Generate axes panel - elif ax is not None: - return ax.colorbar(*args, space=space, width=width, **kwargs) - # Generate figure panel - loc = self._axes_main[0]._loc_translate(loc, 'panel') - ax = self._add_figure_panel( - loc, space=space, width=width, span=span, - row=row, col=col, rows=rows, cols=cols - ) - return ax.colorbar(*args, loc='_fill', **kwargs) - - def get_alignx(self): - """ - Return the *x* axis label alignment mode. - """ - return self._alignx - - def get_aligny(self): - """ - Return the *y* axis label alignment mode. - """ - return self._aligny - - def get_sharex(self): - """ - Return the *x* axis sharing level. - """ - return self._sharex - - def get_sharey(self): - """ - Return the *y* axis sharing level. - """ - return self._sharey - - def get_spanx(self): - """ - Return the *x* axis label spanning mode. - """ - return self._spanx - - def get_spany(self): - """ - Return the *y* axis label spanning mode. - """ - return self._spany - - def draw(self, renderer): - # Certain backends *still* have issues with the tight layout - # algorithm e.g. due to opening windows in *tabs*. Have not found way - # to intervene in the FigureCanvas. For this reason we *also* apply - # the algorithm inside Figure.draw in the same way that matplotlib - # applies its tight layout algorithm. So far we just do this for Qt* - # and MacOSX; corrections are generally *small* but notable! - if not self.get_visible(): - return - if self._auto_tight and ( - rc['backend'] == 'MacOSX' or rc['backend'][:2] == 'Qt' - ): - self._adjust_tight_layout(renderer, resize=False) - self._align_axislabels(True) # if spaces changed need to realign - self._align_labels(renderer) - return super().draw(renderer) - - def legend( - self, *args, - loc='r', width=None, space=None, - row=None, col=None, rows=None, cols=None, span=None, - **kwargs - ): - """ - Draw a legend along the left, right, bottom, or top side of the - figure, centered between the leftmost and rightmost (or - topmost and bottommost) main axes. - - Parameters - ---------- - loc : str, optional - The legend location. Valid location keys are as follows. - - =========== ===================== - Location Valid keys - =========== ===================== - left edge ``'l'``, ``'left'`` - right edge ``'r'``, ``'right'`` - bottom edge ``'b'``, ``'bottom'`` - top edge ``'t'``, ``'top'`` - =========== ===================== - - row, rows : optional - Aliases for `span` for panels on the left or right side. - col, cols : optional - Aliases for `span` for panels on the top or bottom side. - span : int or (int, int), optional - Describes how the legend spans rows and columns of subplots. - For example, ``fig.legend(loc='b', col=1)`` draws a legend - beneath the leftmost column of subplots, and - ``fig.legend(loc='b', cols=(1,2))`` draws a legend beneath the - left two columns of subplots. By default, the legend will span - all rows and columns. - space : float or str, optional - The space between the main subplot grid and the legend, or the - space between successively stacked colorbars. Units are interpreted - by `~proplot.utils.units`. By default, this is adjusted - automatically in the "tight layout" calculation, or is - :rc:`subplots.panelpad` if "tight layout" is turned off. - *args, **kwargs - Passed to `~proplot.axes.Axes.legend`. - """ - ax = kwargs.pop('ax', None) - # Generate axes panel - if ax is not None: - return ax.legend(*args, space=space, width=width, **kwargs) - # Generate figure panel - loc = self._axes_main[0]._loc_translate(loc, 'panel') - ax = self._add_figure_panel( - loc, space=space, width=width, span=span, - row=row, col=col, rows=rows, cols=cols - ) - return ax.legend(*args, loc='_fill', **kwargs) - - def save(self, filename, **kwargs): - # Alias for `~Figure.savefig` because ``fig.savefig`` is redundant. - return self.savefig(filename, **kwargs) - - def savefig(self, filename, **kwargs): - # Automatically expand user the user name. Undocumented because we - # do not want to overwrite the matplotlib docstring. - super().savefig(os.path.expanduser(filename), **kwargs) - - def set_canvas(self, canvas): - # Set the canvas and add monkey patches to the instance-level - # `~matplotlib.backend_bases.FigureCanvasBase.draw_idle` and - # `~matplotlib.backend_bases.FigureCanvasBase.print_figure` - # methods. The latter is called by save() and by the inline backend. - # See `_canvas_preprocess` for details.""" - # NOTE: Cannot use draw_idle() because it causes complications for qt5 - # backend (wrong figure size). Even though usage is less consistent we - # *must* use draw() and _draw() instead. - if hasattr(canvas, '_draw'): - canvas._draw = _canvas_preprocess(canvas, '_draw') - else: - canvas.draw = _canvas_preprocess(canvas, 'draw') - canvas.print_figure = _canvas_preprocess(canvas, 'print_figure') - super().set_canvas(canvas) - - def set_size_inches(self, w, h=None, forward=True, auto=False): - # Set the figure size and, if this is being called manually or from - # an interactive backend, override the geometry tracker so users can - # use interactive backends. If figure size is unchaged we *do not* - # update the geometry tracker (figure backends often do this when - # the figure is being initialized). See #76. Undocumented because this - # is only relevant internally. - # NOTE: Bitmap renderers calculate the figure size in inches from - # int(Figure.bbox.[width|height]) which rounds to whole pixels. When - # renderer calls set_size_inches, size may be effectively the same, but - # slightly changed due to roundoff error! Therefore, always compare to - # *both* get_size_inches() and the truncated bbox dimensions times dpi. - if h is None: - width, height = w - else: - width, height = w, h - if not all(np.isfinite(_) for _ in (width, height)): - raise ValueError( - 'Figure size must be finite, not ({width}, {height}).' - ) - width_true, height_true = self.get_size_inches() - width_trunc = int(self.bbox.width) / self.dpi - height_trunc = int(self.bbox.height) / self.dpi - if auto: - with self._context_resizing(): - super().set_size_inches(width, height, forward=forward) - else: - if ( # internal resizing not associated with any draws - ( - width not in (width_true, width_trunc) - or height not in (height_true, height_trunc) - ) - and not self._is_resizing - and not self.canvas._is_idle_drawing # standard - and not getattr(self.canvas, '_draw_pending', None) # pyqt5 - ): - self._subplots_kw.update(width=width, height=height) - super().set_size_inches(width, height, forward=forward) - - def set_alignx(self, value): - """ - Set the *x* axis label alignment mode. - """ - self.stale = True - self._alignx = bool(value) - - def set_aligny(self, value): - """ - Set the *y* axis label alignment mode. - """ - self.stale = True - self._aligny = bool(value) - - def set_sharex(self, value): - """ - Set the *x* axis sharing level. - """ - value = int(value) - if value not in range(4): - raise ValueError( - 'Invalid sharing level sharex={value!r}. ' - 'Axis sharing level can be 0 (share nothing), ' - '1 (hide axis labels), ' - '2 (share limits and hide axis labels), or ' - '3 (share limits and hide axis and tick labels).' - ) - self.stale = True - self._sharex = value - - def set_sharey(self, value): - """ - Set the *y* axis sharing level. - """ - value = int(value) - if value not in range(4): - raise ValueError( - 'Invalid sharing level sharey={value!r}. ' - 'Axis sharing level can be 0 (share nothing), ' - '1 (hide axis labels), ' - '2 (share limits and hide axis labels), or ' - '3 (share limits and hide axis and tick labels).' - ) - self.stale = True - self._sharey = value - - def set_spanx(self, value): - """ - Set the *x* axis label spanning mode. - """ - self.stale = True - self._spanx = bool(value) - - def set_spany(self, value): - """ - Set the *y* axis label spanning mode. - """ - self.stale = True - self._spany = bool(value) - - @property - def gridspec(self): - """ - The single `GridSpec` instance used for all subplots - in the figure. - """ - return self._gridspec_main - - @property - def ref(self): - """ - The reference axes number. The `axwidth`, `axheight`, and `aspect` - `subplots` and `figure` arguments are applied to this axes, and aspect - ratio is conserved for this axes in tight layout adjustment. - """ - return self._ref - - @ref.setter - def ref(self, ref): - if not isinstance(ref, Integral) or ref < 1: - raise ValueError( - f'Invalid axes number {ref!r}. Must be integer >=1.') - self.stale = True - self._ref = ref - - def _iter_axes(self): - """ - Iterates over all axes and panels in the figure belonging to the - `~proplot.axes.Axes` class. Excludes inset and twin axes. - """ - axs = [] - for ax in ( - *self._axes_main, - *self._left_panels, *self._right_panels, - *self._bottom_panels, *self._top_panels - ): - if not ax or not ax.get_visible(): - continue - axs.append(ax) - for ax in axs: - for side in ('left', 'right', 'bottom', 'top'): - for iax in getattr(ax, '_' + side + '_panels'): - if not iax or not iax.get_visible(): - continue - axs.append(iax) - return axs - - -def _journals(journal): - """ - Return the width and height corresponding to the given journal. - """ - # Get dimensions for figure from common journals. - value = JOURNAL_SPECS.get(journal, None) - if value is None: - raise ValueError( - f'Unknown journal figure size specifier {journal!r}. ' - 'Current options are: ' - + ', '.join(map(repr, JOURNAL_SPECS.keys())) - ) - # Return width, and optionally also the height - width, height = None, None - try: - width, height = value - except (TypeError, ValueError): - width = value - return width, height - - -def _axes_dict(naxs, value, kw=False, default=None): - """ - Return a dictionary that looks like ``{1:value1, 2:value2, ...}`` or - ``{1:{key1:value1, ...}, 2:{key2:value2, ...}, ...}`` for storing - standardized axes-specific properties or keyword args. - """ - # First build up dictionary - # 1) 'string' or {1:'string1', (2,3):'string2'} - if not kw: - if np.iterable(value) and not isinstance(value, (str, dict)): - value = {num + 1: item for num, item in enumerate(value)} - elif not isinstance(value, dict): - value = {range(1, naxs + 1): value} - # 2) {'prop':value} or {1:{'prop':value1}, (2,3):{'prop':value2}} - else: - nested = [isinstance(value, dict) for value in value.values()] - if not any(nested): # any([]) == False - value = {range(1, naxs + 1): value.copy()} - elif not all(nested): - raise ValueError( - 'Pass either of dictionary of key value pairs or ' - 'a dictionary of dictionaries of key value pairs.' - ) - # Then *unfurl* keys that contain multiple axes numbers, i.e. are meant - # to indicate properties for multiple axes at once - kwargs = {} - for nums, item in value.items(): - nums = np.atleast_1d(nums) - for num in nums.flat: - if not kw: - kwargs[num] = item - else: - kwargs[num] = item.copy() - # Fill with default values - for num in range(1, naxs + 1): - if num not in kwargs: - if kw: - kwargs[num] = {} - else: - kwargs[num] = default - # Verify numbers - if {*range(1, naxs + 1)} != {*kwargs.keys()}: - raise ValueError( - f'Have {naxs} axes, but {value!r} has properties for axes ' - + ', '.join(map(repr, sorted(kwargs))) + '.' - ) - return kwargs - - -def subplots( - array=None, ncols=1, nrows=1, - ref=1, order='C', - aspect=1, figsize=None, - width=None, height=None, journal=None, - axwidth=None, axheight=None, - hspace=None, wspace=None, space=None, - hratios=None, wratios=None, - width_ratios=None, height_ratios=None, - left=None, bottom=None, right=None, top=None, - basemap=False, proj=None, projection=None, - proj_kw=None, projection_kw=None, - **kwargs -): - """ - Create a figure with a single subplot or arbitrary grids of subplots, - analogous to `matplotlib.pyplot.subplots`. The subplots can be drawn with - arbitrary projections. - - Parameters - ---------- - array : 2d array-like of int, optional - Array specifying complex grid of subplots. Think of - this array as a "picture" of your figure. For example, the array - ``[[1, 1], [2, 3]]`` creates one long subplot in the top row, two - smaller subplots in the bottom row. Integers must range from 1 to the - number of plots. - - ``0`` indicates an empty space. For example, ``[[1, 1, 1], [2, 0, 3]]`` - creates one long subplot in the top row with two subplots in the bottom - row separated by a space. - ncols, nrows : int, optional - Number of columns, rows. Ignored if `array` was passed. - Use these arguments for simpler subplot grids. - order : {'C', 'F'}, optional - Whether subplots are numbered in column-major (``'C'``) or row-major - (``'F'``) order. Analogous to `numpy.array` ordering. This controls - the order that subplots appear in the `subplot_grid` returned by this - function, and the order of subplot a-b-c labels (see - `~proplot.axes.Axes.format`). - figsize : length-2 tuple, optional - Tuple specifying the figure `(width, height)`. - width, height : float or str, optional - The figure width and height. If you specify just one, the aspect - ratio `aspect` of the reference subplot `ref` will be preserved. - ref : int, optional - The reference subplot number. The `axwidth`, `axheight`, and `aspect` - keyword args are applied to this subplot, and the aspect ratio is - conserved for this subplot in the tight layout adjustment. If you - did not specify `width_ratios` and `height_ratios`, the `axwidth`, - `axheight`, and `aspect` settings will apply to *all* subplots -- - not just the `ref` subplot. - axwidth, axheight : float or str, optional - The width, height of the reference subplot. Units are interpreted by - `~proplot.utils.units`. Default is :rc:`subplots.axwidth`. Ignored - if `width`, `height`, or `figsize` was passed. - aspect : float or length-2 list of floats, optional - The reference subplot aspect ratio, in numeric form (width divided by - height) or as a (width, height) tuple. Ignored if `width`, `height`, - or `figsize` was passed. - journal : str, optional - String name corresponding to an academic journal standard that is used - to control the figure width and, if specified, the height. See the - below table. - - =========== ==================== ========================================================================================================================================================== - Key Size description Organization - =========== ==================== ========================================================================================================================================================== - ``'aaas1'`` 1-column `American Association for the Advancement of Science `__ (e.g. *Science*) - ``'aaas2'`` 2-column ” - ``'agu1'`` 1-column `American Geophysical Union `__ - ``'agu2'`` 2-column ” - ``'agu3'`` full height 1-column ” - ``'agu4'`` full height 2-column ” - ``'ams1'`` 1-column `American Meteorological Society `__ - ``'ams2'`` small 2-column ” - ``'ams3'`` medium 2-column ” - ``'ams4'`` full 2-column ” - ``'nat1'`` 1-column `Nature Research `__ - ``'nat2'`` 2-column ” - ``'pnas1'`` 1-column `Proceedings of the National Academy of Sciences `__ - ``'pnas2'`` 2-column ” - ``'pnas3'`` landscape page ” - =========== ==================== ========================================================================================================================================================== - - width_ratios, height_ratios : float or list thereof, optional - Passed to `GridSpec`, denotes the width - and height ratios for the subplot grid. Length of `width_ratios` - must match the number of rows, and length of `height_ratios` must - match the number of columns. - wratios, hratios - Aliases for `height_ratios`, `width_ratios`. - wspace, hspace, space : float or str or list thereof, optional - Passed to `GridSpec`, denotes the - spacing between grid columns, rows, and both, respectively. If float - or string, expanded into lists of length ``ncols-1`` (for `wspace`) - or length ``nrows-1`` (for `hspace`). - - Units are interpreted by `~proplot.utils.units` for each element of - the list. By default, these are determined by the "tight - layout" algorithm. - left, right, top, bottom : float or str, optional - Passed to `GridSpec`, denotes the width of padding between the - subplots and the figure edge. Units are interpreted by - `~proplot.utils.units`. By default, these are determined by the - "tight layout" algorithm. - proj, projection : str or dict-like, optional - The map projection name. The argument is interpreted as follows. - - * If string, this projection is used for all subplots. For valid - names, see the `~proplot.projs.Proj` documentation. - * If list of string, these are the projections to use for each - subplot in their `array` order. - * If dict-like, keys are integers or tuple integers that indicate - the projection to use for each subplot. If a key is not provided, - that subplot will be a `~proplot.axes.XYAxes`. For example, - in a 4-subplot figure, ``proj={2:'merc', (3,4):'stere'}`` - draws a Cartesian axes for the first subplot, a Mercator - projection for the second subplot, and a Stereographic projection - for the second and third subplots. - - proj_kw, projection_kw : dict-like, optional - Keyword arguments passed to `~mpl_toolkits.basemap.Basemap` or - cartopy `~cartopy.crs.Projection` classes on instantiation. - If dictionary of properties, applies globally. If *dictionary of - dictionaries* of properties, applies to specific subplots, as - with `proj`. - - For example, with ``ncols=2`` and - ``proj_kw={1:{'lon_0':0}, 2:{'lon_0':180}}``, the projection in - the left subplot is centered on the prime meridian, and the projection - in the right subplot is centered on the international dateline. - basemap : bool or dict-like, optional - Whether to use `~mpl_toolkits.basemap.Basemap` or - `~cartopy.crs.Projection` for map projections. Default is ``False``. - If boolean, applies to all subplots. If dictionary, values apply to - specific subplots, as with `proj`. - - Other parameters - ---------------- - **kwargs - Passed to `Figure`. - - Returns - ------- - f : `Figure` - The figure instance. - axs : `subplot_grid` - A special list of axes instances. See `subplot_grid`. - """ # noqa - # Build array - if order not in ('C', 'F'): # better error message - raise ValueError( - f'Invalid order {order!r}. Choose from "C" (row-major, default) ' - f'and "F" (column-major).' - ) - if array is None: - array = np.arange(1, nrows * ncols + 1)[..., None] - array = array.reshape((nrows, ncols), order=order) - # Standardize array - try: - array = np.array(array, dtype=int) # enforce array type - if array.ndim == 1: - # interpret as single row or column - array = array[None, :] if order == 'C' else array[:, None] - elif array.ndim != 2: - raise ValueError( - 'array must be 1-2 dimensional, but got {array.ndim} dims' - ) - array[array == None] = 0 # use zero for placeholder # noqa - except (TypeError, ValueError): - raise ValueError( - f'Invalid subplot array {array!r}. ' - 'Must be 1d or 2d array of integers.' - ) - # Get other props - nums = np.unique(array[array != 0]) - naxs = len(nums) - if {*nums.flat} != {*range(1, naxs + 1)}: - raise ValueError( - f'Invalid subplot array {array!r}. Numbers must span integers ' - '1 to naxs (i.e. cannot skip over numbers), with 0 representing ' - 'empty spaces.' - ) - if ref not in nums: - raise ValueError( - f'Invalid reference number {ref!r}. For array {array!r}, must be ' - 'one of {nums}.' - ) - nrows, ncols = array.shape - - # Get some axes properties, where locations are sorted by axes id. - # NOTE: These ranges are endpoint exclusive, like a slice object! - axids = [np.where(array == i) for i in np.sort( - np.unique(array)) if i > 0] # 0 stands for empty - xrange = np.array([[x.min(), x.max()] for _, x in axids]) - yrange = np.array([[y.min(), y.max()] for y, _ in axids]) - xref = xrange[ref - 1, :] # range for reference axes - yref = yrange[ref - 1, :] - - # Get basemap.Basemap or cartopy.crs.Projection instances for map - proj = _notNone(projection, proj, None, names=('projection', 'proj')) - proj_kw = _notNone( - projection_kw, proj_kw, {}, names=('projection_kw', 'proj_kw') - ) - proj = _axes_dict(naxs, proj, kw=False, default='xy') - proj_kw = _axes_dict(naxs, proj_kw, kw=True) - basemap = _axes_dict(naxs, basemap, kw=False, default=False) - axes_kw = {num: {} - for num in range(1, naxs + 1)} # stores add_subplot arguments - for num, name in proj.items(): - # The default is XYAxes - if name is None or name == 'xy': - axes_kw[num]['projection'] = 'xy' - # Builtin matplotlib polar axes, just use my overridden version - elif name == 'polar': - axes_kw[num]['projection'] = 'polar' - if num == ref: - aspect = 1 - # Custom Basemap and Cartopy axes - else: - package = 'basemap' if basemap[num] else 'geo' - m = projs.Proj( - name, basemap=basemap[num], **proj_kw[num] - ) - if num == ref: - if basemap[num]: - aspect = ( - (m.urcrnrx - m.llcrnrx) / (m.urcrnry - m.llcrnry) - ) - else: - aspect = ( - np.diff(m.x_limits) / np.diff(m.y_limits) - )[0] - axes_kw[num].update({'projection': package, 'map_projection': m}) - - # Figure and/or axes dimensions - names, values = (), () - if journal: - # if user passed width= , will use that journal size - figsize = _journals(journal) - spec = f'journal={journal!r}' - names = ('axwidth', 'axheight', 'width') - values = (axwidth, axheight, width) - width, height = figsize - elif figsize: - spec = f'figsize={figsize!r}' - names = ('axwidth', 'axheight', 'width', 'height') - values = (axwidth, axheight, width, height) - width, height = figsize - elif width is not None or height is not None: - spec = [] - if width is not None: - spec.append(f'width={width!r}') - if height is not None: - spec.append(f'height={height!r}') - spec = ', '.join(spec) - names = ('axwidth', 'axheight') - values = (axwidth, axheight) - # Raise warning - for name, value in zip(names, values): - if value is not None: - _warn_proplot( - f'You specified both {spec} and {name}={value!r}. ' - f'Ignoring {name!r}.' - ) - - # Standardized dimensions - width, height = units(width), units(height) - axwidth, axheight = units(axwidth), units(axheight) - # Standardized user input border spaces - left, right = units(left), units(right) - bottom, top = units(bottom), units(top) - # Standardized user input spaces - wspace = np.atleast_1d(units(_notNone(wspace, space))) - hspace = np.atleast_1d(units(_notNone(hspace, space))) - if len(wspace) == 1: - wspace = np.repeat(wspace, (ncols - 1,)) - if len(wspace) != ncols - 1: - raise ValueError( - f'Require {ncols-1} width spacings for {ncols} columns, ' - 'got {len(wspace)}.' - ) - if len(hspace) == 1: - hspace = np.repeat(hspace, (nrows - 1,)) - if len(hspace) != nrows - 1: - raise ValueError( - f'Require {nrows-1} height spacings for {nrows} rows, ' - 'got {len(hspace)}.' - ) - # Standardized user input ratios - wratios = np.atleast_1d(_notNone( - width_ratios, wratios, 1, names=('width_ratios', 'wratios') - )) - hratios = np.atleast_1d(_notNone( - height_ratios, hratios, 1, names=('height_ratios', 'hratios') - )) - if len(wratios) == 1: - wratios = np.repeat(wratios, (ncols,)) - if len(hratios) == 1: - hratios = np.repeat(hratios, (nrows,)) - if len(wratios) != ncols: - raise ValueError(f'Got {ncols} columns, but {len(wratios)} wratios.') - if len(hratios) != nrows: - raise ValueError(f'Got {nrows} rows, but {len(hratios)} hratios.') - - # Fill subplots_orig_kw with user input values - # NOTE: 'Ratios' are only fixed for panel axes, but we store entire array - wspace, hspace = wspace.tolist(), hspace.tolist() - wratios, hratios = wratios.tolist(), hratios.tolist() - subplots_orig_kw = { - 'left': left, 'right': right, 'top': top, 'bottom': bottom, - 'wspace': wspace, 'hspace': hspace, - } - - # Apply default spaces - share = kwargs.get('share', None) - sharex = _notNone(kwargs.get('sharex', None), share, rc['share']) - sharey = _notNone(kwargs.get('sharey', None), share, rc['share']) - left = _notNone(left, _get_space('left')) - right = _notNone(right, _get_space('right')) - bottom = _notNone(bottom, _get_space('bottom')) - top = _notNone(top, _get_space('top')) - wspace, hspace = np.array(wspace), np.array(hspace) # also copies! - wspace[wspace == None] = _get_space('wspace', sharex) # noqa - hspace[hspace == None] = _get_space('hspace', sharey) # noqa - wratios, hratios = list(wratios), list(hratios) - wspace, hspace = list(wspace), list(hspace) - - # Parse arguments, fix dimensions in light of desired aspect ratio - figsize, gridspec_kw, subplots_kw = _subplots_geometry( - nrows=nrows, ncols=ncols, - aspect=aspect, xref=xref, yref=yref, - left=left, right=right, bottom=bottom, top=top, - width=width, height=height, axwidth=axwidth, axheight=axheight, - wratios=wratios, hratios=hratios, wspace=wspace, hspace=hspace, - wpanels=[''] * ncols, hpanels=[''] * nrows, - ) - fig = plt.figure( - FigureClass=Figure, figsize=figsize, ref=ref, - gridspec_kw=gridspec_kw, subplots_kw=subplots_kw, - subplots_orig_kw=subplots_orig_kw, - **kwargs - ) - gridspec = fig._gridspec_main - - # Draw main subplots - axs = naxs * [None] # list of axes - for idx in range(naxs): - # Get figure gridspec ranges - num = idx + 1 - x0, x1 = xrange[idx, 0], xrange[idx, 1] - y0, y1 = yrange[idx, 0], yrange[idx, 1] - # Draw subplot - subplotspec = gridspec[y0:y1 + 1, x0:x1 + 1] - with fig._authorize_add_subplot(): - axs[idx] = fig.add_subplot( - subplotspec, number=num, main=True, - **axes_kw[num] - ) - - # Shared axes setup - # TODO: Figure out how to defer this to drawtime in #50 - # For some reason just adding _share_setup() to draw() doesn't work - for ax in axs: - ax._share_setup() - - # Return figure and axes - n = (ncols if order == 'C' else nrows) - return fig, subplot_grid(axs, n=n, order=order) diff --git a/proplot/tests/test_journals.py b/proplot/tests/test_journals.py index 7bbd8b01e..5fe002027 100644 --- a/proplot/tests/test_journals.py +++ b/proplot/tests/test_journals.py @@ -1,6 +1,6 @@ import pytest -import proplot as plot +import proplot as pplt from proplot.subplots import JOURNAL_SPECS @@ -8,4 +8,4 @@ @pytest.mark.parametrize('journal', JOURNAL_SPECS.keys()) def test_journal_subplots(journal): """Tests that subplots can be generated with journal specifications.""" - f, axs = plot.subplots(journal=journal) + f, axs = pplt.subplots(journal=journal) diff --git a/proplot/ticker.py b/proplot/ticker.py new file mode 100644 index 000000000..4779f8dba --- /dev/null +++ b/proplot/ticker.py @@ -0,0 +1,861 @@ +#!/usr/bin/env python3 +""" +Various `~matplotlib.ticker.Locator` and `~matplotlib.ticker.Formatter` classes. +""" +import locale +import re +from fractions import Fraction + +import matplotlib.axis as maxis +import matplotlib.ticker as mticker +import numpy as np + +from .config import rc +from .internals import ic # noqa: F401 +from .internals import _not_none, context, docstring + +try: + import cartopy.crs as ccrs + from cartopy.mpl.ticker import ( + LatitudeFormatter, + LongitudeFormatter, + _PlateCarreeFormatter, + ) +except ModuleNotFoundError: + ccrs = None + LatitudeFormatter = LongitudeFormatter = _PlateCarreeFormatter = object + +__all__ = [ + 'IndexLocator', + 'DiscreteLocator', + 'DegreeLocator', + 'LongitudeLocator', + 'LatitudeLocator', + 'AutoFormatter', + 'SimpleFormatter', + 'IndexFormatter', + 'SciFormatter', + 'SigFigFormatter', + 'FracFormatter', + 'DegreeFormatter', + 'LongitudeFormatter', + 'LatitudeFormatter', +] + +REGEX_ZERO = re.compile('\\A[-\N{MINUS SIGN}]?0(.0*)?\\Z') +REGEX_MINUS = re.compile('\\A[-\N{MINUS SIGN}]\\Z') +REGEX_MINUS_ZERO = re.compile('\\A[-\N{MINUS SIGN}]0(.0*)?\\Z') + +_precision_docstring = """ +precision : int, default: {6, 2} + The maximum number of digits after the decimal point. Default is ``6`` + when `zerotrim` is ``True`` and ``2`` otherwise. +""" +_zerotrim_docstring = """ +zerotrim : bool, default: :rc:`format.zerotrim` + Whether to trim trailing decimal zeros. +""" +_auto_docstring = """ +tickrange : 2-tuple of float, optional + Range within which major tick marks are labeled. + All ticks are labeled by default. +wraprange : 2-tuple of float, optional + Range outside of which tick values are wrapped. For example, + ``(-180, 180)`` will format a value of ``200`` as ``-160``. +prefix, suffix : str, optional + Prefix and suffix for all tick strings. The suffix is added before + the optional `negpos` suffix. +negpos : str, optional + Length-2 string indicating the suffix for "negative" and "positive" + numbers, meant to replace the minus sign. +""" +_formatter_call = """ +Convert number to a string. + +Parameters +---------- +x : float + The value. +pos : float, optional + The position. +""" +docstring._snippet_manager['ticker.precision'] = _precision_docstring +docstring._snippet_manager['ticker.zerotrim'] = _zerotrim_docstring +docstring._snippet_manager['ticker.auto'] = _auto_docstring +docstring._snippet_manager['ticker.call'] = _formatter_call + +_dms_docstring = """ +Parameters +---------- +dms : bool, default: False + Locate the ticks on clean degree-minute-second intervals and format the + ticks with minutes and seconds instead of decimals. +""" +docstring._snippet_manager['ticker.dms'] = _dms_docstring + + +def _default_precision_zerotrim(precision=None, zerotrim=None): + """ + Return the default zerotrim and precision. Shared by several formatters. + """ + zerotrim = _not_none(zerotrim, rc['formatter.zerotrim']) + if precision is None: + precision = 6 if zerotrim else 2 + return precision, zerotrim + + +class IndexLocator(mticker.Locator): + """ + Format numbers by assigning fixed strings to non-negative indices. The ticks + are restricted to the extent of plotted content when content is present. + """ + def __init__(self, base=1, offset=0): + self._base = base + self._offset = offset + + def set_params(self, base=None, offset=None): + if base is not None: + self._base = base + if offset is not None: + self._offset = offset + + def __call__(self): + # NOTE: We adapt matplotlib IndexLocator to support case where + # the data interval is empty. Only restrict after data is plotted. + dmin, dmax = self.axis.get_data_interval() + vmin, vmax = self.axis.get_view_interval() + min_ = max(dmin, vmin) + max_ = min(dmax, vmax) + return self.tick_values(min_, max_) + + def tick_values(self, vmin, vmax): + base, offset = self._base, self._offset + vmin = max(base * np.ceil(vmin / base), offset) + vmax = max(base * np.floor(vmax / base), offset) + locs = np.arange(vmin, vmax + 0.5 * base, base) + return self.raise_if_exceeds(locs) + + +class DiscreteLocator(mticker.Locator): + """ + A tick locator suitable for discretized colorbars. Adds ticks to some + subset of the location list depending on the available space determined from + `~matplotlib.axis.Axis.get_tick_space`. Zero will be used if it appears in the + location list, and step sizes along the location list are restricted to "nice" + intervals by default. + """ + default_params = { + 'nbins': None, + 'minor': False, + 'steps': np.array([1, 2, 3, 4, 5, 6, 8, 10]), + 'vcenter': 0.0, + 'min_n_ticks': 2 + } + + @docstring._snippet_manager + def __init__(self, locs, **kwargs): + """ + Parameters + ---------- + locs : array-like + The tick location list. + nbins : int, optional + Maximum number of ticks to select. By default this is automatically + determined based on the the axis length and tick label font size. + minor : bool, default: False + Whether this is for "minor" ticks. Setting to ``True`` will select more + ticks with an index step that divides the index step used for "major" ticks. + steps : array-like of int, default: ``[1 2 3 4 5 6 8]`` + Valid integer index steps when selecting from the tick list. Must fall + between 1 and 9. Powers of 10 of these step sizes will also be permitted. + vcenter : float, optional + The optional non-zero center of the original diverging normalizer. + min_n_ticks : int, default: 1 + The minimum number of ticks to select. See also `nbins`. + """ + self.locs = np.array(locs) + self._nbins = None # otherwise unset + self.set_params(**{**self.default_params, **kwargs}) + + def __call__(self): + """ + Return the locations of the ticks. + """ + return self.tick_values(None, None) + + def set_params(self, nbins=None, minor=None, steps=None, vcenter=None, min_n_ticks=None): # noqa: E501 + """ + Set the parameters for this locator. See `DiscreteLocator` for details. + """ + if steps is not None: + steps = np.unique(np.array(steps, dtype=int)) # also sorts, makes 1D + if np.any(steps < 1) or np.any(steps > 10): + raise ValueError('Steps must fall between one and ten (inclusive).') + if steps[0] != 1: + steps = np.concatenate([[1], steps]) + if steps[-1] != 10: + steps = np.concatenate([steps, [10]]) + self._steps = steps + if nbins is not None: + self._nbins = nbins + if minor is not None: + self._minor = bool(minor) # needed to scale tick space + if vcenter is not None: + self._vcenter = vcenter + if min_n_ticks is not None: + self._min_n_ticks = int(min_n_ticks) # compare to MaxNLocator + + def tick_values(self, vmin, vmax): # noqa: U100 + """ + Return the locations of the ticks. + """ + # NOTE: Critical that minor tick interval evenly divides major tick + # interval. Otherwise get misaligned major and minor tick steps. + # NOTE: This tries to select ticks that are integer steps away from zero (like + # AutoLocator). The list minimum is used if this fails (like FixedLocator) + # NOTE: This avoids awkward steps like '7' or '13' that produce strange + # jumps and have no integer divisors (and therefore eliminate minor ticks) + # NOTE: We virtually always want to subsample the level list rather than + # using continuous minor locators (e.g. LogLocator or SymLogLocator) because + # _parse_autolev interpolates evenly in the norm-space (e.g. 1, 3.16, 10, 31.6 + # for a LogNorm) rather than in linear-space (e.g. 1, 5, 10, 15, 20). + locs = self.locs + axis = self.axis + if axis is None: + return locs + nbins = self._nbins + steps = self._steps + if nbins is None: + nbins = axis.get_tick_space() + nbins = max((1, self._min_n_ticks - 1, nbins)) + step = max(1, int(np.ceil(locs.size / nbins))) + fact = 10 ** max(0, -AutoFormatter._decimal_place(step)) # e.g. 2 for 100 + idx = min(len(steps) - 1, np.searchsorted(steps, step / fact)) + step = int(np.round(steps[idx] * fact)) + if self._minor: # tick every half font size + if isinstance(axis, maxis.XAxis): + fact = 6 # unscale heuristic scaling of 3 em-widths + elif isinstance(axis, maxis.YAxis): + fact = 4 # unscale standard scaling of 2 em-widths + else: + fact = 2 # fall back to just one em-width + for i in range(fact, 0, -1): + if step % i == 0: + step = step // i + break + locs = locs - self._vcenter + diff = np.abs(np.diff(locs[:step + 1:step])) + offset, = np.where(np.isclose(locs % diff if diff.size else 0.0, 0.0)) + offset = offset[0] if offset.size else np.argmin(np.abs(locs)) + locs = locs[offset % step::step] # even multiples from zero or zero-close + return locs + self._vcenter + + +class DegreeLocator(mticker.MaxNLocator): + """ + Locate geographic gridlines with degree-minute-second support. + Adapted from cartopy. + """ + # NOTE: This is identical to cartopy except they only define LongitutdeLocator + # for common methods whereas we use DegreeLocator. More intuitive this way in + # case users need degree-minute-seconds for non-specific degree axis. + # NOTE: Locator implementation is weird AF. __init__ just calls set_params with all + # keyword args and fills in missing params with default_params class attribute. + # Unknown params result in warning instead of error. + default_params = mticker.MaxNLocator.default_params.copy() + default_params.update(nbins=8, dms=False) + + @docstring._snippet_manager + def __init__(self, *args, **kwargs): + """ + %(ticker.dms)s + """ + super().__init__(*args, **kwargs) + + def set_params(self, **kwargs): + if 'dms' in kwargs: + self._dms = kwargs.pop('dms') + super().set_params(**kwargs) + + def _guess_steps(self, vmin, vmax): + dv = abs(vmax - vmin) + if dv > 180: + dv -= 180 + if dv > 50: + steps = np.array([1, 2, 3, 6, 10]) + elif not self._dms or dv > 3.0: + steps = np.array([1, 1.5, 2, 2.5, 3, 5, 10]) + else: + steps = np.array([1, 10 / 6.0, 15 / 6.0, 20 / 6.0, 30 / 6.0, 10]) + self.set_params(steps=np.array(steps)) + + def _raw_ticks(self, vmin, vmax): + self._guess_steps(vmin, vmax) + return super()._raw_ticks(vmin, vmax) + + def bin_boundaries(self, vmin, vmax): # matplotlib < 2.2.0 + return self._raw_ticks(vmin, vmax) # may call Latitude/Longitude Locator copies + + +class LongitudeLocator(DegreeLocator): + """ + Locate longitude gridlines with degree-minute-second support. + Adapted from cartopy. + """ + @docstring._snippet_manager + def __init__(self, *args, **kwargs): + """ + %(ticker.dms)s + """ + super().__init__(*args, **kwargs) + + +class LatitudeLocator(DegreeLocator): + """ + Locate latitude gridlines with degree-minute-second support. + Adapted from cartopy. + """ + @docstring._snippet_manager + def __init__(self, *args, **kwargs): + """ + %(ticker.dms)s + """ + super().__init__(*args, **kwargs) + + def tick_values(self, vmin, vmax): + vmin = max(vmin, -90) + vmax = min(vmax, 90) + return super().tick_values(vmin, vmax) + + def _guess_steps(self, vmin, vmax): + vmin = max(vmin, -90) + vmax = min(vmax, 90) + super()._guess_steps(vmin, vmax) + + def _raw_ticks(self, vmin, vmax): + ticks = super()._raw_ticks(vmin, vmax) + return [t for t in ticks if -90 <= t <= 90] + + +class AutoFormatter(mticker.ScalarFormatter): + """ + The default formatter used for proplot tick labels. + Replaces `~matplotlib.ticker.ScalarFormatter`. + """ + @docstring._snippet_manager + def __init__( + self, + zerotrim=None, tickrange=None, wraprange=None, + prefix=None, suffix=None, negpos=None, + **kwargs + ): + """ + Parameters + ---------- + %(ticker.zerotrim)s + %(ticker.auto)s + + Other parameters + ---------------- + **kwargs + Passed to `matplotlib.ticker.ScalarFormatter`. + + See also + -------- + proplot.constructor.Formatter + proplot.ticker.SimpleFormatter + + Note + ---- + `matplotlib.ticker.ScalarFormatter` determines the number of + significant digits based on the axis limits, and therefore may + truncate digits while formatting ticks on highly non-linear axis + scales like `~proplot.scale.LogScale`. `AutoFormatter` corrects + this behavior, making it suitable for arbitrary axis scales. We + therefore use `AutoFormatter` with every axis scale by default. + """ + tickrange = tickrange or (-np.inf, np.inf) + super().__init__(**kwargs) + zerotrim = _not_none(zerotrim, rc['formatter.zerotrim']) + self._zerotrim = zerotrim + self._tickrange = tickrange + self._wraprange = wraprange + self._prefix = prefix or '' + self._suffix = suffix or '' + self._negpos = negpos or '' + + @docstring._snippet_manager + def __call__(self, x, pos=None): + """ + %(ticker.call)s + """ + # Tick range limitation + x = self._wrap_tick_range(x, self._wraprange) + if self._outside_tick_range(x, self._tickrange): + return '' + + # Negative positive handling + x, tail = self._neg_pos_format(x, self._negpos, wraprange=self._wraprange) + + # Default string formatting + string = super().__call__(x, pos) + + # Fix issue where non-zero string is formatted as zero + string = self._fix_small_number(x, string) + + # Custom string formatting + string = self._minus_format(string) + if self._zerotrim: + string = self._trim_trailing_zeros(string, self._get_decimal_point()) + + # Prefix and suffix + string = self._add_prefix_suffix(string, self._prefix, self._suffix) + string = string + tail # add negative-positive indicator + return string + + def get_offset(self): + """ + Get the offset but *always* use math text. + """ + with context._state_context(self, _useMathText=True): + return super().get_offset() + + @staticmethod + def _add_prefix_suffix(string, prefix=None, suffix=None): + """ + Add prefix and suffix to string. + """ + sign = '' + prefix = prefix or '' + suffix = suffix or '' + if string and REGEX_MINUS.match(string[0]): + sign, string = string[0], string[1:] + return sign + prefix + string + suffix + + def _fix_small_number(self, x, string, precision_offset=2): + """ + Fix formatting for non-zero formatted as zero. The `offset` controls the offset + from true floating point precision at which we want to limit string precision. + """ + # Add just enough precision for small numbers. Default formatter is + # only meant to be used for linear scales and cannot handle the wide + # range of magnitudes in e.g. log scales. To correct this, we only + # truncate if value is within `offset` order of magnitude of the float + # precision. Common issue is e.g. levels=pplt.arange(-1, 1, 0.1). + # This choice satisfies even 1000 additions of 0.1 to -100. + m = REGEX_ZERO.match(string) + decimal_point = self._get_decimal_point() + + if m and x != 0: + # Get initial precision spit out by algorithm + decimals, = m.groups() + precision_init = len(decimals.lstrip(decimal_point)) if decimals else 0 + + # Format with precision below floating point error + x -= getattr(self, 'offset', 0) # guard against API change + x /= 10 ** getattr(self, 'orderOfMagnitude', 0) # guard against API change + precision_true = max(0, self._decimal_place(x)) + precision_max = max(0, np.finfo(type(x)).precision - precision_offset) + precision = min(precision_true, precision_max) + string = ('{:.%df}' % precision).format(x) + + # If zero ignoring floating point error then match original precision + if REGEX_ZERO.match(string): + string = ('{:.%df}' % precision_init).format(0) + + # Fix decimal point + string = string.replace('.', decimal_point) + + return string + + def _get_decimal_point(self, use_locale=None): + """ + Get decimal point symbol for current locale (e.g. in Europe will be comma). + """ + use_locale = _not_none(use_locale, self.get_useLocale()) + return self._get_default_decimal_point(use_locale) + + @staticmethod + def _get_default_decimal_point(use_locale=None): + """ + Get decimal point symbol for current locale. Called externally. + """ + use_locale = _not_none(use_locale, rc['formatter.use_locale']) + return locale.localeconv()['decimal_point'] if use_locale else '.' + + @staticmethod + def _decimal_place(x): + """ + Return the decimal place of the number (e.g., 100 is -2 and 0.01 is 2). + """ + if x == 0: + digits = 0 + else: + digits = -int(np.log10(abs(x)) // 1) + return digits + + @staticmethod + def _minus_format(string): + """ + Format the minus sign and avoid "negative zero," e.g. ``-0.000``. + """ + if rc['axes.unicode_minus'] and not rc['text.usetex']: + string = string.replace('-', '\N{MINUS SIGN}') + if REGEX_MINUS_ZERO.match(string): + string = string[1:] + return string + + @staticmethod + def _neg_pos_format(x, negpos, wraprange=None): + """ + Permit suffixes indicators for "negative" and "positive" numbers. + """ + # NOTE: If input is a symmetric wraprange, the value conceptually has + # no "sign", so trim tail and format as absolute value. + if not negpos or x == 0: + tail = '' + elif ( + wraprange is not None + and np.isclose(-wraprange[0], wraprange[1]) + and np.any(np.isclose(x, wraprange)) + ): + x = abs(x) + tail = '' + elif x > 0: + tail = negpos[1] + else: + x *= -1 + tail = negpos[0] + return x, tail + + @staticmethod + def _outside_tick_range(x, tickrange): + """ + Return whether point is outside tick range up to some precision. + """ + eps = abs(x) / 1000 + return (x + eps) < tickrange[0] or (x - eps) > tickrange[1] + + @staticmethod + def _trim_trailing_zeros(string, decimal_point='.'): + """ + Sanitize tick label strings. + """ + if decimal_point in string: + string = string.rstrip('0').rstrip(decimal_point) + return string + + @staticmethod + def _wrap_tick_range(x, wraprange): + """ + Wrap the tick range to within these values. + """ + if wraprange is None: + return x + base = wraprange[0] + modulus = wraprange[1] - wraprange[0] + return (x - base) % modulus + base + + +class SimpleFormatter(mticker.Formatter): + """ + A general purpose number formatter. This is similar to `AutoFormatter` + but suitable for arbitrary formatting not necessarily associated with + an `~matplotlib.axis.Axis` instance. + """ + @docstring._snippet_manager + def __init__( + self, precision=None, zerotrim=None, + tickrange=None, wraprange=None, + prefix=None, suffix=None, negpos=None, + ): + """ + Parameters + ---------- + %(ticker.precision)s + %(ticker.zerotrim)s + %(ticker.auto)s + + See also + -------- + proplot.constructor.Formatter + proplot.ticker.AutoFormatter + """ + precision, zerotrim = _default_precision_zerotrim(precision, zerotrim) + self._precision = precision + self._prefix = prefix or '' + self._suffix = suffix or '' + self._negpos = negpos or '' + self._tickrange = tickrange or (-np.inf, np.inf) + self._wraprange = wraprange + self._zerotrim = zerotrim + + @docstring._snippet_manager + def __call__(self, x, pos=None): # noqa: U100 + """ + %(ticker.call)s + """ + # Tick range limitation + x = AutoFormatter._wrap_tick_range(x, self._wraprange) + if AutoFormatter._outside_tick_range(x, self._tickrange): + return '' + + # Negative positive handling + x, tail = AutoFormatter._neg_pos_format( + x, self._negpos, wraprange=self._wraprange + ) + + # Default string formatting + decimal_point = AutoFormatter._get_default_decimal_point() + string = ('{:.%df}' % self._precision).format(x) + string = string.replace('.', decimal_point) + + # Custom string formatting + string = AutoFormatter._minus_format(string) + if self._zerotrim: + string = AutoFormatter._trim_trailing_zeros(string, decimal_point) + + # Prefix and suffix + string = AutoFormatter._add_prefix_suffix(string, self._prefix, self._suffix) + string = string + tail # add negative-positive indicator + return string + + +class IndexFormatter(mticker.Formatter): + """ + Format numbers by assigning fixed strings to non-negative indices. Generally + paired with `IndexLocator` or `~matplotlib.ticker.FixedLocator`. + """ + # NOTE: This was deprecated in matplotlib 3.3. For details check out + # https://github.com/matplotlib/matplotlib/issues/16631 and bring some popcorn. + def __init__(self, labels): + self.labels = labels + self.n = len(labels) + + def __call__(self, x, pos=None): # noqa: U100 + i = int(round(x)) + if i < 0 or i >= self.n: + return '' + else: + return self.labels[i] + + +class SciFormatter(mticker.Formatter): + """ + Format numbers with scientific notation. + """ + @docstring._snippet_manager + def __init__(self, precision=None, zerotrim=None): + """ + Parameters + ---------- + %(ticker.precision)s + %(ticker.zerotrim)s + + See also + -------- + proplot.constructor.Formatter + proplot.ticker.AutoFormatter + """ + precision, zerotrim = _default_precision_zerotrim(precision, zerotrim) + self._precision = precision + self._zerotrim = zerotrim + + @docstring._snippet_manager + def __call__(self, x, pos=None): # noqa: U100 + """ + %(ticker.call)s + """ + # Get string + decimal_point = AutoFormatter._get_default_decimal_point() + string = ('{:.%de}' % self._precision).format(x) + parts = string.split('e') + + # Trim trailing zeros + significand = parts[0].rstrip(decimal_point) + if self._zerotrim: + significand = AutoFormatter._trim_trailing_zeros(significand, decimal_point) + + # Get sign and exponent + sign = parts[1][0].replace('+', '') + exponent = parts[1][1:].lstrip('0') + if exponent: + exponent = f'10^{{{sign}{exponent}}}' + if significand and exponent: + string = rf'{significand}{{\times}}{exponent}' + else: + string = rf'{significand}{exponent}' + + # Ensure unicode minus sign + string = AutoFormatter._minus_format(string) + + # Return TeX string + return f'${string}$' + + +class SigFigFormatter(mticker.Formatter): + """ + Format numbers by retaining the specified number of significant digits. + """ + @docstring._snippet_manager + def __init__(self, sigfig=None, zerotrim=None, base=None): + """ + Parameters + ---------- + sigfig : float, default: 3 + The number of significant digits. + %(ticker.zerotrim)s + base : float, default: 1 + The base unit for rounding. For example ``SigFigFormatter(2, base=5)`` + rounds to the nearest 5 with up to 2 digits (e.g., 87 --> 85, 8.7 --> 8.5). + + See also + -------- + proplot.constructor.Formatter + proplot.ticker.AutoFormatter + """ + self._sigfig = _not_none(sigfig, 3) + self._zerotrim = _not_none(zerotrim, rc['formatter.zerotrim']) + self._base = _not_none(base, 1) + + @docstring._snippet_manager + def __call__(self, x, pos=None): # noqa: U100 + """ + %(ticker.call)s + """ + # Limit to significant figures + digits = AutoFormatter._decimal_place(x) + self._sigfig - 1 + scale = self._base * 10 ** -digits + x = scale * round(x / scale) + + # Create the string + decimal_point = AutoFormatter._get_default_decimal_point() + precision = max(0, digits) + max(0, AutoFormatter._decimal_place(self._base)) + string = ('{:.%df}' % precision).format(x) + string = string.replace('.', decimal_point) + + # Custom string formatting + string = AutoFormatter._minus_format(string) + if self._zerotrim: + string = AutoFormatter._trim_trailing_zeros(string, decimal_point) + return string + + +class FracFormatter(mticker.Formatter): + r""" + Format numbers as integers or integer fractions. Optionally express the + values relative to some constant like `numpy.pi`. + """ + def __init__(self, symbol='', number=1): + r""" + Parameters + ---------- + symbol : str, default: '' + The constant symbol, e.g. ``r'$\pi$'``. + number : float, default: 1 + The constant value, e.g. `numpy.pi`. + + Note + ---- + The fractions shown by this formatter are resolved using the builtin + `fractions.Fraction` class and `fractions.Fraction.limit_denominator`. + + See also + -------- + proplot.constructor.Formatter + proplot.ticker.AutoFormatter + """ + self._symbol = symbol + self._number = number + super().__init__() + + @docstring._snippet_manager + def __call__(self, x, pos=None): # noqa: U100 + """ + %(ticker.call)s + """ + frac = Fraction(x / self._number).limit_denominator() + symbol = self._symbol + if x == 0: + string = '0' + elif frac.denominator == 1: # denominator is one + if frac.numerator == 1 and symbol: + string = f'{symbol:s}' + elif frac.numerator == -1 and symbol: + string = f'-{symbol:s}' + else: + string = f'{frac.numerator:d}{symbol:s}' + else: + if frac.numerator == 1 and symbol: # numerator is +/-1 + string = f'{symbol:s}/{frac.denominator:d}' + elif frac.numerator == -1 and symbol: + string = f'-{symbol:s}/{frac.denominator:d}' + else: # and again make sure we use unicode minus! + string = f'{frac.numerator:d}{symbol:s}/{frac.denominator:d}' + string = AutoFormatter._minus_format(string) + return string + + +class _CartopyFormatter(object): + """ + Mixin class for cartopy formatters. + """ + # NOTE: Cartopy formatters pre 0.18 required axis, and *always* translated + # input values from map projection coordinates to Plate Carrée coordinates. + # After 0.18 you can avoid this behavior by not setting axis but really + # dislike that inconsistency. Solution is temporarily assign PlateCarre(). + def __init__(self, *args, **kwargs): + import cartopy # noqa: F401 (ensure available) + super().__init__(*args, **kwargs) + + def __call__(self, value, pos=None): + ctx = context._empty_context() + if self.axis is not None: + ctx = context._state_context(self.axis.axes, projection=ccrs.PlateCarree()) + with ctx: + return super().__call__(value, pos) + + +class DegreeFormatter(_CartopyFormatter, _PlateCarreeFormatter): + """ + Formatter for longitude and latitude gridline labels. + Adapted from cartopy. + """ + @docstring._snippet_manager + def __init__(self, *args, **kwargs): + """ + %(ticker.dms)s + """ + super().__init__(*args, **kwargs) + + def _apply_transform(self, value, *args, **kwargs): # noqa: U100 + return value + + def _hemisphere(self, value, *args, **kwargs): # noqa: U100 + return '' + + +class LongitudeFormatter(_CartopyFormatter, LongitudeFormatter): + """ + Format longitude gridline labels. Adapted from + `cartopy.mpl.ticker.LongitudeFormatter`. + """ + @docstring._snippet_manager + def __init__(self, *args, **kwargs): + """ + %(ticker.dms)s + """ + super().__init__(*args, **kwargs) + + +class LatitudeFormatter(_CartopyFormatter, LatitudeFormatter): + """ + Format latitude gridline labels. Adapted from + `cartopy.mpl.ticker.LatitudeFormatter`. + """ + @docstring._snippet_manager + def __init__(self, *args, **kwargs): + """ + %(ticker.dms)s + """ + super().__init__(*args, **kwargs) diff --git a/proplot/ui.py b/proplot/ui.py new file mode 100644 index 000000000..b1e046376 --- /dev/null +++ b/proplot/ui.py @@ -0,0 +1,233 @@ +#!/usr/bin/env python3 +""" +The starting point for creating proplot figures. +""" +import matplotlib.pyplot as plt + +from . import axes as paxes +from . import figure as pfigure +from . import gridspec as pgridspec +from .internals import ic # noqa: F401 +from .internals import _not_none, _pop_params, _pop_props, _pop_rc, docstring + +__all__ = [ + 'figure', + 'subplot', + 'subplots', + 'show', + 'close', + 'switch_backend', + 'ion', + 'ioff', + 'isinteractive', +] + + +# Docstrings +_pyplot_docstring = """ +This is included so you don't have to import `~matplotlib.pyplot`. +""" +docstring._snippet_manager['ui.pyplot'] = _pyplot_docstring + + +def _parse_figsize(kwargs): + """ + Translate `figsize` into proplot-specific `figwidth` and `figheight` keys. + """ + # WARNING: Cannot have Figure.__init__() interpret figsize() because + # the figure manager fills it with the matplotlib default. + figsize = kwargs.pop('figsize', None) + figwidth = kwargs.pop('figwidth', None) + figheight = kwargs.pop('figheight', None) + if figsize is not None: + figsize_width, figsize_height = figsize + figwidth = _not_none(figwidth=figwidth, figsize_width=figsize_width) + figheight = _not_none(figheight=figheight, figsize_height=figsize_height) + kwargs['figwidth'] = figwidth + kwargs['figheight'] = figheight + + +@docstring._snippet_manager +def show(*args, **kwargs): + """ + Call `matplotlib.pyplot.show`. + %(ui.pyplot)s + + Parameters + ---------- + *args, **kwargs + Passed to `matplotlib.pyplot.show`. + """ + return plt.show(*args, **kwargs) + + +@docstring._snippet_manager +def close(*args, **kwargs): + """ + Call `matplotlib.pyplot.close`. + %(ui.pyplot)s + + Parameters + ---------- + *args, **kwargs + Passed to `matplotlib.pyplot.close`. + """ + return plt.close(*args, **kwargs) + + +@docstring._snippet_manager +def switch_backend(*args, **kwargs): + """ + Call `matplotlib.pyplot.switch_backend`. + %(ui.pyplot)s + + Parameters + ---------- + *args, **kwargs + Passed to `matplotlib.pyplot.switch_backend`. + """ + return plt.switch_backend(*args, **kwargs) + + +@docstring._snippet_manager +def ion(): + """ + Call `matplotlib.pyplot.ion`. + %(ui.pyplot)s + """ + return plt.ion() + + +@docstring._snippet_manager +def ioff(): + """ + Call `matplotlib.pyplot.ioff`. + %(ui.pyplot)s + """ + return plt.ioff() + + +@docstring._snippet_manager +def isinteractive(): + """ + Call `matplotlib.pyplot.isinteractive`. + %(ui.pyplot)s + """ + return plt.isinteractive() + + +@docstring._snippet_manager +def figure(**kwargs): + """ + Create an empty figure. Subplots can be subsequently added using + `~proplot.figure.Figure.add_subplot` or `~proplot.figure.Figure.subplots`. + This command is analogous to `matplotlib.pyplot.figure`. + + Parameters + ---------- + %(figure.figure)s + + Other parameters + ---------------- + **kwargs + Passed to `proplot.figure.Figure.format`. + + See also + -------- + proplot.ui.subplots + proplot.figure.Figure.add_subplot + proplot.figure.Figure.subplots + proplot.figure.Figure + matplotlib.figure.Figure + """ + _parse_figsize(kwargs) + return plt.figure(FigureClass=pfigure.Figure, **kwargs) + + +@docstring._snippet_manager +def subplot(**kwargs): + """ + Return a figure and a single subplot. + This command is analogous to `matplotlib.pyplot.subplot`, + except the figure instance is also returned. + + Other parameters + ---------------- + %(figure.figure)s + **kwargs + Passed to `proplot.figure.Figure.format` or the + projection-specific ``format`` command for the axes. + + Returns + ------- + fig : `proplot.figure.Figure` + The figure instance. + ax : `proplot.axes.Axes` + The axes instance. + + See also + -------- + proplot.ui.figure + proplot.figure.Figure.subplot + proplot.figure.Figure + matplotlib.figure.Figure + """ + _parse_figsize(kwargs) + rc_kw, rc_mode = _pop_rc(kwargs) + kwsub = _pop_props(kwargs, 'patch') # e.g. 'color' + kwsub.update(_pop_params(kwargs, pfigure.Figure._parse_proj)) + for sig in paxes.Axes._format_signatures.values(): + kwsub.update(_pop_params(kwargs, sig)) + kwargs['aspect'] = kwsub.pop('aspect', None) # keyword conflict + fig = figure(rc_kw=rc_kw, **kwargs) + ax = fig.add_subplot(rc_kw=rc_kw, **kwsub) + return fig, ax + + +@docstring._snippet_manager +def subplots(*args, **kwargs): + """ + Return a figure and an arbitrary grid of subplots. + This command is analogous to `matplotlib.pyplot.subplots`, + except the subplots are stored in a `~proplot.gridspec.SubplotGrid`. + + Parameters + ---------- + %(figure.subplots_params)s + + Other parameters + ---------------- + %(figure.figure)s + **kwargs + Passed to `proplot.figure.Figure.format` or the + projection-specific ``format`` command for each axes. + + Returns + ------- + fig : `proplot.figure.Figure` + The figure instance. + axs : `proplot.gridspec.SubplotGrid` + The axes instances stored in a `~proplot.gridspec.SubplotGrid`. + + See also + -------- + proplot.ui.figure + proplot.figure.Figure.subplots + proplot.gridspec.SubplotGrid + proplot.figure.Figure + matplotlib.figure.Figure + """ + _parse_figsize(kwargs) + rc_kw, rc_mode = _pop_rc(kwargs) + kwsubs = _pop_props(kwargs, 'patch') # e.g. 'color' + kwsubs.update(_pop_params(kwargs, pfigure.Figure._add_subplots)) + kwsubs.update(_pop_params(kwargs, pgridspec.GridSpec._update_params)) + for sig in paxes.Axes._format_signatures.values(): + kwsubs.update(_pop_params(kwargs, sig)) + for key in ('subplot_kw', 'gridspec_kw'): # deprecated args + if key in kwargs: + kwsubs[key] = kwargs.pop(key) + kwargs['aspect'] = kwsubs.pop('aspect', None) # keyword conflict + fig = figure(rc_kw=rc_kw, **kwargs) + axs = fig.add_subplots(*args, rc_kw=rc_kw, **kwsubs) + return fig, axs diff --git a/proplot/utils.py b/proplot/utils.py index 27c4adbe0..2b0da3c1f 100644 --- a/proplot/utils.py +++ b/proplot/utils.py @@ -1,166 +1,147 @@ #!/usr/bin/env python3 """ -Simple tools used in various places across this package. +Various tools that may be useful while making plots. """ -import re -import time +# WARNING: Cannot import 'rc' anywhere in this file or we get circular import +# issues. The rc param validators need functions in this file. import functools -import warnings -import numpy as np -from matplotlib import rcParams -from numbers import Number, Integral -try: # use this for debugging instead of print()! - from icecream import ic -except ImportError: # graceful fallback if IceCream isn't installed - ic = lambda *a: None if not a else (a[0] if len(a) == 1 else a) # noqa - -__all__ = ['arange', 'edges', 'edges2d', 'units'] - -# Change this to turn on benchmarking -BENCHMARK = False - -# Units regex -NUMBER = re.compile('^([-+]?[0-9._]+(?:[eE][-+]?[0-9_]+)?)(.*)$') - - -class _benchmark(object): - """ - Context object for timing arbitrary blocks of code. - """ - def __init__(self, message): - self.message = message - - def __enter__(self): - if BENCHMARK: - self.time = time.perf_counter() - - def __exit__(self, *args): # noqa: U100 - if BENCHMARK: - print(f'{self.message}: {time.perf_counter() - self.time}s') - - -class _setstate(object): - """ - Temporarily modify attribute(s) for an arbitrary object. - """ - def __init__(self, obj, **kwargs): - self._obj = obj - self._kwargs = kwargs - self._kwargs_orig = { - key: getattr(obj, key) for key in kwargs if hasattr(obj, key) - } - - def __enter__(self): - for key, value in self._kwargs.items(): - setattr(self._obj, key, value) - - def __exit__(self, *args): # noqa: U100 - for key in self._kwargs.keys(): - if key in self._kwargs_orig: - setattr(self._obj, key, self._kwargs_orig[key]) - else: - delattr(self._obj, key) - +import re +from numbers import Integral, Real -def _counter(func): - """ - Decorator that counts and prints the cumulative time a function - has benn running. See `this link \ -`__. +import matplotlib.colors as mcolors +import matplotlib.font_manager as mfonts +import numpy as np +from matplotlib import rcParams as rc_matplotlib + +from .externals import hsluv +from .internals import ic # noqa: F401 +from .internals import _not_none, docstring, warnings + +__all__ = [ + 'arange', + 'edges', + 'edges2d', + 'get_colors', + 'set_hue', + 'set_saturation', + 'set_luminance', + 'set_alpha', + 'shift_hue', + 'scale_saturation', + 'scale_luminance', + 'to_hex', + 'to_rgb', + 'to_xyz', + 'to_rgba', + 'to_xyza', + 'units', + 'shade', # deprecated + 'saturate', # deprecated +] + +UNIT_REGEX = re.compile( + r'\A([-+]?[0-9._]+(?:[eE][-+]?[0-9_]+)?)(.*)\Z' # float with trailing units +) +UNIT_DICT = { + 'in': 1.0, + 'ft': 12.0, + 'yd': 36.0, + 'm': 39.37, + 'dm': 3.937, + 'cm': 0.3937, + 'mm': 0.03937, + 'pc': 1 / 6.0, + 'pt': 1 / 72.0, + 'ly': 3.725e17, +} + + +# Color docstrings +_docstring_rgba = """ +color : color-spec + The color. Sanitized with `to_rgba`. """ - @functools.wraps(func) - def decorator(*args, **kwargs): - if BENCHMARK: - t = time.perf_counter() - res = func(*args, **kwargs) - if BENCHMARK: - decorator.time += (time.perf_counter() - t) - decorator.count += 1 - print(f'{func.__name__}() cumulative time: {decorator.time}s ' - f'({decorator.count} calls)') - return res - decorator.time = 0 - decorator.count = 0 # initialize - return decorator - - -def _timer(func): - """ - Decorator that prints the time a function takes to execute. - See: https://stackoverflow.com/a/1594484/4970632 - """ - @functools.wraps(func) - def decorator(*args, **kwargs): - if BENCHMARK: - t = time.perf_counter() - res = func(*args, **kwargs) - if BENCHMARK: - print(f'{func.__name__}() time: {time.perf_counter()-t}s') - return res - return decorator - - -def _format_warning(message, category, filename, lineno, line=None): # noqa: U100, E501 - """ - Simple format for warnings issued by ProPlot. See the - `internal warning call signature \ -`__ - and the `default warning source code \ -`__. +_docstring_to_rgb = """ +color : color-spec + The color. Can be a 3-tuple or 4-tuple of channel values, a hex + string, a registered color name, a cycle color like ``'C0'``, or + a 2-tuple colormap coordinate specification like ``('magma', 0.5)`` + (see `~proplot.colors.ColorDatabase` for details). + + If `space` is ``'rgb'``, this is a tuple of RGB values, and any + channels are larger than ``2``, the channels are assumed to be + on the ``0`` to ``255`` scale and are divided by ``255``. +space : {'rgb', 'hsv', 'hcl', 'hpl', 'hsl'}, optional + The colorspace for the input channel values. Ignored unless `color` + is a tuple of numbers. +cycle : str, default: :rcraw:`cycle` + The registered color cycle name used to interpret colors that + look like ``'C0'``, ``'C1'``, etc. +clip : bool, default: True + Whether to clip channel values into the valid ``0`` to ``1`` range. + Setting this to ``False`` can result in invalid colors. """ - return f'{filename}:{lineno}: ProPlotWarning: {message}\n' # needs newline +_docstring_space = """ +space : {'hcl', 'hpl', 'hsl', 'hsv'}, optional + The hue-saturation-luminance-like colorspace used to transform the color. + Default is the strictly perceptually uniform colorspace ``'hcl'``. +""" +_docstring_hex = """ +color : str + An 8-digit HEX string indicating the + red, green, blue, and alpha channel values. +""" +docstring._snippet_manager['utils.color'] = _docstring_rgba +docstring._snippet_manager['utils.hex'] = _docstring_hex +docstring._snippet_manager['utils.space'] = _docstring_space +docstring._snippet_manager['utils.to'] = _docstring_to_rgb -def _warn_proplot(message): +def _keep_units(func): """ - *Temporarily* apply the `_format_warning` monkey patch and emit the - warning. Do not want to affect warnings emitted by other modules. + Very simple decorator to strip and re-apply the same units. """ - with _setstate(warnings, formatwarning=_format_warning): - warnings.warn(message) + # NOTE: Native UnitRegistry.wraps() is not sufficient since it enforces + # unit types rather than arbitrary units. This wrapper is similar. + @functools.wraps(func) + def _with_stripped_units(data, *args, **kwargs): + units = 1 + if hasattr(data, 'units') and hasattr(data, 'magnitude'): + data, units = data.magnitude, data.units + result = func(data, *args, **kwargs) + return result * units + return _with_stripped_units -def _notNone(*args, names=None): - """ - Return the first non-``None`` value. This is used with keyword arg - aliases and for setting default values. Ugly name but clear purpose. Pass - the `names` keyword arg to issue warning if multiple args were passed. Must - be list of non-empty strings. +def arange(min_, *args): """ - if names is None: - for arg in args: - if arg is not None: - return arg - return arg # last one - else: - first = None - kwargs = {} - if len(names) != len(args) - 1: - raise ValueError( - f'Need {len(args)+1} names for {len(args)} args, ' - f'but got {len(names)} names.' - ) - names = [*names, ''] - for name, arg in zip(names, args): - if arg is not None: - if first is None: - first = arg - if name: - kwargs[name] = arg - if len(kwargs) > 1: - warnings.warn( - f'Got conflicting or duplicate keyword args: {kwargs}. ' - 'Using the first one.' - ) - return first + Identical to `numpy.arange` but with inclusive endpoints. For example, + ``pplt.arange(2, 4)`` returns the numpy array ``[2, 3, 4]`` instead of + ``[2, 3]``. This is useful for generating lists of tick locations or + colormap levels, e.g. ``ax.format(xlocator=pplt.arange(0, 10))`` + or ``ax.pcolor(levels=pplt.arange(0, 10))``. + Parameters + ---------- + *args : float + If three arguments are passed, these are the minimum, maximum, and step + size. If fewer than three arguments are passed, the step size is ``1``. + If one argument is passed, this is the maximum, and the minimum is ``0``. -def arange(min_, *args): - """ - Identical to `numpy.arange` but with inclusive endpoints. For - example, ``plot.arange(2,4)`` returns ``np.array([2,3,4])`` instead - of ``np.array([2,3])``. This command is useful for generating lists of - tick locations or colorbar level boundaries. + Returns + ------- + numpy.ndarray + Array of points. + + See also + -------- + numpy.arange + proplot.constructor.Locator + proplot.axes.CartesianAxes.format + proplot.axes.PolarAxes.format + proplot.axes.GeoAxes.format + proplot.axes.Axes.colorbar + proplot.axes.PlotAxes """ # Optional arguments just like np.arange if len(args) == 0: @@ -178,212 +159,747 @@ def arange(min_, *args): # All input is integer if all(isinstance(val, Integral) for val in (min_, max_, step)): min_, max_, step = np.int64(min_), np.int64(max_), np.int64(step) - max_ += np.sign(step) * 1 + max_ += np.sign(step) # Input is float or mixed, cast to float64 # Don't use np.nextafter with np.finfo(np.dtype(np.float64)).max, because # round-off errors from continually adding step to min mess this up else: min_, max_, step = np.float64(min_), np.float64(max_), np.float64(step) - max_ += np.sign(step) * (step / 2) + max_ += 0.5 * step return np.arange(min_, max_, step) -def edges(Z, axis=-1): +@_keep_units +def edges(z, axis=-1): """ - Calculate the approximate "edge" values along an arbitrary axis, given - "center" values. This is used internally to calculate graticule edges when - you supply centers to `~matplotlib.axes.Axes.pcolor` or - `~matplotlib.axes.Axes.pcolormesh` and to calculate colormap levels - when you supply centers to any method wrapped by - `~proplot.wrappers.cmap_changer`. + Calculate the approximate "edge" values along an axis given "center" values. + The size of the axis is increased by one. This is used internally to calculate + coordinate edges when you supply coordinate centers to pseudocolor commands. Parameters ---------- - Z : array-like - Array of any shape or size. Generally, should be monotonically - increasing or decreasing along `axis`. + z : array-like + An array of any shape. axis : int, optional - The axis along which "edges" are calculated. The size of this axis - will be increased by one. + The axis along which "edges" are calculated. The size of this + axis will be increased by one. Returns ------- - `~numpy.ndarray` + numpy.ndarray Array of "edge" coordinates. + + See also + -------- + edges2d + proplot.axes.PlotAxes.pcolor + proplot.axes.PlotAxes.pcolormesh + proplot.axes.PlotAxes.pcolorfast """ - Z = np.asarray(Z) - Z = np.swapaxes(Z, axis, -1) - Z = np.concatenate(( - Z[..., :1] - (Z[..., 1] - Z[..., 0]) / 2, - (Z[..., 1:] + Z[..., :-1]) / 2, - Z[..., -1:] + (Z[..., -1] - Z[..., -2]) / 2, - ), axis=-1) - return np.swapaxes(Z, axis, -1) + z = np.asarray(z) + z = np.swapaxes(z, axis, -1) + *dims, n = z.shape + zb = np.zeros((*dims, n + 1)) + + # Inner edges + zb[..., 1:-1] = 0.5 * (z[..., :-1] + z[..., 1:]) + + # Outer edges + zb[..., 0] = 1.5 * z[..., 0] - 0.5 * z[..., 1] + zb[..., -1] = 1.5 * z[..., -1] - 0.5 * z[..., -2] + return np.swapaxes(zb, axis, -1) -def edges2d(Z): + +@_keep_units +def edges2d(z): """ - Like `edges` but for 2d arrays. - The size of both axes are increased by one. This is used - internally to calculate graitule edges when you supply centers to - `~matplotlib.axes.Axes.pcolor` or `~matplotlib.axes.Axes.pcolormesh`. + Calculate the approximate "edge" values given a 2D grid of "center" values. + The size of both axes is increased by one. This is used internally to calculate + coordinate edges when you supply coordinate to pseudocolor commands. Parameters ---------- - Z : array-like - A 2d array. + z : array-like + A 2D array. Returns ------- - `~numpy.ndarray` + numpy.ndarray Array of "edge" coordinates. + + See also + -------- + edges + proplot.axes.PlotAxes.pcolor + proplot.axes.PlotAxes.pcolormesh + proplot.axes.PlotAxes.pcolorfast """ - Z = np.asarray(Z) - if Z.ndim != 2: - raise ValueError(f'Input must be a 2d array, but got {Z.ndim}d.') - ny, nx = Z.shape - Zb = np.zeros((ny + 1, nx + 1)) - # Inner - Zb[1:-1, 1:-1] = 0.25 * ( - Z[1:, 1:] + Z[:-1, 1:] + Z[1:, :-1] + Z[:-1, :-1] - ) - # Lower and upper - Zb[0] += edges(1.5 * Z[0] - 0.5 * Z[1]) - Zb[-1] += edges(1.5 * Z[-1] - 0.5 * Z[-2]) - # Left and right - Zb[:, 0] += edges(1.5 * Z[:, 0] - 0.5 * Z[:, 1]) - Zb[:, -1] += edges(1.5 * Z[:, -1] - 0.5 * Z[:, -2]) - # Corners - Zb[[0, 0, -1, -1], [0, -1, -1, 0]] *= 0.5 - return Zb + z = np.asarray(z) + if z.ndim != 2: + raise ValueError(f'Input must be a 2D array, but got {z.ndim}D.') + ny, nx = z.shape + zb = np.zeros((ny + 1, nx + 1)) + + # Inner edges + zb[1:-1, 1:-1] = 0.25 * (z[1:, 1:] + z[:-1, 1:] + z[1:, :-1] + z[:-1, :-1]) + + # Outer edges + zb[0, :] += edges(1.5 * z[0, :] - 0.5 * z[1, :]) + zb[-1, :] += edges(1.5 * z[-1, :] - 0.5 * z[-2, :]) + zb[:, 0] += edges(1.5 * z[:, 0] - 0.5 * z[:, 1]) + zb[:, -1] += edges(1.5 * z[:, -1] - 0.5 * z[:, -2]) + zb[[0, 0, -1, -1], [0, -1, -1, 0]] *= 0.5 # corner correction + + return zb + + +def get_colors(*args, **kwargs): + """ + Get the colors associated with a registered or + on-the-fly color cycle or colormap. + + Parameters + ---------- + *args, **kwargs + Passed to `~proplot.constructor.Cycle`. + + Returns + ------- + colors : list of str + A list of HEX strings. + + See also + -------- + proplot.constructor.Cycle + proplot.constructor.Colormap + """ + from .constructor import Cycle # delayed to avoid cyclic imports + cycle = Cycle(*args, **kwargs) + colors = [to_hex(dict_['color']) for dict_ in cycle] + return colors + + +def _transform_color(func, color, space): + """ + Standardize input for color transformation functions. + """ + *color, opacity = to_rgba(color) + color = to_xyz(color, space=space) + color = func(list(color)) # apply transform + return to_hex((*color, opacity), space=space) + + +@docstring._snippet_manager +def shift_hue(color, shift=0, space='hcl'): + """ + Shift the hue channel of a color. + + Parameters + ---------- + %(utils.color)s + shift : float, optional + The HCL hue channel is offset by this value. + %(utils.space)s + + Returns + ------- + %(utils.hex)s + + See also + -------- + set_hue + set_saturation + set_luminance + set_alpha + scale_saturation + scale_luminance + """ + def func(channels): + channels[0] += shift + channels[0] %= 360 + return channels + + return _transform_color(func, color, space) + + +@docstring._snippet_manager +def scale_saturation(color, scale=1, space='hcl'): + """ + Scale the saturation channel of a color. + + Parameters + ---------- + %(utils.color)s + scale : float, optional + The HCL saturation channel is multiplied by this value. + %(utils.space)s + + Returns + ------- + %(utils.hex)s + + See also + -------- + set_hue + set_saturation + set_luminance + set_alpha + shift_hue + scale_luminance + """ + def func(channels): + channels[1] *= scale + return channels + + return _transform_color(func, color, space) + + +@docstring._snippet_manager +def scale_luminance(color, scale=1, space='hcl'): + """ + Scale the luminance channel of a color. + + Parameters + ---------- + %(utils.color)s + scale : float, optional + The luminance channel is multiplied by this value. + %(utils.space)s + + Returns + ------- + %(utils.hex)s + + See also + -------- + set_hue + set_saturation + set_luminance + set_alpha + shift_hue + scale_saturation + """ + def func(channels): + channels[2] *= scale + return channels + + return _transform_color(func, color, space) + + +@docstring._snippet_manager +def set_hue(color, hue, space='hcl'): + """ + Return a color with a different hue and the same luminance and saturation + as the input color. + + Parameters + ---------- + %(utils.color)s + hue : float, optional + The new hue. Should lie between ``0`` and ``360`` degrees. + %(utils.space)s + + Returns + ------- + %(utils.hex)s + + See also + -------- + set_saturation + set_luminance + set_alpha + shift_hue + scale_saturation + scale_luminance + """ + def func(channels): + channels[0] = hue + return channels + + return _transform_color(func, color, space) + + +@docstring._snippet_manager +def set_saturation(color, saturation, space='hcl'): + """ + Return a color with a different saturation and the same hue and luminance + as the input color. + + Parameters + ---------- + %(utils.color)s + saturation : float, optional + The new saturation. Should lie between ``0`` and ``360`` degrees. + %(utils.space)s + + Returns + ------- + %(utils.hex)s + + See also + -------- + set_hue + set_luminance + set_alpha + shift_hue + scale_saturation + scale_luminance + """ + def func(channels): + channels[1] = saturation + return channels + + return _transform_color(func, color, space) + + +@docstring._snippet_manager +def set_luminance(color, luminance, space='hcl'): + """ + Return a color with a different luminance and the same hue and saturation + as the input color. + + Parameters + ---------- + %(utils.color)s + luminance : float, optional + The new luminance. Should lie between ``0`` and ``100``. + %(utils.space)s + + Returns + ------- + %(utils.hex)s + + See also + -------- + set_hue + set_saturation + set_alpha + shift_hue + scale_saturation + scale_luminance + """ + def func(channels): + channels[2] = luminance + return channels + + return _transform_color(func, color, space) + + +@docstring._snippet_manager +def set_alpha(color, alpha): + """ + Return a color with the opacity channel set to the specified value. + + Parameters + ---------- + %(utils.color)s + alpha : float, optional + The new opacity. Should be between ``0`` and ``1``. + + Returns + ------- + %(utils.hex)s + + See also + -------- + set_hue + set_saturation + set_luminance + shift_hue + scale_saturation + scale_luminance + """ + color = list(to_rgba(color)) + color[3] = alpha + return to_hex(color) + + +def _translate_cycle_color(color, cycle=None): + """ + Parse the input cycle color. + """ + if isinstance(cycle, str): + from .colors import _cmap_database + try: + cycle = _cmap_database[cycle].colors + except (KeyError, AttributeError): + cycles = sorted( + name + for name, cmap in _cmap_database.items() + if isinstance(cmap, mcolors.ListedColormap) + ) + raise ValueError( + f'Invalid color cycle {cycle!r}. Options are: ' + + ', '.join(map(repr, cycles)) + + '.' + ) + elif cycle is None: + cycle = rc_matplotlib['axes.prop_cycle'].by_key() + if 'color' not in cycle: + cycle = ['k'] + else: + cycle = cycle['color'] + else: + raise ValueError(f'Invalid cycle {cycle!r}.') + + return cycle[int(color[-1]) % len(cycle)] + + +@docstring._snippet_manager +def to_hex(color, space='rgb', cycle=None, keep_alpha=True): + """ + Translate the color from an arbitrary colorspace to a HEX string. + This is a generalization of `matplotlib.colors.to_hex`. + + Parameters + ---------- + %(utils.to)s + keep_alpha : bool, default: True + Whether to keep the opacity channel. If ``True`` an 8-digit HEX + is returned. Otherwise a 6-digit HEX is returned. + + Returns + ------- + %(utils.hex)s + + See also + -------- + to_rgb + to_rgba + to_xyz + to_xyza + """ + rgba = to_rgba(color, space=space, cycle=cycle) + return mcolors.to_hex(rgba, keep_alpha=keep_alpha) + + +@docstring._snippet_manager +def to_rgb(color, space='rgb', cycle=None): + """ + Translate the color from an arbitrary colorspace to an RGB tuple. This is + a generalization of `matplotlib.colors.to_rgb` and the inverse of `to_xyz`. + + Parameters + ---------- + %(utils.to)s + + Returns + ------- + color : 3-tuple + An RGB tuple. + + See also + -------- + to_hex + to_rgba + to_xyz + to_xyza + """ + return to_rgba(color, space=space, cycle=cycle)[:3] + + +@docstring._snippet_manager +def to_rgba(color, space='rgb', cycle=None, clip=True): + """ + Translate the color from an arbitrary colorspace to an RGBA tuple. This is + a generalization of `matplotlib.colors.to_rgba` and the inverse of `to_xyz`. + + Parameters + ---------- + %(utils.to)s + + Returns + ------- + color : 4-tuple + An RGBA tuple. + + See also + -------- + to_hex + to_rgb + to_xyz + to_xyza + """ + # Translate color cycle strings + if isinstance(color, str) and re.match(r'\AC[0-9]\Z', color): + color = _translate_cycle_color(color, cycle=cycle) + + # Translate RGB strings and (colormap, index) tuples + # NOTE: Cannot use is_color_like because might have HSL channel values + opacity = 1 + if ( + isinstance(color, str) + or np.iterable(color) and len(color) == 2 + ): + color = mcolors.to_rgba(color) # also enforced validity + if ( + not np.iterable(color) + or len(color) not in (3, 4) + or not all(isinstance(c, Real) for c in color) + ): + raise ValueError(f'Invalid color-spec {color!r}.') + if len(color) == 4: + *color, opacity = color + + # Translate arbitrary colorspaces + if space == 'rgb': + if any(c > 2 for c in color): + color = tuple(c / 255 for c in color) # scale to within 0-1 + else: + pass + elif space == 'hsv': + color = hsluv.hsl_to_rgb(*color) + elif space == 'hcl': + color = hsluv.hcl_to_rgb(*color) + elif space == 'hsl': + color = hsluv.hsluv_to_rgb(*color) + elif space == 'hpl': + color = hsluv.hpluv_to_rgb(*color) + else: + raise ValueError(f'Invalid colorspace {space!r}.') + + # Clip values. This should only be disabled when testing + # translation functions. + if clip: + color = np.clip(color, 0, 1) # clip to valid range + + # Return RGB or RGBA + return (*color, opacity) + + +@docstring._snippet_manager +def to_xyz(color, space='hcl'): + """ + Translate color in *any* format to a tuple of channel values in *any* + colorspace. This is the inverse of `to_rgb`. + + Parameters + ---------- + %(utils.color)s + space : {'hcl', 'hpl', 'hsl', 'hsv', 'rgb'}, optional + The colorspace for the output channel values. + + Returns + ------- + color : 3-tuple + Tuple of channel values for the colorspace `space`. + + See also + -------- + to_hex + to_rgb + to_rgba + to_xyza + """ + return to_xyza(color, space)[:3] + + +@docstring._snippet_manager +def to_xyza(color, space='hcl'): + """ + Translate color in *any* format to a tuple of channel values in *any* + colorspace. This is the inverse of `to_rgba`. + + Parameters + ---------- + %(utils.color)s + space : {'hcl', 'hpl', 'hsl', 'hsv', 'rgb'}, optional + The colorspace for the output channel values. + + Returns + ------- + color : 3-tuple + Tuple of channel values for the colorspace `space`. + + See also + -------- + to_hex + to_rgb + to_rgba + to_xyz + """ + # Run tuple conversions + # NOTE: Don't pass color tuple, because we may want to permit + # out-of-bounds RGB values to invert conversion + *color, opacity = to_rgba(color) + if space == 'rgb': + pass + elif space == 'hsv': + color = hsluv.rgb_to_hsl(*color) # rgb_to_hsv would also work + elif space == 'hcl': + color = hsluv.rgb_to_hcl(*color) + elif space == 'hsl': + color = hsluv.rgb_to_hsluv(*color) + elif space == 'hpl': + color = hsluv.rgb_to_hpluv(*color) + else: + raise ValueError(f'Invalid colorspace {space}.') + return (*color, opacity) + + +def _fontsize_to_pt(size): + """ + Translate font preset size or unit string to points. + """ + scalings = mfonts.font_scalings + if not isinstance(size, str): + return size + if size in mfonts.font_scalings: + return rc_matplotlib['font.size'] * scalings[size] + try: + return units(size, 'pt') + except ValueError: + raise KeyError( + f'Invalid font size {size!r}. Can be points or one of the preset scalings: ' + + ', '.join(f'{key!r} ({value})' for key, value in scalings.items()) + + '.' + ) -def units(value, dest='in', axes=None, figure=None, width=True): +@warnings._rename_kwargs('0.6.0', units='dest') +def units( + value, numeric=None, dest=None, *, fontsize=None, figure=None, axes=None, width=None +): """ - Convert values and lists of values between arbitrary physical units. This - is used internally all over ProPlot, permitting flexible units for various - keyword arguments. + Convert values between arbitrary physical units. This is used internally all + over proplot, permitting flexible units for various keyword arguments. Parameters ---------- - value : float or str or list thereof - A size specifier or *list thereof*. If numeric, nothing is done. - If string, it is converted to the units `dest`. The string should look - like ``'123.456unit'``, where the number is the magnitude and - ``'unit'`` is one of the following. + value : float or str or sequence + A size specifier or sequence of size specifiers. If numeric, units are + converted from `numeric` to `dest`. If string, units are converted to + `dest` according to the string specifier. The string should look like + ``'123.456unit'``, where the number is the magnitude and ``'unit'`` + matches a key in the below table. - ========= ========================================================================================= + .. _units_table: + + ========= ===================================================== Key Description - ========= ========================================================================================= + ========= ===================================================== ``'m'`` Meters + ``'dm'`` Decimeters ``'cm'`` Centimeters ``'mm'`` Millimeters + ``'yd'`` Yards ``'ft'`` Feet ``'in'`` Inches - ``'pt'`` `Points `__ (1/72 inches) - ``'pc'`` `Pica `__ (1/6 inches) - ``'px'`` Pixels on screen, uses dpi of :rcraw:`figure.dpi` - ``'pp'`` Pixels once printed, uses dpi of :rcraw:`savefig.dpi` - ``'em'`` `Em square `__ for :rcraw:`font.size` - ``'en'`` `En square `__ for :rcraw:`font.size` - ``'Em'`` `Em square `__ for :rcraw:`axes.titlesize` - ``'En'`` `En square `__ for :rcraw:`axes.titlesize` - ``'ax'`` Axes relative units. Not always available. - ``'fig'`` Figure relative units. Not always available. + ``'pc'`` `Pica `_ (1/6 inches) + ``'pt'`` `Points `_ (1/72 inches) + ``'px'`` Pixels on screen, using dpi of :rcraw:`figure.dpi` + ``'pp'`` Pixels once printed, using dpi of :rcraw:`savefig.dpi` + ``'em'`` `Em square `_ for :rcraw:`font.size` + ``'en'`` `En square `_ for :rcraw:`font.size` + ``'Em'`` `Em square `_ for :rcraw:`axes.titlesize` + ``'En'`` `En square `_ for :rcraw:`axes.titlesize` + ``'ax'`` Axes-relative units (not always available) + ``'fig'`` Figure-relative units (not always available) ``'ly'`` Light years ;) - ========= ========================================================================================= - - dest : str, optional - The destination units. Default is inches, i.e. ``'in'``. + ========= ===================================================== + + .. _pt: https://en.wikipedia.org/wiki/Point_(typography) + .. _pc: https://en.wikipedia.org/wiki/Pica_(typography) + .. _em: https://en.wikipedia.org/wiki/Em_(typography) + .. _en: https://en.wikipedia.org/wiki/En_(typography) + + numeric : str, default: 'in' + The units associated with numeric input. + dest : str, default: `numeric` + The destination units. + fontsize : str or float, default: :rc:`font.size` or :rc:`axes.titlesize` + The font size in points used for scaling. Default is + :rcraw:`font.size` for ``em`` and ``en`` units and + :rcraw:`axes.titlesize` for ``Em`` and ``En`` units. axes : `~matplotlib.axes.Axes`, optional The axes to use for scaling units that look like ``'0.1ax'``. figure : `~matplotlib.figure.Figure`, optional - The figure to use for scaling units that look like ``'0.1fig'``. If - ``None`` we try to get the figure from ``axes.figure``. + The figure to use for scaling units that look like ``'0.1fig'``. + If not provided we try to get the figure from ``axes.figure``. width : bool, optional - Whether to use the width or height for the axes and figure relative - coordinates. - """ # noqa - # Font unit scales - # NOTE: Delay font_manager import, because want to avoid rebuilding font - # cache, which means import must come after TTFPATH added to environ - # by styletools.register_fonts()! - small = rcParams['font.size'] # must be absolute - large = rcParams['axes.titlesize'] - if isinstance(large, str): - import matplotlib.font_manager as mfonts - # error will be raised somewhere else if string name is invalid! - scale = mfonts.font_scalings.get(large, 1) - large = small * scale - + Whether to use the width or height for the axes and figure + relative coordinates. + """ # Scales for converting physical units to inches - unit_dict = { - 'in': 1.0, - 'm': 39.37, - 'ft': 12.0, - 'cm': 0.3937, - 'mm': 0.03937, - 'pt': 1 / 72.0, - 'pc': 1 / 6.0, - 'em': small / 72.0, - 'en': 0.5 * small / 72.0, - 'Em': large / 72.0, - 'En': 0.5 * large / 72.0, - 'ly': 3.725e+17, - } + fontsize_small = _not_none(fontsize, rc_matplotlib['font.size']) # always absolute + fontsize_small = _fontsize_to_pt(fontsize_small) + fontsize_large = _not_none(fontsize, rc_matplotlib['axes.titlesize']) + fontsize_large = _fontsize_to_pt(fontsize_large) + unit_dict = UNIT_DICT.copy() + unit_dict.update( + { + 'em': fontsize_small / 72.0, + 'en': 0.5 * fontsize_small / 72.0, + 'Em': fontsize_large / 72.0, + 'En': 0.5 * fontsize_large / 72.0, + } + ) + # Scales for converting display units to inches # WARNING: In ipython shell these take the value 'figure' - if not isinstance(rcParams['figure.dpi'], str): - # once generated by backend - unit_dict['px'] = 1 / rcParams['figure.dpi'] - if not isinstance(rcParams['savefig.dpi'], str): - # once 'printed' i.e. saved - unit_dict['pp'] = 1 / rcParams['savefig.dpi'] + if not isinstance(rc_matplotlib['figure.dpi'], str): + unit_dict['px'] = 1 / rc_matplotlib['figure.dpi'] # once generated by backend + if not isinstance(rc_matplotlib['savefig.dpi'], str): + unit_dict['pp'] = 1 / rc_matplotlib['savefig.dpi'] # once 'printed' i.e. saved + # Scales relative to axes and figure objects - if axes is not None and hasattr(axes, 'get_size_inches'): # proplot axes - unit_dict['ax'] = axes.get_size_inches()[1 - int(width)] + if axes is not None and hasattr(axes, '_get_size_inches'): # proplot axes + unit_dict['ax'] = axes._get_size_inches()[1 - int(width)] if figure is None: figure = getattr(axes, 'figure', None) - if figure is not None and hasattr( - figure, 'get_size_inches'): # proplot axes + if figure is not None and hasattr(figure, 'get_size_inches'): unit_dict['fig'] = figure.get_size_inches()[1 - int(width)] + # Scale for converting inches to arbitrary other unit + if numeric is None and dest is None: + numeric = dest = 'in' + elif numeric is None: + numeric = dest + elif dest is None: + dest = numeric + options = 'Valid units are ' + ', '.join(map(repr, unit_dict)) + '.' try: - scale = unit_dict[dest] + nscale = unit_dict[numeric] except KeyError: - raise ValueError( - f'Invalid destination units {dest!r}. Valid units are ' - + ', '.join(map(repr, unit_dict.keys())) + '.' - ) + raise ValueError(f'Invalid numeric units {numeric!r}. ' + options) + try: + dscale = unit_dict[dest] + except KeyError: + raise ValueError(f'Invalid destination units {dest!r}. ' + options) # Convert units for each value in list result = [] - singleton = (not np.iterable(value) or isinstance(value, str)) - for val in ((value,) if singleton else value): - if val is None or isinstance(val, Number): + singleton = not np.iterable(value) or isinstance(value, str) + for val in (value,) if singleton else value: + # Silently pass None + if val is None: result.append(val) continue - elif not isinstance(val, str): - raise ValueError( - f'Size spec must be string or number or list thereof. ' - f'Got {value!r}.' - ) - regex = NUMBER.match(val) - if not regex: - raise ValueError( - f'Invalid size spec {val!r}. Valid units are ' - + ', '.join(map(repr, unit_dict.keys())) + '.' - ) - number, units = regex.groups() # second group is exponential - try: - result.append( - float(number) * (unit_dict[units] / scale if units else 1) - ) - except (KeyError, ValueError): - raise ValueError( - f'Invalid size spec {val!r}. Valid units are ' - + ', '.join(map(repr, unit_dict.keys())) + '.' - ) - if singleton: - result = result[0] - return result + # Get unit string + if isinstance(val, Real): + number, units = val, None + elif isinstance(val, str): + regex = UNIT_REGEX.match(val) + if regex: + number, units = regex.groups() # second group is exponential + else: + raise ValueError(f'Invalid unit size spec {val!r}.') + else: + raise ValueError(f'Invalid unit size spec {val!r}.') + # Convert with units + if not units: + result.append(float(number) * nscale / dscale) + elif units in unit_dict: + result.append(float(number) * unit_dict[units] / dscale) + else: + raise ValueError(f'Invalid input units {units!r}. ' + options) + return result[0] if singleton else result + + +# Deprecations +shade, saturate = warnings._rename_objs( + '0.6.0', + shade=scale_luminance, + saturate=scale_saturation, +) diff --git a/proplot/wrappers.py b/proplot/wrappers.py deleted file mode 100644 index 6b0872faa..000000000 --- a/proplot/wrappers.py +++ /dev/null @@ -1,3186 +0,0 @@ -#!/usr/bin/env python3 -""" -These "wrapper" functions are applied to various `~proplot.axes.Axes` plotting -methods. When a function is "wrapped", it accepts the parameters that the -"wrapper" function accepts. In a future version, these features will be -documented on the individual plotting methods, but for now they are documented -separately on the "wrappers". -""" -import sys -import numpy as np -import numpy.ma as ma -import functools -from . import styletools, axistools -from .utils import _warn_proplot, _notNone, edges, edges2d, units -import matplotlib.axes as maxes -import matplotlib.container as mcontainer -import matplotlib.contour as mcontour -import matplotlib.ticker as mticker -import matplotlib.transforms as mtransforms -import matplotlib.patheffects as mpatheffects -import matplotlib.patches as mpatches -import matplotlib.colors as mcolors -import matplotlib.artist as martist -import matplotlib.legend as mlegend -from numbers import Number -from .rctools import rc -try: # use this for debugging instead of print()! - from icecream import ic -except ImportError: # graceful fallback if IceCream isn't installed - ic = lambda *a: None if not a else (a[0] if len(a) == 1 else a) # noqa -try: - from cartopy.crs import PlateCarree -except ModuleNotFoundError: - PlateCarree = object - -__all__ = [ - 'add_errorbars', 'bar_wrapper', 'barh_wrapper', 'boxplot_wrapper', - 'default_crs', 'default_latlon', 'default_transform', - 'cmap_changer', - 'cycle_changer', - 'colorbar_wrapper', - 'fill_between_wrapper', 'fill_betweenx_wrapper', 'hist_wrapper', - 'legend_wrapper', 'plot_wrapper', 'scatter_wrapper', - 'standardize_1d', 'standardize_2d', 'text_wrapper', - 'violinplot_wrapper', -] - - -def _load_objects(): - """ - Delay loading expensive modules. We just want to detect if *input - arrays* belong to these types -- and if this is the case, it means the - module has already been imported! So, we only try loading these classes - within autoformat calls. This saves >~500ms of import time. - """ - global DataArray, DataFrame, Series, Index, ndarray - ndarray = np.ndarray - DataArray = getattr(sys.modules.get('xarray', None), 'DataArray', ndarray) - DataFrame = getattr(sys.modules.get('pandas', None), 'DataFrame', ndarray) - Series = getattr(sys.modules.get('pandas', None), 'Series', ndarray) - Index = getattr(sys.modules.get('pandas', None), 'Index', ndarray) - - -_load_objects() - -# Keywords for styling cmap overridden plots -# TODO: Deprecate this when #45 merged! Pcolor *already* accepts lw, -# linewidth, *and* linewidths! -STYLE_ARGS_TRANSLATE = { - 'contour': { - 'colors': 'colors', - 'linewidths': 'linewidths', - 'linestyles': 'linestyles'}, - 'hexbin': { - 'colors': 'edgecolors', - 'linewidths': 'linewidths'}, - 'tricontour': { - 'colors': 'colors', - 'linewidths': 'linewidths', - 'linestyles': 'linestyles'}, - 'parametric': { - 'colors': 'color', - 'linewidths': 'linewidth', - 'linestyles': 'linestyle'}, - 'pcolor': { - 'colors': 'edgecolors', - 'linewidths': 'linewidth', - 'linestyles': 'linestyle'}, - 'tripcolor': { - 'colors': 'edgecolors', - 'linewidths': 'linewidth', - 'linestyles': 'linestyle'}, - 'pcolormesh': { - 'colors': 'edgecolors', - 'linewidths': 'linewidth', - 'linestyles': 'linestyle'}, -} - - -def _is_number(data): - """ - Test whether input is numeric array rather than datetime or strings. - """ - return len(data) and np.issubdtype(_to_array(data).dtype, np.number) - - -def _is_string(data): - """ - Test whether input is array of strings. - """ - return len(data) and isinstance(_to_array(data).flat[0], str) - - -def _to_array(data): - """ - Convert to ndarray cleanly. - """ - return np.asarray(getattr(data, 'values', data)) - - -def _to_arraylike(data): - """ - Converts list of lists to array. - """ - _load_objects() - if not isinstance(data, (ndarray, DataArray, DataFrame, Series, Index)): - data = np.array(data) - if not np.iterable(data): - data = np.atleast_1d(data) - return data - - -def _to_iloc(data): - """ - Indexible attribute of array. - """ - return getattr(data, 'iloc', data) - - -def default_latlon(self, func, *args, latlon=True, **kwargs): - """ - Makes ``latlon=True`` the default for basemap plots. - Wraps %(methods)s for `~proplot.axes.BasemapAxes`. - - This means you no longer have to pass ``latlon=True`` if your data - coordinates are longitude and latitude. - """ - return func(self, *args, latlon=latlon, **kwargs) - - -def default_transform(self, func, *args, transform=None, **kwargs): - """ - Makes ``transform=cartopy.crs.PlateCarree()`` the default - for cartopy plots. - Wraps %(methods)s for `~proplot.axes.GeoAxes`. - - This means you no longer have to - pass ``transform=cartopy.crs.PlateCarree()`` if your data - coordinates are longitude and latitude. - """ - # Apply default transform - # TODO: Do some cartopy methods reset backgroundpatch or outlinepatch? - # Deleted comment reported this issue - if transform is None: - transform = PlateCarree() - result = func(self, *args, transform=transform, **kwargs) - return result - - -def default_crs(self, func, *args, crs=None, **kwargs): - """ - Fixes the `~cartopy.mpl.geoaxes.GeoAxes.set_extent` bug associated with - tight bounding boxes and makes ``crs=cartopy.crs.PlateCarree()`` the - default for cartopy plots. Wraps %(methods)s - for `~proplot.axes.GeoAxes`. - """ - # Apply default crs - name = func.__name__ - if crs is None: - crs = PlateCarree() - try: - result = func(self, *args, crs=crs, **kwargs) - except TypeError as err: # duplicate keyword args, i.e. crs is positional - if not args: - raise err - result = func(self, *args[:-1], crs=args[-1], **kwargs) - # Fix extent, so axes tight bounding box gets correct box! - # From this issue: - # https://github.com/SciTools/cartopy/issues/1207#issuecomment-439975083 - if name == 'set_extent': - clipped_path = self.outline_patch.orig_path.clip_to_bbox(self.viewLim) - self.outline_patch._path = clipped_path - self.background_patch._path = clipped_path - return result - - -def _standard_label(data, axis=None, units=True): - """ - Get data and label for pandas or xarray objects or their coordinates. - """ - label = '' - _load_objects() - if isinstance(data, ndarray): - if axis is not None and data.ndim > axis: - data = np.arange(data.shape[axis]) - # Xarray with common NetCDF attribute names - elif isinstance(data, DataArray): - if axis is not None and data.ndim > axis: - data = data.coords[data.dims[axis]] - label = getattr(data, 'name', '') or '' - for key in ('standard_name', 'long_name'): - label = data.attrs.get(key, label) - if units: - units = data.attrs.get('units', '') - if label and units: - label = f'{label} ({units})' - elif units: - label = units - # Pandas object with name attribute - # if not label and isinstance(data, DataFrame) and data.columns.size == 1: - elif isinstance(data, (DataFrame, Series, Index)): - if axis == 0 and isinstance(data, (DataFrame, Series)): - data = data.index - elif axis == 1 and isinstance(data, DataFrame): - data = data.columns - elif axis is not None: - data = np.arange(len(data)) # e.g. for Index - # DataFrame has no native name attribute but user can add one: - # https://github.com/pandas-dev/pandas/issues/447 - label = getattr(data, 'name', '') or '' - return data, str(label).strip() - - -def standardize_1d(self, func, *args, **kwargs): - """ - Interprets positional arguments for the "1d" plotting methods - %(methods)s. This also optionally modifies the x axis label, y axis label, - title, and axis ticks if a `~xarray.DataArray`, `~pandas.DataFrame`, or - `~pandas.Series` is passed. - - Positional arguments are standardized as follows: - - * If a 2D array is passed, the corresponding plot command is called for - each column of data (except for ``boxplot`` and ``violinplot``, in which - case each column is interpreted as a distribution). - * If *x* and *y* or *latitude* and *longitude* coordinates were not - provided, and a `~pandas.DataFrame` or `~xarray.DataArray`, we - try to infer them from the metadata. Otherwise, - ``np.arange(0, data.shape[0])`` is used. - """ - # Sanitize input - # TODO: Add exceptions for methods other than 'hist'? - name = func.__name__ - _load_objects() - if not args: - return func(self, *args, **kwargs) - elif len(args) == 1: - x = None - y, *args = args - elif len(args) in (2, 3, 4): - x, y, *args = args # same - else: - raise ValueError(f'Too many arguments passed to {name}. Max is 4.') - vert = kwargs.get('vert', None) - if vert is not None: - orientation = ('vertical' if vert else 'horizontal') - else: - orientation = kwargs.get('orientation', 'vertical') - - # Iterate through list of ys that we assume are identical - # Standardize based on the first y input - if len(args) >= 1 and 'fill_between' in name: - ys, args = (y, args[0]), args[1:] - else: - ys = (y,) - ys = [_to_arraylike(y) for y in ys] - - # Auto x coords - y = ys[0] # test the first y input - if x is None: - axis = 1 if (name in ('hist', 'boxplot', 'violinplot') or any( - kwargs.get(s, None) for s in ('means', 'medians'))) else 0 - x, _ = _standard_label(y, axis=axis) - x = _to_arraylike(x) - if x.ndim != 1: - raise ValueError( - f'x coordinates must be 1-dimensional, but got {x.ndim}.' - ) - - # Auto formatting - xi = None # index version of 'x' - if not hasattr(self, 'projection'): - # First handle string-type x-coordinates - kw = {} - xax = 'y' if orientation == 'horizontal' else 'x' - yax = 'x' if xax == 'y' else 'y' - if _is_string(x): - xi = np.arange(len(x)) - kw[xax + 'locator'] = mticker.FixedLocator(xi) - kw[xax + 'formatter'] = mticker.IndexFormatter(x) - kw[xax + 'minorlocator'] = mticker.NullLocator() - if name == 'boxplot': - kwargs['labels'] = x - elif name == 'violinplot': - kwargs['positions'] = xi - if name in ('boxplot', 'violinplot'): - kwargs['positions'] = xi - # Next handle labels if 'autoformat' is on - if self.figure._auto_format: - # Ylabel - y, label = _standard_label(y) - if label: - # for histogram, this indicates x coordinate - iaxis = xax if name in ('hist',) else yax - kw[iaxis + 'label'] = label - # Xlabel - x, label = _standard_label(x) - if label and name not in ('hist',): - kw[xax + 'label'] = label - if name != 'scatter' and len(x) > 1 and xi is None and x[1] < x[0]: - kw[xax + 'reverse'] = True - # Appply - if kw: - self.format(**kw) - - # Standardize args - if xi is not None: - x = xi - if name in ('boxplot', 'violinplot'): - ys = [_to_array(yi) for yi in ys] # store naked array - - # Basemap shift x coordiantes without shifting y, we fix this! - if getattr(self, 'name', '') == 'basemap' and kwargs.get('latlon', None): - ix, iys = x, [] - xmin, xmax = self.projection.lonmin, self.projection.lonmax - for y in ys: - # Ensure data is monotonic and falls within map bounds - ix, iy = _enforce_bounds(*_standardize_latlon(x, y), xmin, xmax) - iys.append(iy) - x, ys = ix, iys - - # WARNING: For some functions, e.g. boxplot and violinplot, we *require* - # cycle_changer is also applied so it can strip 'x' input. - return func(self, x, *ys, *args, **kwargs) - - -def _interp_poles(y, Z): - """ - Add data points on the poles as the average of highest latitude data. - """ - # Get means - with np.errstate(all='ignore'): - p1 = Z[0, :].mean() # pole 1, make sure is not 0D DataArray! - p2 = Z[-1, :].mean() # pole 2 - if hasattr(p1, 'item'): - p1 = np.asscalar(p1) # happens with DataArrays - if hasattr(p2, 'item'): - p2 = np.asscalar(p2) - # Concatenate - ps = (-90, 90) if (y[0] < y[-1]) else (90, -90) - Z1 = np.repeat(p1, Z.shape[1])[None, :] - Z2 = np.repeat(p2, Z.shape[1])[None, :] - y = ma.concatenate((ps[:1], y, ps[1:])) - Z = ma.concatenate((Z1, Z, Z2), axis=0) - return y, Z - - -def _standardize_latlon(x, y): - """ - Ensure longitudes are monotonic and make `~numpy.ndarray` copies so the - contents can be modified. Ignores 2D coordinate arrays. - """ - # Sanitization and bail if 2D - if x.ndim == 1: - x = ma.array(x) - if y.ndim == 1: - y = ma.array(y) - if x.ndim != 1 or all(x < x[0]): # skip monotonic backwards data - return x, y - # Enforce monotonic longitudes - lon1 = x[0] - while True: - filter_ = (x < lon1) - if filter_.sum() == 0: - break - x[filter_] += 360 - return x, y - - -def _enforce_bounds(x, y, xmin, xmax): - """ - Ensure data for basemap plots is restricted between the minimum and - maximum longitude of the projection. Input is the ``x`` and ``y`` - coordinates. The ``y`` coordinates are rolled along the rightmost axis. - """ - if x.ndim != 1: - return x, y - # Roll in same direction if some points on right-edge extend - # more than 360 above min longitude; *they* should be on left side - lonroll = np.where(x > xmin + 360)[0] # tuple of ids - if lonroll.size: # non-empty - roll = x.size - lonroll.min() - x = np.roll(x, roll) - y = np.roll(y, roll, axis=-1) - x[:roll] -= 360 # make monotonic - - # Set NaN where data not in range xmin, xmax. Must be done - # for regional smaller projections or get weird side-effects due - # to having valid data way outside of the map boundaries - y = y.copy() - if x.size - 1 == y.shape[-1]: # test western/eastern grid cell edges - y[..., (x[1:] < xmin) | (x[:-1] > xmax)] = np.nan - elif x.size == y.shape[-1]: # test the centers and pad by one for safety - where = np.where((x < xmin) | (x > xmax))[0] - y[..., where[1:-1]] = np.nan - return x, y - - -def standardize_2d(self, func, *args, order='C', globe=False, **kwargs): - """ - Interprets positional arguments for the "2d" plotting methods - %(methods)s. This also optionally modifies the x axis label, y axis label, - title, and axis ticks if a `~xarray.DataArray`, `~pandas.DataFrame`, or - `~pandas.Series` is passed. - - Positional arguments are standardized as follows: - - * If *x* and *y* or *latitude* and *longitude* coordinates were not - provided, and a `~pandas.DataFrame` or `~xarray.DataArray` is passed, we - try to infer them from the metadata. Otherwise, - ``np.arange(0, data.shape[0])`` and ``np.arange(0, data.shape[1])`` - are used. - * For ``pcolor`` and ``pcolormesh``, coordinate *edges* are calculated - if *centers* were provided. For all other methods, coordinate *centers* - are calculated if *edges* were provided. - - For `~proplot.axes.GeoAxes` and `~proplot.axes.BasemapAxes`, the - `globe` keyword arg is added, suitable for plotting datasets with global - coverage. Passing ``globe=True`` does the following: - - 1. "Interpolates" input data to the North and South poles. - 2. Makes meridional coverage "circular", i.e. the last longitude coordinate - equals the first longitude coordinate plus 360\N{DEGREE SIGN}. - - For `~proplot.axes.BasemapAxes`, 1D longitude vectors are also cycled to - fit within the map edges. For example, if the projection central longitude - is 90\N{DEGREE SIGN}, the data is shifted so that it spans - -90\N{DEGREE SIGN} to 270\N{DEGREE SIGN}. - """ - # Sanitize input - name = func.__name__ - _load_objects() - if not args: - return func(self, *args, **kwargs) - elif len(args) > 4: - raise ValueError(f'Too many arguments passed to {name}. Max is 4.') - x, y = None, None - if len(args) > 2: - x, y, *args = args - - # Ensure DataArray, DataFrame or ndarray - Zs = [] - for Z in args: - Z = _to_arraylike(Z) - if Z.ndim != 2: - raise ValueError(f'Z must be 2-dimensional, got shape {Z.shape}.') - Zs.append(Z) - if not all(Zs[0].shape == Z.shape for Z in Zs): - raise ValueError( - f'Zs must be same shape, got shapes {[Z.shape for Z in Zs]}.' - ) - - # Retrieve coordinates - if x is None and y is None: - Z = Zs[0] - if order == 'C': # TODO: check order stuff works - idx, idy = 1, 0 - else: - idx, idy = 0, 1 - if isinstance(Z, ndarray): - x = np.arange(Z.shape[idx]) - y = np.arange(Z.shape[idy]) - elif isinstance(Z, DataArray): # DataArray - x = Z.coords[Z.dims[idx]] - y = Z.coords[Z.dims[idy]] - else: # DataFrame; never Series or Index because these are 1D - x = Z.index - y = Z.columns - - # Check coordinates - x, y = _to_arraylike(x), _to_arraylike(y) - if x.ndim != y.ndim: - raise ValueError( - f'x coordinates are {x.ndim}-dimensional, ' - f'but y coordinates are {y.ndim}-dimensional.' - ) - for s, array in zip(('x', 'y'), (x, y)): - if array.ndim not in (1, 2): - raise ValueError( - f'{s} coordinates are {array.ndim}-dimensional, ' - f'but must be 1 or 2-dimensional.' - ) - - # Auto formatting - kw = {} - xi, yi = None, None - if not hasattr(self, 'projection'): - # First handle string-type x and y-coordinates - if _is_string(x): - xi = np.arange(len(x)) - kw['xlocator'] = mticker.FixedLocator(xi) - kw['xformatter'] = mticker.IndexFormatter(x) - kw['xminorlocator'] = mticker.NullLocator() - if _is_string(x): - yi = np.arange(len(y)) - kw['ylocator'] = mticker.FixedLocator(yi) - kw['yformatter'] = mticker.IndexFormatter(y) - kw['yminorlocator'] = mticker.NullLocator() - # Handle labels if 'autoformat' is on - if self.figure._auto_format: - for key, xy in zip(('xlabel', 'ylabel'), (x, y)): - _, label = _standard_label(xy) - if label: - kw[key] = label - if len(xy) > 1 and all(isinstance(xy, Number) - for xy in xy[:2]) and xy[1] < xy[0]: - kw[key[0] + 'reverse'] = True - if xi is not None: - x = xi - if yi is not None: - y = yi - # Handle figure titles - if self.figure._auto_format: - _, colorbar_label = _standard_label(Zs[0], units=True) - _, title = _standard_label(Zs[0], units=False) - if title: - kw['title'] = title - if kw: - self.format(**kw) - - # Enforce edges - if name in ('pcolor', 'pcolormesh'): - # Get centers or raise error. If 2D, don't raise error, but don't fix - # either, because matplotlib pcolor just trims last column and row. - xlen, ylen = x.shape[-1], y.shape[0] - for Z in Zs: - if Z.ndim != 2: - raise ValueError( - f'Input arrays must be 2D, instead got shape {Z.shape}.' - ) - elif Z.shape[1] == xlen and Z.shape[0] == ylen: - if all( - z.ndim == 1 and z.size > 1 - and _is_number(z) for z in (x, y) - ): - x = edges(x) - y = edges(y) - else: - if ( - x.ndim == 2 and x.shape[0] > 1 and x.shape[1] > 1 - and _is_number(x) - ): - x = edges2d(x) - if ( - y.ndim == 2 and y.shape[0] > 1 and y.shape[1] > 1 - and _is_number(y) - ): - y = edges2d(y) - elif Z.shape[1] != xlen - 1 or Z.shape[0] != ylen - 1: - raise ValueError( - f'Input shapes x {x.shape} and y {y.shape} must match ' - f'Z centers {Z.shape} or ' - f'Z borders {tuple(i+1 for i in Z.shape)}.' - ) - # Optionally re-order - # TODO: Double check this - if order == 'F': - x, y = x.T, y.T # in case they are 2-dimensional - Zs = (Z.T for Z in Zs) - elif order != 'C': - raise ValueError( - f'Invalid order {order!r}. Choose from ' - '"C" (row-major, default) and "F" (column-major).' - ) - - # Enforce centers - else: - # Get centers given edges. If 2D, don't raise error, let matplotlib - # raise error down the line. - xlen, ylen = x.shape[-1], y.shape[0] - for Z in Zs: - if Z.ndim != 2: - raise ValueError( - f'Input arrays must be 2D, instead got shape {Z.shape}.' - ) - elif Z.shape[1] == xlen - 1 and Z.shape[0] == ylen - 1: - if all( - z.ndim == 1 and z.size > 1 - and _is_number(z) for z in (x, y) - ): - x = (x[1:] + x[:-1]) / 2 - y = (y[1:] + y[:-1]) / 2 - else: - if ( - x.ndim == 2 and x.shape[0] > 1 and x.shape[1] > 1 - and _is_number(x) - ): - x = 0.25 * ( - x[:-1, :-1] + x[:-1, 1:] + x[1:, :-1] + x[1:, 1:] - ) - if ( - y.ndim == 2 and y.shape[0] > 1 and y.shape[1] > 1 - and _is_number(y) - ): - y = 0.25 * ( - y[:-1, :-1] + y[:-1, 1:] + y[1:, :-1] + y[1:, 1:] - ) - elif Z.shape[1] != xlen or Z.shape[0] != ylen: - raise ValueError( - f'Input shapes x {x.shape} and y {y.shape} ' - f'must match Z centers {Z.shape} ' - f'or Z borders {tuple(i+1 for i in Z.shape)}.' - ) - # Optionally re-order - # TODO: Double check this - if order == 'F': - x, y = x.T, y.T # in case they are 2-dimensional - Zs = (Z.T for Z in Zs) - elif order != 'C': - raise ValueError( - f'Invalid order {order!r}. Choose from ' - '"C" (row-major, default) and "F" (column-major).' - ) - - # Cartopy projection axes - if (getattr(self, 'name', '') == 'geo' - and isinstance(kwargs.get('transform', None), PlateCarree)): - x, y = _standardize_latlon(x, y) - ix, iZs = x, [] - for Z in Zs: - if globe and x.ndim == 1 and y.ndim == 1: - # Fix holes over poles by *interpolating* there - y, Z = _interp_poles(y, Z) - - # Fix seams by ensuring circular coverage. Unlike basemap, - # cartopy can plot across map edges. - if (x[0] % 360) != ((x[-1] + 360) % 360): - ix = ma.concatenate((x, [x[0] + 360])) - Z = ma.concatenate((Z, Z[:, :1]), axis=1) - iZs.append(Z) - x, Zs = ix, iZs - - # Basemap projection axes - elif getattr(self, 'name', '') == 'basemap' and kwargs.get('latlon', None): - # Fix grid - xmin, xmax = self.projection.lonmin, self.projection.lonmax - x, y = _standardize_latlon(x, y) - ix, iZs = x, [] - for Z in Zs: - # Ensure data is within map bounds - ix, Z = _enforce_bounds(x, Z, xmin, xmax) - - # Globe coverage fixes - if globe and ix.ndim == 1 and y.ndim == 1: - # Fix holes over poles by interpolating there (equivalent to - # simple mean of highest/lowest latitude points) - y, Z = _interp_poles(y, Z) - - # Fix seams at map boundary; 3 scenarios here: - # Have edges (e.g. for pcolor), and they fit perfectly against - # basemap seams. Does not augment size. - if ix[0] == xmin and ix.size - 1 == Z.shape[1]: - pass # do nothing - # Have edges (e.g. for pcolor), and the projection edge is - # in-between grid cell boundaries. Augments size by 1. - elif ix.size - 1 == Z.shape[1]: # just add grid cell - ix = ma.append(xmin, ix) - ix[-1] = xmin + 360 - Z = ma.concatenate((Z[:, -1:], Z), axis=1) - # Have centers (e.g. for contourf), and we need to interpolate - # to left/right edges of the map boundary. Augments size by 2. - elif ix.size == Z.shape[1]: - xi = np.array([ix[-1], ix[0] + 360]) # x - if xi[0] != xi[1]: - Zq = ma.concatenate((Z[:, -1:], Z[:, :1]), axis=1) - xq = xmin + 360 - Zq = ( - Zq[:, :1] * (xi[1] - xq) + Zq[:, 1:] * (xq - xi[0]) - ) / (xi[1] - xi[0]) - ix = ma.concatenate(([xmin], ix, [xmin + 360])) - Z = ma.concatenate((Zq, Z, Zq), axis=1) - else: - raise ValueError( - 'Unexpected shape of longitude/latitude/data arrays.' - ) - iZs.append(Z) - x, Zs = ix, iZs - - # Convert to projection coordinates - if x.ndim == 1 and y.ndim == 1: - x, y = np.meshgrid(x, y) - x, y = self.projection(x, y) - kwargs['latlon'] = False - - # Finally return result - # WARNING: Must apply default colorbar label *here* in case metadata - # was stripped by globe=True. - colorbar_kw = kwargs.pop('colorbar_kw', None) or {} - colorbar_kw.setdefault('label', colorbar_label) - return func(self, x, y, *Zs, colorbar_kw=colorbar_kw, **kwargs) - - -def _errorbar_values(data, idata, bardata=None, barrange=None, barstd=False): - """ - Return values that can be passed to the `~matplotlib.axes.Axes.errorbar` - `xerr` and `yerr` keyword args. - """ - if bardata is not None: - err = np.array(bardata) - if err.ndim == 1: - err = err[:, None] - if err.ndim != 2 or err.shape[0] != 2 \ - or err.shape[1] != idata.shape[-1]: - raise ValueError( - f'bardata must have shape (2, {idata.shape[-1]}), ' - f'but got {err.shape}.' - ) - elif barstd: - err = np.array(idata) + \ - np.std(data, axis=0)[None, :] * np.array(barrange)[:, None] - else: - err = np.percentile(data, barrange, axis=0) - err = err - np.array(idata) - err[0, :] *= -1 # array now represents error bar sizes - return err - - -def add_errorbars( - self, func, *args, - medians=False, means=False, - boxes=None, bars=None, - boxdata=None, bardata=None, - boxstd=False, barstd=False, - boxmarker=True, boxmarkercolor='white', - boxrange=(25, 75), barrange=(5, 95), boxcolor=None, barcolor=None, - boxlw=None, barlw=None, capsize=None, - boxzorder=3, barzorder=3, - **kwargs -): - """ - Adds support for drawing error bars to the "1d" plotting methods - %(methods)s. Includes options for interpreting columns of data as ranges, - representing the mean or median of each column with lines, points, or bars, - and drawing error bars representing percentile ranges or standard deviation - multiples for the data in each column. - - Parameters - ---------- - *args - The input data. - bars : bool, optional - Toggles *thin* error bars with optional "whiskers" (i.e. caps). Default - is ``True`` when `means` is ``True``, `medians` is ``True``, or - `bardata` is not ``None``. - boxes : bool, optional - Toggles *thick* boxplot-like error bars with a marker inside - representing the mean or median. Default is ``True`` when `means` is - ``True``, `medians` is ``True``, or `boxdata` is not ``None``. - means : bool, optional - Whether to plot the means of each column in the input data. - medians : bool, optional - Whether to plot the medians of each column in the input data. - bardata, boxdata : 2xN ndarray, optional - Arrays that manually specify the thin and thick error bar coordinates. - The first row contains lower bounds, and the second row contains - upper bounds. Columns correspond to points in the dataset. - barstd, boxstd : bool, optional - Whether `barrange` and `boxrange` refer to multiples of the standard - deviation, or percentile ranges. Default is ``False``. - barrange : (float, float), optional - Percentile ranges or standard deviation multiples for drawing thin - error bars. The defaults are ``(-3,3)`` (i.e. +/-3 standard deviations) - when `barstd` is ``True``, and ``(0,100)`` (i.e. the full data range) - when `barstd` is ``False``. - boxrange : (float, float), optional - Percentile ranges or standard deviation multiples for drawing thick - error bars. The defaults are ``(-1,1)`` (i.e. +/-1 standard deviation) - when `boxstd` is ``True``, and ``(25,75)`` (i.e. the middle 50th - percentile) when `boxstd` is ``False``. - barcolor, boxcolor : color-spec, optional - Colors for the thick and thin error bars. Default is ``'k'``. - barlw, boxlw : float, optional - Line widths for the thin and thick error bars, in points. Default - `barlw` is ``0.7`` and default `boxlw` is ``4*barlw``. - boxmarker : bool, optional - Whether to draw a small marker in the middle of the box denoting - the mean or median position. Ignored if `boxes` is ``False``. - Default is ``True``. - boxmarkercolor : color-spec, optional - Color for the `boxmarker` marker. Default is ``'w'``. - capsize : float, optional - The cap size for thin error bars, in points. - barzorder, boxzorder : float, optional - The "zorder" for the thin and thick error bars. - lw, linewidth : float, optional - If passed, this is used for the default `barlw`. - edgecolor : float, optional - If passed, this is used for the default `barcolor` and `boxcolor`. - """ - name = func.__name__ - x, y, *args = args - # Sensible defaults - if boxdata is not None: - bars = _notNone(bars, True) - if bardata is not None: - boxes = _notNone(boxes, True) - if boxdata is not None or bardata is not None: - # e.g. if boxdata passed but bardata not passed, use bars=False - bars = _notNone(bars, False) - boxes = _notNone(boxes, False) - - # Get means or medians for plotting - iy = y - if (means or medians): - bars = _notNone(bars, True) - boxes = _notNone(boxes, True) - if y.ndim != 2: - raise ValueError( - f'Need 2D data array for means=True or medians=True, ' - f'got {y.ndim}D array.' - ) - if means: - iy = np.mean(y, axis=0) - elif medians: - iy = np.percentile(y, 50, axis=0) - - # Call function, accounting for different signatures of plot and violinplot - get = kwargs.pop if name == 'violinplot' else kwargs.get - lw = _notNone(get('lw', None), get('linewidth', None), 0.7) - get = kwargs.pop if name != 'bar' else kwargs.get - edgecolor = _notNone(get('edgecolor', None), 'k') - if name == 'violinplot': - xy = (x, y) # full data - else: - xy = (x, iy) # just the stats - obj = func(self, *xy, *args, **kwargs) - if not boxes and not bars: - return obj - - # Account for horizontal bar plots - if 'vert' in kwargs: - orientation = 'vertical' if kwargs['vert'] else 'horizontal' - else: - orientation = kwargs.get('orientation', 'vertical') - if orientation == 'horizontal': - axis = 'x' # xerr - xy = (iy, x) - else: - axis = 'y' # yerr - xy = (x, iy) - - # Defaults settings - barlw = _notNone(barlw, lw) - boxlw = _notNone(boxlw, 4 * barlw) - capsize = _notNone(capsize, 3) - barcolor = _notNone(barcolor, edgecolor) - boxcolor = _notNone(boxcolor, edgecolor) - - # Draw boxes and bars - if boxes: - default = (-1, 1) if barstd else (25, 75) - boxrange = _notNone(boxrange, default) - err = _errorbar_values(y, iy, boxdata, boxrange, boxstd) - if boxmarker: - self.scatter( - *xy, marker='o', color=boxmarkercolor, - s=boxlw, zorder=5 - ) - self.errorbar(*xy, **{ - axis + 'err': err, 'capsize': 0, 'zorder': boxzorder, - 'color': boxcolor, 'linestyle': 'none', 'linewidth': boxlw - }) - if bars: # now impossible to make thin bar width different from cap width! - default = (-3, 3) if barstd else (0, 100) - barrange = _notNone(barrange, default) - err = _errorbar_values(y, iy, bardata, barrange, barstd) - self.errorbar(*xy, **{ - axis + 'err': err, 'capsize': capsize, 'zorder': barzorder, - 'color': barcolor, 'linewidth': barlw, 'linestyle': 'none', - 'markeredgecolor': barcolor, 'markeredgewidth': barlw - }) - return obj - - -def plot_wrapper(self, func, *args, cmap=None, values=None, **kwargs): - """ - Calls `~proplot.axes.Axes.parametric` if the `cmap` argument was supplied, - otherwise calls `~matplotlib.axes.Axes.plot`. Wraps %(methods)s. - - Parameters - ---------- - *args : (y,), (x, y), or (x, y, fmt) - Passed to `~matplotlib.axes.Axes.plot`. - cmap, values : optional - Passed to `~proplot.axes.Axes.parametric`. - **kwargs - `~matplotlib.lines.Line2D` properties. - """ - if len(args) > 3: # e.g. with fmt string - raise ValueError(f'Expected 1-3 positional args, got {len(args)}.') - if cmap is None: - lines = func(self, *args, values=values, **kwargs) - else: - lines = self.parametric(*args, cmap=cmap, values=values, **kwargs) - return lines - - -def scatter_wrapper( - self, func, *args, - s=None, size=None, markersize=None, - c=None, color=None, markercolor=None, - smin=None, smax=None, - cmap=None, cmap_kw=None, vmin=None, vmax=None, norm=None, norm_kw=None, - lw=None, linewidth=None, linewidths=None, - markeredgewidth=None, markeredgewidths=None, - edgecolor=None, edgecolors=None, - markeredgecolor=None, markeredgecolors=None, - **kwargs -): - """ - Adds keyword arguments to `~matplotlib.axes.Axes.scatter` that are more - consistent with the `~matplotlib.axes.Axes.plot` keyword arguments, and - interpret the `cmap` and `norm` keyword arguments with - `~proplot.styletools.Colormap` and `~proplot.styletools.Normalize` like - in `cmap_changer`. Wraps %(methods)s. - - Parameters - ---------- - s, size, markersize : float or list of float, optional - Aliases for the marker size. - smin, smax : float, optional - Used to scale the `s` array. These are the minimum and maximum marker - sizes. Defaults are the minimum and maximum of the `s` array. - c, color, markercolor : color-spec or list thereof, or array, optional - Aliases for the marker fill color. If just an array of values, the - colors will be generated by passing the values through the `norm` - normalizer and drawing from the `cmap` colormap. - cmap : colormap-spec, optional - The colormap specifer, passed to the `~proplot.styletools.Colormap` - constructor. - cmap_kw : dict-like, optional - Passed to `~proplot.styletools.Colormap`. - vmin, vmax : float, optional - Used to generate a `norm` for scaling the `c` array. These are the - values corresponding to the leftmost and rightmost colors in the - colormap. Defaults are the minimum and maximum values of the `c` array. - norm : normalizer spec, optional - The colormap normalizer, passed to the `~proplot.styletools.Norm` - constructor. - norm_kw : dict, optional - Passed to `~proplot.styletools.Norm`. - lw, linewidth, linewidths, markeredgewidth, markeredgewidths : \ -float or list thereof, optional - Aliases for the marker edge width. - edgecolors, markeredgecolor, markeredgecolors : \ -color-spec or list thereof, optional - Aliases for the marker edge color. - **kwargs - Passed to `~matplotlib.axes.Axes.scatter`. - """ - # Manage input arguments - # NOTE: Parse 1D must come before this - nargs = len(args) - if len(args) > 4: - raise ValueError(f'Expected 1-4 positional args, got {nargs}.') - args = list(args) - if len(args) == 4: - c = args.pop(1) - if len(args) == 3: - s = args.pop(0) - - # Format cmap and norm - cmap_kw = cmap_kw or {} - norm_kw = norm_kw or {} - if cmap is not None: - cmap = styletools.Colormap(cmap, **cmap_kw) - if norm is not None: - norm = styletools.Norm(norm, **norm_kw) - - # Apply some aliases for keyword arguments - c = _notNone( - c, color, markercolor, None, - names=('c', 'color', 'markercolor') - ) - s = _notNone( - s, size, markersize, None, - names=('s', 'size', 'markersize') - ) - lw = _notNone( - lw, linewidth, linewidths, markeredgewidth, markeredgewidths, None, - names=( - 'lw', 'linewidth', 'linewidths', - 'markeredgewidth', 'markeredgewidths' - ), - ) - ec = _notNone( - edgecolor, edgecolors, markeredgecolor, markeredgecolors, None, - names=( - 'edgecolor', 'edgecolors', 'markeredgecolor', 'markeredgecolors' - ), - ) - - # Scale s array - if np.iterable(s): - smin_true, smax_true = min(s), max(s) - if smin is None: - smin = smin_true - if smax is None: - smax = smax_true - s = smin + (smax - smin) * (np.array(s) - smin_true) / \ - (smax_true - smin_true) - return func( - self, *args, c=c, s=s, - cmap=cmap, vmin=vmin, vmax=vmax, - norm=norm, linewidths=lw, edgecolors=ec, - **kwargs - ) - - -def _fill_between_apply( - self, func, *args, - negcolor='blue', poscolor='red', negpos=False, - **kwargs -): - """ - Parse args and call function. - """ - # Allow common keyword usage - x = 'y' if 'x' in func.__name__ else 'y' - y = 'x' if x == 'y' else 'y' - if x in kwargs: - args = (kwargs.pop(x), *args) - for y in (y + '1', y + '2'): - if y in kwargs: - args = (*args, kwargs.pop(y)) - if len(args) == 1: - args = (np.arange(len(args[0])), *args) - if len(args) == 2: - if kwargs.get('stacked', False): - args = (*args, 0) - else: - args = (args[0], 0, args[1]) # default behavior - if len(args) != 3: - raise ValueError(f'Expected 2-3 positional args, got {len(args)}.') - if not negpos: - obj = func(self, *args, **kwargs) - return obj - - # Get zero points - objs = [] - kwargs.setdefault('interpolate', True) - y1, y2 = np.atleast_1d( - args[-2]).squeeze(), np.atleast_1d(args[-1]).squeeze() - if y1.ndim > 1 or y2.ndim > 1: - raise ValueError(f'When "negpos" is True, y must be 1-dimensional.') - if kwargs.get('where', None) is not None: - raise ValueError( - 'When "negpos" is True, you cannot set the "where" keyword.' - ) - for i in range(2): - kw = {**kwargs} - kw.setdefault('color', negcolor if i == 0 else poscolor) - where = (y2 < y1) if i == 0 else (y2 >= y1) - obj = func(self, *args, where=where, **kw) - objs.append(obj) - return (*objs,) - - -def fill_between_wrapper(self, func, *args, **kwargs): - """ - Supports overlaying and stacking successive columns of data, and permits - using different colors for "negative" and "positive" regions. - Wraps `~matplotlib.axes.Axes.fill_between` and `~proplot.axes.Axes.area`. - - Parameters - ---------- - *args : (y1,), (x,y1), or (x,y1,y2) - The *x* and *y* coordinates. If `x` is not provided, it will be - inferred from `y1`. If `y1` and `y2` are provided, their shapes - must be identical, and we fill between respective columns of these - arrays. - stacked : bool, optional - If `y2` is ``None``, this indicates whether to "stack" successive - columns of the `y1` array. - negpos : bool, optional - Whether to shade where `y2` is greater than `y1` with the color - `poscolor`, and where `y1` is greater than `y2` with the color - `negcolor`. For example, to shade positive values red and negtive blue, - use ``ax.fill_between(x, 0, y)``. - negcolor, poscolor : color-spec, optional - Colors to use for the negative and positive values. Ignored if `negpos` - is ``False``. - where : ndarray, optional - Boolean ndarray mask for points you want to shade. See `this example \ -`__. - **kwargs - Passed to `~matplotlib.axes.Axes.fill_between`. - """ # noqa - return _fill_between_apply(self, func, *args, **kwargs) - - -def fill_betweenx_wrapper(self, func, *args, **kwargs): - """ - Supports overlaying and stacking successive columns of data, and permits - using different colors for "negative" and "positive" regions. - Wraps `~matplotlib.axes.Axes.fillx_between` and `~proplot.axes.Axes.areax`. - Usage is same as `fill_between_wrapper`. - """ - return _fill_between_apply(self, func, *args, **kwargs) - - -def hist_wrapper(self, func, x, bins=None, **kwargs): - """ - Enforces that all arguments after `bins` are keyword-only and sets the - default patch linewidth to ``0``. Wraps %(methods)s. - """ - kwargs.setdefault('linewidth', 0) - return func(self, x, bins=bins, **kwargs) - - -def barh_wrapper( # noqa: U100 - self, func, y=None, width=None, height=0.8, left=None, **kwargs -): - """ - Supports grouping and stacking successive columns of data, and changes - the default bar style. Wraps %(methods)s. - """ - kwargs.setdefault('orientation', 'horizontal') - if y is None and width is None: - raise ValueError( - f'barh() requires at least 1 positional argument, got 0.' - ) - return self.bar(x=left, height=height, width=width, bottom=y, **kwargs) - - -def bar_wrapper( - self, func, x=None, height=None, width=0.8, bottom=None, *, left=None, - vert=None, orientation='vertical', stacked=False, - lw=None, linewidth=0.7, edgecolor='k', - **kwargs -): - """ - Supports grouping and stacking successive columns of data, and changes - the default bar style. Wraps %(methods)s. - - Parameters - ---------- - x, height, width, bottom : float or list of float, optional - The dimensions of the bars. If the *x* coordinates are not provided, - they are set to ``np.arange(0, len(height))``. - orientation : {'vertical', 'horizontal'}, optional - The orientation of the bars. - vert : bool, optional - Alternative to the `orientation` keyword arg. If ``False``, horizontal - bars are drawn. This is for consistency with - `~matplotlib.axes.Axes.boxplot` and `~matplotlib.axes.Axes.violinplot`. - stacked : bool, optional - Whether to stack columns of input data, or plot the bars side-by-side. - edgecolor : color-spec, optional - The edge color for the bar patches. - lw, linewidth : float, optional - The edge width for the bar patches. - """ - # Barh converts y-->bottom, left-->x, width-->height, height-->width. - # Convert back to (x, bottom, width, height) so we can pass stuff through - # cycle_changer. - # NOTE: You *must* do juggling of barh keyword order --> bar keyword order - # --> barh keyword order, because horizontal hist passes arguments to bar - # directly and will not use a 'barh' method with overridden argument order! - if vert is not None: - orientation = ('vertical' if vert else 'horizontal') - if orientation == 'horizontal': - x, bottom = bottom, x - width, height = height, width - - # Parse args - # TODO: Stacked feature is implemented in `cycle_changer`, but makes more - # sense do document here; figure out way to move it here? - if left is not None: - _warn_proplot( - f'The "left" keyword with bar() is deprecated. Use "x" instead.' - ) - x = left - if x is None and height is None: - raise ValueError( - f'bar() requires at least 1 positional argument, got 0.' - ) - elif height is None: - x, height = None, x - - # Call func - # TODO: This *must* also be wrapped by cycle_changer, which ultimately - # permutes back the x/bottom args for horizontal bars! Need to clean up. - lw = _notNone(lw, linewidth, None, names=('lw', 'linewidth')) - return func( - self, x, height, width=width, bottom=bottom, - linewidth=lw, edgecolor=edgecolor, - stacked=stacked, orientation=orientation, - **kwargs - ) - - -def boxplot_wrapper( - self, func, *args, - color='k', fill=True, fillcolor=None, fillalpha=0.7, - lw=None, linewidth=0.7, orientation=None, - marker=None, markersize=None, - boxcolor=None, boxlw=None, - capcolor=None, caplw=None, - meancolor=None, meanlw=None, - mediancolor=None, medianlw=None, - whiskercolor=None, whiskerlw=None, - fliercolor=None, flierlw=None, - **kwargs -): - """ - Adds convenient keyword arguments and changes the default boxplot style. - Wraps %(methods)s. - - Parameters - ---------- - *args : 1D or 2D ndarray - The data array. - color : color-spec, optional - The color of all objects. - fill : bool, optional - Whether to fill the box with a color. - fillcolor : color-spec, optional - The fill color for the boxes. Default is the next color cycler color. - fillalpha : float, optional - The opacity of the boxes. Default is ``1``. - lw, linewidth : float, optional - The linewidth of all objects. - orientation : {None, 'horizontal', 'vertical'}, optional - Alternative to the native `vert` keyword arg. Controls orientation. - marker : marker-spec, optional - Marker style for the 'fliers', i.e. outliers. - markersize : float, optional - Marker size for the 'fliers', i.e. outliers. - boxcolor, capcolor, meancolor, mediancolor, whiskercolor : \ -color-spec, optional - The color of various boxplot components. These are shorthands so you - don't have to pass e.g. a ``boxprops`` dictionary. - boxlw, caplw, meanlw, medianlw, whiskerlw : float, optional - The line width of various boxplot components. These are shorthands so - you don't have to pass e.g. a ``boxprops`` dictionary. - """ - # Call function - if len(args) > 2: - raise ValueError(f'Expected 1-2 positional args, got {len(args)}.') - if orientation is not None: - if orientation == 'horizontal': - kwargs['vert'] = False - elif orientation != 'vertical': - raise ValueError( - 'Orientation must be "horizontal" or "vertical", ' - f'got {orientation!r}.' - ) - obj = func(self, *args, **kwargs) - if not args: - return obj - - # Modify results - # TODO: Pass props keyword args instead? Maybe does not matter. - lw = _notNone(lw, linewidth, None, names=('lw', 'linewidth')) - if fillcolor is None: - cycler = next(self._get_lines.prop_cycler) - fillcolor = cycler.get('color', None) - for key, icolor, ilw in ( - ('boxes', boxcolor, boxlw), - ('caps', capcolor, caplw), - ('whiskers', whiskercolor, whiskerlw), - ('means', meancolor, meanlw), - ('medians', mediancolor, medianlw), - ('fliers', fliercolor, flierlw), - ): - if key not in obj: # possible if not rendered - continue - artists = obj[key] - ilw = _notNone(ilw, lw) - icolor = _notNone(icolor, color) - for artist in artists: - if icolor is not None: - artist.set_color(icolor) - artist.set_markeredgecolor(icolor) - if ilw is not None: - artist.set_linewidth(ilw) - artist.set_markeredgewidth(ilw) - if key == 'boxes' and fill: - patch = mpatches.PathPatch( - artist.get_path(), color=fillcolor, - alpha=fillalpha, linewidth=0) - self.add_artist(patch) - if key == 'fliers': - if marker is not None: - artist.set_marker(marker) - if markersize is not None: - artist.set_markersize(markersize) - return obj - - -def violinplot_wrapper( - self, func, *args, - lw=None, linewidth=0.7, fillcolor=None, edgecolor='k', - fillalpha=0.7, orientation=None, - **kwargs -): - """ - Adds convenient keyword arguments and changes the default violinplot style - to match `this matplotlib example \ -`__. - It is also no longer possible to show minima and maxima with whiskers -- - while this is useful for `~matplotlib.axes.Axes.boxplot`\\ s it is - redundant for `~matplotlib.axes.Axes.violinplot`\\ s. Wraps %(methods)s. - - Parameters - ---------- - *args : 1D or 2D ndarray - The data array. - lw, linewidth : float, optional - The linewidth of the line objects. Default is ``1``. - edgecolor : color-spec, optional - The edge color for the violin patches. Default is ``'k'``. - fillcolor : color-spec, optional - The violin plot fill color. Default is the next color cycler color. - fillalpha : float, optional - The opacity of the violins. Default is ``1``. - orientation : {None, 'horizontal', 'vertical'}, optional - Alternative to the native `vert` keyword arg. Controls orientation. - boxrange, barrange : (float, float), optional - Percentile ranges for the thick and thin central bars. The defaults - are ``(25, 75)`` and ``(5, 95)``, respectively. - """ - # Orientation and checks - if len(args) > 2: - raise ValueError(f'Expected 1-2 positional args, got {len(args)}.') - if orientation is not None: - if orientation == 'horizontal': - kwargs['vert'] = False - elif orientation != 'vertical': - raise ValueError( - 'Orientation must be "horizontal" or "vertical", ' - f'got {orientation!r}.' - ) - - # Sanitize input - lw = _notNone(lw, linewidth, None, names=('lw', 'linewidth')) - if kwargs.pop('showextrema', None): - _warn_proplot(f'Ignoring showextrema=True.') - if 'showmeans' in kwargs: - kwargs.setdefault('means', kwargs.pop('showmeans')) - if 'showmedians' in kwargs: - kwargs.setdefault('medians', kwargs.pop('showmedians')) - kwargs.setdefault('capsize', 0) - obj = func( - self, *args, - showmeans=False, showmedians=False, showextrema=False, - edgecolor=edgecolor, lw=lw, **kwargs - ) - if not args: - return obj - - # Modify body settings - for artist in obj['bodies']: - artist.set_alpha(fillalpha) - artist.set_edgecolor(edgecolor) - artist.set_linewidths(lw) - if fillcolor is not None: - artist.set_facecolor(fillcolor) - return obj - - -def _get_transform(self, transform): - """ - Translates user input transform. Also used in an axes method. - """ - try: - from cartopy.crs import CRS - except ModuleNotFoundError: - CRS = None - cartopy = (getattr(self, 'name', '') == 'geo') - if (isinstance(transform, mtransforms.Transform) - or CRS and isinstance(transform, CRS)): - return transform - elif transform == 'figure': - return self.figure.transFigure - elif transform == 'axes': - return self.transAxes - elif transform == 'data': - return PlateCarree() if cartopy else self.transData - elif cartopy and transform == 'map': - return self.transData - else: - raise ValueError(f'Unknown transform {transform!r}.') - - -def _update_text(self, props): - """ - Monkey patch that adds pseudo "border" properties to text objects - without wrapping the entire class. We override update to facilitate - updating inset titles. - """ - props = props.copy() # shallow copy - border = props.pop('border', None) - bordercolor = props.pop('bordercolor', 'w') - borderinvert = props.pop('borderinvert', False) - borderwidth = props.pop('borderwidth', 2) - if border: - facecolor, bgcolor = self.get_color(), bordercolor - if borderinvert: - facecolor, bgcolor = bgcolor, facecolor - kwargs = { - 'linewidth': borderwidth, - 'foreground': bgcolor, - 'joinstyle': 'miter' - } - self.update({ - 'color': facecolor, - 'path_effects': - [mpatheffects.Stroke(**kwargs), mpatheffects.Normal()] - }) - return type(self).update(self, props) - - -def text_wrapper( - self, func, - x=0, y=0, text='', transform='data', - fontfamily=None, fontname=None, fontsize=None, size=None, - border=False, bordercolor='w', borderwidth=2, borderinvert=False, - **kwargs -): - """ - Wraps %(methods)s, and enables specifying `tranform` with a string name and - adds feature for drawing borders around text. - - Parameters - ---------- - x, y : float - The *x* and *y* coordinates for the text. - text : str - The text string. - transform : {'data', 'axes', 'figure'} or \ -`~matplotlib.transforms.Transform`, optional - The transform used to interpret `x` and `y`. Can be a - `~matplotlib.transforms.Transform` object or a string representing the - `~matplotlib.axes.Axes.transData`, `~matplotlib.axes.Axes.transAxes`, - or `~matplotlib.figure.Figure.transFigure` transforms. Default is - ``'data'``, i.e. the text is positioned in data coordinates. - size, fontsize : float or str, optional - The font size. If float, units are inches. If string, units are - interpreted by `~proplot.utils.units`. - fontname, fontfamily : str, optional - Aliases for the ``fontfamily`` `~matplotlib.text.Text` property. - border : bool, optional - Whether to draw border around text. - borderwidth : float, optional - The width of the text border. Default is ``2`` points. - bordercolor : color-spec, optional - The color of the text border. Default is ``'w'``. - borderinvert : bool, optional - If ``True``, the text and border colors are swapped. - - Other parameters - ---------------- - **kwargs - Passed to `~matplotlib.text.Text` instantiator. - """ - # Default transform by string name - if not transform: - transform = self.transData - else: - transform = _get_transform(self, transform) - - # Helpful warning if invalid font is specified - fontname = _notNone( - fontfamily, fontname, None, names=('fontfamily', 'fontname') - ) - if fontname is not None: - if ( - not isinstance(fontname, str) - and np.iterable(fontname) - and len(fontname) == 1 - ): - fontname = fontname[0] - if fontname.lower() in list(map(str.lower, styletools.fonts)): - kwargs['fontfamily'] = fontname - else: - _warn_proplot( - f'Font {fontname!r} unavailable. Available fonts are ' - + ', '.join(map(repr, styletools.fonts)) + '.' - ) - - # Units support for font sizes - # TODO: Document this feature - # TODO: Why only support this here, and not in arbitrary places throughout - # rest of matplotlib API? Units engine needs better implementation. - size = _notNone(size, fontsize, None, names=('size', 'fontsize')) - if size is not None: - kwargs['fontsize'] = units(size, 'pt') - obj = func(self, x, y, text, transform=transform, **kwargs) - obj.update = _update_text.__get__(obj) - obj.update({ - 'border': border, - 'bordercolor': bordercolor, - 'borderinvert': borderinvert, - 'borderwidth': borderwidth, - }) - return obj - - -def cycle_changer( - self, func, *args, - cycle=None, cycle_kw=None, - label=None, labels=None, values=None, - legend=None, legend_kw=None, - colorbar=None, colorbar_kw=None, - **kwargs -): - """ - Wraps methods that use the property cycler (%(methods)s), - adds features for controlling colors in the property cycler and drawing - legends or colorbars in one go. - - This wrapper also *standardizes acceptable input* -- these methods now all - accept 2D arrays holding columns of data, and *x*-coordinates are always - optional. Note this alters the behavior of `~matplotlib.axes.Axes.boxplot` - and `~matplotlib.axes.Axes.violinplot`, which now compile statistics on - *columns* of data instead of *rows*. - - Parameters - ---------- - cycle : cycle-spec, optional - The cycle specifer, passed to the `~proplot.styletools.Cycle` - constructor. If the returned list of colors is unchanged from the - current axes color cycler, the axes cycle will **not** be reset to the - first position. - cycle_kw : dict-like, optional - Passed to `~proplot.styletools.Cycle`. - label : float or str, optional - The legend label to be used for this plotted element. - labels, values : list of float or list of str, optional - Used with 2D input arrays. The legend labels or colorbar coordinates - for each column in the array. Can be numeric or string, and must match - the number of columns in the 2D array. - legend : bool, int, or str, optional - If not ``None``, this is a location specifying where to draw an *inset* - or *panel* legend from the resulting handle(s). If ``True``, the - default location is used. Valid locations are described in - `~proplot.axes.Axes.legend`. - legend_kw : dict-like, optional - Ignored if `legend` is ``None``. Extra keyword args for our call - to `~proplot.axes.Axes.legend`. - colorbar : bool, int, or str, optional - If not ``None``, this is a location specifying where to draw an *inset* - or *panel* colorbar from the resulting handle(s). If ``True``, the - default location is used. Valid locations are described in - `~proplot.axes.Axes.colorbar`. - colorbar_kw : dict-like, optional - Ignored if `colorbar` is ``None``. Extra keyword args for our call - to `~proplot.axes.Axes.colorbar`. - - Other parameters - ---------------- - *args, **kwargs - Passed to the matplotlib plotting method. - - See also - -------- - `~proplot.styletools.Cycle`, `~proplot.styletools.colors` - - Notes - ----- - See the `matplotlib source \ -`_. - The `set_prop_cycle` command modifies underlying - `_get_lines` and `_get_patches_for_fill`. - """ - # No mutable defaults - cycle_kw = cycle_kw or {} - legend_kw = legend_kw or {} - colorbar_kw = colorbar_kw or {} - - # Test input - # NOTE: Requires standardize_1d wrapper before reaching this. Also note - # that the 'x' coordinates are sometimes ignored below. - name = func.__name__ - if not args: - return func(self, *args, **kwargs) - barh = (name == 'bar' and kwargs.get('orientation', None) == 'horizontal') - x, y, *args = args - if len(args) >= 1 and 'fill_between' in name: - ys = (y, args[0]) - args = args[1:] - else: - ys = (y,) - is1d = (y.ndim == 1) - - # Determine and temporarily set cycler - # NOTE: Axes cycle has no getter, only set_prop_cycle, which sets a - # prop_cycler attribute on the hidden _get_lines and _get_patches_for_fill - # objects. This is the only way to query current axes cycler! Should not - # wrap set_prop_cycle because would get messy and fragile. - # NOTE: The _get_lines cycler is an *itertools cycler*. Has no length, so - # we must cycle over it with next(). We try calling next() the same number - # of times as the length of user input cycle. If the input cycle *is* in - # fact the same, below does not reset the color position, cycles us to - # start! - if cycle is not None or cycle_kw: - # Get the new cycler - cycle_args = () if cycle is None else (cycle,) - if not is1d and y.shape[1] > 1: # default samples count - cycle_kw.setdefault('N', y.shape[1]) - cycle = styletools.Cycle(*cycle_args, **cycle_kw) - # Get the original property cycle - # NOTE: Matplotlib saves itertools.cycle(cycler), not the original - # cycler object, so we must build up the keys again. - i = 0 - by_key = {} - cycle_orig = self._get_lines.prop_cycler - for i in range(len(cycle)): # use the cycler object length as a guess - prop = next(cycle_orig) - for key, value in prop.items(): - if key not in by_key: - by_key[key] = {*()} # set - if isinstance(value, (list, np.ndarray)): - value = tuple(value) - by_key[key].add(value) - # Reset property cycler if it differs - reset = ({*by_key} != {*cycle.by_key()}) # reset if keys are different - if not reset: # test individual entries - for key, value in cycle.by_key().items(): - if by_key[key] != {*value}: - reset = True - break - if reset: - self.set_prop_cycle(cycle) - - # Custom property cycler additions - # NOTE: By default matplotlib uses _get_patches_for_fill.get_next_color - # for scatter properties! So we simultaneously iterate through the - # _get_lines property cycler and apply them. - apply = {*()} # which keys to apply from property cycler - if name == 'scatter': - # Figure out which props should be updated - keys = {*self._get_lines._prop_keys} - {'color', 'linestyle', 'dashes'} - for key, prop in ( - ('markersize', 's'), - ('linewidth', 'linewidths'), - ('markeredgewidth', 'linewidths'), - ('markeredgecolor', 'edgecolors'), - ('alpha', 'alpha'), - ('marker', 'marker'), - ): - prop = kwargs.get(prop, None) - if key in keys and prop is None: - apply.add(key) - - # Plot susccessive columns - # WARNING: Most methods that accept 2D arrays use columns of data, but when - # pandas DataFrame passed to hist, boxplot, or violinplot, rows of data - # assumed! This is fixed in parse_1d by converting to values. - objs = [] - ncols = 1 - label_leg = None # for colorbar or legend - labels = _notNone(values, labels, label, None, - names=('values', 'labels', 'label')) - stacked = kwargs.pop('stacked', False) - if name in ('pie', 'boxplot', 'violinplot'): - if labels is not None: - kwargs['labels'] = labels - else: - ncols = (1 if is1d else y.shape[1]) - if labels is None or isinstance(labels, str): - labels = [labels] * ncols - if name in ('bar',): - # for bar plots; 0.8 is matplotlib default - width = kwargs.pop('width', 0.8) - kwargs['height' if barh else 'width'] = ( - width if stacked else width / ncols) - for i in range(ncols): - # Prop cycle properties - kw = {**kwargs} # copy - if apply: - props = next(self._get_lines.prop_cycler) - for key in apply: - value = props[key] - if key in ('size', 'markersize'): - key = 's' - elif key in ('linewidth', 'markeredgewidth'): # translate - key = 'linewidths' - elif key == 'markeredgecolor': - key = 'edgecolors' - kw[key] = value - # Get x coordinates - ix, iy = x, ys[0] # samples - if name in ('pie',): - kw['labels'] = _notNone(labels, ix) # TODO: move to pie wrapper? - if name in ('bar',): # adjust - if not stacked: - ix = x + (i - ncols / 2 + 0.5) * width / ncols - elif stacked and not is1d: - key = 'x' if barh else 'bottom' - # sum of empty slice will be zero - kw[key] = _to_iloc(iy)[:, :i].sum(axis=1) - # Get y coordinates and labels - if name in ('pie', 'boxplot', 'violinplot'): - iys = (iy,) # only ever have one y value, cannot have legend labs - else: - # The coordinates - if stacked and 'fill_between' in name: - iys = tuple(iy if is1d else _to_iloc( - iy)[:, :j].sum(axis=1) for j in (i, i + 1)) - else: - iys = tuple(iy if is1d else _to_iloc(iy)[:, i] for iy in ys) - # Possible legend labels - if len(labels) != ncols: - raise ValueError( - f'Got {ncols} columns in data array, ' - f'but {len(labels)} labels.' - ) - label = labels[i] - values, label_leg = _standard_label(iy, axis=1) - if label_leg and label is None: - label = _to_array(values)[i] - if label is not None: - kw['label'] = label - # Call with correct args - xy = () - if barh: # special, use kwargs only! - kw.update({'bottom': ix, 'width': iys[0]}) - # must always be provided - kw.setdefault('x', kwargs.get('bottom', 0)) - elif name in ('pie', 'hist', 'boxplot', 'violinplot'): - xy = (*iys,) - else: # has x-coordinates, and maybe more than one y - xy = (ix, *iys) - obj = func(self, *xy, *args, **kw) - # plot always returns list or tuple - if isinstance(obj, (list, tuple)) and len(obj) == 1: - obj = obj[0] - objs.append(obj) - - # Add colorbar and/or legend - if colorbar: - # Add handles - loc = self._loc_translate(colorbar, 'colorbar', allow_manual=False) - if loc not in self._auto_colorbar: - self._auto_colorbar[loc] = ([], {}) - self._auto_colorbar[loc][0].extend(objs) - # Add keywords - if loc != 'fill': - colorbar_kw.setdefault('loc', loc) - if label_leg: - colorbar_kw.setdefault('label', label_leg) - self._auto_colorbar[loc][1].update(colorbar_kw) - if legend: - # Add handles - loc = self._loc_translate(legend, 'legend', allow_manual=False) - if loc not in self._auto_legend: - self._auto_legend[loc] = ([], {}) - self._auto_legend[loc][0].extend(objs) - # Add keywords - if loc != 'fill': - legend_kw.setdefault('loc', loc) - if label_leg: - legend_kw.setdefault('label', label_leg) - self._auto_legend[loc][1].update(legend_kw) - - # Return - # WARNING: Make sure plot always returns tuple of objects, and bar always - # returns singleton unless we have bulk drawn bar plots! Other matplotlib - # methods call these internally! - if name == 'plot': - return (*objs,) # always return tuple of objects - elif name in ('boxplot', 'violinplot'): - # always singleton, because these methods accept the whole 2D object - return objs[0] - else: - return objs[0] if is1d else (*objs,) # sensible default behavior - - -def cmap_changer( - self, func, *args, cmap=None, cmap_kw=None, - extend='neither', norm=None, norm_kw=None, - N=None, levels=None, values=None, centers=None, vmin=None, vmax=None, - locator=None, symmetric=False, locator_kw=None, - edgefix=None, labels=False, labels_kw=None, fmt=None, precision=2, - colorbar=False, colorbar_kw=None, - lw=None, linewidth=None, linewidths=None, - ls=None, linestyle=None, linestyles=None, - color=None, colors=None, edgecolor=None, edgecolors=None, - **kwargs -): - """ - Wraps methods that take a `cmap` argument (%(methods)s), - adds several new keyword args and features. - Uses the `~proplot.styletools.BinNorm` normalizer to bin data into - discrete color levels (see notes). - - Parameters - ---------- - cmap : colormap spec, optional - The colormap specifer, passed to the `~proplot.styletools.Colormap` - constructor. - cmap_kw : dict-like, optional - Passed to `~proplot.styletools.Colormap`. - norm : normalizer spec, optional - The colormap normalizer, used to warp data before passing it - to `~proplot.styletools.BinNorm`. This is passed to the - `~proplot.styletools.Norm` constructor. - norm_kw : dict-like, optional - Passed to `~proplot.styletools.Norm`. - extend : {'neither', 'min', 'max', 'both'}, optional - Where to assign unique colors to out-of-bounds data and draw - "extensions" (triangles, by default) on the colorbar. - levels, N : int or list of float, optional - The number of level edges, or a list of level edges. If the former, - `locator` is used to generate this many levels at "nice" intervals. - Default is :rc:`image.levels`. - - Since this function also wraps `~matplotlib.axes.Axes.pcolor` and - `~matplotlib.axes.Axes.pcolormesh`, this means they now - accept the `levels` keyword arg. You can now discretize your - colors in a ``pcolor`` plot just like with ``contourf``. - values, centers : int or list of float, optional - The number of level centers, or a list of level centers. If provided, - levels are inferred using `~proplot.utils.edges`. This will override - any `levels` input. - vmin, vmax : float, optional - Used to determine level locations if `levels` is an integer. Actual - levels may not fall exactly on `vmin` and `vmax`, but the minimum - level will be no smaller than `vmin` and the maximum level will be - no larger than `vmax`. - - If `vmin` or `vmax` is not provided, the minimum and maximum data - values are used. - locator : locator-spec, optional - The locator used to determine level locations if `levels` or `values` - is an integer and `vmin` and `vmax` were not provided. Passed to the - `~proplot.axistools.Locator` constructor. Default is - `~matplotlib.ticker.MaxNLocator` with ``levels`` or ``values+1`` - integer levels. - locator_kw : dict-like, optional - Passed to `~proplot.axistools.Locator`. - symmetric : bool, optional - Toggle this to make automatically generated levels symmetric - about zero. - edgefix : bool, optional - Whether to fix the the `white-lines-between-filled-contours \ -`__ - and `white-lines-between-pcolor-rectangles \ -`__ - issues. This slows down figure rendering by a bit. Default is - :rc:`image.edgefix`. - labels : bool, optional - For `~matplotlib.axes.Axes.contour`, whether to add contour labels - with `~matplotlib.axes.Axes.clabel`. For `~matplotlib.axes.Axes.pcolor` - or `~matplotlib.axes.Axes.pcolormesh`, whether to add labels to the - center of grid boxes. In the latter case, the text will be black - when the luminance of the underlying grid box color is >50%%, and - white otherwise (see the `~proplot.styletools` documentation). - labels_kw : dict-like, optional - Ignored if `labels` is ``False``. Extra keyword args for the labels. - For `~matplotlib.axes.Axes.contour`, passed to - `~matplotlib.axes.Axes.clabel`. For `~matplotlib.axes.Axes.pcolor` - or `~matplotlib.axes.Axes.pcolormesh`, passed to - `~matplotlib.axes.Axes.text`. - fmt : format-spec, optional - Passed to the `~proplot.styletools.Norm` constructor, used to format - number labels. You can also use the `precision` keyword arg. - precision : int, optional - Maximum number of decimal places for the number labels. - Number labels are generated with the - `~proplot.axistools.SimpleFormatter` formatter, which allows us to - limit the precision. - colorbar : bool, int, or str, optional - If not ``None``, this is a location specifying where to draw an *inset* - or *panel* colorbar from the resulting mappable. If ``True``, the - default location is used. Valid locations are described in - `~proplot.axes.Axes.colorbar`. - colorbar_kw : dict-like, optional - Ignored if `colorbar` is ``None``. Extra keyword args for our call - to `~proplot.axes.Axes.colorbar`. - - Other parameters - ---------------- - lw, linewidth, linewidths - The width of `~matplotlib.axes.Axes.contour` lines and - `~proplot.axes.Axes.parametric` lines. Also the width of lines - *between* `~matplotlib.axes.Axes.pcolor` boxes, - `~matplotlib.axes.Axes.pcolormesh` boxes, and - `~matplotlib.axes.Axes.contourf` filled contours. - ls, linestyle, linestyles - As above, but for the line style. - color, colors, edgecolor, edgecolors - As above, but for the line color. - *args, **kwargs - Passed to the matplotlib plotting method. - - Notes - ----- - The `~proplot.styletools.BinNorm` normalizer, used with all colormap - plots, makes sure that your "levels" always span the full range of colors - in the colormap, whether you are extending max, min, neither, or both. By - default, when you select `extend` not ``'both'``, matplotlib seems to just - cut off the most intense colors (reserved for coloring "out of bounds" - data), even though they are not being used. - - This could also be done by limiting the number of colors in the colormap - lookup table by selecting a smaller ``N`` (see - `~matplotlib.colors.LinearSegmentedColormap`). But I prefer the approach - of always building colormaps with hi-res lookup tables, and leaving the job - of normalizing data values to colormap locations to the - `~matplotlib.colors.Normalize` object. - - See also - -------- - `~proplot.styletools.Colormap`, `~proplot.styletools.Norm`, - `~proplot.styletools.BinNorm` - """ - # No mutable defaults - cmap_kw = cmap_kw or {} - norm_kw = norm_kw or {} - locator_kw = locator_kw or {} - labels_kw = labels_kw or {} - colorbar_kw = colorbar_kw or {} - - # Parse args - # Disable edgefix=True for certain keyword combos e.g. if user wants - # white lines around their pcolor mesh. - name = func.__name__ - if not args: - return func(self, *args, **kwargs) - vmin = _notNone( - vmin, norm_kw.pop('vmin', None), None, - names=('vmin', 'norm_kw={"vmin":value}')) - vmax = _notNone( - vmax, norm_kw.pop('vmax', None), None, - names=('vmax', 'norm_kw={"vmax":value}')) - levels = _notNone( - N, levels, norm_kw.pop('levels', None), rc['image.levels'], - names=('N', 'levels', 'norm_kw={"levels":value}')) - values = _notNone( - values, centers, None, - names=('values', 'centers')) - colors = _notNone( - color, colors, edgecolor, edgecolors, None, - names=('color', 'colors', 'edgecolor', 'edgecolors')) - linewidths = _notNone( - lw, linewidth, linewidths, None, - names=('lw', 'linewidth', 'linewidths')) - linestyles = _notNone( - ls, linestyle, linestyles, None, - names=('ls', 'linestyle', 'linestyles')) - style_kw = STYLE_ARGS_TRANSLATE.get(name, {}) - edgefix = _notNone(edgefix, rc['image.edgefix']) - for key, value in ( - ('colors', colors), - ('linewidths', linewidths), - ('linestyles', linestyles)): - if value is None: - continue - elif 'contourf' in name: # special case, we re-draw our own contours - continue - if key in style_kw: - edgefix = False # override! - kwargs[style_kw[key]] = value - else: - raise ValueError( - f'Unknown keyword arg {key!r} for function {name!r}.' - ) - # Check input - for key, val in (('levels', levels), ('values', values)): - if not np.iterable(val): - continue - if 'contour' in name and 'contourf' not in name: - continue - if len(val) < 2 or any( - np.sign(np.diff(val)) != np.sign(val[1] - val[0]) - ): - raise ValueError( - f'{key!r} must be monotonically increasing or decreasing and ' - f'at least length 2, got {val}.' - ) - - # Get level edges from level centers - if values is not None: - if isinstance(values, Number): - levels = values + 1 - elif np.iterable(values): - # Try to generate levels such that a LinearSegmentedNorm will - # place values ticks at the center of each colorbar level. - # utile.edges works only for evenly spaced values arrays. - # We solve for: (x1 + x2)/2 = y --> x2 = 2*y - x1 - # with arbitrary starting point x1. - if norm is None or norm in ('segments', 'segmented'): - levels = [values[0] - (values[1] - values[0]) / 2] - for i, val in enumerate(values): - levels.append(2 * val - levels[-1]) - if any(np.diff(levels) <= 0): # algorithm failed - levels = edges(values) - # Generate levels by finding in-between points in the - # normalized numeric space - else: - inorm = styletools.Norm(norm, **norm_kw) - levels = inorm.inverse(edges(inorm(values))) - if name in ('parametric',): - kwargs['values'] = values - else: - raise ValueError( - f'Unexpected input values={values!r}. ' - 'Must be integer or list of numbers.' - ) - - # Input colormap, for methods that accept a colormap and normalizer - # contour, tricontour, i.e. not a method where cmap is optional - if not ('contour' in name and 'contourf' not in name): - cmap = _notNone(cmap, rc['image.cmap']) - if cmap is not None: - # Get colormap object - cmap = styletools.Colormap(cmap, **cmap_kw) - cyclic = getattr(cmap, '_cyclic', False) - if cyclic and extend != 'neither': - _warn_proplot( - f'Cyclic colormap requires extend="neither". ' - 'Overriding user input extend={extend!r}.' - ) - extend = 'neither' - kwargs['cmap'] = cmap - - # Get default normalizer - # Only use LinearSegmentedNorm if necessary, because it is slow - if name not in ('hexbin',): - if norm is None: - if not np.iterable(levels) or len(levels) == 1: - norm = 'linear' - else: - diff = np.abs(np.diff(levels)) # permit descending - eps = diff.mean() / 1e3 - if (np.abs(np.diff(diff)) >= eps).any(): - norm = 'segmented' - norm_kw.setdefault('levels', levels) - else: - norm = 'linear' - elif norm in ('segments', 'segmented'): - norm_kw.setdefault('levels', levels) - norm = styletools.Norm(norm, **norm_kw) - - # Get default levels - # TODO: Add kernel density plot to hexbin! - if isinstance(levels, Number): - # Cannot infer counts a priori, so do nothing - if name in ('hexbin',): - levels = None - # Use the locator to determine levels - # Mostly copied from the hidden contour.ContourSet._autolev - else: - # Get the locator - N = levels - if locator is not None: - locator = axistools.Locator(locator, **locator_kw) - elif isinstance(norm, mcolors.LogNorm): - locator = mticker.LogLocator(**locator_kw) - elif isinstance(norm, getattr(mcolors, 'SymLogNorm', type(None))): - locator = mticker.SymmetricalLogLocator(**locator_kw) - else: - locator_kw.setdefault('symmetric', symmetric) - locator = mticker.MaxNLocator(N, min_n_ticks=1, **locator_kw) - # Get locations - automin = (vmin is None) - automax = (vmax is None) - if automin or automax: - Z = ma.masked_invalid(args[-1], copy=False) - if automin: - vmin = float(Z.min()) - if automax: - vmax = float(Z.max()) - if vmin == vmax or ma.is_masked(vmin) or ma.is_masked(vmax): - vmin, vmax = 0, 1 - try: - levels = locator.tick_values(vmin, vmax) - except RuntimeError: - levels = np.linspace(vmin, vmax, N) # TODO: orig used N+1 - # Trim excess levels the locator may have supplied - if not locator_kw.get('symmetric', None): - i0, i1 = 0, len(levels) # defaults - under, = np.where(levels < vmin) - if len(under): - i0 = under[-1] - if not automin or extend in ('min', 'both'): - i0 += 1 # permit out-of-bounds data - over, = np.where(levels > vmax) - if len(over): - i1 = over[0] + 1 if len(over) else len(levels) - if not automax or extend in ('max', 'both'): - i1 -= 1 # permit out-of-bounds data - if i1 - i0 < 3: - i0, i1 = 0, len(levels) # revert - levels = levels[i0:i1] - # Special consideration if not enough levels - # how many times more levels did we want than what we got? - nn = N // len(levels) - if nn >= 2: - olevels = norm(levels) - nlevels = [] - for i in range(len(levels) - 1): - l1, l2 = olevels[i], olevels[i + 1] - nlevels.extend(np.linspace(l1, l2, nn + 1)[:-1]) - nlevels.append(olevels[-1]) - levels = norm.inverse(nlevels) - - # Generate BinNorm and update "child" norm with vmin and vmax from levels - # This is important for the colorbar setting tick locations properly! - if norm is not None: - if not isinstance(norm, mcolors.BoundaryNorm): - if levels is not None: - norm.vmin, norm.vmax = min(levels), max(levels) - if levels is not None: - bin_kw = {'extend': extend} - if cyclic: - bin_kw.update({'step': 0.5, 'extend': 'both'}) - norm = styletools.BinNorm(norm=norm, levels=levels, **bin_kw) - kwargs['norm'] = norm - - # Call function - if 'contour' in name: # contour, contourf, tricontour, tricontourf - kwargs.update({'levels': levels, 'extend': extend}) - obj = func(self, *args, **kwargs) - obj.extend = extend # for colorbar to determine 'extend' property - if values is not None: - obj.values = values # preferred tick locations - if levels is not None: - obj.levels = levels # for colorbar to determine tick locations - if locator is not None and not isinstance(locator, mticker.MaxNLocator): - obj.locator = locator # for colorbar to determine tick locations - - # Call again to add "edges" to contourf plots - if 'contourf' in name and any( - _ is not None for _ in (colors, linewidths, linestyles) - ): - colors = _notNone(colors, 'k') - self.contour( - *args, levels=levels, linewidths=linewidths, - linestyles=linestyles, colors=colors - ) - - # Apply labels - # TODO: Add quiverkey to this! - if labels: - # Formatting for labels - # Respect if 'fmt' was passed in labels_kw instead of as a main - # argument - fmt = _notNone(labels_kw.pop('fmt', None), fmt, 'simple') - fmt = axistools.Formatter(fmt, precision=precision) - # Use clabel method - if 'contour' in name: - if 'contourf' in name: - lums = [styletools.to_xyz(cmap(norm(level)), 'hcl')[ - 2] for level in levels] - colors = ['w' if lum < 50 else 'k' for lum in lums] - cobj = self.contour(*args, levels=levels, linewidths=0) - else: - cobj = obj - colors = None - text_kw = {} - for key in (*labels_kw,): # allow dict to change size - if key not in ( - 'levels', 'fontsize', 'colors', 'inline', 'inline_spacing', - 'manual', 'rightside_up', 'use_clabeltext', - ): - text_kw[key] = labels_kw.pop(key) - labels_kw.setdefault('colors', colors) - labels_kw.setdefault('inline_spacing', 3) - labels_kw.setdefault('fontsize', rc['small']) - labs = self.clabel(cobj, fmt=fmt, **labels_kw) - for lab in labs: - lab.update(text_kw) - # Label each box manually - # See: https://stackoverflow.com/a/20998634/4970632 - elif 'pcolor' in name: - # populates the _facecolors attribute, initially filled with just a - # single color - obj.update_scalarmappable() - labels_kw_ = {'size': rc['small'], 'ha': 'center', 'va': 'center'} - labels_kw_.update(labels_kw) - array = obj.get_array() - paths = obj.get_paths() - colors = np.asarray(obj.get_facecolors()) - edgecolors = np.asarray(obj.get_edgecolors()) - if len(colors) == 1: # weird flex but okay - colors = np.repeat(colors, len(array), axis=0) - if len(edgecolors) == 1: - edgecolors = np.repeat(edgecolors, len(array), axis=0) - for i, (color, path, num) in enumerate(zip(colors, paths, array)): - if not np.isfinite(num): - edgecolors[i, :] = 0 - continue - bbox = path.get_extents() - x = (bbox.xmin + bbox.xmax) / 2 - y = (bbox.ymin + bbox.ymax) / 2 - if 'color' not in labels_kw: - _, _, lum = styletools.to_xyz(color, 'hcl') - if lum < 50: - color = 'w' - else: - color = 'k' - labels_kw_['color'] = color - self.text(x, y, fmt(num), **labels_kw_) - obj.set_edgecolors(edgecolors) - else: - raise RuntimeError(f'Not possible to add labels to {name!r} plot.') - - # Fix white lines between filled contours/mesh, allow user to override! - # 0.4 points is thick enough to hide lines but thin enough to not - # add "dots" in corner of pcolor plots - # *Never* use this when colormap has opacity - # See: https://stackoverflow.com/q/15003353/4970632 - if 'pcolor' in name or 'contourf' in name: - cmap = obj.get_cmap() - if not cmap._isinit: - cmap._init() - if edgefix and all(cmap._lut[:-1, 3] == 1): - if 'pcolor' in name: # 'pcolor', 'pcolormesh', 'tripcolor' - obj.set_edgecolor('face') - obj.set_linewidth(0.4) - elif 'contourf' in name: # 'contourf', 'tricontourf' - for contour in obj.collections: - contour.set_edgecolor('face') - contour.set_linewidth(0.4) - contour.set_linestyle('-') - - # Add colorbar - if colorbar: - loc = self._loc_translate(colorbar, 'colorbar', allow_manual=False) - if 'label' not in colorbar_kw and self.figure._auto_format: - _, label = _standard_label(args[-1]) # last one is data, we assume - if label: - colorbar_kw.setdefault('label', label) - if name in ('parametric',) and values is not None: - colorbar_kw.setdefault('values', values) - if loc != 'fill': - colorbar_kw.setdefault('loc', loc) - self.colorbar(obj, **colorbar_kw) - return obj - - -def legend_wrapper( - self, handles=None, labels=None, ncol=None, ncols=None, - center=None, order='C', loc=None, label=None, title=None, - fontsize=None, fontweight=None, fontcolor=None, - color=None, marker=None, lw=None, linewidth=None, - dashes=None, linestyle=None, markersize=None, frameon=None, frame=None, - **kwargs -): - """ - Wraps `~proplot.axes.Axes` `~proplot.axes.Axes.legend` and - `~proplot.subplots.Figure` `~proplot.subplots.Figure.legend`, adds some - handy features. - - Parameters - ---------- - handles : list of `~matplotlib.artist.Artist`, optional - List of artists instances, or list of lists of artist instances (see - the `center` keyword). If ``None``, the artists are retrieved with - `~matplotlib.axes.Axes.get_legend_handles_labels`. - labels : list of str, optional - Matching list of string labels, or list of lists of string labels (see - the `center` keywod). If ``None``, the labels are retrieved by calling - `~matplotlib.artist.Artist.get_label` on each - `~matplotlib.artist.Artist` in `handles`. - ncol, ncols : int, optional - The number of columns. `ncols` is an alias, added - for consistency with `~matplotlib.pyplot.subplots`. - order : {'C', 'F'}, optional - Whether legend handles are drawn in row-major (``'C'``) or column-major - (``'F'``) order. Analagous to `numpy.array` ordering. For some reason - ``'F'`` was the original matplotlib default. Default is ``'C'``. - center : bool, optional - Whether to center each legend row individually. If ``True``, we - actually draw successive single-row legends stacked on top of each - other. - - If ``None``, we infer this setting from `handles`. Default is ``True`` - if `handles` is a list of lists; each sublist is used as a *row* - in the legend. Otherwise, default is ``False``. - loc : int or str, optional - The legend location. The following location keys are valid: - - ================== ================================================ - Location Valid keys - ================== ================================================ - "best" possible ``0``, ``'best'``, ``'b'``, ``'i'``, ``'inset'`` - upper right ``1``, ``'upper right'``, ``'ur'`` - upper left ``2``, ``'upper left'``, ``'ul'`` - lower left ``3``, ``'lower left'``, ``'ll'`` - lower right ``4``, ``'lower right'``, ``'lr'`` - center left ``5``, ``'center left'``, ``'cl'`` - center right ``6``, ``'center right'``, ``'cr'`` - lower center ``7``, ``'lower center'``, ``'lc'`` - upper center ``8``, ``'upper center'``, ``'uc'`` - center ``9``, ``'center'``, ``'c'`` - ================== ================================================ - - label, title : str, optional - The legend title. The `label` keyword is also accepted, for consistency - with `colorbar`. - fontsize, fontweight, fontcolor : optional - The font size, weight, and color for legend text. - color, lw, linewidth, marker, linestyle, dashes, markersize : \ -property-spec, optional - Properties used to override the legend handles. For example, if you - want a legend that describes variations in line style ignoring - variations in color, you might want to use ``color='k'``. For now this - does not include `facecolor`, `edgecolor`, and `alpha`, because - `~matplotlib.axes.Axes.legend` uses these keyword args to modify the - frame properties. - - Other parameters - ---------------- - **kwargs - Passed to `~matplotlib.axes.Axes.legend`. - """ - # First get legend settings and interpret kwargs. - if order not in ('F', 'C'): - raise ValueError( - f'Invalid order {order!r}. Choose from ' - '"C" (row-major, default) and "F" (column-major).' - ) - # may still be None, wait till later - ncol = _notNone(ncols, ncol, None, names=('ncols', 'ncol')) - title = _notNone(label, title, None, names=('label', 'title')) - frameon = _notNone( - frame, frameon, rc['legend.frameon'], names=('frame', 'frameon')) - if title is not None: - kwargs['title'] = title - if frameon is not None: - kwargs['frameon'] = frameon - if fontsize is not None: - kwargs['fontsize'] = fontsize - # Text properties, some of which have to be set after-the-fact - kw_text = {} - if fontcolor is not None: - kw_text['color'] = fontcolor - if fontweight is not None: - kw_text['weight'] = fontweight - - # Automatically get labels and handles - # TODO: Use legend._parse_legend_args instead? This covers functionality - # just fine, _parse_legend_args seems overkill. - if handles is None: - if self._filled: - raise ValueError( - 'You must pass a handles list for panel axes ' - '"filled" with a legend.' - ) - else: - # ignores artists with labels '_nolegend_' - handles, labels_default = self.get_legend_handles_labels() - if labels is None: - labels = labels_default - if not handles: - raise ValueError( - 'No labeled artists found. To generate a legend without ' - 'providing the artists explicitly, pass label="label" in ' - 'your plotting commands.' - ) - if not np.iterable(handles): # e.g. a mappable object - handles = [handles] - if labels is not None and ( - not np.iterable(labels) or isinstance(labels, str) - ): - labels = [labels] - - # Legend entry for colormap or scatterplot object - # TODO: Idea is we pass a scatter plot or contourf or whatever, and legend - # is generating by drawing patch rectangles or markers with different - # colors. - if any(not hasattr(handle, 'get_facecolor') and hasattr(handle, 'get_cmap') - for handle in handles) and len(handles) > 1: - raise ValueError( - f'Handles must be objects with get_facecolor attributes or ' - 'a single mappable object from which we can draw colors.' - ) - - # Build pairs of handles and labels - # This allows alternative workflow where user specifies labels when - # creating the legend. - pairs = [] - # e.g. not including BarContainer - list_of_lists = (not hasattr(handles[0], 'get_label')) - if labels is None: - for handle in handles: - if list_of_lists: - ipairs = [] - for ihandle in handle: - if not hasattr(ihandle, 'get_label'): - raise ValueError( - f'Object {ihandle} must have "get_label" method.' - ) - ipairs.append((ihandle, ihandle.get_label())) - pairs.append(ipairs) - else: - if not hasattr(handle, 'get_label'): - raise ValueError( - f'Object {handle} must have "get_label" method.' - ) - pairs.append((handle, handle.get_label())) - else: - if len(labels) != len(handles): - raise ValueError( - f'Got {len(labels)} labels, but {len(handles)} handles.' - ) - for label, handle in zip(labels, handles): - if list_of_lists: - ipairs = [] - if not np.iterable(label) or isinstance(label, str): - raise ValueError( - f'Got list of lists of handles, but list of labels.' - ) - elif len(label) != len(handle): - raise ValueError( - f'Got {len(label)} labels in sublist, ' - f'but {len(handle)} handles.' - ) - for ilabel, ihandle in zip(label, handle): - ipairs.append((ihandle, ilabel)) - pairs.append(ipairs) - else: - if not isinstance(label, str) and np.iterable(label): - raise ValueError( - f'Got list of lists of labels, but list of handles.' - ) - pairs.append((handle, label)) - - # Manage pairs in context of 'center' option - if center is None: # automatically guess - center = list_of_lists - elif center and list_of_lists and ncol is not None: - _warn_proplot( - 'Detected list of *lists* of legend handles. ' - 'Ignoring user input property "ncol".' - ) - elif not center and list_of_lists: # standardize format based on input - list_of_lists = False # no longer is list of lists - pairs = [pair for ipairs in pairs for pair in ipairs] - elif center and not list_of_lists: - list_of_lists = True - ncol = _notNone(ncol, 3) - pairs = [pairs[i * ncol:(i + 1) * ncol] - for i in range(len(pairs))] # to list of iterables - if list_of_lists: # remove empty lists, pops up in some examples - pairs = [ipairs for ipairs in pairs if ipairs] - - # Now draw legend(s) - legs = [] - width, height = self.get_size_inches() - # Individual legend - if not center: - # Optionally change order - # See: https://stackoverflow.com/q/10101141/4970632 - # Example: If 5 columns, but final row length 3, columns 0-2 have - # N rows but 3-4 have N-1 rows. - ncol = _notNone(ncol, 3) - if order == 'C': - fpairs = [] - # split into rows - split = [pairs[i * ncol:(i + 1) * ncol] - for i in range(len(pairs) // ncol + 1)] - # max possible row count, and columns in final row - nrowsmax, nfinalrow = len(split), len(split[-1]) - nrows = [nrowsmax] * nfinalrow + \ - [nrowsmax - 1] * (ncol - nfinalrow) - for col, nrow in enumerate(nrows): # iterate through cols - fpairs.extend(split[row][col] for row in range(nrow)) - pairs = fpairs - # Make legend object - leg = mlegend.Legend(self, *zip(*pairs), ncol=ncol, loc=loc, **kwargs) - legs = [leg] - # Legend with centered rows, accomplished by drawing separate legends for - # each row. The label spacing/border spacing will be exactly replicated. - else: - # Message when overriding some properties - overridden = [] - kwargs.pop('frameon', None) # then add back later! - for override in ('bbox_transform', 'bbox_to_anchor'): - prop = kwargs.pop(override, None) - if prop is not None: - overridden.append(override) - if overridden: - _warn_proplot( - f'For centered-row legends, must override ' - 'user input properties ' - + ', '.join(map(repr, overridden)) + '.' - ) - # Determine space we want sub-legend to occupy as fraction of height - # NOTE: Empirical testing shows spacing fudge factor necessary to - # exactly replicate the spacing of standard aligned legends. - fontsize = kwargs.get('fontsize', None) or rc['legend.fontsize'] - spacing = kwargs.get('labelspacing', None) or rc['legend.labelspacing'] - interval = 1 / len(pairs) # split up axes - interval = (((1 + spacing * 0.85) * fontsize) / 72) / height - # Iterate and draw - # NOTE: We confine possible bounding box in *y*-direction, but do not - # confine it in *x*-direction. Matplotlib will automatically move - # left-to-right if you request this. - ymin, ymax = None, None - if order == 'F': - raise NotImplementedError( - f'When center=True, ProPlot vertically stacks successive ' - 'single-row legends. Column-major (order="F") ordering ' - 'is un-supported.' - ) - loc = _notNone(loc, 'upper center') - if not isinstance(loc, str): - raise ValueError( - f'Invalid location {loc!r} for legend with center=True. ' - 'Must be a location *string*.' - ) - elif loc == 'best': - _warn_proplot( - 'For centered-row legends, cannot use "best" location. ' - 'Using "upper center" instead.' - ) - for i, ipairs in enumerate(pairs): - if i == 1: - kwargs.pop('title', None) - if i >= 1 and title is not None: - i += 1 # extra space! - # Legend position - if 'upper' in loc: - y1 = 1 - (i + 1) * interval - y2 = 1 - i * interval - elif 'lower' in loc: - y1 = (len(pairs) + i - 2) * interval - y2 = (len(pairs) + i - 1) * interval - else: # center - y1 = 0.5 + interval * len(pairs) / 2 - (i + 1) * interval - y2 = 0.5 + interval * len(pairs) / 2 - i * interval - ymin = min(y1, _notNone(ymin, y1)) - ymax = max(y2, _notNone(ymax, y2)) - # Draw legend - bbox = mtransforms.Bbox([[0, y1], [1, y2]]) - leg = mlegend.Legend( - self, *zip(*ipairs), loc=loc, ncol=len(ipairs), - bbox_transform=self.transAxes, bbox_to_anchor=bbox, - frameon=False, **kwargs) - legs.append(leg) - - # Add legends manually so matplotlib does not remove old ones - # Also apply override settings - kw_handle = {} - outline = rc.fill({ - 'linewidth': 'axes.linewidth', - 'edgecolor': 'axes.edgecolor', - 'facecolor': 'axes.facecolor', - 'alpha': 'legend.framealpha', - }) - for key in (*outline,): - if key != 'linewidth': - if kwargs.get(key, None): - outline.pop(key, None) - for key, value in ( - ('color', color), - ('marker', marker), - ('linewidth', lw), - ('linewidth', linewidth), - ('markersize', markersize), - ('linestyle', linestyle), - ('dashes', dashes), - ): - if value is not None: - kw_handle[key] = value - for leg in legs: - self.add_artist(leg) - leg.legendPatch.update(outline) # or get_frame() - for obj in leg.legendHandles: - if isinstance(obj, martist.Artist): - obj.update(kw_handle) - for obj in leg.get_texts(): - if isinstance(obj, martist.Artist): - obj.update(kw_text) - # Draw manual fancy bounding box for un-aligned legend - # WARNING: The matplotlib legendPatch transform is the default transform, - # i.e. universal coordinates in points. Means we have to transform - # mutation scale into transAxes sizes. - # WARNING: Tempting to use legendPatch for everything but for some reason - # coordinates are messed up. In some tests all coordinates were just result - # of get window extent multiplied by 2 (???). Anyway actual box is found in - # _legend_box attribute, which is accessed by get_window_extent. - if center and frameon: - if len(legs) == 1: - legs[0].set_frame_on(True) # easy! - else: - # Get coordinates - renderer = self.figure._get_renderer() - bboxs = [leg.get_window_extent(renderer).transformed( - self.transAxes.inverted()) for leg in legs] - xmin, xmax = min(bbox.xmin for bbox in bboxs), max( - bbox.xmax for bbox in bboxs) - ymin, ymax = min(bbox.ymin for bbox in bboxs), max( - bbox.ymax for bbox in bboxs) - fontsize = (fontsize / 72) / width # axes relative units - fontsize = renderer.points_to_pixels(fontsize) - # Draw and format patch - patch = mpatches.FancyBboxPatch( - (xmin, ymin), xmax - xmin, ymax - ymin, - snap=True, zorder=4.5, - mutation_scale=fontsize, transform=self.transAxes) - if kwargs.get('fancybox', rc['legend.fancybox']): - patch.set_boxstyle('round', pad=0, rounding_size=0.2) - else: - patch.set_boxstyle('square', pad=0) - patch.set_clip_on(False) - patch.update(outline) - self.add_artist(patch) - # Add shadow - # TODO: This does not work, figure out - if kwargs.get('shadow', rc['legend.shadow']): - shadow = mpatches.Shadow(patch, 20, -20) - self.add_artist(shadow) - # Add patch to list - legs = (patch, *legs) - # Append attributes and return, and set clip property!!! This is critical - # for tight bounding box calcs! - for leg in legs: - leg.set_clip_on(False) - return legs[0] if len(legs) == 1 else (*legs,) - - -def colorbar_wrapper( - self, mappable, values=None, - extend=None, extendsize=None, - title=None, label=None, - grid=None, tickminor=None, - tickloc=None, ticklocation=None, - locator=None, ticks=None, maxn=None, maxn_minor=None, - minorlocator=None, minorticks=None, - locator_kw=None, minorlocator_kw=None, - formatter=None, ticklabels=None, formatter_kw=None, - norm=None, norm_kw=None, # normalizer to use when passing colors/lines - orientation='horizontal', - edgecolor=None, linewidth=None, - labelsize=None, labelweight=None, labelcolor=None, - ticklabelsize=None, ticklabelweight=None, ticklabelcolor=None, - **kwargs -): - """ - Wraps `~proplot.axes.Axes` `~proplot.axes.Axes.colorbar` and - `~proplot.subplots.Figure` `~proplot.subplots.Figure.colorbar`, adds some - handy features. - - Parameters - ---------- - mappable : mappable, list of plot handles, list of color-spec, \ -or colormap-spec - There are four options here: - - 1. A mappable object. Basically, any object with a ``get_cmap`` method, - like the objects returned by `~matplotlib.axes.Axes.contourf` and - `~matplotlib.axes.Axes.pcolormesh`. - 2. A list of "plot handles". Basically, any object with a ``get_color`` - method, like `~matplotlib.lines.Line2D` instances. A colormap will - be generated from the colors of these objects, and colorbar levels - will be selected using `values`. If `values` is ``None``, we try - to infer them by converting the handle labels returned by - `~matplotlib.artist.Artist.get_label` to `float`. Otherwise, it is - set to ``np.linspace(0, 1, len(mappable))``. - 3. A list of hex strings, color string names, or RGB tuples. A colormap - will be generated from these colors, and colorbar levels will be - selected using `values`. If `values` is ``None``, it is set to - ``np.linspace(0, 1, len(mappable))``. - 4. A `~matplotlib.colors.Colormap` instance. In this case, a colorbar - will be drawn using this colormap and with levels determined by - `values`. If `values` is ``None``, it is set to - ``np.linspace(0, 1, cmap.N)``. - - values : list of float, optional - Ignored if `mappable` is a mappable object. This maps each color or - plot handle in the `mappable` list to numeric values, from which a - colormap and normalizer are constructed. - extend : {None, 'neither', 'both', 'min', 'max'}, optional - Direction for drawing colorbar "extensions" (i.e. references to - out-of-bounds data with a unique color). These are triangles by - default. If ``None``, we try to use the ``extend`` attribute on the - mappable object. If the attribute is unavailable, we use ``'neither'``. - extendsize : float or str, optional - The length of the colorbar "extensions" in *physical units*. - If float, units are inches. If string, units are interpreted - by `~proplot.utils.units`. Default is :rc:`colorbar.insetextend` - for inset colorbars and :rc:`colorbar.extend` for outer colorbars. - - This is handy if you have multiple colorbars in one figure. - With the matplotlib API, it is really hard to get triangle - sizes to match, because the `extendsize` units are *relative*. - tickloc, ticklocation : {'bottom', 'top', 'left', 'right'}, optional - Where to draw tick marks on the colorbar. - label, title : str, optional - The colorbar label. The `title` keyword is also accepted for - consistency with `legend`. - grid : bool, optional - Whether to draw "gridlines" between each level of the colorbar. - Default is :rc:`colorbar.grid`. - tickminor : bool, optional - Whether to add minor ticks to the colorbar with - `~matplotlib.colorbar.ColorbarBase.minorticks_on`. Default is - ``False``. - locator, ticks : locator spec, optional - Used to determine the colorbar tick positions. Passed to the - `~proplot.axistools.Locator` constructor. - maxn : int, optional - Used if `locator` is ``None``. Determines the maximum number of levels - that are ticked. Default depends on the colorbar length relative - to the font size. The keyword name "maxn" is meant to mimic - the `~matplotlib.ticker.MaxNLocator` class name. - maxn_minor : int, optional - As with `maxn`, but for minor tick positions. Default depends - on the colorbar length. - locator_kw : dict-like, optional - The locator settings. Passed to `~proplot.axistools.Locator`. - minorlocator, minorticks - As with `locator`, but for the minor ticks. - minorlocator_kw - As for `locator_kw`, but for the minor ticks. - formatter, ticklabels : formatter spec, optional - The tick label format. Passed to the `~proplot.axistools.Formatter` - constructor. - formatter_kw : dict-like, optional - The formatter settings. Passed to `~proplot.axistools.Formatter`. - norm : normalizer spec, optional - Ignored if `values` is ``None``. The normalizer - for converting `values` to colormap colors. Passed to the - `~proplot.styletools.Norm` constructor. - norm_kw : dict-like, optional - The normalizer settings. Passed to `~proplot.styletools.Norm`. - edgecolor, linewidth : optional - The edge color and line width for the colorbar outline. - labelsize, labelweight, labelcolor : optional - The font size, weight, and color for colorbar label text. - ticklabelsize, ticklabelweight, ticklabelcolor : optional - The font size, weight, and color for colorbar tick labels. - orientation : {'horizontal', 'vertical'}, optional - The colorbar orientation. You should not have to explicitly set this. - - Other parameters - ---------------- - **kwargs - Passed to `~matplotlib.figure.Figure.colorbar`. - """ - # Developer notes - # * Colorbar axes must be of type `matplotlib.axes.Axes`, - # not `~proplot.axes.Axes`, because colorbar uses some internal methods - # that are wrapped by `~proplot.axes.Axes`. - # * There is an insanely weird problem with colorbars when simultaneously - # passing levels and norm object to a mappable; fixed by passing - # vmin/vmax instead of levels. - # (see: https://stackoverflow.com/q/40116968/4970632). - # * Problem is often want levels instead of vmin/vmax, while simultaneously - # using a Normalize (for example) to determine colors between the levels - # (see: https://stackoverflow.com/q/42723538/4970632). Workaround makes - # sure locators are in vmin/vmax range exclusively; cannot match values. - # No mutable defaults - locator_kw = locator_kw or {} - minorlocator_kw = minorlocator_kw or {} - formatter_kw = formatter_kw or {} - norm_kw = norm_kw or {} - # Parse flexible input - label = _notNone(title, label, None, names=('title', 'label')) - locator = _notNone(ticks, locator, None, names=('ticks', 'locator')) - formatter = _notNone( - ticklabels, formatter, 'auto', - names=('ticklabels', 'formatter') - ) - minorlocator = _notNone( - minorticks, minorlocator, None, - names=('minorticks', 'minorlocator') - ) - ticklocation = _notNone( - tickloc, ticklocation, None, - names=('tickloc', 'ticklocation') - ) - - # Colorbar kwargs - # WARNING: PathCollection scatter objects have an extend method! - grid = _notNone(grid, rc['colorbar.grid']) - if extend is None: - if isinstance(getattr(mappable, 'extend', None), str): - extend = mappable.extend or 'neither' - else: - extend = 'neither' - kwargs.update({ - 'cax': self, - 'use_gridspec': True, - 'orientation': orientation, - 'extend': extend, - 'spacing': 'uniform' - }) - kwargs.setdefault('drawedges', grid) - - # Text property keyword args - kw_label = {} - if labelsize is not None: - kw_label['size'] = labelsize - if labelweight is not None: - kw_label['weight'] = labelweight - if labelcolor is not None: - kw_label['color'] = labelcolor - kw_ticklabels = {} - if ticklabelsize is not None: - kw_ticklabels['size'] = ticklabelsize - if ticklabelweight is not None: - kw_ticklabels['weight'] = ticklabelweight - if ticklabelcolor is not None: - kw_ticklabels['color'] = ticklabelcolor - - # Special case where auto colorbar is generated from 1D methods, a list is - # always passed, but some 1D methods (scatter) do have colormaps. - if ( - np.iterable(mappable) - and len(mappable) == 1 - and hasattr(mappable[0], 'get_cmap') - ): - mappable = mappable[0] - - # For container objects, we just assume color is the same for every item. - # Works for ErrorbarContainer, StemContainer, BarContainer. - if ( - np.iterable(mappable) - and len(mappable) > 0 - and all(isinstance(obj, mcontainer.Container) for obj in mappable) - ): - mappable = [obj[0] for obj in mappable] - - # Test if we were given a mappable, or iterable of stuff; note Container - # and PolyCollection matplotlib classes are iterable. - cmap = None - if not isinstance(mappable, (martist.Artist, mcontour.ContourSet)): - # Any colormap spec, including a list of colors, colormap name, or - # colormap instance. - if isinstance(mappable, mcolors.Colormap): - cmap = mappable - if values is None: - values = np.arange(cmap.N) - - # List of colors - elif np.iterable(mappable) and all( - isinstance(obj, str) or (np.iterable(obj) and len(obj) in (3, 4)) - for obj in mappable - ): - colors = list(mappable) - cmap = mcolors.ListedColormap(colors, '_no_name') - if values is None: - values = np.arange(len(colors)) - - # List of artists - elif np.iterable(mappable) and all( - hasattr(obj, 'get_color') or hasattr(obj, 'get_facecolor') - for obj in mappable - ): - # Generate colormap from colors - colors = [] - for obj in mappable: - if hasattr(obj, 'get_color'): - color = obj.get_color() - else: - color = obj.get_facecolor() - colors.append(color) - cmap = mcolors.ListedColormap(colors, '_no_name') - # Try to infer values from labels - if values is None: - values = [] - for obj in mappable: - val = obj.get_label() - try: - val = float(val) - except ValueError: - values = None - break - values.append(val) - if values is None: - values = np.arange(len(colors)) - - else: - raise ValueError( - 'Input mappable must be a matplotlib artist, ' - 'list of objects, list of colors, or colormap. ' - f'Got {mappable!r}.' - ) - - # Build new ad hoc mappable object from colors - # NOTE: Need to use *wrapped* contourf but this might be native matplotlib - # axes. Call on self.axes, which is parent if child axes, self otherwise. - if cmap is not None: - if np.iterable(mappable) and len(values) != len(mappable): - raise ValueError( - f'Passed {len(values)} values, but only {len(mappable)} ' - f'objects or colors.' - ) - import warnings - with warnings.catch_warnings(): - warnings.simplefilter('ignore') - mappable = self.axes.contourf( - [0, 0], [0, 0], ma.array([[0, 0], [0, 0]], mask=True), - cmap=cmap, extend='neither', values=np.array(values), - norm=norm, norm_kw=norm_kw - ) # workaround - - # Try to get tick locations from *levels* or from *values* rather than - # random points along the axis. If values were provided as keyword arg, - # this is colorbar from lines/colors, and we label *all* values by default. - # TODO: Handle more of the log locator stuff here instead of cmap_changer? - norm = getattr(mappable, 'norm', None) - if values is not None and locator is None: - locator = values - tickminor = False - if locator is None: - for attr in ('values', 'locator', 'levels'): - locator = getattr(mappable, attr, None) - if locator is not None: - break - if locator is None: # i.e. no attributes found - if isinstance(norm, mcolors.LogNorm): - locator = 'log' - else: - locator = 'auto' - # i.e. was a 'values' or 'levels' attribute - elif not isinstance(locator, mticker.Locator): - # Get default maxn, try to allot 2em squares per label maybe? - # NOTE: Cannot use Axes.get_size_inches because this is a - # native matplotlib axes - width, height = self.figure.get_size_inches() - if orientation == 'horizontal': - scale = 3 # em squares alotted for labels - length = width * abs(self.get_position().width) - fontsize = kw_ticklabels.get('size', rc['xtick.labelsize']) - else: - scale = 1 - length = height * abs(self.get_position().height) - fontsize = kw_ticklabels.get('size', rc['ytick.labelsize']) - maxn = _notNone(maxn, int(length / (scale * fontsize / 72))) - maxn_minor = _notNone(maxn_minor, int( - length / (0.5 * fontsize / 72))) - # Get locator - if tickminor and minorlocator is None: - step = 1 + len(locator) // max(1, maxn_minor) - minorlocator = locator[::step] - step = 1 + len(locator) // max(1, maxn) - locator = locator[::step] - - # Final settings - locator = axistools.Locator(locator, **locator_kw) - formatter = axistools.Formatter(formatter, **formatter_kw) - width, height = self.figure.get_size_inches() - if orientation == 'horizontal': - scale = width * abs(self.get_position().width) - else: - scale = height * abs(self.get_position().height) - extendsize = units(_notNone(extendsize, rc['colorbar.extend'])) - extendsize = extendsize / (scale - 2 * extendsize) - kwargs.update({ - 'ticks': locator, - 'format': formatter, - 'ticklocation': ticklocation, - 'extendfrac': extendsize - }) - - # Draw the colorbar - # NOTE: self._use_auto_colorbar_locator() is never True because - # we use the custom BinNorm normalizer. Colorbar._ticks() always called. - try: - self.figure._locked = False - cb = self.figure.colorbar(mappable, **kwargs) - except Exception as err: - self.figure._locked = True - raise err - axis = self.xaxis if orientation == 'horizontal' else self.yaxis - - # The minor locator - # TODO: Document the improved minor locator functionality! - if tickminor and minorlocator is None: - cb.minorticks_on() - elif minorlocator is None: - cb.minorticks_off() - elif not hasattr(cb, '_ticker'): - _warn_proplot( - 'Matplotlib colorbar API has changed. ' - 'Cannot use custom minor tick locator.' - ) - if tickminor: - cb.minorticks_on() - else: - # Set the minor ticks just like matplotlib internally sets the - # major ticks. Private API is the only way! - minorlocator = axistools.Locator(minorlocator, **minorlocator_kw) - ticks, *_ = cb._ticker(minorlocator, mticker.NullFormatter()) - axis.set_ticks(ticks, minor=True) - axis.set_ticklabels([], minor=True) - - # Outline - kw_outline = { - 'edgecolor': _notNone(edgecolor, rc['axes.edgecolor']), - 'linewidth': _notNone(linewidth, rc['axes.linewidth']), - } - if cb.outline is not None: - cb.outline.update(kw_outline) - if cb.dividers is not None: - cb.dividers.update(kw_outline) - - # Fix alpha-blending issues. - # Cannot set edgecolor to 'face' if alpha non-zero because blending will - # occur, will get colored lines instead of white ones. Need manual blending - # NOTE: For some reason cb solids uses listed colormap with always 1.0 - # alpha, then alpha is applied after. - # See: https://stackoverflow.com/a/35672224/4970632 - cmap = cb.cmap - if not cmap._isinit: - cmap._init() - if any(cmap._lut[:-1, 3] < 1): - _warn_proplot( - f'Using manual alpha-blending for {cmap.name!r} colorbar solids.' - ) - # Generate "secret" copy of the colormap! - lut = cmap._lut.copy() - cmap = mcolors.Colormap('_colorbar_fix', N=cmap.N) - cmap._isinit = True - cmap._init = (lambda: None) - # Manually fill lookup table with alpha-blended RGB colors! - for i in range(lut.shape[0] - 1): - alpha = lut[i, 3] - lut[i, :3] = (1 - alpha) * 1 + alpha * \ - lut[i, :3] # blend with *white* - lut[i, 3] = 1 - cmap._lut = lut - # Update colorbar - cb.cmap = cmap - cb.draw_all() - - # Label and tick label settings - # WARNING: Must use colorbar set_label to set text, calling set_text on - # the axis will do nothing! - if label is not None: - cb.set_label(label) - axis.label.update(kw_label) - for obj in axis.get_ticklabels(): - obj.update(kw_ticklabels) - - # Ticks - xy = axis.axis_name - for which in ('minor', 'major'): - kw = rc.category(xy + 'tick.' + which) - kw.pop('visible', None) - if edgecolor: - kw['color'] = edgecolor - if linewidth: - kw['width'] = linewidth - axis.set_tick_params(which=which, **kw) - axis.set_ticks_position(ticklocation) - - # Invert the axis if BinNorm - # TODO: When is norm *not* BinNorm? Should be pretty much always. - if isinstance(norm, styletools.BinNorm): - axis.set_inverted(norm._descending) - - # *Never* rasterize because it causes misalignment with border lines - if cb.solids: - cb.solids.set_rasterized(False) - cb.solids.set_linewidth(0.4) - cb.solids.set_edgecolor('face') - return cb - - -def _redirect(func): - """ - Docorator that calls the basemap version of the function of the - same name. This must be applied as innermost decorator, which means it must - be applied on the base axes class, not the basemap axes. - """ - name = func.__name__ - @functools.wraps(func) - def _wrapper(self, *args, **kwargs): - if getattr(self, 'name', '') == 'basemap': - return getattr(self.projection, name)(*args, ax=self, **kwargs) - else: - return func(self, *args, **kwargs) - _wrapper.__doc__ = None - return _wrapper - - -def _norecurse(func): - """ - Decorator to prevent recursion in basemap method overrides. - See `this post https://stackoverflow.com/a/37675810/4970632`__. - """ - name = func.__name__ - func._has_recurred = False - @functools.wraps(func) - def _wrapper(self, *args, **kwargs): - if func._has_recurred: - # Return the *original* version of the matplotlib method - func._has_recurred = False - result = getattr(maxes.Axes, name)(self, *args, **kwargs) - else: - # Return the version we have wrapped - func._has_recurred = True - result = func(self, *args, **kwargs) - func._has_recurred = False # cleanup, in case recursion never occurred - return result - return _wrapper - - -def _wrapper_decorator(driver): - """ - Generate generic wrapper decorator and dynamically modify the docstring - to list methods wrapped by this function. Also set `__doc__` to ``None`` so - that ProPlot fork of automodapi doesn't add these methods to the website - documentation. Users can still call help(ax.method) because python looks - for superclass method docstrings if a docstring is empty. - """ - driver._docstring_orig = driver.__doc__ or '' - driver._methods_wrapped = [] - proplot_methods = ('parametric', 'heatmap', 'area', 'areax') - cartopy_methods = ('get_extent', 'set_extent') - - def decorator(func): - # Define wrapper and suppress documentation - # We only document wrapper functions, not the methods they wrap - @functools.wraps(func) - def _wrapper(self, *args, **kwargs): - return driver(self, func, *args, **kwargs) - name = func.__name__ - if name not in proplot_methods: - _wrapper.__doc__ = None - - # List wrapped methods in the driver function docstring - # Prevents us from having to both explicitly apply decorators in - # axes.py and explicitly list functions *again* in this file - docstring = driver._docstring_orig - if '%(methods)s' in docstring: - if name in proplot_methods: - link = f'`~proplot.axes.Axes.{name}`' - elif name in cartopy_methods: - link = f'`~cartopy.mpl.geoaxes.GeoAxes.{name}`' - else: - link = f'`~matplotlib.axes.Axes.{name}`' - methods = driver._methods_wrapped - if link not in methods: - methods.append(link) - string = ( - ', '.join(methods[:-1]) - + ',' * int(len(methods) > 2) # Oxford comma bitches - + ' and ' * int(len(methods) > 1) - + methods[-1]) - driver.__doc__ = docstring % {'methods': string} - return _wrapper - return decorator - - -# Auto generated decorators. Each wrapper internally calls -# func(self, ...) somewhere. -_add_errorbars = _wrapper_decorator(add_errorbars) -_bar_wrapper = _wrapper_decorator(bar_wrapper) -_barh_wrapper = _wrapper_decorator(barh_wrapper) -_default_latlon = _wrapper_decorator(default_latlon) -_boxplot_wrapper = _wrapper_decorator(boxplot_wrapper) -_default_crs = _wrapper_decorator(default_crs) -_default_transform = _wrapper_decorator(default_transform) -_cmap_changer = _wrapper_decorator(cmap_changer) -_cycle_changer = _wrapper_decorator(cycle_changer) -_fill_between_wrapper = _wrapper_decorator(fill_between_wrapper) -_fill_betweenx_wrapper = _wrapper_decorator(fill_betweenx_wrapper) -_hist_wrapper = _wrapper_decorator(hist_wrapper) -_plot_wrapper = _wrapper_decorator(plot_wrapper) -_scatter_wrapper = _wrapper_decorator(scatter_wrapper) -_standardize_1d = _wrapper_decorator(standardize_1d) -_standardize_2d = _wrapper_decorator(standardize_2d) -_text_wrapper = _wrapper_decorator(text_wrapper) -_violinplot_wrapper = _wrapper_decorator(violinplot_wrapper) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..acfffc06b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,7 @@ +[build-system] +requires = ["setuptools>=44", "wheel", "setuptools_scm[toml]>=3.4.3"] +build-backend = "setuptools.build_meta" + +[tool.setuptools_scm] +version_scheme = "post-release" +local_scheme = "dirty-tag" diff --git a/requirements.txt b/requirements.txt index 0dea9eb2c..bd606d8e4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ -# Just matplotlib :) -matplotlib +# Just matplotlib and numpy :) +matplotlib>=3.0.0 +numpy diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 000000000..8b422f223 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,34 @@ +[metadata] +name = proplot +author = Luke Davis +author_email = lukelbd@gmail.com +maintainer = Luke Davis +maintainer_email = lukelbd@gmail.com +license = MIT +description = A succinct matplotlib wrapper for making beautiful, publication-quality graphics. +url = https://proplot.readthedocs.io +long_description = file: README.rst +long_description_content_type = text/x-rst +classifiers = + License :: OSI Approved :: MIT License + Operating System :: OS Independent + Intended Audience :: Science/Research + Programming Language :: Python :: 3 :: Only + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 + +project_urls = + Documentation = https://proplot.readthedocs.io + Issue Tracker = https://github.com/proplot-dev/proplot/issues + Source Code = https://github.com/proplot-dev/proplot + +[options] +packages = proplot +install_requires = + matplotlib>=3.0.0,<3.6.0 + numpy +include_package_data = True +python_requires = >=3.6.0 diff --git a/setup.py b/setup.py index 8c87d3244..6b40b52bf 100644 --- a/setup.py +++ b/setup.py @@ -1,54 +1,4 @@ from setuptools import setup -from os.path import exists -with open('requirements.txt') as f: - install_req = [req.strip() for req in f.read().split('\n')] -install_req = [req for req in install_req if req and req[0] != '#'] - -classifiers = [ - 'License :: OSI Approved :: MIT License', - 'Operating System :: OS Independent', - 'Intended Audience :: Science/Research', - 'Programming Language :: Python :: 3 :: Only', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', -] - -if exists('README.rst'): # when does this not exist? - with open('README.rst') as f: - long_description = f.read() -else: - long_description = '' - -setup( - url='https://proplot.readthedocs.io', - name='proplot', - author='Luke Davis', - author_email='lukelbd@gmail.com', - maintainer='Luke Davis', - maintainer_email='lukelbd@gmail.com', - python_requires='>=3.6.0', - project_urls={ - 'Bug Tracker': 'https://github.com/lukelbd/proplot/issues', - 'Documentation': 'https://proplot.readthedocs.io', - 'Source Code': 'https://github.com/lukelbd/proplot' - }, - packages=['proplot'], - classifiers=classifiers, - # normally uses MANIFEST.in but setuptools_scm auto-detects tracked files - include_package_data=True, - install_requires=install_req, - license='MIT', - description=('A comprehensive, easy-to-use matplotlib wrapper ' - 'for making beautiful, publication-quality graphics.'), - long_description=long_description, - long_description_content_type='text/x-rst', - use_scm_version={'version_scheme': 'post-release', - 'local_scheme': 'dirty-tag'}, - setup_requires=[ - 'setuptools_scm', - 'setuptools>=30.3.0', - 'setuptools_scm_git_archive', - ], -) +if __name__ == '__main__': + setup()