diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 000000000..6a84224c0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,31 @@ + + +### Description + +[Description of the bug or feature.] + +### Steps to reproduce + +A "[Minimal, Complete and Verifiable Example](http://matthewrocklin.com/blog/work/2018/02/28/minimal-bug-reports)" will make it much easier for maintainers to help you. + +```python +# your code here +# we should be able to copy-paste this into python and exactly reproduce your bug +``` + +**Expected behavior**: [What you expected to happen] + +**Actual behavior**: [What actually happened] + +### Equivalent steps in matplotlib + +Please try to make sure this bug is related to a ultra-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 +``` + +### Ultra version + +Paste the results of `import matplotlib; print(matplotlib.__version__); import ultra; print(ultra.version)` here. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..5f454fdfb --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + groups: + github-actions: + patterns: + - '*' diff --git a/.github/workflows/build-ultraplot.yml b/.github/workflows/build-ultraplot.yml new file mode 100644 index 000000000..7c3fb5252 --- /dev/null +++ b/.github/workflows/build-ultraplot.yml @@ -0,0 +1,104 @@ +name: Build and Test +on: + workflow_call: + inputs: + python-version: + required: true + type: string + matplotlib-version: + required: true + type: string + +env: + LC_ALL: en_US.UTF-8 + LANG: en_US.UTF-8 + +jobs: + build-ultraplot: + name: Test Python ${{ inputs.python-version }} with MPL ${{ inputs.matplotlib-version }} + runs-on: ubuntu-latest + timeout-minutes: 60 + defaults: + run: + shell: bash -el {0} + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - uses: mamba-org/setup-micromamba@v2.0.7 + with: + environment-file: ./environment.yml + init-shell: bash + create-args: >- + --verbose + python=${{ inputs.python-version }} + matplotlib=${{ inputs.matplotlib-version }} + cache-environment: true + cache-downloads: false + + - name: Build Ultraplot + run: | + pip install --no-build-isolation --no-deps . + + - name: Test Ultraplot + run: | + pytest --cov=ultraplot --cov-branch --cov-report term-missing --cov-report=xml ultraplot + + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + slug: Ultraplot/ultraplot + + compare-baseline: + name: Compare baseline Python ${{ inputs.python-version }} with MPL ${{ inputs.matplotlib-version }} + runs-on: ubuntu-latest + defaults: + run: + shell: bash -el {0} + steps: + - uses: actions/checkout@v6 + + - uses: mamba-org/setup-micromamba@v2.0.7 + with: + environment-file: ./environment.yml + init-shell: bash + create-args: >- + --verbose + python=${{ inputs.python-version }} + matplotlib=${{ inputs.matplotlib-version }} + cache-environment: true + cache-downloads: false + + - name: Generate baseline from main + run: | + mkdir -p baseline + git fetch origin ${{ github.event.pull_request.base.sha }} + git checkout ${{ github.event.pull_request.base.sha }} + python -c "import ultraplot as plt; plt.config.Configurator()._save_yaml('ultraplot.yml')" + pytest -W ignore \ + --mpl-generate-path=./baseline/ \ + --mpl-default-style="./ultraplot.yml"\ + ultraplot/tests + git checkout ${{ github.sha }} # Return to PR branch + + - name: Image Comparison Ultraplot + run: | + mkdir -p results + python -c "import ultraplot as plt; plt.config.Configurator()._save_yaml('ultraplot.yml')" + pytest -W ignore \ + --mpl \ + --mpl-baseline-path=./baseline/ \ + --mpl-results-path=./results/ \ + --mpl-generate-summary=html \ + --mpl-default-style="./ultraplot.yml" \ + ultraplot/tests + + # Return the html output of the comparison even if failed + - name: Upload comparison failures + if: always() + uses: actions/upload-artifact@v6 + with: + name: failed-comparisons-${{ inputs.python-version }}-${{ inputs.matplotlib-version }}-${{ github.sha }} + path: results/* diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 000000000..2cc8b1b68 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,155 @@ +name: Matrix Test +on: + push: + branches: [main, devel] + pull_request: + branches: [main, devel] + +jobs: + run-if-changes: + runs-on: ubuntu-latest + outputs: + run: ${{ (github.event_name == 'push' && github.ref_name == 'main') && 'true' || steps.filter.outputs.python }} + steps: + - uses: actions/checkout@v6 + - uses: dorny/paths-filter@v3 + id: filter + with: + filters: | + python: + - 'ultraplot/**' + + get-versions: + runs-on: ubuntu-latest + needs: + - run-if-changes + if: always() && needs.run-if-changes.outputs.run == 'true' + outputs: + python-versions: ${{ steps.set-versions.outputs.python-versions }} + matplotlib-versions: ${{ steps.set-versions.outputs.matplotlib-versions }} + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - uses: actions/setup-python@v6 + with: + python-version: "3.11" + + - name: Install dependencies + run: pip install tomli + + - id: set-versions + run: | + # Create a Python script to read and parse versions + cat > get_versions.py << 'EOF' + import tomli + import re + import json + + # Read pyproject.toml + with open("pyproject.toml", "rb") as f: + data = tomli.load(f) + + # Get Python version requirement + python_req = data["project"]["requires-python"] + + # Parse min and max versions + min_version = re.search(r">=(\d+\.\d+)", python_req) + max_version = re.search(r"<(\d+\.\d+)", python_req) + + python_versions = [] + if min_version and max_version: + # Convert version strings to tuples + min_v = tuple(map(int, min_version.group(1).split("."))) + max_v = tuple(map(int, max_version.group(1).split("."))) + + # Generate version list + current = min_v + while current < max_v: + python_versions.append(".".join(map(str, current))) + current = (current[0], current[1] + 1) + + + # parse MPL versions + mpl_req = None + for d in data["project"]["dependencies"]: + if d.startswith("matplotlib"): + mpl_req = d + break + assert mpl_req is not None, "matplotlib version not found in dependencies" + min_version = re.search(r">=(\d+\.\d+)", mpl_req) + max_version = re.search(r"<(\d+\.\d+)", mpl_req) + + mpl_versions = [] + if min_version and max_version: + # Convert version strings to tuples + min_v = tuple(map(int, min_version.group(1).split("."))) + max_v = tuple(map(int, max_version.group(1).split("."))) + + # Generate version list + current = min_v + while current < max_v: + mpl_versions.append(".".join(map(str, current))) + current = (current[0], current[1] + 1) + + # If no versions found, default to 3.9 + if not mpl_versions: + mpl_versions = ["3.9"] + + # Create output dictionary + output = { + "python_versions": python_versions, + "matplotlib_versions": mpl_versions + } + + # Print as JSON + print(json.dumps(output)) + EOF + + # Run the script and capture output + OUTPUT=$(python3 get_versions.py) + PYTHON_VERSIONS=$(echo $OUTPUT | jq -r '.python_versions') + MPL_VERSIONS=$(echo $OUTPUT | jq -r '.matplotlib_versions') + + echo "Detected Python versions: ${PYTHON_VERSIONS}" + echo "Detected Matplotlib versions: ${MPL_VERSIONS}" + echo "python-versions=$(echo $PYTHON_VERSIONS | jq -c)" >> $GITHUB_OUTPUT + echo "matplotlib-versions=$(echo $MPL_VERSIONS | jq -c)" >> $GITHUB_OUTPUT + + build: + needs: + - get-versions + - run-if-changes + if: always() && needs.run-if-changes.outputs.run == 'true' && needs.get-versions.result == 'success' + strategy: + matrix: + python-version: ${{ fromJson(needs.get-versions.outputs.python-versions) }} + matplotlib-version: ${{ fromJson(needs.get-versions.outputs.matplotlib-versions) }} + fail-fast: false + uses: ./.github/workflows/build-ultraplot.yml + concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-${{ matrix.python-version }}-${{ matrix.matplotlib-version }} + cancel-in-progress: true + with: + python-version: ${{ matrix.python-version }} + matplotlib-version: ${{ matrix.matplotlib-version }} + + build-success: + needs: + - build + - run-if-changes + if: always() + runs-on: ubuntu-latest + steps: + - run: | + if [[ '${{ needs.run-if-changes.outputs.run }}' == 'false' ]]; then + echo "No changes detected, tests skipped." + else + if [[ '${{ needs.build.result }}' == 'success' ]]; then + echo "All tests passed successfully!" + else + echo "Tests failed!" + exit 1 + fi + fi diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml new file mode 100644 index 000000000..4128d4275 --- /dev/null +++ b/.github/workflows/publish-pypi.yml @@ -0,0 +1,121 @@ +name: Publish to PyPI +on: + pull_request: + release: + types: [published] + push: + tags: ["v*"] + +concurrency: + group: publish-pypi-{{ github.sha }} + cancel-in-progress: false + +jobs: + build-packages: + name: Build packages + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Get tags + run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* + shell: bash + + - uses: actions/setup-python@v6 + with: + python-version: "3.12" + + - name: Build package + run: | + python -m pip install --upgrade pip wheel setuptools setuptools_scm build twine + python -m build --sdist --wheel . --outdir dist + + - name: Check files + run: | + ls dist + shell: bash + + - name: Test wheels + run: | + pushd dist + python -m pip install ultraplot*.whl + + version=$(python -c "import ultraplot; print(ultraplot.__version__)") + echo "Version: $version" + if [[ "$version" == "0."* ]]; then + echo "Version is not set correctly!" + exit 1 + fi + + python -m twine check * + popd + shell: bash + + - name: Upload artifacts + uses: actions/upload-artifact@v6 + with: + name: dist-${{ github.sha }}-${{ github.run_id }}-${{ github.run_number }} + path: dist/* + if-no-files-found: error + + publish-pypi-test: + name: Publish to TestPyPI + needs: build-packages + if: github.event_name != 'pull_request' + environment: + name: test + url: https://test.pypi.org/project/ultraplot/ + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + steps: + - name: Download artifacts + uses: actions/download-artifact@v7 + with: + name: dist-${{ github.sha }}-${{ github.run_id }}-${{ github.run_number }} + path: dist + + - name: Check files + run: | + ls dist + shell: bash + + - name: Publish to TestPyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + repository-url: https://test.pypi.org/legacy/ + verbose: true + # releases generate both release and tag events so + # we get a race condition if we don't skip existing + skip-existing: ${{ (github.event_name == 'release' || github.event_name == 'push') && 'true' || 'false' }} + + publish-pypi: + name: Publish to PyPI + needs: publish-pypi-test + environment: + name: prod + url: https://pypi.org/project/ultraplot/ + runs-on: ubuntu-latest + if: github.event_name == 'release' + permissions: + id-token: write + contents: read + steps: + - name: Download artifacts + uses: actions/download-artifact@v7 + with: + name: dist-${{ github.sha }}-${{ github.run_id }}-${{ github.run_number }} + path: dist + + - name: Check files + run: | + ls dist + shell: bash + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + verbose: true diff --git a/.gitignore b/.gitignore index a25cda9b6..bbd6bf100 100644 --- a/.gitignore +++ b/.gitignore @@ -2,38 +2,48 @@ .session.vim .vimsession .*.sw[a-z] -lottery* -# Private files -*.pdf -*.log -*.tex -*.dfont -*.bdf -empty* -sample* -notes* -trash* -colorbrewer - -# Ignore top-level init used on my machine to make package importable as long -# as $HOME is in $PYTHONPATH (see: https://stackoverflow.com/a/3637678/4970632) -# /__init__.py # need this for other servers -install +# Pytest-mpl test directory ignore +baseline +baseline/* +results +results/* +ultraplot.yml + +# PyPi stuff +build +dist +.eggs +*.egg-info + +# Local docs builds +docs/api +docs/_build +docs/_static/ultraplotrc +docs/_static/rctable.rst +docs/_static/* + +# Development subfolders +dev +sources # Python extras +.ipynb_checkpoints +*.log *.pyc .*.pyc -__trash__ __pycache__ -# Notebooks -*.ipynb -.ipynb_checkpoints -# !showcase.ipynb - -# OS generated files +# OS files .DS_Store .DS_Store? ._* .Trashes + +# Old files +tmp +trash +garbage + +# version file +ultraplot/_version.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..c258b9077 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,16 @@ +ci: + autofix_commit_msg: | + [pre-commit.ci] auto fixes from pre-commit.com hooks + + for more information, see https://pre-commit.ci + autofix_prs: false + autoupdate_commit_msg: '[pre-commit.ci] pre-commit autoupdate' + autoupdate_schedule: monthly + skip: [] + submodules: false + +repos: + - repo: https://github.com/psf/black-pre-commit-mirror + rev: 25.12.0 + hooks: + - id: black diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 000000000..c3cfa3e57 --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,24 @@ +# .readthedocs.yml +# Read the Docs configuration file +version: 2 + +# Set the OS and build tools +build: + os: ubuntu-22.04 + tools: + python: "mambaforge-latest" + +# Conda settings +conda: + environment: ./environment.yml + +# Sphinx settings +sphinx: + configuration: ./docs/conf.py + builder: html + +# Python settings +python: + install: + - method: pip + path: . diff --git a/.zenodo.json b/.zenodo.json new file mode 100644 index 000000000..1d1641fed --- /dev/null +++ b/.zenodo.json @@ -0,0 +1,39 @@ +{ + "title": "UltraPlot: A succinct wrapper for Matplotlib", + "upload_type": "software", + "description": "UltraPlot provides a compact and extensible API on top of Matplotlib, inspired by ProPlot. It simplifies the creation of scientific plots with consistent layout, colorbars, and shared axes.", + "creators": [ + { + "name": "van Elteren, Casper", + "orcid": "0000-0001-9862-8936", + "affiliation": "University of Amsterdam, Polder Center, Institute for Advanced Study Amsterdam" + }, + { + "name": "Becker, Matthew R.", + "orcid": "0000-0001-7774-2246", + "affiliation": "Argonne National Laboratory, Lemont, IL USA" + } + ], + "license": "MIT", + "keywords": [ + "matplotlib", + "scientific visualization", + "plotting", + "wrapper", + "python" + ], + "related_identifiers": [ + { + "relation": "isDerivedFrom", + "identifier": "https://github.com/lukelbd/proplot", + "scheme": "url" + }, + { + "relation": "isDerivedFrom", + "identifier": "https://matplotlib.org/", + "scheme": "url" + } + ], + "version": "1.57", + "publication_date": "2025-01-01" // need to fix +} diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 000000000..2939d7feb --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,33 @@ +cff-version: 1.2.0 +message: "If you use UltraPlot in your work, please cite it using the following metadata." +title: "UltraPlot" +authors: + - family-names: "van Elteren" + given-names: "Casper" + orcid: "https://orcid.org/0000-0001-9862-8936" + - family-names: "Becker" + given-names: "Matthew R." + orcid: "https://orcid.org/0000-0001-7774-2246" +date-released: "2025-01-01" +version: "1.57" +doi: "10.5281/zenodo.15733580" +repository-code: "https://github.com/Ultraplot/UltraPlot" +license: "MIT" +keywords: + - plotting + - matplotlib + - scientific visualization + - wrapper +references: + - type: software + name: "ProPlot" + authors: + - family-names: "Davis" + given-names: "Luke" + url: "https://github.com/lukelbd/proplot" + - type: software + name: "Matplotlib" + authors: + - family-names: "Hunter" + given-names: "John D." + url: "https://matplotlib.org/" 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..6c2d1ae7a --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,264 @@ +.. _contrib: + +================== +How to contribute? +================== + +Contributions of any size are greatly appreciated! You can +make a significant impact on UltraPlot by just using it and +reporting `issues `__. + +The following sections cover some general guidelines +regarding UltraPlot 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, UltraPlot's continuous +integration includes only the examples that appear on the website User Guide (see +`.travis.yml`), and `Casper van Elteren ` runs additional tests +manually. This approach leaves out many use cases and leaves the project more +vulnerable to bugs. Improving ultraplot'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 ultraplot, 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 + :func:`~ultraplot.axes.Axes.format` method, please write + ```:func:`~ultraplot.axes.Axes.format` ``` rather than ``format``. ultraplot 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 ultraplot-dev --file docs/environment.yml + # source activate ultraplot-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 + `ultraplot GitHub repository `__. It's + fine to keep "ultraplot" 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/ultraplot.git + cd ultraplot + git remote add upstream git@github.com:ultraplot/ultraplot.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 ultraplot by running: + + .. code-block:: bash + + pip install -e . + + This way ``import ultraplot`` imports your local copy, + rather than the stable version you last downloaded from PyPi. + You can ``import ultraplot; print(ultraplot.__file__)`` to verify your + local copy has been imported. + +#. Install `pre-commit `__ and its hook on the + ``ultraplot`` 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 ultraplot -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/ultraplot + compare: your-branch-name + + base-fork: ultraplot/ultraplot + 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. UltraPlot +developers and contributors can then review your code and offer suggestions. + +.. _contrib_release: + +Release procedure +================= +Ultraplot follows EffVer (`Effectual Versioning `_). Changes to the version number ``X.Y.Z`` will reflect the effect on users: the major version ``X`` will be incremented for changes that require user attention (like breaking changes), the minor version ``Y`` will be incremented for safe feature additions, and the patch number ``Z`` will be incremented for changes users can safely ignore. + +While version 1.0 has been released, we are still in the process of ensuring proplot is fully replaced by ultraplot as we continue development under the ultraplot name. During this transition, the versioning scheme reflects both our commitment to stable APIs and the ongoing work to complete this transition. The minor version number is incremented when changes require user attention (like deprecations or style changes), and the patch number is incremented for additions and fixes that users can safely adopt. + +For now, `Casper van Eltern `__ 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/ultraplot/ and make sure everything looks ok + # Then make sure the package is installable + pip install --index-url https://test.pypi.org/simple/ ultraplot + # Register and push to pypi + twine upload dist/* diff --git a/INSTALL.rst b/INSTALL.rst new file mode 100644 index 000000000..27714093f --- /dev/null +++ b/INSTALL.rst @@ -0,0 +1,30 @@ +Installation +============ + +Ultraplot is published on `PyPi `__ +and `conda-forge `__. It can be installed +with ``pip`` or ``conda`` as follows: + +.. code-block:: bash + + pip install ultraplot + conda install -c conda-forge ultraplot + +Likewise, an existing installation of ultraplot can be upgraded to the latest version with: + +.. code-block:: bash + + pip install --upgrade ultraplot + conda upgrade ultraplot + + +To install a development version of ultraplot, you can use +``pip install git+https://github.com/ultraplot/ultraplot.git`` +or clone the repository and run ``pip install -e .`` inside +the ``ultraplot`` folder. + +ultraplot's only hard dependency is `matplotlib `__. +The *soft* dependencies are `cartopy `__, +`basemap `__, +`xarray `__, and `pandas `__. +See the documentation for details. diff --git a/README.md b/README.md deleted file mode 100644 index a41148c32..000000000 --- a/README.md +++ /dev/null @@ -1,56 +0,0 @@ -# ProPlot -A library providing helpful and versatile plotting utilities to hasten the process of crafting publication-quality graphics with `matplotlib`. - -## Overview -Import with -``` -import proplot as plot -``` -Most of the features derive from the **`subplots`** command, inspired by the `pyplot` command of the same name. This generates a scaffolding of axes and panels, which may have shared axes and spanning axis labels. - -The next most important utility is the **`format`** method, available on every axes generated by `subplots`. Use this method to fine-tune your axis properties, titles, labels, limits, and much more. - -Quick overview of additional features: - - * Geometry: A smarter "tight subplots" method. Panels and empty spaces are held *fixed*, while the figure and axes dimensions are allowed to change. This acheives a "tight border" without messing up axes aspect ratios or spaces. - * Colors: Perceptually distinct named colors, powerful colormap-generating tools, ability to trivially swap between "color cycles" and "colormaps". A few new, beautiful colormaps and color cycles. Make colorbars from lists of lines or colors. - * Maps: Integration with basemap *and* cartopy. Generate arbitrary grids of map projections in one go. Switch between basemap and cartopy painlessly. Add geographical features as part of the `format` process. - -## Showcase - -For a showcase of all ProPlot features, check out [**this online jupyter notebook**](https://lukelbd.github.io/tools/proplot). - -## Documentation - -The full documentation can be found [**here**](https://lukelbd.github.io/tools/proplot_doc). It is a work-in-progress. - -## Installation -This package is a work-in-progress. Currently there is no formal releas on PyPi. However, feel free to install directly from Github using: - -``` -pip install git+https://github.com/lukelbd/proplot.git#egg=proplot -``` - -I only push to this repo when new features are completed and working properly. - -Dependencies are `matplotlib` and `numpy`. The geographic mapping mapping features require `basemap` or `cartopy`. Note that [basemap is no longer under active development](https://matplotlib.org/basemap/users/intro.html#cartopy-new-management-and-eol-announcement) -- cartopy is integrated more intelligently with the matplotlib API. -However, for the time being, basemap *retains several advantages* over cartopy (namely [more tools for labeling meridians/parallels](https://github.com/SciTools/cartopy/issues/881) and more available projections -- see [basemap](https://matplotlib.org/basemap/users/mapsetup.html) vs. [cartopy](https://scitools.org.uk/cartopy/docs/v0.15/crs/projections.html)). Therefore, I decided to support both. - - -## How is this different from seaborn? -There is already a great matplotlib wrapper called [seaborn](https://seaborn.pydata.org/). What makes this project different? - -While parts of `proplot` were inspired by seaborn (in particular much of `colors.py` is drawn from seaborn's `palettes.py`), the goal for this project was quite different -- it is intended to simplify the task of making publication-quality figures, and no more. - -Seaborn largely attempts to merge the tasks of data analysis and visualization, and many of its features require neatly tabulated data in a standard form. -ProPlot contains no analysis tools -- it is expected that you analyze your data on your own time. - -Anyway, as an atmospheric scientist, the datasets I use usually do not lend themselves to fitting in a simple DataFrame -- so this seaborn feature was not particularly useful for me. -For data analysis tools I use in my physical climatology research, check out my [ClimPy](https://github.com/lukelbd/climpy`) project (still in preliminary stages). - -By focusing on this single task, I was able to create a number of powerful features beyond the scope of `seaborn`. See the documentation and showcase for details. - -## Donations -This package took a shocking amount of time to write. If you've found it useful, feel free to buy me a cup of coffee :) - -[![Donate](https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=5SP6S8RZCYMQA&source=url) diff --git a/README.rst b/README.rst new file mode 100644 index 000000000..e2b40653a --- /dev/null +++ b/README.rst @@ -0,0 +1,166 @@ +.. image:: https://raw.githubusercontent.com/Ultraplot/ultraplot/refs/heads/main/UltraPlotLogo.svg + :alt: UltraPlot Logo + :width: 100% + +|downloads| |build-status| |coverage| |docs| |pypi| |code-style| |pre-commit| |pr-welcome| |license| |zenodo| + +A succinct `matplotlib `__ wrapper for making beautiful, +publication-quality graphics. It builds upon ProPlot_ and transports it into the modern age (supporting mpl 3.9.0+). + +.. _ProPlot: https://github.com/proplot-dev/ + +Why UltraPlot? | Write Less, Create More +========================================= +.. image:: https://raw.githubusercontent.com/Ultraplot/ultraplot/refs/heads/main/logo/whyUltraPlot.svg + :width: 100% + :alt: Comparison of ProPlot and UltraPlot + :align: center + +Checkout our examples +===================== + +Below is a gallery showing random examples of what UltraPlot can do, for more examples checkout our extensive `docs `_. + +.. list-table:: + :widths: 33 33 33 + :header-rows: 0 + + * - .. image:: https://ultraplot.readthedocs.io/en/latest/_static/example_plots/subplot_example.svg + :alt: Subplots & Layouts + :target: https://ultraplot.readthedocs.io/en/latest/subplots.html + :width: 100% + :height: 200px + + **Subplots & Layouts** + + Create complex multi-panel layouts effortlessly. + + - .. image:: https://ultraplot.readthedocs.io/en/latest/_static/example_plots/cartesian_example.svg + :alt: Cartesian Plots + :target: https://ultraplot.readthedocs.io/en/latest/cartesian.html + :width: 100% + :height: 200px + + **Cartesian Plots** + + Easily generate clean, well-formatted plots. + + - .. image:: https://ultraplot.readthedocs.io/en/latest/_static/example_plots/projection_example.svg + :alt: Projections & Maps + :target: https://ultraplot.readthedocs.io/en/latest/projections.html + :width: 100% + :height: 200px + + **Projections & Maps** + + Built-in support for projections and geographic plots. + + * - .. image:: https://ultraplot.readthedocs.io/en/latest/_static/example_plots/colorbars_legends_example.svg + :alt: Colorbars & Legends + :target: https://ultraplot.readthedocs.io/en/latest/colorbars_legends.html + :width: 100% + :height: 200px + + **Colorbars & Legends** + + Customize legends and colorbars with ease. + + - .. image:: https://ultraplot.readthedocs.io/en/latest/_static/example_plots/panels_example.svg + :alt: Insets & Panels + :target: https://ultraplot.readthedocs.io/en/latest/insets_panels.html + :width: 100% + :height: 200px + + **Insets & Panels** + + Add inset plots and panel-based layouts. + + - .. image:: https://ultraplot.readthedocs.io/en/latest/_static/example_plots/colormaps_example.svg + :alt: Colormaps & Cycles + :target: https://ultraplot.readthedocs.io/en/latest/colormaps.html + :width: 100% + :height: 200px + + **Colormaps & Cycles** + + Visually appealing, perceptually uniform colormaps. + + +Documentation +============= + +The documentation is `published on readthedocs `__. + +Installation +============ + +UltraPlot is published on `PyPi `__ and +`conda-forge `__. It can be installed with ``pip`` or +``conda`` as follows: + +.. code-block:: bash + + pip install ultraplot + conda install -c conda-forge ultraplot + +Likewise, an existing installation of UltraPlot can be upgraded +to the latest version with: + +.. code-block:: bash + + pip install --upgrade ultraplot + conda upgrade ultraplot + +To install a development version of UltraPlot, you can use +``pip install git+https://github.com/ultraplot/ultraplot.git`` +or clone the repository and run ``pip install -e .`` +inside the ``ultraplot`` folder. + +If you use UltraPlot in your research, please cite it using the following BibTeX entry:: + + @software{vanElteren2025, + author = {Casper van Elteren and Matthew R. Becker}, + title = {UltraPlot: A succinct wrapper for Matplotlib}, + year = {2025}, + version = {1.57.1}, + publisher = {GitHub}, + url = {https://github.com/Ultraplot/UltraPlot} + } + +.. |downloads| image:: https://static.pepy.tech/personalized-badge/UltraPlot?period=total&units=international_system&left_color=black&right_color=orange&left_text=Downloads + :target: https://pepy.tech/project/ultraplot + :alt: Downloads + +.. |build-status| image:: https://github.com/ultraplot/ultraplot/actions/workflows/build-ultraplot.yml/badge.svg + :target: https://github.com/ultraplot/ultraplot/actions/workflows/build-ultraplot.yml + :alt: Build Status + +.. |coverage| image:: https://codecov.io/gh/Ultraplot/ultraplot/graph/badge.svg?token=C6ZB7Q9II4&style=flat&color=53C334 + :target: https://codecov.io/gh/Ultraplot/ultraplot + :alt: Coverage + +.. |docs| image:: https://readthedocs.org/projects/ultraplot/badge/?version=latest&style=flat&color=4F5D95 + :target: https://ultraplot.readthedocs.io/en/latest/?badge=latest + :alt: Docs + +.. |pypi| image:: https://img.shields.io/pypi/v/ultraplot?style=flat&color=53C334&logo=pypi + :target: https://pypi.org/project/ultraplot/ + :alt: PyPI + +.. |code-style| image:: https://img.shields.io/badge/code%20style-black-000000.svg?style=flat&logo=python + :alt: Code style: black + +.. |pre-commit| image:: https://results.pre-commit.ci/badge/github/Ultraplot/ultraplot/main.svg + :target: https://results.pre-commit.ci/latest/github/Ultraplot/ultraplot/main + :alt: pre-commit.ci status + +.. |pr-welcome| image:: https://img.shields.io/badge/PRs-welcome-f77f00?style=flat&logo=github + :alt: PRs Welcome + +.. |license| image:: https://img.shields.io/github/license/ultraplot/ultraplot.svg?style=flat&color=808080 + :target: LICENSE.txt + :alt: License + +.. |zenodo| image:: https://zenodo.org/badge/909651179.svg + :target: https://doi.org/10.5281/zenodo.15733564 + :alt: DOI diff --git a/UltraPlotLogo.svg b/UltraPlotLogo.svg new file mode 100644 index 000000000..bef8720ec --- /dev/null +++ b/UltraPlotLogo.svg @@ -0,0 +1,1904 @@ + + + + + + + + 2024-12-29T13:14:24.991336 + image/svg+xml + + + Matplotlib v3.9.3, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/__init__.py b/__init__.py deleted file mode 100644 index 6c3987af5..000000000 --- a/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env python3 -#------------------------------------------------------------------------------# -# This makes package locally importable, as long as -# this directory is on PYTHONPATH. -#------------------------------------------------------------------------------# -from .proplot import * diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 000000000..e83811025 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,33 @@ +codecov: + notify: + require_ci_to_pass: no + +coverage: + status: + patch: + default: + target: 80.0% + if_no_uploads: error + if_not_found: success + if_ci_failed: failure + + project: + default: false + library: + target: auto + threshold: 0.5% + if_no_uploads: error + if_not_found: success + if_ci_failed: failure + paths: + - "!logo/*" + - "!docs/*" + - "!ultraplot/tests/*" + + tests: + target: 95.0% + paths: + - "ultraplot/tests/*" + - "!logo/*" + - "!docs/*" + - "!ultraplot/tests/conftest.py" diff --git a/docs/1dplots.py b/docs/1dplots.py new file mode 100644 index 000000000..47cc1a820 --- /dev/null +++ b/docs/1dplots.py @@ -0,0 +1,651 @@ +# --- +# 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 +# ==================== +# +# UltraPlot adds :ref:`several new features ` to matplotlib's +# plotting commands using the intermediate :class:`~ultraplot.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 :func:`~ultraplot.axes.PlotAxes.plot`, :func:`~ultraplot.axes.PlotAxes.scatter`, +# and :func:`~ultraplot.axes.PlotAxes.bar`. + + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_1dstd: +# +# Data arguments +# -------------- +# +# The treatment of data arguments passed to the 1D :class:`~ultraplot.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 +# :class:`~ultraplot.ticker.IndexLocator` and :class:`~ultraplot.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, +# UltraPlot ignores out-of-bounds data along the other axis if it was explicitly +# fixed by :func:`~matplotlib.axes.Axes.set_xlim` or :func:`~matplotlib.axes.Axes.set_ylim` (or, +# equivalently, by passing `xlim` or `ylim` to :func:`ultraplot.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 ultraplot as uplt +import numpy as np + +N = 5 +state = np.random.RandomState(51423) +with uplt.rc.context({"axes.prop_cycle": uplt.Cycle("Grays", N=N, left=0.3)}): + # Sample data + x = np.linspace(-5, 5, N) + y = state.rand(N, 5) + fig = uplt.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 ultraplot as uplt +import numpy as np + +# Sample data +cycle = uplt.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 = uplt.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=uplt.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 :class:`~ultraplot.axes.PlotAxes` commands recognize `pandas`_ +# and `xarray`_ data structures. If you omit dependent variable coordinates, +# the commands try to infer them from the :class:`pandas.Series`, :class:`pandas.DataFrame`, +# or :class:`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 :class:`pandas.DataFrame` or :class:`xarray.DataArray`. +# The commands also recognize :class:`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 UltraPlot. 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 :class:`~xarray.Dataset`, :class:`~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 :class:`pint.Quantity` or :class:`~xarray.DataArray` +# containing a :class:`pint.Quantity`, UltraPlot will automatically call +# :func:`~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 ultraplot as uplt + +fig = uplt.figure(share=False, suptitle="Automatic subplot formatting") + +# Plot DataArray +cycle = uplt.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 = uplt.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 :class:`~ultraplot.axes.PlotAxes` commands. `cycle` and `cycle_kw` are +# passed to the :class:`~ultraplot.constructor.Cycle` :ref:`constructor function +# `, and the resulting property cycle is used for the plot. You +# can specify `cycle` once with 3D 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 ultraplot as uplt +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 uplt.rc.context({"lines.linewidth": 3}): + # Use property cycle for columns of 2D input data + fig = uplt.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 :func:`~ultraplot.axes.PlotAxes.plot` or +# :func:`~ultraplot.axes.PlotAxes.plotx` (or their aliases, :func:`~ultraplot.axes.PlotAxes.line` +# or :func:`~ultraplot.axes.PlotAxes.linex`). For the ``x`` commands, positional +# arguments are interpreted as *x* coordinates or (*y*, *x*) pairs. This is analogous +# to :func:`~ultraplot.axes.PlotAxes.barh` and :func:`~ultraplot.axes.PlotAxes.fill_betweenx`. +# Also, the default *x* bounds for lines drawn with :func:`~ultraplot.axes.PlotAxes.plot` +# and *y* bounds for lines drawn with :func:`~ultraplot.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 :func:`~ultraplot.axes.PlotAxes.step`, +# :func:`~ultraplot.axes.PlotAxes.stepx`, :func:`~ultraplot.axes.PlotAxes.stem`, and +# :func:`~ultraplot.axes.PlotAxes.stemx`. Plots of parallel vertical and horizontal +# lines can be drawn with :func:`~ultraplot.axes.PlotAxes.vlines` and +# :func:`~ultraplot.axes.PlotAxes.hlines`. You can have different colors for "negative" and +# "positive" lines using ``negpos=True`` (see :ref:`below ` for details). + +# %% +import ultraplot as uplt +import numpy as np + +state = np.random.RandomState(51423) +gs = uplt.GridSpec(nrows=3, ncols=2) +fig = uplt.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 :func:`~ultraplot.axes.PlotAxes.scatter` command now permits omitting *x* +# coordinates and accepts 2D *y* coordinates, just like :func:`~ultraplot.axes.PlotAxes.plot`. +# As with :func:`~ultraplot.axes.PlotAxes.plotx`, the :func:`~ultraplot.axes.PlotAxes.scatterx` +# command is just like :func:`~ultraplot.axes.PlotAxes.scatter`, except positional +# arguments are interpreted as *x* coordinates and (*y*, *x*) pairs. +# :func:`~ultraplot.axes.PlotAxes.scatter` also now accepts keywords +# that look like :func:`~ultraplot.axes.PlotAxes.plot` keywords (e.g., `color` instead of +# `c` and `markersize` instead of `s`). This way, :func:`~ultraplot.axes.PlotAxes.scatter` +# can be used to simply "plot markers, not lines" without changing the input +# arguments relative to :func:`~ultraplot.axes.PlotAxes.plot`. +# +# The property cycler used by :func:`~ultraplot.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 +# :class:`~ultraplot.constructor.Colormap` and :class:`~ultraplot.constructor.Norm` constructor +# functions. + +# .. important:: +# +# In matplotlib, arrays passed to the marker size keyword `s` always represent the +# area in units ``points ** 2``. In UltraPlot, 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 :func:`~ultraplot.axes.PlotAxes.scatter` +# or :func:`~ultraplot.axes.PlotAxes.scatterx`. This is done automatically when `seaborn`_ +# calls :func:`~ultraplot.axes.PlotAxes.scatter` internally. + +# %% +import ultraplot as uplt +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 = uplt.GridSpec(ncols=2, nrows=2) +fig = uplt.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 :func:`~ultraplot.axes.PlotAxes.parametric` +# command. This creates :class:`~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 :func:`~ultraplot.axes.PlotAxes.parametric` +# plot with a colorbar indicating the parametric coordinate. + +# %% +import ultraplot as uplt +import numpy as np +import pandas as pd + +gs = uplt.GridSpec(ncols=2, wratios=(2, 1)) +fig = uplt.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 :func:`~ultraplot.axes.PlotAxes.bar` and :func:`~ultraplot.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 :func:`~ultraplot.axes.PlotAxes.fill_between` and :func:`~ultraplot.axes.PlotAxes.fill_betweenx` +# commands have the new shorthands :func:`~ultraplot.axes.PlotAxes.area` +# and :func:`~ultraplot.axes.PlotAxes.areax`. Similar to :func:`~ultraplot.axes.PlotAxes.bar` and +# :func:`~ultraplot.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 +# :func:`~ultraplot.axes.PlotAxes.area` and *y* bounds for shading drawn with +# :func:`~ultraplot.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 :func:`~matplotlib.axes.Axes.barh` plots +# are expressed with the `height` keyword. In UltraPlot, 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 UltraPlot 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 :func:`~ultraplot.axes.PlotAxes.bar` +# or :func:`~ultraplot.axes.PlotAxes.barh`. This is done automatically when `seaborn`_ calls +# :func:`~ultraplot.axes.PlotAxes.bar` or :func:`~ultraplot.axes.PlotAxes.barh` internally. + +# %% +import ultraplot as uplt +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 +uplt.rc.abc = "a." +uplt.rc.titleloc = "l" +gs = uplt.GridSpec(nrows=2, hratios=(3, 2)) +fig = uplt.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") +uplt.rc.reset() + +# %% +import ultraplot as uplt +import numpy as np + +# Sample data +state = np.random.RandomState(51423) +data = state.rand(5, 3).cumsum(axis=0) +cycle = ("gray3", "gray5", "gray7") + +# Figure +uplt.rc.abc = "a." +uplt.rc.titleloc = "l" +fig = uplt.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") +uplt.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 +# :func:`~ultraplot.axes.PlotAxes.fill_between`, :func:`~ultraplot.axes.PlotAxes.fill_betweenx` +# (shorthands :func:`~ultraplot.axes.PlotAxes.area`, :func:`~ultraplot.axes.PlotAxes.areax`), +# :func:`~ultraplot.axes.PlotAxes.vlines`, :func:`~ultraplot.axes.PlotAxes.hlines`, +# :func:`~ultraplot.axes.PlotAxes.bar`, or :func:`~ultraplot.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 :class:`~ultraplot.axes.PlotAxes` commands. + +# %% +import ultraplot as uplt +import numpy as np + +# Sample data +state = np.random.RandomState(51423) +data = 4 * (state.rand(40) - 0.5) + +# Figure +uplt.rc.abc = "a." +uplt.rc.titleloc = "l" +fig, axs = uplt.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 +uplt.rc.reset() diff --git a/docs/2dplots.py b/docs/2dplots.py new file mode 100644 index 000000000..edc22e97c --- /dev/null +++ b/docs/2dplots.py @@ -0,0 +1,715 @@ +# --- +# 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 +# ==================== +# +# UltraPlot adds :ref:`several new features ` to matplotlib's +# plotting commands using the intermediate :class:`~ultraplot.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 :func:`~ultraplot.axes.PlotAxes.contour`, :func:`~ultraplot.axes.PlotAxes.pcolor`, +# and :func:`~ultraplot.axes.PlotAxes.imshow`. +# +# .. important:: +# +# By default, UltraPlot automatically adjusts the width of +# :func:`~ultraplot.axes.PlotAxes.contourf` and :func:`~ultraplot.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 :class:`~ultraplot.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 :class:`~ultraplot.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 +# :class:`~ultraplot.ticker.IndexLocator` and :class:`~ultraplot.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 :func:`~ultraplot.axes.PlotAxes.pcolor` and :func:`~ultraplot.axes.PlotAxes.pcolormesh`, they +# are automatically converted to edges using :func:`~ultraplot.utils.edges` or +# `:func:`~ultraplot.utils.edges2d``, and if coordinate *edges* are passed to commands like +# :func:`~ultraplot.axes.PlotAxes.contour` and :func:`~ultraplot.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 `, UltraPlot ignores data outside the *x* or *y* axis +# limits if they were previously fixed by :func:`~matplotlib.axes.Axes.set_xlim` or +# :func:`~matplotlib.axes.Axes.set_ylim` (or, equivalently, by passing `xlim` or +# `ylim` to :func:`ultraplot.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 numpy as np + +import ultraplot as uplt + +# Sample data +state = np.random.RandomState(51423) +x = y = np.array([-10, -5, 0, 5, 10]) +xedges = uplt.edges(x) +yedges = uplt.edges(y) +data = state.rand(y.size, x.size) # "center" coordinates +lim = (np.min(xedges), np.max(xedges)) + +with uplt.rc.context({"cmap": "Grays", "cmap.levels": 21}): + # Figure + fig = uplt.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 numpy as np + +import ultraplot as uplt + +# 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 = uplt.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=uplt.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 :class:`~ultraplot.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 UltraPlot. 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`, :class:`~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 :class:`~xarray.DataArray` +# containing a `pint.Quantity`, UltraPlot will automatically call +# `~pint.UnitRegistry.setup_matplotlib` so that the axes become unit-aware. + +# %% +import numpy as np +import pandas as pd +import xarray as xr + +# 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 ultraplot as uplt + +fig = uplt.figure(refwidth=2.5, share=False, suptitle="Automatic subplot formatting") + +# Plot DataArray +cmap = uplt.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 :class:`~ultraplot.constructor.Colormap` +# :ref:`constructor function `. You can do so using the `cmap` +# and `cmap_kw` keywords, available with most :class:`~ultraplot.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" :class:`~ultraplot.colors.DiscreteColormap`\ s +# by passing lists of colors to the keyword `c`, `color`, or `colors`. +# +# UltraPlot 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 numpy as np + +import ultraplot as uplt + +# Sample data +N = 18 +state = np.random.RandomState(51423) +data = np.cumsum(state.rand(N, N), axis=0) + +# Custom defaults of each type +uplt.rc["cmap.sequential"] = "PuBuGn" +uplt.rc["cmap.diverging"] = "PiYG" +uplt.rc["cmap.cyclic"] = "bamO" +uplt.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 = uplt.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=uplt.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") +uplt.rc.reset() + +# %% +import numpy as np + +import ultraplot as uplt + +# Sample data +N = 20 +state = np.random.RandomState(51423) +data = np.cumsum(state.rand(N, N), axis=1) - 6 + +# Continuous "diverging" colormap +fig = uplt.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=uplt.arange(-6, 9, 3), + colors=["red5", "blue5", "yellow5", "gray5", "violet5"], + colorbar="b", +) + +import numpy as np + +import ultraplot as uplt + +# Sample data +N = 20 +state = np.random.RandomState(51423) +data = 11 ** (0.25 * np.cumsum(state.rand(N, N), axis=0)) + +# Create figure +gs = uplt.GridSpec(ncols=2) +fig = uplt.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 +# ------------------- +# +# UltraPlot includes two new :ref:`"continuous" normalizers `. The +# `~ultraplot.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 `~ultraplot.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 numpy as np + +import ultraplot as uplt + +# Sample data +state = np.random.RandomState(51423) +data = 11 ** (2 * state.rand(20, 20).cumsum(axis=0) / 7) + +# Linear segmented norm +fig, axs = uplt.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 numpy as np + +import ultraplot as uplt + +# 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 = uplt.subplots(nrows=2, ncols=2, refwidth=2.2, order="F") +axs.format(suptitle="Diverging normalizer demo") +cmap = uplt.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 = uplt.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, UltraPlot uses `~ultraplot.colors.DiscreteNorm` to "discretize" +# the possible colormap colors for contour and pseudocolor :class:`~ultraplot.axes.PlotAxes` +# commands (e.g., :func:`~ultraplot.axes.PlotAxes.contourf`, :func:`~ultraplot.axes.PlotAxes.pcolor`). +# This is analogous to `matplotlib.colors.BoundaryNorm`, except +# `~ultraplot.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. `~ultraplot.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 +# `~ultraplot.colors.DiscreteNorm` can be explicitly specified using the `levels` or +# `values` keywords, respectively (:func:`~ultraplot.utils.arange` and :func:`~ultraplot.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 :func:`~ultraplot.axes.PlotAxes.contour` plots). + +# %% +import numpy as np + +import ultraplot as uplt + +# 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 = uplt.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 numpy as np + +import ultraplot as uplt + +# Sample data +state = np.random.RandomState(51423) +data = (20 * (state.rand(20, 20) - 0.4).cumsum(axis=0).cumsum(axis=1)) % 360 +levels = uplt.arange(0, 360, 45) + +# Figure +gs = uplt.GridSpec(nrows=2, ncols=4, hratios=(1.5, 1)) +fig = uplt.figure(refwidth=2.4, right=2) +fig.format(suptitle="DiscreteNorm end-color standardization") + +# Cyclic colorbar with distinct end colors +cmap = uplt.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. UltraPlot adds the `robust` keyword to change this +# behavior, inspired by the `xarray keyword +# `__ +# of the same name. Passing ``robust=True`` to a :class:`~ultraplot.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 +# `__, +# UltraPlot can automatically detect "diverging" datasets. By default, +# the 2D :class:`~ultraplot.axes.PlotAxes` commands will apply the diverging colormap +# :rc:`cmap.diverging` (rather than :rc:`cmap.sequential`) and the diverging +# normalizer `~ultraplot.colors.DivergingNorm` (rather than :class:`~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 numpy as np + +import ultraplot as uplt + +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 = uplt.subplots( + nrows=2, ncols=2, refwidth=2, suptitle="Auto normalization demo" +) + +# Auto diverging +uplt.rc["cmap.sequential"] = "lapaz_r" +uplt.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 +uplt.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")) +uplt.rc.reset() + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_labels: +# +# Quick labels +# ------------ +# +# You can now quickly add labels to :func:`~ultraplot.axes.PlotAxes.contour`, +# :func:`~ultraplot.axes.PlotAxes.contourf`, :func:`~ultraplot.axes.PlotAxes.pcolor`, +# :func:`~ultraplot.axes.PlotAxes.pcolormesh`, and :func:`~ultraplot.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 :func:`~ultraplot.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 numpy as np +import pandas as pd + +import ultraplot as uplt + +# 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 = uplt.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 :func:`~ultraplot.axes.PlotAxes.heatmap` command can be used to draw "heatmaps" of +# 2-dimensional data. This is a convenience function equivalent to +# :func:`~ultraplot.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. :func:`~ultraplot.axes.PlotAxes.heatmap` should generally only be used with +# `~ultraplot.axes.CartesianAxes`. + +# %% +import numpy as np +import pandas as pd + +import ultraplot as uplt + +# 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 = uplt.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 new file mode 100644 index 000000000..9cd3086b6 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,28 @@ +# Minimal makefile for Sphinx documentation +# You can set these variables from the command line. +# WARNING: Sphinx 2.0 fails with numpydoc style strings so far! +# See issue: https://github.com/readthedocs/sphinx_rtd_theme/issues/750 +# BUILDDIR = ../../ultraplot-doc # from gh-pages workflow +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SPHINXPROJ = UltraPlot +SOURCEDIR = . +BUILDDIR = _build + +.PHONY: help clean Makefile + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) + +# Make make clean ignore .git folder +# The -f doesn't raise error when files/folders not found +clean: + rm -rf api/* + rm -rf "$(BUILDDIR)"/html/* + rm -rf "$(BUILDDIR)"/doctrees/* + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. See: https://github.com/sphinx-doc/sphinx/issues/6603 +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) diff --git a/docs/_ext/notoc.py b/docs/_ext/notoc.py new file mode 100644 index 000000000..ccdb8e474 --- /dev/null +++ b/docs/_ext/notoc.py @@ -0,0 +1,21 @@ +from docutils.parsers.rst import Directive +from docutils import nodes + + +class NoTocDirective(Directive): + has_content = False + + def run(self): + # Create a raw HTML node to add the no-right-toc class to body + html = '' + return [nodes.raw("", html, format="html")] + + +def setup(app): + app.add_directive("notoc", NoTocDirective) + + return { + "version": "0.1", + "parallel_read_safe": True, + "parallel_write_safe": True, + } diff --git a/docs/_scripts/fetch_releases.py b/docs/_scripts/fetch_releases.py new file mode 100644 index 000000000..af964a814 --- /dev/null +++ b/docs/_scripts/fetch_releases.py @@ -0,0 +1,114 @@ +""" +Dynamically build what's new page based on github releases +""" + +import requests, re +from pathlib import Path + +GITHUB_REPO = "ultraplot/ultraplot" +OUTPUT_RST = Path("whats_new.rst") + + +GITHUB_API_URL = f"https://api.github.com/repos/{GITHUB_REPO}/releases" + + +def format_release_body(text): + """Formats GitHub release notes for better RST readability.""" + lines = text.strip().split("\n") + formatted = [] + + for line in lines: + line = line.strip() + + # Convert Markdown ## Headers to RST H2 + if line.startswith("## "): + title = line[3:].strip() # Remove "## " from start + formatted.append(f"{title}\n{'~' * len(title)}\n") # RST H2 Format + else: + formatted.append(line) + + # Convert PR references (remove "by @user in ..." but keep the link) + formatted_text = "\n".join(formatted) + formatted_text = re.sub( + r" by @\w+ in (https://github.com/[^\s]+)", r" (\1)", formatted_text + ) + + return formatted_text.strip() + + +def fetch_all_releases(): + """Fetches all GitHub releases across multiple pages.""" + releases = [] + page = 1 + + while True: + response = requests.get(GITHUB_API_URL, params={"per_page": 30, "page": page}) + if response.status_code != 200: + print(f"Error fetching releases: {response.status_code}") + break + + page_data = response.json() + # If the page is empty, stop fetching + if not page_data: + break + + releases.extend(page_data) + page += 1 + + return releases + + +def fetch_releases(): + """Fetches the latest releases from GitHub and formats them as RST.""" + releases = fetch_all_releases() + if not releases: + print(f"Error fetching releases!") + return "" + + header = "What's new?" + rst_content = f".. _whats_new:\n\n{header}\n{'=' * len(header)}\n\n" # H1 + + for release in releases: + # ensure title is formatted as {tag}: {title} + tag = release["tag_name"].lower() + title = release["name"] + if title.startswith(tag): + title = title[len(tag) :] + while title: + if not title[0].isalpha(): + title = title[1:] + title = title.strip() + else: + title = title.strip() + break + + if title: + title = f"{tag}: {title}" + else: + title = tag + + date = release["published_at"][:10] + body = format_release_body(release["body"] or "") + + # Version header (H2) + rst_content += f"{title} ({date})\n{'-' * (len(title) + len(date) + 3)}\n\n" + + # Process body content + rst_content += f"{body}\n\n" + + return rst_content + + +def write_rst(): + """Writes fetched releases to an RST file.""" + content = fetch_releases() + if content: + with open(OUTPUT_RST, "w", encoding="utf-8") as f: + f.write(content) + print(f"Updated {OUTPUT_RST}") + else: + print("No updates to write.") + + +if __name__ == "__main__": + write_rst() diff --git a/docs/_static/custom.css b/docs/_static/custom.css new file mode 100644 index 000000000..3732c6131 --- /dev/null +++ b/docs/_static/custom.css @@ -0,0 +1,225 @@ +.grid-item-card .card-img-top { + height: 100%; + object-fit: cover; + width: 100%; + background-color: slategrey; +} + +/* Make all cards with this class use flexbox for vertical layout */ +.card-with-bottom-text { + display: flex !important; + flex-direction: column !important; + height: 100% !important; +} + +/* Style the card content areas */ +.card-with-bottom-text .sd-card-body { + display: flex !important; + flex-direction: column !important; + flex-grow: 1 !important; +} + +/* Make images not grow or shrink */ +.card-with-bottom-text img { + flex-shrink: 0 !important; + margin-bottom: 0.5rem !important; +} + +/* Push the last paragraph to the bottom */ +.card-with-bottom-text .sd-card-body > p:last-child { + margin-top: auto !important; + padding-top: 0.5rem !important; + text-align: center !important; +} + +.img-container img { + object-fit: cover; + width: 100%; + height: 100%; +} + +/* .right-toc { + position: fixed; + top: 90px; + right: 20px; + width: 280px; + font-size: 0.9em; + max-height: calc(100vh - 150px); + background-color: #f8f9fa; + z-index: 100; + border-radius: 6px; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); + transition: all 0.3s ease; + border-left: 3px solid #2980b9; +} */ + +.right-toc-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 15px; + border-bottom: 1px solid #e1e4e5; +} + +.right-toc-title { + font-weight: 600; + font-size: 1.1em; + color: #2980b9; +} + +.right-toc-buttons { + display: flex; + align-items: center; +} + +.right-toc-toggle-btn { + background: none; + border: none; + color: #2980b9; + font-size: 16px; + cursor: pointer; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 3px; + padding: 0; + transition: background-color 0.2s; +} + +.right-toc-toggle-btn:hover { + background-color: rgba(41, 128, 185, 0.1); +} + +.right-toc-content { + padding: 15px 15px 15px 20px; + overflow-y: auto; + max-height: calc(100vh - 200px); +} + +.right-toc-list { + list-style-type: none; + padding-left: 0; + margin: 0; +} + +.right-toc-link { + display: block; + padding: 5px 0; + text-decoration: none; + color: #404040; + border-radius: 4px; + transition: all 0.2s ease; + margin-bottom: 3px; +} + +.right-toc-link:hover { + background-color: rgba(41, 128, 185, 0.1); + padding-left: 5px; + color: #2980b9; +} + +.right-toc-level-h1 { + font-weight: 600; + font-size: 1em; +} + +.right-toc-level-h2 { + padding-left: 1.2em; + font-size: 0.95em; +} + +.right-toc-level-h3 { + padding-left: 2.4em; + font-size: 0.9em; + color: #606060; +} + +/* Active TOC item highlighting */ +.right-toc-link.active { + background-color: rgba(41, 128, 185, 0.15); + color: #2980b9; + font-weight: 500; + padding-left: 5px; +} + +/* Collapsed state */ +.right-toc-collapsed { + width: auto; + border-left-width: 0; +} + +.right-toc-collapsed .right-toc-header { + border-bottom: none; + padding: 8px 12px; +} + +/* Scrollbar styling */ +.right-toc-content::-webkit-scrollbar { + width: 5px; +} + +.right-toc-content::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 10px; +} + +.right-toc-content::-webkit-scrollbar-thumb { + background: #cdcdcd; + border-radius: 10px; +} + +.right-toc-content::-webkit-scrollbar-thumb:hover { + background: #9e9e9e; +} + +.toc-wrapper { + position: relative; + display: flex; + max-width: 1100px; /* match .rst-content if needed */ + margin: 0 auto; + gap: 20px; +} + +.rst-content { + flex: 1; +} +.right-toc { + position: fixed; + top: 90px; + width: 280px; + left: 1125px; + font-size: 0.9em; + background-color: #f8f9fa; + z-index: 100; + border-radius: 6px; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); + border-left: 3px solid #2980b9; + transition: all 0.3s ease; + max-height: calc(100vh - 150px); +} + +/* Responsive adjustments */ +@media screen and (max-width: 1200px) { + .right-toc { + width: 230px; + } +} + +@media screen and (max-width: 992px) { + .right-toc { + display: none; /* Hide on smaller screens */ + } +} + +.output_area.docutils.container { + text-align: center; /* Centers inline or inline-block children horizontally */ +} + +/* Optional: Ensure image respects container boundaries */ +.output_area.docutils.container img { + width: 100%; + height: auto; + display: block; +} diff --git a/docs/_static/custom.js b/docs/_static/custom.js new file mode 100644 index 000000000..ef54f6847 --- /dev/null +++ b/docs/_static/custom.js @@ -0,0 +1,175 @@ +document.addEventListener("DOMContentLoaded", function () { + // Check if current page has opted out of the TOC + if (document.body.classList.contains("no-right-toc")) { + return; + } + + const content = document.querySelector(".rst-content"); + if (!content) return; + + // Find all headers in the main content + const headers = Array.from( + content.querySelectorAll("h1:not(.document-title), h2, h3"), + ).filter((header) => !header.classList.contains("no-toc")); + + // Only create TOC if there are headers + if (headers.length === 0) return; + + // Create TOC container + const toc = document.createElement("div"); + toc.className = "right-toc"; + toc.innerHTML = + '
' + + '
On This Page
' + + '
' + + '' + + "
" + + '
    '; + + const tocList = toc.querySelector(".right-toc-list"); + const tocContent = toc.querySelector(".right-toc-content"); + const tocToggleBtn = toc.querySelector( + ".right-toc-toggle-btn", + ); + + // Set up the toggle button + tocToggleBtn.addEventListener("click", function () { + if (tocContent.style.display === "none") { + tocContent.style.display = "block"; + tocToggleBtn.textContent = "−"; + toc.classList.remove("right-toc-collapsed"); + localStorage.setItem("tocVisible", "true"); + } else { + tocContent.style.display = "none"; + tocToggleBtn.textContent = "+"; + toc.classList.add("right-toc-collapsed"); + localStorage.setItem("tocVisible", "false"); + } + }); + + // Check saved state + if (localStorage.getItem("tocVisible") === "false") { + tocContent.style.display = "none"; + tocToggleBtn.textContent = "+"; + toc.classList.add("right-toc-collapsed"); + } + + // Track used IDs to avoid duplicates + const usedIds = new Set(); + + // Get all existing IDs in the document + document.querySelectorAll("[id]").forEach((el) => { + usedIds.add(el.id); + }); + + // Generate unique IDs for headers that need them + headers.forEach((header, index) => { + // If header already has a unique ID, use that + if (header.id && !usedIds.has(header.id)) { + usedIds.add(header.id); + return; + } + + // Create a slug from the header text + let headerText = header.textContent || ""; + + // Clean the text (remove icons and special characters) + headerText = headerText.replace(/\s*\uf0c1\s*$/, ""); + headerText = headerText.replace(/\s*[¶§#†‡]\s*$/, ""); + headerText = headerText.trim(); + + let slug = headerText + .toLowerCase() + .replace(/[^\w\s-]/g, "") + .replace(/\s+/g, "-") + .replace(/--+/g, "-") + .trim(); + + // Make sure slug is not empty + if (!slug) { + slug = "section"; + } + + // Ensure the ID is unique + let uniqueId = slug; + let counter = 1; + + while (usedIds.has(uniqueId)) { + uniqueId = `${slug}-${counter}`; + counter++; + } + + // Set the unique ID and add to our tracking set + header.id = uniqueId; + usedIds.add(uniqueId); + }); + + // Add entries for each header + headers.forEach((header) => { + const item = document.createElement("li"); + const link = document.createElement("a"); + + link.href = "#" + header.id; + + // Get clean text without icons + let headerText = header.textContent || ""; + headerText = headerText.replace(/\s*\uf0c1\s*$/, ""); + headerText = headerText.replace(/\s*[¶§#†‡]\s*$/, ""); + + link.textContent = headerText.trim(); + link.className = + "right-toc-link right-toc-level-" + + header.tagName.toLowerCase(); + + item.appendChild(link); + tocList.appendChild(item); + }); + + // Add TOC to page + document.body.appendChild(toc); + + // Add active link highlighting + const tocLinks = document.querySelectorAll(".right-toc-link"); + const headerElements = Array.from(headers); + + if (tocLinks.length > 0 && headerElements.length > 0) { + // Highlight the current section on scroll + window.addEventListener( + "scroll", + debounce(function () { + let currentSection = null; + let smallestDistanceFromTop = Infinity; + + headerElements.forEach((header) => { + const distance = Math.abs( + header.getBoundingClientRect().top, + ); + if (distance < smallestDistanceFromTop) { + smallestDistanceFromTop = distance; + currentSection = header.id; + } + }); + + tocLinks.forEach((link) => { + link.classList.remove("active"); + if ( + link.getAttribute("href") === `#${currentSection}` + ) { + link.classList.add("active"); + } + }); + }, 100), + ); + } +}); + +// Debounce function to limit scroll event firing +function debounce(func, wait) { + let timeout; + return function () { + const context = this; + const args = arguments; + clearTimeout(timeout); + timeout = setTimeout(() => func.apply(context, args), wait); + }; +} diff --git a/docs/_static/example_plots/cartesian_example.svg b/docs/_static/example_plots/cartesian_example.svg new file mode 100644 index 000000000..01e85b7d1 --- /dev/null +++ b/docs/_static/example_plots/cartesian_example.svg @@ -0,0 +1,2822 @@ + + + + + + + + 2025-04-06T22:10:31.205536 + image/svg+xml + + + Matplotlib v3.9.4, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/_static/example_plots/colorbars_legends_example.svg b/docs/_static/example_plots/colorbars_legends_example.svg new file mode 100644 index 000000000..baa3437a0 --- /dev/null +++ b/docs/_static/example_plots/colorbars_legends_example.svg @@ -0,0 +1,10066 @@ + + + + + + + + 2025-04-06T22:10:13.472963 + image/svg+xml + + + Matplotlib v3.9.4, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/_static/example_plots/colormaps_example.svg b/docs/_static/example_plots/colormaps_example.svg new file mode 100644 index 000000000..5aa11a021 --- /dev/null +++ b/docs/_static/example_plots/colormaps_example.svg @@ -0,0 +1,39886 @@ + + + + + + + + 2025-04-06T22:10:41.993025 + image/svg+xml + + + Matplotlib v3.9.4, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/_static/example_plots/panels_example.svg b/docs/_static/example_plots/panels_example.svg new file mode 100644 index 000000000..a2ef05f8e --- /dev/null +++ b/docs/_static/example_plots/panels_example.svg @@ -0,0 +1,6472 @@ + + + + + + + + 2025-04-06T22:15:27.253835 + image/svg+xml + + + Matplotlib v3.9.4, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/_static/example_plots/projection_example.svg b/docs/_static/example_plots/projection_example.svg new file mode 100644 index 000000000..7143e5050 --- /dev/null +++ b/docs/_static/example_plots/projection_example.svg @@ -0,0 +1,10840 @@ + + + + + + + + 2025-04-06T22:15:36.054830 + image/svg+xml + + + Matplotlib v3.9.4, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/_static/example_plots/subplot_example.svg b/docs/_static/example_plots/subplot_example.svg new file mode 100644 index 000000000..e504aba85 --- /dev/null +++ b/docs/_static/example_plots/subplot_example.svg @@ -0,0 +1,1488 @@ + + + + + + + + 2025-04-06T22:22:36.450669 + image/svg+xml + + + Matplotlib v3.9.4, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/_static/logo_blank.svg b/docs/_static/logo_blank.svg new file mode 100644 index 000000000..932660f92 --- /dev/null +++ b/docs/_static/logo_blank.svg @@ -0,0 +1,1507 @@ + + + + + + + + 2025-04-09T15:42:08.110875 + image/svg+xml + + + Matplotlib v3.10.1, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/_static/logo_long.png b/docs/_static/logo_long.png new file mode 100644 index 000000000..79028e65e Binary files /dev/null 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..afdd3d11c --- /dev/null +++ b/docs/_static/logo_long.svg @@ -0,0 +1,1468 @@ + + + + + + + + 2025-01-04T11:16:59.222545 + image/svg+xml + + + Matplotlib v3.8.4, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/_static/logo_social.png b/docs/_static/logo_social.png new file mode 100644 index 000000000..c11651981 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..afdd3d11c --- /dev/null +++ b/docs/_static/logo_social.svg @@ -0,0 +1,1468 @@ + + + + + + + + 2025-01-04T11:16:59.222545 + image/svg+xml + + + Matplotlib v3.8.4, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/_static/logo_square.png b/docs/_static/logo_square.png new file mode 100644 index 000000000..c11651981 Binary files /dev/null and b/docs/_static/logo_square.png differ diff --git a/docs/about.rst b/docs/about.rst new file mode 100644 index 000000000..cdf305104 --- /dev/null +++ b/docs/about.rst @@ -0,0 +1,52 @@ +.. _about: + +About UltraPlot +================= + +History +------- + +UltraPlot began in 2025 as a fork of `ProPlot `__, +an excellent plotting package created by Luke Davis. Building upon ProPlot's solid foundation, +UltraPlot was developed to extend its capabilities with new features focused on advanced +statistical visualization, enhanced interactive elements, and improved integration with +modern data science workflows. + +ProPlot Legacy +------------- + +UltraPlot stands on the shoulders of ProPlot, which was developed by Luke Davis starting in 2021. +As a PhD candidate at Colorado State University's Department of Atmospheric Science, Luke +created ProPlot to address the challenges of repetitive and cumbersome plotting code in +scientific research. ProPlot itself evolved from Luke's earlier MATLAB plotting utilities. + +Riley Brady, an early ProPlot user, made significant contributions to the original project, +helping with automatic testing, PyPi deployment, and improving the new user experience. + +The UltraPlot team is grateful to all ProPlot contributors who laid the groundwork for +this next generation of scientific visualization tools. + +Proplot's Original Contributors +------------------------------- + +* `Luke Davis`_ (Original ProPlot creator) +* `Riley Brady`_ +* `Mark Harfouche`_ +* `Stephane Raynaud`_ +* `Mickaël Lalande`_ +* `Pratiman Patel`_ +* `Zachary Moon`_ + +.. _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/api.rst b/docs/api.rst new file mode 100644 index 000000000..6b9c718c7 --- /dev/null +++ b/docs/api.rst @@ -0,0 +1,145 @@ +.. _api: + +============= +API reference +============= + +The comprehensive API reference. All of the below objects are imported +into the top-level namespace. Use ``help(uplt.object)`` to read +the docs during a python session. + +Please note that UltraPlot removes the associated documentation when functionality +is deprecated (see :ref:`What's New `). However, UltraPlot adheres to +`effort versioning `__, which means old code that uses +deprecated functionality will still work and issue warnings rather than errors. + +.. important:: + + The documentation for "wrapper" functions like `standardize_1d` and `cmap_changer` + from UltraPlot < 0.8.0 can now be found under individual :class:`~ultraplot.axes.PlotAxes` + methods like :func:`~ultraplot.axes.PlotAxes.plot` and :func:`~ultraplot.axes.PlotAxes.pcolor`. Note + that calling ``help(ax.method)`` in a python session will show both the UltraPlot + documentation and the original matplotlib documentation. + +Figure class +============ + +.. automodule:: ultraplot.figure + :no-private-members: + +.. automodsumm:: ultraplot.figure + :toctree: api + + +Grid classes +============ + +.. automodule:: ultraplot.gridspec + :no-private-members: + +.. automodsumm:: ultraplot.gridspec + :toctree: api + :skip: SubplotsContainer + + +Axes classes +============ + +.. automodule:: ultraplot.axes + :no-private-members: + +.. automodsumm:: ultraplot.axes + :toctree: api + +Top-level functions +=================== + +.. automodule:: ultraplot.ui + :no-private-members: + +.. automodsumm:: ultraplot.ui + :toctree: api + + +Configuration tools +=================== + +.. automodule:: ultraplot.config + :no-private-members: + +.. automodsumm:: ultraplot.config + :toctree: api + :skip: inline_backend_fmt, RcConfigurator + + +Constructor functions +===================== + +.. automodule:: ultraplot.constructor + :no-private-members: + +.. automodsumm:: ultraplot.constructor + :toctree: api + :skip: Colors + + +Locators and formatters +======================= + +.. automodule:: ultraplot.ticker + :no-private-members: + +.. automodsumm:: ultraplot.ticker + :toctree: api + + +Axis scale classes +================== + +.. automodule:: ultraplot.scale + :no-private-members: + +.. automodsumm:: ultraplot.scale + :toctree: api + + +Colormaps and normalizers +========================= + +.. automodule:: ultraplot.colors + :no-private-members: + +.. automodsumm:: ultraplot.colors + :toctree: api + :skip: ListedColormap, LinearSegmentedColormap, PerceptuallyUniformColormap, LinearSegmentedNorm + + +Projection classes +================== + +.. automodule:: ultraplot.proj + :no-private-members: + +.. automodsumm:: ultraplot.proj + :toctree: api + + +Demo functions +============== + +.. automodule:: ultraplot.demos + :no-private-members: + +.. automodsumm:: ultraplot.demos + :toctree: api + + +Miscellaneous functions +======================= + +.. automodule:: ultraplot.utils + :no-private-members: + +.. automodsumm:: ultraplot.utils + :toctree: api + :skip: shade, saturate diff --git a/docs/basics.py b/docs/basics.py new file mode 100644 index 000000000..95a747b0a --- /dev/null +++ b/docs/basics.py @@ -0,0 +1,557 @@ +# --- +# 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 +# ---------------- +# +# UltraPlot works by `subclassing +# `__ +# three fundamental matplotlib classes: :class:`~ultraplot.figure.Figure` replaces +# :class:`matplotlib.figure.Figure`, :class:`~ultraplot.axes.Axes` replaces :class:`matplotlib.axes.Axes`, +# and :class:`~ultraplot.gridspec.GridSpec` replaces :class:`matplotlib.gridspec.GridSpec` +# (see this `tutorial +# `__ +# for more on gridspecs). +# +# To make plots with these classes, you must start with the top-level commands +# :func:`~ultraplot.ui.figure`, :func:`~ultraplot.ui.subplot`, or :func:`~ultraplot.ui.subplots`. These are +# modeled after the :mod:`~matplotlib.pyplot` commands of the same name. As in +# :mod:`~matplotlib.pyplot`, :func:`~ultraplot.ui.subplot` creates a figure and a single +# subplot, :func:`~ultraplot.ui.subplots` creates a figure and a grid of subplots, and +# :func:`~ultraplot.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:: +# +# UltraPlot 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). +# UltraPlot also controls the appearance of figures in Jupyter notebooks +# using the new :rcraw:`inlineformat` setting, which is passed to +# :func:`~ultraplot.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. +# +# UltraPlot 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, UltraPlot 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 ultraplot as uplt + +state = np.random.RandomState(51423) +data = 2 * (state.rand(100, 5) - 0.5).cumsum(axis=0) +fig, ax = uplt.subplot(suptitle="Single subplot", xlabel="x axis", ylabel="y axis") +# fig = uplt.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 +# :class:`~ultraplot.axes.Axes`. To add subplots all at once, use +# :func:`~ultraplot.figure.Figure.add_subplots` (or its shorthand, +# :func:`~ultraplot.figure.Figure.subplots`). Note that under the hood, the top-level +# UltraPlot command :func:`~ultraplot.ui.subplots` simply calls :func:`~ultraplot.ui.figure` +# followed by :func:`~ultraplot.figure.Figure.add_subplots`. +# +# * With no arguments, :func:`~ultraplot.figure.Figure.add_subplots` returns a subplot +# generated from a 1-row, 1-column :class:`~ultraplot.gridspec.GridSpec`. +# * With `ncols` or `nrows`, :func:`~ultraplot.figure.Figure.add_subplots` returns a +# simple grid of subplots from a :class:`~ultraplot.gridspec.GridSpec` with +# matching geometry in either row-major or column-major `order`. +# * With `array`, :func:`~ultraplot.figure.Figure.add_subplots` returns an arbitrarily +# complex grid of subplots from a :class:`~ultraplot.gridspec.GridSpec` with matching +# geometry. Here `array` is a 2D array representing a "picture" of the subplot +# layout, where each unique integer indicates a :class:`~matplotlib.gridspec.GridSpec` +# slot occupied by the corresponding subplot and ``0`` indicates an empty space. +# The returned subplots are contained in a :class:`~ultraplot.gridspec.SubplotGrid` +# (:ref:`see below ` for details). +# +# To add subplots one-by-one, use the :func:`~ultraplot.figure.Figure.add_subplot` +# command (or its shorthand :func:`~ultraplot.figure.Figure.subplot`). +# +# * With no arguments, :func:`~ultraplot.figure.Figure.add_subplot` returns a subplot +# generated from a 1-row, 1-column :class:`~ultraplot.gridspec.GridSpec`. +# * With integer arguments, :func:`~ultraplot.figure.Figure.add_subplot` returns +# a subplot matching the corresponding :class:`~ultraplot.gridspec.GridSpec` geometry, +# as in matplotlib. Note that unlike matplotlib, the geometry must be compatible +# with the geometry implied by previous :func:`~ultraplot.figure.Figure.add_subplot` calls. +# * With a :class:`~matplotlib.gridspec.SubplotSpec` generated by indexing a +# :class:`~ultraplot.gridspec.GridSpec`, :func:`~ultraplot.figure.Figure.add_subplot` returns a +# subplot at the corresponding location. Note that unlike matplotlib, only +# one :func:`~ultraplot.figure.Figure.gridspec` can be used with each figure. +# +# As in matplotlib, to save figures, use :func:`~matplotlib.figure.Figure.savefig` (or its +# shorthand :func:`~ultraplot.figure.Figure.save`). User paths in the filename are expanded +# with :func:`~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:: +# +# UltraPlot 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. ``uplt.rc.update('subplots', share=False, span=False)``. See the +# :ref:`axis sharing section ` for details. + +# %% +# Simple subplot grid +import numpy as np +import ultraplot as uplt + +state = np.random.RandomState(51423) +data = 2 * (state.rand(100, 5) - 0.5).cumsum(axis=0) +fig = uplt.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 ultraplot as uplt + +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 = uplt.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 ultraplot as uplt + +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 = uplt.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.show() +# fig.save('~/example3.png') # save the figure +# fig.savefig('~/example3.png') # alternative + +# %% +# Using a GridSpec +import numpy as np +import ultraplot as uplt + +state = np.random.RandomState(51423) +data = 2 * (state.rand(100, 5) - 0.5).cumsum(axis=0) +gs = uplt.GridSpec(nrows=2, ncols=2, pad=1) +fig = uplt.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. :func:`~ultraplot.ui.subplots`, +# UltraPlot returns a :class:`~ultraplot.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 :func:`matplotlib.pyplot.subplots`: +# +# * :class:`~ultraplot.gridspec.SubplotGrid` behaves like a scalar when it is singleton. +# In other words, if you make a single subplot with ``fig, axs = uplt.subplots()``, +# then ``axs[0].method(...)`` is equivalent to ``axs.method(...)``. +# * :class:`~ultraplot.gridspec.SubplotGrid` permits list-like 1D indexing, e.g. ``axs[1]`` +# to return the second subplot. The subplots in the grid are sorted by +# :func:`~ultraplot.axes.Axes.number` (see :ref:`this page ` for details +# on changing the :func:`~ultraplot.axes.Axes.number` order). +# * :class:`~ultraplot.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 :class:`~ultraplot.gridspec.SubplotGrid` of every subplot +# in the first column. The 2D indexing is powered by the underlying +# :func:`~ultraplot.gridspec.SubplotGrid.gridspec`. +# +# :class:`~ultraplot.gridspec.SubplotGrid` includes methods for working +# simultaneously with different subplots. Currently, this includes +# the commands :func:`~ultraplot.gridspec.SubplotGrid.format`, +# :func:`~ultraplot.gridspec.SubplotGrid.panel_axes`, +# :func:`~ultraplot.gridspec.SubplotGrid.inset_axes`, +# :func:`~ultraplot.gridspec.SubplotGrid.altx`, and :func:`~ultraplot.gridspec.SubplotGrid.alty`. +# In the below example, we use :func:`~ultraplot.gridspec.SubplotGrid.format` on the grid +# returned by :func:`~ultraplot.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 :func:`~ultraplot.figure.Figure.subplot` or +# :func:`~ultraplot.figure.Figure.add_subplot`, a :class:`~ultraplot.gridspec.SubplotGrid` +# containing the numbered subplots is available via the +# :class:`~ultraplot.figure.Figure.subplotgrid` property. As with subplots made +# all-at-once, the subplots in the grid are sorted by :func:`~ultraplot.axes.Axes.number`. + +# %% +import ultraplot as uplt +import numpy as np + +state = np.random.RandomState(51423) + +# Selected subplots in a simple grid +fig, axs = uplt.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 = uplt.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 :method:`matplotlib.axes.Axes.plot`, and a MATLAB-style :mod:`~matplotlib.pyplot` interface +# with global commands like :func:`matplotlib.pyplot.plot` that track the "current" axes. +# UltraPlot builds upon the python-style interface using the `~ultraplot.axes.PlotAxes` +# class. Since every axes used by UltraPlot is a child of :class:`~ultraplot.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 :mod:`~matplotlib.pyplot` commands, this is not officially supported). +# +# For the most part, the features added by :class:`~ultraplot.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 +# UltraPlot'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 :func:`~ultraplot.axes.PlotAxes.plot` and +# :func:`~ultraplot.axes.PlotAxes.scatter`, along with the "2D" plotting commands +# :func:`~ultraplot.axes.PlotAxes.pcolormesh` and :func:`~ultraplot.axes.PlotAxes.contourf`. +# See the :ref:`1D plotting ` and :ref:`2D plotting ` +# sections for details on the features added by UltraPlot. + + +# %% +import ultraplot as uplt +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 = uplt.Cycle("greys", left=0.2, N=5) +fig, axs = uplt.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 :func:`matplotlib.axes.Axes.set_title`, and a "MATLAB-style" interface +# that tracks current axes and provides global commands like +# :func:`matplotlib.pyplot.title`. +# +# UltraPlot 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 :func:`~ultraplot.figure.Figure.subplots`, :func:`~ultraplot.figure.Figure.add_subplot`, +# :func:`~ultraplot.axes.Axes.inset_axes`, :func:`~ultraplot.axes.Axes.panel_axes`, and +# :func:`~ultraplot.axes.CartesianAxes.altx` or :func:`~ultraplot.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 :func:`~ultraplot.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 :func:`~ultraplot.axes.Axes.format` for details. +# +# * Cartesian axes settings (valid only for :class:`~ultraplot.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 :func:`~ultraplot.axes.CartesianAxes.format` and +# :ref:`this section ` for details. +# +# * Polar axes settings (valid only for :class:`~ultraplot.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 :func:`~ultraplot.axes.PolarAxes.format` +# and :ref:`this section ` for details. +# +# * Geographic axes settings (valid only for :class:`~ultraplot.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 :func:`~ultraplot.axes.GeoAxes.format` +# and :ref:`this section ` for details. +# +# * :func:`~ultraplot.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. +# :func:`~ultraplot.figure.Figure.format` accepts both figure and axes +# settings (applying them to each numbered subplot by default). +# Similarly, :func:`~ultraplot.axes.Axes.format` accepts both axes and figure +# settings. There is also a :func:`~ultraplot.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 ultraplot as uplt +import numpy as np + +fig, axs = uplt.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=uplt.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 :func:`~ultraplot.config.rc` is created when you import +# UltraPlot. :func:`~ultraplot.config.rc` is similar to the matplotlib :data:`~matplotlib.rcParams` +# dictionary, but can be used to change both `matplotlib settings +# `__ and +# :ref:`ultraplot settings `. The matplotlib-specific settings are +# stored in :func:`~ultraplot.config.rc_matplotlib` (our name for :data:`~matplotlib.rcParams`) and +# the UltraPlot-specific settings are stored in :class:`~ultraplot.config.rc_ultraplot`. +# UltraPlot 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 +# :func:`~ultraplot.axes.Axes.format` or :func:`~ultraplot.figure.Figure.format`. To temporarily +# modify setting(s) for a block of code, use :func:`~ultraplot.config.Configurator.context`. +# To modify setting(s) for the entire python session, just assign it to the +# :func:`~ultraplot.config.rc` dictionary or use :func:`~ultraplot.config.Configurator.update`. +# To reset everything to the default state, use :func:`~ultraplot.config.Configurator.reset`. +# See the below example. + + +# %% +import ultraplot as uplt +import numpy as np + +# Update global settings in several different ways +uplt.rc.metacolor = "gray6" +uplt.rc.update({"fontname": "Source Sans Pro", "fontsize": 11}) +uplt.rc["figure.facecolor"] = "gray3" +uplt.rc.axesfacecolor = "gray4" +# uplt.rc.save() # save the current settings to ~/.ultraplotrc + +# Apply settings to figure with context() +with uplt.rc.context({"suptitle.size": 13}, toplabelcolor="gray6", metawidth=1.5): + fig = uplt.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 = uplt.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 +uplt.rc.reset() + + +# %% +import ultraplot as uplt +import numpy as np + +# uplt.rc.style = 'style' # set the style everywhere + +# Sample data +state = np.random.RandomState(51423) +data = state.rand(10, 5) + +# Set up figure +fig, axs = uplt.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..6e3910ac5 --- /dev/null +++ b/docs/cartesian.py @@ -0,0 +1,742 @@ +# -*- 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 UltraPlot, you can +# change the tick locator using the :func:`~ultraplot.axes.CartesianAxes.format` keyword +# arguments `xlocator`, `ylocator`, `xminorlocator`, and `yminorlocator` (or their +# aliases, `xticks`, `yticks`, `xminorticks`, and `yminorticks`). This is powered by +# the :class:`~ultraplot.constructor.Locator` :ref:`constructor function `. +# +# You can use these keyword arguments to apply built-in matplotlib +# :class:`~matplotlib.ticker.Locator`\ s by their "registered" names +# (e.g., ``xlocator='log'``), to draw ticks every ``N`` data values with +# :class:`~matplotlib.ticker.MultipleLocator` (e.g., ``xlocator=2``), or to tick the +# specific locations in a list using :class:`~matplotlib.ticker.FixedLocator` (just +# like :meth:`~matplotlib.axes.Axes.set_xticks` and :meth:`~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=uplt.MultipleLocator(...)`` is allowed). +# +# To generate lists of tick locations, we recommend using UltraPlot's +# :func:`~ultraplot.utils.arange` function -- it’s basically an endpoint-inclusive +# version of :func:`~numpy.arange`, which is usually what you'll want in this context. + +# %% +import ultraplot as uplt +import numpy as np + +state = np.random.RandomState(51423) +uplt.rc.update( + metawidth=1, + fontsize=10, + metacolor="dark blue", + suptitlecolor="dark blue", + titleloc="upper center", + titlecolor="dark blue", + titleborder=False, + axesfacecolor=uplt.scale_luminance("powderblue", 1.15), +) +fig = uplt.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") + +uplt.rc.reset() + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_formatters: +# +# Tick formatting +# --------------- +# +# Matplotlib `tick formatters +# `__ +# convert floating point numbers to nicely-formatted tick labels. In UltraPlot, you can +# change the tick formatter using the :func:`~ultraplot.axes.CartesianAxes.format` keyword +# arguments `xformatter` and `yformatter` (or their aliases, `xticklabels` and +# `yticklabels`). This is powered by the :class:`~ultraplot.constructor.Formatter` +# :ref:`constructor function `. +# +# You can use these keyword arguments to apply built-in matplotlib +# :class:`~matplotlib.ticker.Formatter`\ s by their "registered" names +# (e.g., ``xformatter='log'``), to apply a ``%``-style format directive with +# :class:`~matplotlib.ticker.FormatStrFormatter` (e.g., ``xformatter='%.0f'``), or +# to apply custom tick labels with :class:`~matplotlib.ticker.FixedFormatter` (just +# like :meth:`~matplotlib.axes.Axes.set_xticklabels`). You can also apply one of UltraPlot'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=uplt.SciFormatter(...)`` is allowed). +# +# UltraPlot also changes the default tick formatter to +# :class:`~ultraplot.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 +# :class:`~ultraplot.ticker.AutoFormatter` for details. To disable the trailing +# zero-trimming feature, set :rcraw:`formatter.zerotrim` to ``False``. + +# %% +import ultraplot as uplt + +uplt.rc.fontsize = 11 +uplt.rc.metawidth = 1.5 +uplt.rc.gridwidth = 1 + +# Create the figure +fig, axs = uplt.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="UltraPlot 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, +) +uplt.rc.reset() + + +# %% +import ultraplot as uplt +import numpy as np + +uplt.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 = uplt.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") +uplt.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 +# :func:`~ultraplot.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 :meth:`~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 :func:`~ultraplot.axes.CartesianAxes.format` or by setting +# :rcraw:`formatter.timerotation` to ``0``. See :class:`~ultraplot.constructor.Locator` +# and :class:`~ultraplot.constructor.Formatter` for details. + +# %% +import ultraplot as uplt +import numpy as np + +uplt.rc.update( + metawidth=1.2, + fontsize=10, + ticklenratio=0.7, + figurefacecolor="w", + axesfacecolor="pastel blue", + titleloc="upper center", + titleborder=False, +) +fig, axs = uplt.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") +uplt.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 +# :func:`ultraplot.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 +# :meth:`~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 :meth:`~matplotlib.axis.YAxis.tick_left`, +# :meth:`~matplotlib.axis.YAxis.tick_right`, :meth:`~matplotlib.axis.XAxis.tick_top`, and +# :meth:`~matplotlib.axis.XAxis.tick_bottom`, and :meth:`~matplotlib.spines.Spine.set_position`, +# but with additional flexibility. + +# %% +import ultraplot as uplt + +uplt.rc.update( + metawidth=1.2, + fontsize=10, + gridcolor="coral", + axesedgecolor="deep orange", + figurefacecolor="white", +) +fig = uplt.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") +uplt.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 :func:`~ultraplot.axes.Axes.format`. This is powered by the +# :class:`~ultraplot.constructor.Scale` :ref:`constructor function `. +# UltraPlot makes several changes to the axis scale API: +# +# * The :class:`~ultraplot.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 +# :func:`~ultraplot.axes.CartesianAxes.format`. +# * To make its behavior consistent with :class:`~ultraplot.constructor.Locator` and +# :class:`~ultraplot.constructor.Formatter`, the :class:`~ultraplot.constructor.Scale` +# constructor function returns instances of :class:`~matplotlib.scale.ScaleBase`, +# and :meth:`~matplotlib.axes.Axes.set_xscale` and +# :meth:`~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 +# :class:`~matplotlib.axis.Axis` instance (for backwards compatibility reasons), +# UltraPlot axis scales can be instantiated without the axis instance +# (e.g., ``uplt.LogScale()`` instead of ``uplt.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``. +# +# UltraPlot also includes a few new axis scales. The ``'cutoff'`` scale +# :class:`~ultraplot.scale.CutoffScale` is useful when the statistical distribution +# of your data is very unusual. The ``'sine'`` scale :class:`~ultraplot.scale.SineLatitudeScale` +# scales the axis with a sine function (resulting in an area-weighted spherical latitude +# coordinate) and the ``'mercator'`` scale :class:`~ultraplot.scale.MercatorLatitudeScale` +# scales the axis with the Mercator projection latitude coordinate. The +# ``'inverse'`` scale :class:`~ultraplot.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=uplt.CutoffScale(...)`` is allowed). + +# %% +import ultraplot as uplt +import numpy as np + +N = 200 +lw = 3 +uplt.rc.update({"meta.width": 1, "label.weight": "bold", "tick.labelweight": "bold"}) +fig = uplt.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) +uplt.rc.reset() + + +# %% +import ultraplot as uplt +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 = uplt.Colormap("grays", right=0.8) +fig, axs = uplt.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 ultraplot as uplt +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 = uplt.Colormap("grays", right=0.8) +gs = uplt.GridSpec(nrows=2, ncols=2) +fig = uplt.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 :class:`~matplotlib.axes.Axes` class includes :meth:`~matplotlib.axes.Axes.twinx` +# and :meth:`~matplotlib.axes.Axes.twiny` commands for drawing "twin" *x* and +# *y* axes in the same subplot. UltraPlot expands on these commands and adds +# the arguably more intuitive :func:`~ultraplot.axes.CartesianAxes.altx` and +# :func:`~ultraplot.axes.CartesianAxes.alty` options. Here :func:`~ultraplot.axes.CartesianAxes.altx` +# is equivalent to :func:`~ultraplot.axes.CartesianAxes.twiny` (makes an alternate *x* +# axes and an identical twin *y* axes) and :func:`~ultraplot.axes.CartesianAxes.alty` +# is equivalent to :func:`~ultraplot.axes.CartesianAxes.twinx` (makes an alternate *y* +# axes and an identical twin *x* axes). The UltraPlot versions can be quickly +# formatted by passing :func:`ultraplot.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, UltraPlot 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 ultraplot as uplt +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 = uplt.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 :func:`~ultraplot.axes.CartesianAxes.dualx` and +# :func:`~ultraplot.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 :class:`~ultraplot.scale.FuncScale` class. +# :func:`~ultraplot.axes.CartesianAxes.dualx` and :func:`~ultraplot.axes.CartesianAxes.dualy` accept +# the same axis formatting keyword arguments as :func:`~ultraplot.axes.CartesianAxes.altx` +# and :func:`~ultraplot.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 :func:`~ultraplot.axes.CartesianAxes.dualx` axis for a `symlog-scaled +# `__ axis. + +# %% +import ultraplot as uplt + +uplt.rc.update({"grid.alpha": 0.4, "meta.width": 1, "grid.linewidth": 1}) +c1 = uplt.scale_luminance("cerulean", 0.5) +c2 = uplt.scale_luminance("red", 0.5) +fig = uplt.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, +) +uplt.rc.reset() + +# %% +import ultraplot as uplt + +uplt.rc.update({"grid.alpha": 0.4, "meta.width": 1, "grid.linewidth": 1}) +c1 = uplt.scale_luminance("cerulean", 0.5) +c2 = uplt.scale_luminance("red", 0.5) +fig = uplt.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 +) +uplt.rc.reset() + +# %% +import ultraplot as uplt +import numpy as np + +uplt.rc.margin = 0 +c1 = uplt.scale_luminance("cerulean", 0.5) +c2 = uplt.scale_luminance("red", 0.5) +fig, ax = uplt.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)" +) +uplt.rc.reset() diff --git a/docs/colorbars_legends.py b/docs/colorbars_legends.py new file mode 100644 index 000000000..10a4099c8 --- /dev/null +++ b/docs/colorbars_legends.py @@ -0,0 +1,471 @@ +# --- +# 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 +# ===================== +# +# UltraPlot 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. UltraPlot tries to improve this behavior: +# +# * :meth:`~ultraplot.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. +# * UltraPlot adds the axes command `ultraplot.axes.Axes.colorbar`, +# analogous to :meth:`~ultraplot.axes.Axes.legend` and equivalent to +# calling :func:`~ultraplot.figure.Figure.colorbar` with an `ax` keyword. +# :func:`~ultraplot.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 :func:`~ultraplot.axes.Axes.colorbar` keywords. + +# :func:`~ultraplot.axes.Axes.colorbar` and :meth:`~ultraplot.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, UltraPlot adds "outer" colorbars and legends by allocating +# new rows and columns in the :class:`~ultraplot.gridspec.GridSpec` rather than +# "stealing" space from the parent subplot (note that subsequently indexing +# the :class:`~ultraplot.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 +# UltraPlot 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 numpy as np + +import ultraplot as uplt + +state = np.random.RandomState(51423) +fig = uplt.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 UltraPlot, you can add colorbars and legends on-the-fly by supplying keyword +# arguments to various :class:`~ultraplot.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., :func:`~ultraplot.axes.PlotAxes.plot` +# or :func:`~ultraplot.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 :func:`~ultraplot.axes.Axes.colorbar` can also +# build colorbars from lists of arbitrary matplotlib artists, for example the +# lines generated by :func:`~ultraplot.axes.PlotAxes.plot` or :func:`~ultraplot.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 ultraplot as uplt + +labels = list("xyzpq") +state = np.random.RandomState(51423) +fig = uplt.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 numpy as np + +import ultraplot as uplt + +N = 10 +state = np.random.RandomState(51423) +fig, axs = uplt.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 UltraPlot, colorbars and legends can be added to the edge of figures using the +# figure methods `ultraplot.figure.Figure.colorbar` and :class:`ultraplot.figure.Figure.legend`. +# These methods align colorbars and legends between the edges +# of the :func:`~ultraplot.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 numpy as np + +import ultraplot as uplt + +state = np.random.RandomState(51423) +fig, axs = uplt.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 numpy as np + +import ultraplot as uplt + +state = np.random.RandomState(51423) +fig, axs = uplt.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 = uplt.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 `ultraplot.axes.Axes.colorbar` and `ultraplot.figure.Figure.colorbar` commands are +# somehwat more flexible than their matplotlib counterparts. The following core +# features are unique to UltraPlot: + +# * Calling ``colorbar`` with a list of :class:`~matplotlib.artist.Artist`\ s, +# a :class:`~matplotlib.colors.Colormap` name or object, or a list of colors +# will build the required `~matplotlib.cm.ScalarMappable` on-the-fly. Lists +# of :class:`~matplotlib.artist.Artists`\ s are used when you use the `colorbar` +# keyword with :ref:`1D commands ` like :func:`~ultraplot.axes.PlotAxes.plot`. +# * The associated :ref:`colormap normalizer ` can be specified with the +# `vmin`, `vmax`, `norm`, and `norm_kw` keywords. The `~ultraplot.colors.DiscreteNorm` +# levels can be specified with `values`, or UltraPlot will infer them from the +# :class:`~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. +# +# UltraPlot also includes improvements for adding ticks and tick labels to colorbars. +# Similar to :func:`ultraplot.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 :class:`~ultraplot.constructor.Locator` and +# :class:`~ultraplot.constructor.Formatter` :ref:`constructor functions `. +# Unlike matplotlib, the default ticks for :ref:`discrete colormaps ` +# are restricted based on the axis length using `~ultraplot.ticker.DiscreteLocator`. +# You can easily toggle minor ticks using ``tickminor=True``. +# +# Similar to :ref:`axes panels `, the geometry of UltraPlot 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 :func:`~ultraplot.axes.Axes.colorbar` for details. + +# %% +import numpy as np + +import ultraplot as uplt + +fig = uplt.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 = uplt.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=uplt.arange(0, 3, 0.5)) +fig.colorbar( + m, loc="r", length=1, label="interior ticks", tickloc="left" # length is relative +) +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 :meth:`~ultraplot.axes.Axes.legend` and :meth:`~ultraplot.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 :meth:`~ultraplot.axes.Axes.legend`) or +# or the parent figure (when using :meth:`~ultraplot.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 UltraPlot: + +# * 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., :func:`~ultraplot.axes.PlotAxes.contour` +# or :func:`~ultraplot.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 :func:`~ultraplot.axes.PlotAxes.plot`, +# :class:`~matplotlib.container.BarContainer`\ s returned by :func:`~ultraplot.axes.PlotAxes.bar`, +# and :class:`~matplotlib.collections.PolyCollection`\ s +# returned by :func:`~ultraplot.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. +# +# :meth:`~ultraplot.axes.Axes.legend` and :func:`ultraplot.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., :func:`~ultraplot.axes.PlotAxes.plot` or :func:`~ultraplot.axes.PlotAxes.scatter` handles) +# pass the relevant properties like `color`, `linewidth`, or `markersize` to ``legend`` +# (or use the `handle_kw` keyword). See `ultraplot.axes.Axes.legend` for details. + +# %% +import numpy as np + +import ultraplot as uplt + +uplt.rc.cycle = "538" +fig, axs = uplt.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.py b/docs/colormaps.py new file mode 100644 index 000000000..48255c270 --- /dev/null +++ b/docs/colormaps.py @@ -0,0 +1,501 @@ +# -*- 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 +# ========= +# +# UltraPlot 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 UltraPlot with the :class:`~ultraplot.colors.ContinuousColormap` and +# :class:`~ultraplot.colors.PerceptualColormap` classes, which are +# :ref:`subclassed from ` +# :class:`~matplotlib.colors.LinearSegmentedColormap`. +# +# UltraPlot :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, UltraPlot registers a few sample +# :ref:`perceptually uniform colormaps `, plus several +# colormaps from other online data viz projects. Use :func:`~ultraplot.demos.show_cmaps` +# to generate a table of registered colormaps. The figure is broken down into +# the following sections: +# +# * "User" colormaps created with :class:`~ultraplot.constructor.Colormap` +# or loaded from :func:`~ultraplot.config.Configurator.user_folder`. +# * `Matplotlib `_ and `seaborn `_ original colormaps. +# * UltraPlot 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 :func:`~ultraplot.utils.get_colors`. +# +# .. note:: +# +# Colormap and :ref:`color cycle ` identification is more flexible in +# UltraPlot. 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 +# :attr:`~ultraplot.colors.ContinuousColormap.reversed` or +# :attr:`~ultraplot.colors.ContinuousColormap.shifted` version of the colormap +# or color cycle. See :class:`~ultraplot.colors.ColormapDatabase` for more info. + +# %% +import ultraplot as uplt + +fig, axs = uplt.show_cmaps(rasterized=True) + + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_perceptual: +# +# Perceptually uniform colormaps +# ------------------------------ +# +# UltraPlot's custom colormaps are instances of the +# :class:`~ultraplot.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 :class:`~ultraplot.colors.PerceptualColormap` +# is set with the `space` keyword arg. To plot arbitrary cross-sections of +# these colorspaces, use :func:`~ultraplot.demos.show_colorspaces` (the black +# regions represent impossible colors). To see how colormaps vary with +# respect to each channel, use :func:`~ultraplot.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. UltraPlot'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 ultraplot as uplt + +fig, axs = uplt.show_colorspaces(refwidth=1.6, luminance=50) +fig, axs = uplt.show_colorspaces(refwidth=1.6, saturation=60) +fig, axs = uplt.show_colorspaces(refwidth=1.6, hue=0) + +# %% +# Compare colormaps +import ultraplot as uplt + +for cmaps in (("magma", "rocket"), ("fire", "dusk")): + fig, axs = uplt.show_channels( + *cmaps, refwidth=1.5, minhue=-180, maxsat=400, rgb=False + ) + + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_cmaps_new: +# +# Making colormaps +# ---------------- +# +# UltraPlot 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 +# :class:`~ultraplot.constructor.Colormap` :ref:`constructor function `. +# Note that every :class:`~ultraplot.axes.PlotAxes` command that accepts a `cmap` keyword passes +# it through this function (see the :ref:`2D plotting section `). +# +# To make :class:`~ultraplot.colors.PerceptualColormap`\ s from +# scratch, you have the following three options: +# +# * Pass a color name, HEX string, or RGB tuple to :class:`~ultraplot.constructor.Colormap`. +# This builds a monochromatic (single hue) colormap by calling +# :func:`~ultraplot.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 :class:`~ultraplot.constructor.Colormap`. This calls +# :func:`~ultraplot.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 :class:`~ultraplot.constructor.Colormap` without any positional arguments +# (or pass a dictionary containing these keys as a positional argument). +# This calls :func:`~ultraplot.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 :class:`~ultraplot.colors.PerceptualColormap`\ s +# in the ``'hsl'`` and ``'hpl'`` colorspaces. + +# %% +# Sample data +import ultraplot as uplt +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 = uplt.figure(refwidth=2, span=False) +ax = fig.subplot(121, title="From single named color") +cmap1 = uplt.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 = uplt.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 = uplt.show_channels(cmap1, cmap2, refwidth=1.5, rgb=False) + +# %% +# Sequential colormap from channel values +cmap3 = uplt.Colormap( + h=("red", "red-720"), s=(80, 20), l=(20, 100), space="hpl", name="CubeHelix" +) +fig = uplt.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 = uplt.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 = uplt.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 +# :class:`~ultraplot.constructor.Colormap` constructor function. This calls the +# :func:`~ultraplot.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 UltraPlot. 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 +# :class:`~ultraplot.constructor.Colormap`. + +# %% +import ultraplot as uplt +import numpy as np + +state = np.random.RandomState(51423) +data = state.rand(30, 30).cumsum(axis=1) + +# Generate figure +fig, axs = uplt.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 = uplt.Colormap("Blues4_r", "Reds3", name="Diverging", save=True) + +# SciVisColor examples +title2 = "SciVisColor example" +cmap2 = uplt.Colormap( + "Greens1_r", + "Oranges1", + "Blues1_r", + "Blues6", + ratios=(1, 3, 5, 10), + name="SciVisColorUneven", + save=True, +) +title3 = "SciVisColor with equal ratios" +cmap3 = uplt.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 +# ------------------- +# +# UltraPlot lets you create modified versions of *existing* colormaps +# using the :class:`~ultraplot.constructor.Colormap` constructor function and the +# new :class:`~ultraplot.colors.ContinuousColormap` and +# :class:`~ultraplot.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 :class:`~ultraplot.constructor.Colormap`. This calls the +# :func:`~ultraplot.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 +# :class:`~ultraplot.constructor.Colormap`. This calls the +# :func:`~ultraplot.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 +# :class:`~ultraplot.constructor.Colormap`. This calls the +# :func:`~ultraplot.colors.ContinuousColormap.shifted` method. UltraPlot 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 :class:`~ultraplot.constructor.Colormap`. This calls the +# :func:`~ultraplot.colors.ContinuousColormap.set_alpha` method, and can be +# useful when *layering* filled contour or mesh elements. +# * To change the "gamma" of a :class:`~ultraplot.colors.PerceptualColormap`, +# pass `gamma` to :class:`~ultraplot.constructor.Colormap`. This calls the +# :func:`~ultraplot.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 ultraplot as uplt +import numpy as np + +state = np.random.RandomState(51423) +data = state.rand(40, 40).cumsum(axis=0) + +# Generate figure +fig, axs = uplt.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 ultraplot as uplt +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 = uplt.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": uplt.arange(-10, 10, 2)} # negative-positive cutoff + else: + kw = {"values": uplt.arange(-10, 10, 2)} # dedicated center + if cut is not None: + fmt = uplt.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 ultraplot as uplt +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 = uplt.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 ultraplot as uplt +import numpy as np + +state = np.random.RandomState(51423) +data = state.rand(20, 20).cumsum(axis=1) + +# Changing the colormap opacity +fig, axs = uplt.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 = uplt.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 ultraplot as uplt +import numpy as np + +state = np.random.RandomState(51423) +data = state.rand(20, 20).cumsum(axis=1) + +# Changing the colormap gamma +fig, axs = uplt.subplots(ncols=3, refwidth=1.7, span=False) +for ax, gamma in zip(axs, (0.7, 1.0, 1.4)): + cmap = uplt.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 :func:`~ultraplot.config.Configurator.user_folder`, +# or to a folder named ``ultraplot_cmaps`` in the same directory as your python session +# or an arbitrary parent directory (see :func:`~ultraplot.config.Configurator.local_folders`). +# After adding the file, call :func:`~ultraplot.config.register_cmaps` or restart your python +# session. You can also use :func:`~ultraplot.colors.ContinuousColormap.from_file` or manually +# pass :class:`~ultraplot.colors.ContinuousColormap` instances or file paths to +# :func:`~ultraplot.config.register_cmaps`. See :func:`~ultraplot.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..1a7b07f0f --- /dev/null +++ b/docs/colors.py @@ -0,0 +1,201 @@ +# -*- 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 +# =========== +# +# UltraPlot 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 +# --------------- +# +# UltraPlot adds new color names from the `XKCD color survey +# `__ and +# the `Open Color `__ UI design color +# palettes. You can use :func:`~ultraplot.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 UltraPlot 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, UltraPlot ensures that ``'grey'`` is a synonym of ``'gray'`` +# (for example, ``'grey5'`` and ``'gray5'`` are both valid). UltraPlot 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 +# :func:`~ultraplot.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 :func:`~ultraplot.config.register_colors` +# with the `space` or `margin` keywords. + +# %% +import ultraplot as uplt + +fig, axs = uplt.show_colors() + + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_colors_change: +# +# Modifying colors +# ---------------- +# +# UltraPlot provides the top-level :func:`~ultraplot.utils.set_alpha`, +# :func:`~ultraplot.utils.set_hue`, :func:`~ultraplot.utils.set_saturation`, +# :func:`~ultraplot.utils.set_luminance`, :func:`~ultraplot.utils.shift_hue`, +# :func:`~ultraplot.utils.scale_saturation`, and :func:`~ultraplot.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, +# ``uplt.scale_luminance('color', 1.2)`` makes ``'color'`` 20% brighter. These +# are useful for creating color gradations outside of :class:`~ultraplot.colors.Cycle` +# or if you simply spot a color you like and want to make it a bit +# brighter, less vibrant, etc. + + +# %% +import ultraplot as uplt +import numpy as np + +# Figure +state = np.random.RandomState(51423) +fig, axs = uplt.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 = uplt.SimpleFormatter() +marker = "o" +for shift in (0, -60, 60): + x, y = state.rand(2, N) + color = uplt.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 = uplt.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 = uplt.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 +# `~ultraplot.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 :func:`~ultraplot.utils.to_rgb` or +# :func:`~ultraplot.utils.to_rgba` functions to retrieve the RGB or RGBA channel values. + +# %% +import ultraplot as uplt +import numpy as np + +# Initial figure and random state +state = np.random.RandomState(51423) +fig = uplt.figure(refwidth=2.2, share=False) + +# Drawing from colormaps +name = "Deep" +idxs = uplt.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(uplt.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(uplt.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 :func:`~ultraplot.config.Configurator.user_folder`, +# or to a folder named ``ultraplot_colors`` in the same directory as your python session +# or an arbitrary parent directory (see :func:`~ultraplot.config.Configurator.local_folders`). +# After adding the file, call :func:`~ultraplot.config.register_colors` or restart your python +# session. You can also manually pass file paths, dictionaries, ``name=color`` +# keyword arguments to :func:`~ultraplot.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/conf.py b/docs/conf.py new file mode 100644 index 000000000..e26d68230 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,424 @@ +# -*- coding: utf-8 -*- +# +# Configuration file for the Sphinx documentation builder. +# +# For autodoc compilation see: +# https://medium.com/@eikonomega/getting-started-with-sphinx-autodoc-part-1-2cebbbca5365 +# +# This file does only contain a selection of the most common options. For a +# full list see the documentation: +# http://www.sphinx-doc.org/en/master/config + +# -- Imports and paths -------------------------------------------------------------- + +# Import statements +import os +import sys +import datetime +import subprocess +from pathlib import Path + +# Surpress warnings from cartopy when downloading data inside docs env +import warnings + +try: + from cartopy.io import DownloadWarning + + warnings.filterwarnings("ignore", category=DownloadWarning) +except ImportError: + # In case cartopy isn't installed yet when conf.py is executed + pass + +# Handle sphinx.util.console deprecation +# Note this has been deprecated in Sphinx 5.0 and some extensions still use the console module. Needs to be updated later +try: + # For newer Sphinx versions where sphinx.util.console is removed + import sphinx + + if not hasattr(sphinx.util, "console"): + # Create a compatibility layer + import sys + import sphinx.util + from sphinx.util import logging + + class ConsoleColorFallback: + def __getattr__(self, name): + return ( + lambda text: text + ) # Return a function that returns the text unchanged + + sphinx.util.console = ConsoleColorFallback() +except Exception: + pass + +# Build what's news page from github releases +from subprocess import run + +run("python _scripts/fetch_releases.py".split(), check=False) + +# Update path for sphinx-automodapi and sphinxext extension +sys.path.append(os.path.abspath(".")) +sys.path.insert(0, os.path.abspath("..")) + +# Print available system fonts +from matplotlib.font_manager import fontManager + + +# -- Project information ------------------------------------------------------- +# The basic info +project = "UltraPlot" +copyright = f"{datetime.datetime.today().year}, UltraPlot" +author = "Luke L. B. Davis" + +# The short X.Y version +version = "" + +# The full version, including alpha/beta/rc tags +release = "" + +# Faster builds +parallel_read_safe = True +parallel_write_safe = True + +# -- Create files -------------------------------------------------------------- + +# Create RST table and sample ultraplotrc file +from ultraplot.config import rc + +folder = (Path(__file__).parent / "_static").absolute() +if not folder.is_dir(): + folder.mkdir() + +rc._save_rst(str(folder / "rctable.rst")) +rc._save_yaml(str(folder / "ultraplotrc")) + +# -- 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 = ( + Path(os.environ["CONDA_ENVS_PATH"]) / os.environ["CONDA_DEFAULT_ENV"] + ).absolute() +else: + conda = Path(os.environ["CONDA_PREFIX"]).absolute() +os.environ["GEOS_DIR"] = str(conda) +os.environ["PROJ_LIB"] = str((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", "basemap"] + # ["pip", "install", "git+https://github.com/matplotlib/basemap@v1.2.2rel"] + ) + + +# -- General configuration --------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = [ + "matplotlib.sphinxext.plot_directive", # see: https://matplotlib.org/sampledoc/extensions.html # noqa: E501 + "sphinx.ext.autodoc", # include documentation from docstrings + "sphinx_design", + "sphinx.ext.doctest", # >>> examples + "sphinx.ext.extlinks", # for :pr:, :issue:, :commit: + "sphinx.ext.autosectionlabel", # use :ref:`Heading` for any heading + "sphinx.ext.todo", # Todo headers and todo:: directives + "sphinx.ext.mathjax", # LaTeX style math + "sphinx.ext.viewcode", # view code links + "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", # fork of automodapi + "sphinx_rtd_light_dark", # use custom theme + "sphinx_copybutton", # add copy button to code + "_ext.notoc", + "nbsphinx", # parse rst books +] + + +# The master toctree document. +master_doc = "index" + +# The suffix(es) of source filenames, either a string or list. +source_suffix = [".rst", ".html"] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + +# List of file patterns relative to source dir that should be ignored +exclude_patterns = [ + "conf.py", + "sphinxext", + "_build", + "_scripts", + "_templates", + "_themes", + "*.ipynb", + "**.ipynb_checkpoints" ".DS_Store", + "trash", + "tmp", +] + +autodoc_default_options = { + "private-members": False, + "special-members": False, + "undoc-members": False, +} + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +language = "en" + +# 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 # UltraPlot 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 + +# 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 + +# Doctest configuration. For now do not run tests, they are just to show syntax +# and expected output may be graphical +doctest_test_doctest_blocks = "" + +# 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 + +# Links for What's New github commits, issues, and pull requests +extlinks = { + "issue": ("https://github.com/ultraplot/ultraplot/issues/%s", "GH#%s"), + "commit": ("https://github.com/Ultraplot/ultraplot/commit/%s", "@%s"), + "pr": ("https://github.com/Ultraplot/ultraplot/pull/%s", "GH#%s"), +} + +# Set up mapping for other projects' docs +intersphinx_mapping = { + "cycler": ("https://matplotlib.org/cycler/", None), + "matplotlib": ("https://matplotlib.org/stable/", None), + "sphinx": ("https://www.sphinx-doc.org/en/master/", None), + "python": ("https://docs.python.org/3/", None), + "numpy": ("https://numpy.org/doc/stable/", None), + "scipy": ("https://docs.scipy.org/doc/scipy/", None), + "xarray": ("https://docs.xarray.dev/en/stable/", None), + "cartopy": ("https://cartopy.readthedocs.io/stable/", None), + "basemap": ("https://matplotlib.org/basemap/stable/", None), + "pandas": ("https://pandas.pydata.org/pandas-docs/stable/", None), + "pint": ("https://pint.readthedocs.io/en/stable/", None), + "networkx": ("https://networkx.org/documentation/stable/", None), +} + + +# 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 +napoleon_use_rtype = False +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 `", + # UltraPlot 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 `", +} + +# Fail on error. Note nbsphinx compiles all notebooks in docs unless excluded +nbsphinx_allow_errors = False + +# Give *lots* of time for cell execution +nbsphinx_timeout = 300 + +# Add jupytext support to nbsphinx +nbsphinx_custom_formats = {".py": ["jupytext.reads", {"fmt": "py:percent"}]} + +nbsphinx_execute = "auto" + +# The name of the Pygments (syntax highlighting) style to use. +# The light-dark theme toggler overloads this, but set default anyway +pygments_style = "none" + + +# -- Options for HTML output ------------------------------------------------- + +# Logo +html_logo = str(Path("_static") / "logo_square.png") + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# Use modified RTD theme with overrides in custom.css and custom.js +style = None +html_theme = "sphinx_rtd_light_dark" +# html_theme = "alabaster" +# html_theme = "sphinx_rtd_theme" +html_theme_options = { + "logo_only": True, + "display_version": False, + "collapse_navigation": True, + "navigation_depth": 4, + "prev_next_buttons_location": "bottom", # top and bottom + "includehidden": True, + "titles_only": True, + "display_toc": True, + "sticky_navigation": True, +} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ["_static"] + +# Custom sidebar templates, must be a dictionary that maps document names +# to template names. +# The default sidebars (for documents that don't match any pattern) are +# defined by theme itself. Builtin themes are using these templates by +# default: ``['localtoc.html', 'relations.html', 'sourcelink.html', +# 'searchbox.html']``. +# html_sidebars = {} + +# 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. Use ImageMagick to +# convert png to ico on command line with 'convert image.png image.ico' +html_favicon = str(Path("_static") / "logo_blank.svg") + +# -- Options for HTMLHelp output --------------------------------------------- + +# Output file base name for HTML help builder. +htmlhelp_basename = "ultraplotdoc" + + +html_css_files = [ + "custom.css", +] +html_js_files = [ + "custom.js", +] + +# -- Options for LaTeX output ------------------------------------------------ + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # 'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + # 'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + # 'preamble': '', + # Latex figure (float) alignment + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, "UltraPlot.tex", "UltraPlot Documentation", "UltraPlot", "manual"), +] + +primary_domain = "py" + +# -- Options for manual page output ------------------------------------------ + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [(master_doc, "UltraPlot", "UltraPlot Documentation", [author], 1)] + + +# -- Options for Texinfo output ---------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ( + master_doc, + "UltraPlot", + "UltraPlot Documentation", + author, + "UltraPlot", + "A succinct matplotlib wrapper for making beautiful, " + "publication-quality graphics.", + "Miscellaneous", + ) +] + + +# -- Options for todo extension ---------------------------------------------- + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = True + +from ultraplot.internals.docstring import _snippet_manager + + +def process_docstring(app, what, name, obj, options, lines): + if lines: + try: + # Create a proper format string + doc = "\n".join(lines) + expanded = doc % _snippet_manager # Use dict directly + lines[:] = expanded.split("\n") + except Exception as e: + print(f"Warning: Could not expand docstring for {name}: {e}") + # Keep original lines if expansion fails + pass + + +def setup(app): + app.connect("autodoc-process-docstring", process_docstring) diff --git a/docs/configuration.rst b/docs/configuration.rst new file mode 100644 index 000000000..16974ba30 --- /dev/null +++ b/docs/configuration.rst @@ -0,0 +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 UltraPlot +=================== + +Overview +-------- + +A dictionary-like object named :obj:`~ultraplot.config.rc`, belonging to the +:class:`~ultraplot.config.Configurator` class, is created when you import UltraPlot. +This is your one-stop shop for working with +`matplotlib settings `_ +stored in :obj:`~ultraplot.config.rc_matplotlib` +(our name for the :obj:`~matplotlib.rcParams` dictionary) +and :ref:`ultraplot settings ` +stored in :obj:`~ultraplot.config.rc_ultraplot`. + +To change global settings on-the-fly, simply update :obj:`~ultraplot.config.rc` +using either dot notation or as you would any other dictionary: + +.. code-block:: python + + import ultraplot as uplt + uplt.rc.name = value + uplt.rc['name'] = value + uplt.rc.update(name1=value1, name2=value2) + uplt.rc.update({'name1': value1, 'name2': value2}) + +To apply settings to a particular axes or figure, pass the setting +to :func:`~ultraplot.axes.Axes.format` or :func:`~ultraplot.figure.Figure.format`: + +.. code-block:: python + + import ultraplot as uplt + fig, ax = uplt.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 :func:`~ultraplot.config.Configurator.context` command: + +.. code-block:: python + + import ultraplot as uplt + with uplt.rc.context(name1=value1, name2=value2): + fig, ax = uplt.subplots() + with uplt.rc.context({'name1': value1, 'name2': value2}): + fig, ax = uplt.subplots() + + +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 ultraplot as uplt + # Apply globally + uplt.rc.titleloc = value + uplt.rc.update(titleloc=value) + # Apply locally + fig, ax = uplt.subplots() + ax.format(titleloc=value) + +.. _ug_rcmatplotlib: + +Matplotlib settings +------------------- + +Matplotlib settings are natively stored in the :obj:`~matplotlib.rcParams` +dictionary. UltraPlot makes this dictionary available in the top-level namespace as +:obj:`~ultraplot.config.rc_matplotlib`. All matplotlib settings can also be changed with +:obj:`~ultraplot.config.rc`. Details on the matplotlib settings can be found on +`this page `_. + +.. _ug_rcUltraPlot: + +UltraPlot settings +---------------- + +UltraPlot settings are natively stored in the :obj:`~ultraplot.config.rc_ultraplot` dictionary. +They should almost always be changed with :obj:`~ultraplot.config.rc` rather than +:obj:`~ultraplot.config.rc_ultraplot` to ensure that :ref:`meta-settings ` are +synced. These settings are not found in :obj:`~matplotlib.rcParams` -- they either +control UltraPlot-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 :class:`~ultraplot.ticker.AutoFormatter` behavior. +* The ``cmap`` category supersedes matplotlib's ``image`` and includes + settings relevant to colormaps and the :class:`~ultraplot.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 :class:`~ultraplot.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 :class:`~ultraplot.axes.GeoAxes`. + +.. _ug_rcmeta: + +Meta-settings +------------- + +Some UltraPlot settings may be more accurately described as "meta-settings", +as they change several matplotlib and UltraPlot settings at once (note that settings +are only synced when they are changed on the :obj:`~ultraplot.config.rc` object rather than +the :obj:`~ultraplot.config.rc_UltraPlot` and :obj:`~ultraplot.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 UltraPlot settings is shown below. + +.. include:: _static/rctable.rst + +.. _ug_ultraplotrc: + +The ultraplotrc file +------------------ + +When you import UltraPlot for the first time, a ``ultraplotrc`` file is generated with +all lines commented out. This file is just like `matplotlibrc `_, +except it controls both matplotlib *and* UltraPlot 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 ``~/.UltraPlot/ultraplotrc``, but a loose hidden file in the +home directory named ``~/.ultraplotrc`` is also allowed (use +:func:`~ultraplot.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 :obj:`~ultraplot.config.rc` settings, edit and uncomment the lines +in the ``ultraplotrc`` file. To change the settings for a specific project, place a file +named either ``.ultraplotrc`` or ``ultraplotrc`` in the same directory as your python +session, or in an arbitrary parent directory. To generate a ``ultraplotrc`` file +containing the settings you have changed during a python session, use +:func:`~ultraplot.config.Configurator.save` (use :func:`~ultraplot.config.Configurator.changed` +to preview a dictionary of the changed settings). To explicitly load a ``ultraplotrc`` +file, use :func:`~ultraplot.config.Configator.load`. + +As an example, a ``ultraplotrc`` file containing the default settings +is shown below. + +.. include:: _static/ultraplotrc + :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/cycles.py b/docs/cycles.py new file mode 100644 index 000000000..13e32af1d --- /dev/null +++ b/docs/cycles.py @@ -0,0 +1,220 @@ +# --- +# 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 +# ============ +# +# UltraPlot 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. UltraPlot's +# color cycles are registered as :class:`~ultraplot.colors.DiscreteColormap`\ s, +# and can be easily converted into `property cyclers +# `__ +# for use with distinct plot elements using the :class:`~ultraplot.constructor.Cycle` +# constructor function. :class:`~ultraplot.constructor.Cycle` can also +# :ref:`extract colors ` from :class:`~ultraplot.colors.ContinuousColormap`\ s. +# +# UltraPlot :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 :func:`~ultraplot.demos.show_cycles` to generate a table of registered color +# cycles. The table includes the default color cycles registered by UltraPlot and +# "user" color cycles created with the :class:`~ultraplot.constructor.Cycle` constructor +# function or loaded from :func:`~ultraplot.config.Configurator.user_folder`. If you need +# the list of colors associated with a registered or on-the-fly color cycle, +# simply use :func:`~ultraplot.utils.get_colors`. + +# %% +import ultraplot as uplt + +fig, axs = uplt.show_cycles(rasterized=True) + + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_cycles_changing: +# +# Changing the color cycle +# ------------------------ +# +# Most 1D :class:`~ultraplot.axes.PlotAxes` commands like :func:`~ultraplot.axes.PlotAxes.line` +# and :func:`~ultraplot.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 +# :class:`~ultraplot.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 :class:`~ultraplot.colors.DiscreteColormap` or cycle name +# to :rcraw:`cycle` or pass the result of :class:`~ultraplot.constructor.Cycle` +# to :rcraw:`axes.prop_cycle` (see the :ref:`configuration guide `). + +# %% +import ultraplot as uplt +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 +uplt.rc.cycle = "538" +fig = uplt.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 +# ------------------- +# +# UltraPlot 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 :class:`~ultraplot.constructor.Cycle` +# :ref:`constructor function `. This command returns +# :class:`~cycler.Cycler` instances whose `color` properties are determined by the +# positional arguments (see :ref:`below ` for changing other +# properties). Note that every :class:`~ultraplot.axes.PlotAxes` command that accepts a +# `cycle` keyword passes it through this function (see the :ref:`1D plotting +# section `). + +# Positional arguments passed to :class:`~ultraplot.constructor.Cycle` are interpreted +# by the :class:`~ultraplot.constructor.Colormap` constructor function. If the result +# is a :class:`~ultraplot.colors.DiscreteColormap`, those colors are used for the resulting +# :class:`~cycler.Cycler`. If the result is a :class:`~ultraplot.colors.ContinuousColormap`, the +# colormap is sampled at `N` discrete values -- for example, ``uplt.Cycle('Blues', 5)`` +# selects 5 evenly-spaced values. When building color cycles on-the-fly, for example +# with ``ax.plot(data, cycle='Blues')``, UltraPlot 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, UltraPlot 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 :class:`~ultraplot.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 UltraPlot permits +# generating colorbars from :ref:`lists of artists `. + +# %% +import ultraplot as uplt +import numpy as np + +fig = uplt.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 = uplt.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 +# -------------------------- +# +# :class:`~ultraplot.constructor.Cycle` can generate :class:`~cycler.Cycler` instances that +# change :func:`~ultraplot.axes.PlotAxes.line` and :func:`~ultraplot.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 ``uplt.rc['axes.prop_cycle'] = cycle``. + +# %% +import ultraplot as uplt +import numpy as np +import pandas as pd + +# Cycle that loops through 'dashes' Line2D property +cycle = uplt.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 = uplt.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 :func:`~ultraplot.config.Configurator.user_folder`, +# or to a folder named ``ultraplot_cycles`` in the same directory as your python session +# or an arbitrary parent directory (see :func:`~ultraplot.config.Configurator.local_folders`). +# After adding the file, call :func:`~ultraplot.config.register_cycles` or restart your python +# session. You can also use :func:`~ultraplot.colors.DiscreteColormap.from_file` or manually +# pass :class:`~ultraplot.colors.DiscreteColormap` instances or file paths to +# :func:`~ultraplot.config.register_cycles`. See :func:`~ultraplot.config.register_cycles` +# for a table of recognized data file extensions. diff --git a/docs/external-links.rst b/docs/external-links.rst new file mode 100644 index 000000000..f95894554 --- /dev/null +++ b/docs/external-links.rst @@ -0,0 +1,125 @@ +.. _external_links: + +============== +External links +============== + +This page contains links to related external projects. + +Python packages +=============== + +The following packages inspired UltraPlot, are required or optional +dependencies of UltraPlot, or are distributed with UltraPlot: + +* `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 :class:`~ultraplot.colors.PerceptualColormap`. +* `TeX Gyre `__ - + An open source re-implementation of popular fonts like + `Helvetica `__ + and `Century `__. + These are distributed with UltraPlot 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 UltraPlot as a viable alternative to + `DejaVu Sans `__. + +Downloadable colormaps +====================== + +The following colormap repositories are +imported and registered by UltraPlot. + +* `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. + +.. + * `Cube Helix `__ - A + series of colormaps generated by rotating through RGB channel values. The colormaps + were added from `Palletable `__. + +Tools for making new colormaps +============================== + +Use these resources to make colormaps from scratch. Then import +them into UltraPlot by adding files to the ``.UltraPlot/cmaps`` folder +(see :ref:`this section ` for details). + +* `The UltraPlot API `__ - + Namely, the :class:`~ultraplot.colors.ContinuousColormap` class and + :class:`~ultraplot.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. + +Tools for making new color cycles +================================= + +Use these resources to make color cycles from scratch. Then import +them into UltraPlot by adding files to the ``.UltraPlot/cycles`` folder +(see :ref:`this section ` for details). + +* `The UltraPlot API `__ - + Namely, the :class:`~ultraplot.colors.DiscreteColormap` class and + :class:`~ultraplot.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 new file mode 100644 index 000000000..ad528797d --- /dev/null +++ b/docs/faq.rst @@ -0,0 +1,110 @@ +========================== +Frequently asked questions +========================== + +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 UltraPlot compare against these tools? + +* UltraPlot, seaborn, pandas, and xarray all offer tools for generating rigid, simple, + nice-looking plots from data stored in :class:`~pandas.DataFrame`\ s and + :class:`~xarray.DataArray`\ s (UltraPlot tries to apply labels from these objects, just like + pandas and xarray). +* UltraPlot is integrated with *cartopy* and *basemap*. You will find plotting geophysical + data in UltraPlot to be much more concise than working with cartopy and basemap + directly. +* UltraPlot *expands upon* the seaborn tools for working with color and global settings. + For example, see :class:`~ultraplot.constructor.Colormap`, + :class:`~ultraplot.colors.PerceptualColormap`, and :class:`~ultraplot.config.Configurator`. +* UltraPlot *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. +* UltraPlot is *built right into the matplotlib API*, thanks to special subclasses of the + :class:`~matplotlib.figure.Figure` and :class:`~matplotlib.axes.Axes` classes, while seaborn, + pandas, and xarray are meant to be used separately from the matplotlib API. + +In a nutshell, UltraPlot is intended to *unify the convenience of seaborn, pandas, and +xarray plotting with the power and customizability of the underlying matplotlib API*. + +.. + So while UltraPlot includes similar tools, the scope and goals are largely different. + Indeed, parts of UltraPlot were inspired by these projects -- in particular, + ``setup.py`` and ``colortools.py`` are modeled after seaborn. However the goals and + scope of UltraPlot are largely different: + +Why didn't you add to matplotlib directly? +========================================== + +Since UltraPlot 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, UltraPlot's tight + layout algorithm conflicts with matplotlib's `tight layout + `__ by + permitting *fluid figure dimensions*, and the new :class:`~ultraplot.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, :func:`~ultraplot.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 :func:`~ultraplot.ui.subplots` can be + replicated with `axes_grid1 + `__. Following `TOOWTDI + `__ philosophy, these features should probably + not be integrated. + +.. + * UltraPlot 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 UltraPlot can make more dramatic improvements. + +.. + Nevertheless, if any core matplotlib developers think that some + of UltraPlot'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). + +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. + +UltraPlot 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 :class:`~ultraplot.figure.Figure` which can be used to employ figure +dimensions from a particular journal standard. To keep the inline figures legible, +UltraPlot 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! diff --git a/docs/fonts.py b/docs/fonts.py new file mode 100644 index 000000000..e1212c30a --- /dev/null +++ b/docs/fonts.py @@ -0,0 +1,154 @@ +# -*- 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 +# ============== +# +# UltraPlot 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, UltraPlot is packaged +# the open source `TeX Gyre fonts `__ and a few +# additional open source sans-serif fonts. UltraPlot 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 UltraPlot, 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 ultraplotrc file `. +# To compare different fonts, use the :func:`~ultraplot.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 UltraPlot are shown below. + +# %% +import ultraplot as uplt + +fig, axs = uplt.show_fonts(family="sans-serif") + +# %% +import ultraplot as uplt + +fig, axs = uplt.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 `, UltraPlot 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 UltraPlot-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 UltraPlot 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 :func:`~ultraplot.demos.show_fonts` disables +# this fallback font in order to highlight the missing characters). +# +# .. note:: +# +# UltraPlot 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 +# UltraPlot (see the :ref:`ultraplotrc file ` for details). This means +# that if :rcraw:`mathtext.fontset` is ``'custom'`` and the font family is changed +# for an arbitrary :class:`~matplotlib.text.Text` instance, then any LaTeX-generated math +# in the text string will also use this font family. + +# %% +import ultraplot as uplt + +fig, axs = uplt.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 :func:`~ultraplot.config.Configurator.user_folder` and calling +# :func:`~ultraplot.config.register_fonts`. This command is called on import. You can +# also manually pass file paths to :func:`~ultraplot.config.register_fonts`. +# To change the default font, use the :func:`~ultraplot.config.rc` +# object or modify your ``ultraplotrc``. 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, UltraPlot 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 ``~/.UltraPlot/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 new file mode 100644 index 000000000..bd55c3882 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,162 @@ +.. notoc:: +.. image:: _static/logo_long.png + :align: center + +**UltraPlot** is a succinct wrapper around `matplotlib `__ +for creating **beautiful, publication-quality graphics** with ease. + +🚀 **Key Features** | Create More, Code Less +################### +✔ **Simplified Subplot Management** – Create multi-panel plots effortlessly. + +🎨 **Smart Aesthetics** – Optimized colormaps, fonts, and styles out of the box. + +📊 **Versatile Plot Types** – Cartesian plots, insets, colormaps, and more. + +📌 **Get Started** → :doc:`Installation guide ` | :doc:`Why UltraPlot? ` | :doc:`Usage ` + +-------------------------------------- + +**📖 User Guide** +################# +A preview of what UltraPlot can do. For more see the sidebar! + +.. grid:: 1 2 3 3 + :gutter: 2 + + .. grid-item-card:: + :link: subplots.html + :shadow: md + :class-card: card-with-bottom-text + + **Subplots & Layouts** + ^^^ + + .. image:: _static/example_plots/subplot_example.svg + :align: center + + Create complex multi-panel layouts effortlessly. + + .. grid-item-card:: + :link: cartesian.html + :shadow: md + :class-card: card-with-bottom-text + + **Cartesian Plots** + ^^^ + + .. image:: _static/example_plots/cartesian_example.svg + :align: center + + .. container:: bottom-aligned-text + + Easily generate clean, well-formatted plots. + + .. grid-item-card:: + :link: projections.html + :shadow: md + :class-card: card-with-bottom-text + + **Projections & Maps** + ^^^ + + .. image:: _static/example_plots/projection_example.svg + :align: center + + .. container:: bottom-aligned-text + Built-in support for projections and geographic plots. + + .. grid-item-card:: + :link: colorbars_legends.html + :shadow: md + :class-card: card-with-bottom-text + + **Colorbars & Legends** + ^^^ + + .. image:: _static/example_plots/colorbars_legends_example.svg + :align: center + + Customize legends and colorbars with ease. + + .. grid-item-card:: + :link: insets_panels.html + :shadow: md + :class-card: card-with-bottom-text + + **Insets & Panels** + ^^^ + + .. image:: _static/example_plots/panels_example.svg + :align: center + + Add inset plots and panel-based layouts. + + .. grid-item-card:: + :link: colormaps.html + :shadow: md + :class-card: card-with-bottom-text + + **Colormaps & Cycles** + ^^^ + + .. image:: _static/example_plots/colormaps_example.svg + :align: center + + Use prebuilt colormaps and define your own color cycles. + + +**📚 Reference & More** +####################### +For more details, check the full :doc:`User guide ` and :doc:`API Reference `. + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`glossary` +.. toctree:: + :maxdepth: 1 + :caption: Getting Started + :hidden: + + install + why + usage + +.. toctree:: + :maxdepth: 1 + :caption: User Guide + :hidden: + + basics + subplots + cartesian + networks + projections + colorbars_legends + insets_panels + 1dplots + 2dplots + stats + colormaps + cycles + colors + fonts + configuration + +.. toctree:: + :maxdepth: 1 + :caption: Reference + :hidden: + + api + external-links + whats_new + contributing + about + +.. toctree:: + :maxdepth: 1 + :caption: Dev Zone + :hidden: + + plot_comparison_results diff --git a/docs/insets_panels.py b/docs/insets_panels.py new file mode 100644 index 000000000..03f9d6cbd --- /dev/null +++ b/docs/insets_panels.py @@ -0,0 +1,194 @@ +# --- +# 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 UltraPlot, you can generate panels using the :func:`~ultraplot.axes.Axes.panel_axes` +# command (or its shorthand, :func:`~ultraplot.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 +# :class:`~ultraplot.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 :func:`~ultraplot.axes.Axes.panel_axes` more than once. To generate several +# panels at once, call :func:`~ultraplot.gridspec.SubplotGrid.panel_axes` on +# the :class:`~ultraplot.gridspec.SubplotGrid` returned by :func:`~ultraplot.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 :class:`~ultraplot.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 :class:`~ultraplot.figure.Figure`. +# +# .. important:: +# +# UltraPlot adds panel axes by allocating new rows and columns in the +# :class:`~ultraplot.gridspec.GridSpec` rather than "stealing" space from the parent +# subplot (note that subsequently indexing the :class:`~ultraplot.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 +# UltraPlot 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 ultraplot as uplt + +# Demonstrate that complex arrangements preserve +# spacing, aspect ratios, and axis sharing +gs = uplt.GridSpec(nrows=2, ncols=2) +fig = uplt.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 ultraplot as uplt +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 = uplt.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 :func:`~ultraplot.axes.Axes.inset_axes` command (or its +# shorthand, :func:`~ultraplot.axes.Axes.inset`). To generate several insets at once, call +# :func:`~ultraplot.gridspec.SubplotGrid.inset_axes` on the :class:`~ultraplot.gridspec.SubplotGrid` +# returned by :func:`~ultraplot.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 :class:`~ultraplot.axes.CartesianAxes`, you can pass ``zoom=True`` +# to :func:`~ultraplot.axes.Axes.inset_axes` to quickly add a "zoom indication" box and +# lines (this uses :func:`~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 ultraplot as uplt +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 = uplt.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 new file mode 100644 index 000000000..b914117c9 --- /dev/null +++ b/docs/install.rst @@ -0,0 +1 @@ +.. include:: ../INSTALL.rst \ No newline at end of file diff --git a/docs/networks.py b/docs/networks.py new file mode 100644 index 000000000..8e7053029 --- /dev/null +++ b/docs/networks.py @@ -0,0 +1,84 @@ +# -*- 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" +# Networks +# ======== + +# Visualizing Connections: Graphs! +# -------------------------------- +# Networks form a core aspect of any disciplines of science from engineering to biology. UltraPlot supports plotting networks using `networkx `__. It provides an intuitive interface for plotting networks using :func:`~ultraplot.axes.PlotAxes.graph` to plot networks. Plot customization can be passed to the networkx backend, see for full details their documentation. +# Layouts can be passed using strings, dicts or functions as long as they are compatible with networkx's layout functions. + +# Minimal Example +# --------------- +# To plot a graph, use :func:`~ultraplot.axes.PlotAxes.graph`. You need to merely provide a graph and UltraPlot will take care of the rest. UltraPlot will automatically style the layout to give sensible default. Every setting can be overidden if the user wants to customize the nodes, edges, or layout. +# +# By default, UltraPlot automatically styles the plot by removing the background and spines, and adjusting the layout to fit within a normalized [0,1] coordinate box. It also applies an equal aspect ratio to ensure square dimensions, which is ideal for the default circular markers. Additional customization—such as modifying labels, legends, axis limits, and more—can be done using :func:`~ultraplot.axes.CartesianAxes.format` to override the default styling. +# %% +import networkx as nx, ultraplot as uplt, numpy as np + +g = nx.path_graph(10) +A = nx.to_numpy_array(g) +el = np.array(g.edges()) + +fig, ax = uplt.subplots(ncols=3, refheight="3cm") +ax[0].graph(g) # We can plot graphs +ax[1].graph(A) # Or adjacency matrices and edgeslists +ax[2].graph(el) +ax.format(title=["From Graph", "From Adjacency Matrix", "From Edgelist"]) +uplt.show() + + +# %% [raw] raw_mimetype="text/restructuredtext" +# More Advanced Customization +# --------------------------- +# To customize a network plot, you can pass a dictionary of parameters to the :func:`~ultraplot.axes.PlotAxes.graph` function. These parameters are passed to the networkx backend, so you can refer to their documentation for more details (:func:`~networkx.drawing.nx_pylab.draw`, :func:`~networkx.drawing.nx_pylab.draw_networkx`, :func:`~networkx.drawing.nx_pylab.draw_networkx_nodes`, :func:`~networkx.drawing.nx_pylab.draw_networkx_edges`, :func:`~networkx.drawing.nx_pylab.draw_networkx_labels`). A more complicated example is shown below. +# %% +import networkx as nx, ultraplot as uplt, numpy as np + +# Generate some mock data +g = nx.gn_graph(n=100).to_undirected() +x = np.linspace(0, 10, 300) +y = np.sin(x) + np.cos(10 * x) + np.random.randn(*x.shape) * 0.3 +layout = [[1, 2, 3], [1, 4, 5]] +fig, ax = uplt.subplots(layout, share=0, figsize=(10, 4)) + +# Plot network on an inset +inax = ax[0].inset_axes([0.25, 0.75, 0.5, 0.5], zoom=False) +ax[0].plot(x, y) +ax[0].plot(x, y - np.random.rand(*x.shape)) +ax[0].format(xlabel="time $(t)$", ylabel="Amplitude", title="Inset example") +inax.graph( + g, + layout="forceatlas2", + node_kw=dict(node_size=0.2), +) +inax.format( + facecolor="white", + xspineloc="both", + yspineloc="both", +) + +# Show off different way of parsing inputs. When None is set it defaults to a Kamada Kawai +circular = nx.circular_layout(g) +layouts = [None, nx.arf_layout, circular, "random"] +names = ["Kamada Kawai", "Arf layout", "Circular", "Random"] +cmaps = ["viko", "bamo", "roma", "fes"] +for axi, layout, name, cmap in zip(ax[1:], layouts, names, cmaps): + cmap = uplt.colormaps.get_cmap(cmap) + colors = cmap(np.linspace(0, 1, g.number_of_nodes(), 0)) + axi.graph(g, layout=layout, node_kw=dict(node_color=colors)) + axi.set_title(name) +uplt.show() diff --git a/docs/projections.py b/docs/projections.py new file mode 100644 index 000000000..6584f9db5 --- /dev/null +++ b/docs/projections.py @@ -0,0 +1,608 @@ +# -*- 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://cartopy.readthedocs.io/stable/ +# +# .. _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 UltraPlot. +# +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_polar: +# +# Polar axes +# ---------- +# +# To create `polar axes `_, pass ``proj='polar'`` to an axes-creation +# command like :meth:`ultraplot.figure.Figure.add_subplot`. Polar axes are represented with the +# :class:`~ultraplot.axes.PolarAxes` subclass, which has its own :func:`~ultraplot.axes.PolarAxes.format` +# command. :meth:`ultraplot.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 :func:`ultraplot.axes.CartesianAxes.format`), +# and creating "annular" or "sector" plots by changing the radial or azimuthal +# bounds `rlim` and `thetalim`. Finally, since :meth:`ultraplot.axes.PolarAxes.format` +# calls :meth:`ultraplot.axes.Axes.format`, it can be used to add axes titles, a-b-c +# labels, and figure titles. +# +# For details, see :meth:`ultraplot.axes.PolarAxes.format`. + +# %% +import ultraplot as uplt +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 = uplt.subplots([[1, 1, 2, 2], [0, 3, 3, 0]], proj="polar", share=0) +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=uplt.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=uplt.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 +# :meth:`ultraplot.figure.Figure.add_subplot`, where ``name`` is any valid :ref:`PROJ projection +# name `. Alternatively, you can pass a :class:`cartopy.crs.Projection` or +# :class:`~mpl_toolkits.basemap.Basemap` instance returned by the :class:`~ultraplot.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. +# :func:`~ultraplot.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 :func:`~ultraplot.figure.Figure.subplots` for details). +# Geographic axes are represented with the :class:`~ultraplot.axes.GeoAxes` subclass, which +# has its own :meth:`~ultraplot.axes.GeoAxes.format` command. :meth:`ultraplot.axes.GeoAxes.format` +# facilitates :ref:`geographic-specific modifications ` like meridional +# and parallel gridlines and land mass outlines. The syntax is very similar to +# :func:`ultraplot.axes.CartesianAxes.format`. +# .. important:: +# The internal reference system used for plotting in ultraplot is **PlateCarree**. +# External libraries, such as `contextily`, may use different internal reference systems, +# such as **EPSG:3857** (Web Mercator). When interfacing with such libraries, it is important +# to provide the appropriate `transform` parameter to ensure proper alignment between coordinate systems. +# Note that the `proj` keyword and several of +# the :func:`~ultraplot.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 ultraplot as uplt + +fig = uplt.figure(refwidth=3, share=0) +axs = fig.subplots( + nrows=2, + proj="robin", + proj_kw={"lon0": 150}, +) +axs.format( + suptitle="Figure with single projection", + land=True, + latlines=30, + lonlines=60, +) + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_backends: +# +# Geographic backends +# ------------------- +# +# The :class:`~ultraplot.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 :class:`cartopy.crs.Projection` to the `proj` keyword), the +# returned axes is a subclass of :class:`cartopy.mpl.geoaxes.GeoAxes`. Under the hood, +# invoking :func:`~ultraplot.axes.GeoAxes.format` with cartopy as the backend changes map +# bounds using :meth:`~cartopy.mpl.geoaxes.GeoAxes.set_extent`, adds major and minor +# gridlines using :mod:`~cartopy.mpl.geoaxes.GeoAxes.gridlines`, and adds geographic +# features using :meth:`~cartopy.mpl.geoaxes.GeoAxes.add_feature`. If you prefer, you can +# use the standard :class:`cartopy.mpl.geoaxes.GeoAxes` methods just like you would in +# cartopy. If you need to use the underlying :class:`~cartopy.crs.Projection` instance, it +# is available via the :func:`~ultraplot.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=uplt.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 +# :class:`~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 :class:`~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 :func:`~ultraplot.axes.GeoAxes.format` +# with basemap as the backend adds major and minor gridlines using +# :meth:`~mpl_toolkits.basemap.Basemap.drawmeridians` and +# :meth:`~mpl_toolkits.basemap.Basemap.drawparallels` and adds geographic features +# using methods like :meth:`~mpl_toolkits.basemap.Basemap.fillcontinents` +# and :meth:`~mpl_toolkits.basemap.Basemap.drawcoastlines`. If you need to +# use the underlying :class:`~mpl_toolkits.basemap.Basemap` instance, it is +# available as the :attr:`~ultraplot.axes.GeoAxes.projection` attribute. +# +# Together, these features let you work with geophysical data without invoking +# verbose cartopy classes like :class:`~cartopy.crs.LambertAzimuthalEqualArea` or +# keeping track of separate :class:`~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, UltraPlot bounds polar cartopy projections like +# :classs:`~cartopy.crs.NorthPolarStereo` at the equator and gives non-polar cartopy +# projections global extent by calling :meth:`~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 :func:`~ultraplot.axes.GeoAxes.format`. +# * By default, UltraPlot gives circular boundaries to polar cartopy and basemap +# projections like :class:`~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 :func:`~ultraplot.axes.GeoAxes.format`. Please note +# that older versions of cartopy cannot add gridlines to maps bounded by circles. +# * To make things more consistent, the :class:`~ultraplot.constructor.Proj` constructor +# function lets you supply native `PROJ `__ keyword names +# for the cartopy :class:`~cartopy.crs.Projection` classes (e.g., `lon0` instead +# of `central_longitude`) and instantiates :class:`~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 now being actively maintained again with a short hiatus for a few years. We originally +# included basemap support because its gridline labeling was more powerful +# than cartopy gridline labeling. While cartopy gridline labeling has +# significantly improved since version 0.18, UltraPlot continues to support +# both mapping libraries to give users flexibility in their visualization choices. + +# %% +import ultraplot as uplt + +fig = uplt.figure(share=0) + +# Add projections +gs = uplt.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) +uplt.rc.reset() + + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_geoplot: +# +# Plotting in projections +# ----------------------- +# +# In UltraPlot, plotting with :class:`~ultraplot.axes.GeoAxes` is just like plotting +# with :class:`~ultraplot.axes.CartesianAxes`. UltraPlot 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 `ultraplot.axes.GeoAxes` +# instance rather than the :class:`~mpl_toolkits.basemap.Basemap` instance. +# +# To ensure that a 2D :class:`~ultraplot.axes.PlotAxes` command like +# :func:`~ultraplot.axes.PlotAxes.contour` or :func:`~ultraplot.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 +# :meth:`~cartopy.util.add_cyclic_point` and basemap's :meth:`~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 +# ``uplt.rc['land.zorder'] = 4`` (see the :ref:`next section ` +# for details). + +# %% +import ultraplot as uplt +import numpy as np + +# Fake data with unusual longitude seam location and without coverage over poles +offset = -40 +lon = uplt.arange(offset, 360 + offset - 1, 60) +lat = uplt.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 = uplt.GridSpec(nrows=2, ncols=2) + fig = uplt.figure(refwidth=2.5, share=0) + 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 :meth:`~ultraplot.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 :meth:`ultraplot.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`. +# +# :meth:`~ultraplot.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 :meth:`ultraplot.axes.GeoAxes.format` +# calls :meth:`ultraplot.axes.Axes.format`, it can be used to add axes titles, a-b-c labels, +# and figure titles, just like :func:`ultraplot.axes.CartesianAxes.format`. UltraPlot also adds the ability to add tick marks for longitude and latitude using the keywords `lontick` and `lattick` for rectilinear projections only. This can enhance contrast and readability under some conditions, e.g. when overlaying contours. +# +# For details, see the :meth:`ultraplot.axes.GeoAxes.format` documentation. + +# %% +import ultraplot as uplt + +gs = uplt.GridSpec(ncols=3, nrows=2, wratios=(1, 1, 1.2), hratios=(1, 1.2)) +fig = uplt.figure(refwidth=4, share=0) + +# 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 +# :meth:`~cartopy.mpl.geoaxes.GeoAxes.set_extent` or pass `lonlim`, +# `latlim`, or `boundinglat` to :meth:`~ultraplot.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, UltraPlot 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 :meth:`~ultraplot.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 :class:`~ultraplot.constructor.Proj` constructor function either directly or via +# the `proj_kw` :func:`~ultraplot.ui.subplots` keyword argument. You can also pass +# `lonlim` and `latlim` to :class:`~ultraplot.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 ultraplot as uplt + +# Plate Carrée map projection +uplt.rc.reso = "med" # use higher res for zoomed in geographic features +basemap = uplt.Proj("cyl", lonlim=(-20, 180), latlim=(-10, 50), backend="basemap") +fig, axs = uplt.subplots(nrows=2, refwidth=5, proj=("cyl", basemap), share=0) +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 ultraplot as uplt + +# Pole-centered map projections +basemap = uplt.Proj("npaeqd", boundinglat=60, backend="basemap") +fig, axs = uplt.subplots(ncols=2, refwidth=2.7, proj=("splaea", basemap), share=0) +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 ultraplot as uplt + +# Zooming in on continents +fig = uplt.figure(refwidth=3, share=0) +ax = fig.subplot(121, proj="lcc", proj_kw={"lon0": 0}) +ax.format(lonlim=(-20, 50), latlim=(30, 70), title="Cartopy example") +proj = uplt.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 ultraplot as uplt + +# Zooming in with cartopy degree-minute-second labels +# Set TeX Gyre Heros as the primary font but fall back to DejaVu Sans +uplt.rc["font.family"] = ["TeX Gyre Heros", "DejaVu Sans"] +uplt.rc.reso = "hi" +fig = uplt.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", +) +uplt.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 `. +# +# UltraPlot 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 :class:`~cartopy.crs.NorthPolarStereo` +# and :class:`~cartopy.crs.SouthPolarStereo` projections. + +# %% +import ultraplot as uplt + +# 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 = uplt.subplots(ncols=3, nrows=10, figwidth=7, proj=projs, share=0) +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 ultraplot as uplt + +# 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 = uplt.subplots( + ncols=3, nrows=8, figwidth=7, proj=projs, backend="basemap", share=0 +) +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/sphinxext/custom_roles.py b/docs/sphinxext/custom_roles.py new file mode 100644 index 000000000..f625d0835 --- /dev/null +++ b/docs/sphinxext/custom_roles.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +""" +Custom :rc: and :rcraw: roles for rc settings. +""" +import os + +from docutils import nodes +from matplotlib import rcParams + +from ultraplot.internals import rcsetup + + +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 [] + 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_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(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_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..6303aac52 --- /dev/null +++ b/docs/stats.py @@ -0,0 +1,286 @@ +# --- +# 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 :class:`~ultraplot.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 UltraPlot 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 +# :func:`~ultraplot.axes.PlotAxes.line`, :func:`~ultraplot.axes.PlotAxes.linex` +# (equivalently, :func:`~ultraplot.axes.PlotAxes.plot`, +# :func:`~ultraplot.axes.PlotAxes.plotx`), :func:`~ultraplot.axes.PlotAxes.scatter`, +# :func:`~ultraplot.axes.PlotAxes.scatterx`, :func:`~ultraplot.axes.PlotAxes.bar`, and +# :func:`~ultraplot.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 :class:`~ultraplot.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 ultraplot as uplt +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 = uplt.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 +# :func:`~ultraplot.axes.PlotAxes.boxplot`, :func:`~ultraplot.axes.PlotAxes.violinplot`, +# :func:`~ultraplot.axes.PlotAxes.boxploth`, and :func:`~ultraplot.axes.PlotAxes.violinploth` (or +# their new shorthands, :func:`~ultraplot.axes.PlotAxes.box`, :func:`~ultraplot.axes.PlotAxes.violin`, +# :func:`~ultraplot.axes.PlotAxes.boxh`, and :func:`~ultraplot.axes.PlotAxes.violinh`). The +# UltraPlot 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 :class:`~pandas.DataFrame` +# or :class:`~xarray.DataArray` column labels. Violin plot error bars are controlled +# with the same keywords used for :ref:`on-the-fly error bars `. + +# %% +import ultraplot as uplt +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 = uplt.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 +# :func:`~ultraplot.axes.PlotAxes.hist` and :func:`~ultraplot.axes.PlotAxes.histh`. +# As with the other 1D :class:`~ultraplot.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 :func:`~ultraplot.axes.PlotAxes.hist2d` +# :func:`~ultraplot.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, UltraPlot will include options for adding "smooth" kernel density +# estimations to histograms plots using a `kde` keyword. It will also include +# separate `ultraplot.axes.PlotAxes.kde` and `ultraplot.axes.PlotAxes.kde2d` commands. +# The :func:`~ultraplot.axes.PlotAxes.violin` and :func:`~ultraplot.axes.PlotAxes.violinh` commands +# will use the same algorithm for kernel density estimation as the `kde` commands. + +# %% +import ultraplot as uplt +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 = uplt.subplots(refwidth=4, refaspect=(3, 2)) +ax.format(suptitle="Overlaid histograms", xlabel="distribution", ylabel="count") +res = ax.hist( + x, + uplt.arange(-3, 8, 0.2), + filled=True, + alpha=0.7, + edgecolor="k", + cycle=("indigo9", "gray3", "red9"), + labels=list("abc"), + legend="ul", +) + +# %% +import ultraplot as uplt +import numpy as np + +# Sample data +N = 500 +state = np.random.RandomState(51423) +x = state.normal(size=(N,)) +y = state.normal(size=(N,)) +bins = uplt.arange(-3, 3, 0.25) + +# Histogram with marginal distributions +fig, axs = uplt.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 = uplt.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.py b/docs/subplots.py new file mode 100644 index 000000000..ced109c96 --- /dev/null +++ b/docs/subplots.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_layout: +# +# Subplots +# ======== +# +# This section documents a variety of features related to UltraPlot 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:: +# +# UltraPlot only supports one :class:`~ultraplot.gridspec.GridSpec` per figure +# (see the section on :ref:`adding subplots `), and UltraPlot +# does not officially support the "nested" matplotlib structures +# :class:`~matplotlib.gridspec.GridSpecFromSubplotSpec` and :class:`~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 UltraPlot -- 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 +# ------------ +# +# UltraPlot can quickly add labels to subplots using the `abc` parameter. This parameter +# can be a template (with a letter "a" or "A") used to format the subplot labels, such as "A.", which assigns +# an alphabetic label based on the axis number. Alternatively, you can pass a list to +# the `abc` parameter, where the list elements are mapped as labels for the subplots one by one. +# If you add subplots one-by-one with :func:`~ultraplot.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 :func:`~ultraplot.figure.Figure.add_subplot`. +# If you draw all of your subplots at once with :func:`~ultraplot.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 :class:`~ultraplot.gridspec.SubplotGrid` returned by :func:`~ultraplot.figure.Figure.add_subplots`). +# +# To turn on "a-b-c" labels, set :rcraw:`abc` to ``True`` or pass ``abc=True`` +# to :func:`~ultraplot.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 :func:`~ultraplot.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 `ultraplot.axes.Axes.text`. + +# %% +import ultraplot as uplt + +fig = uplt.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 ultraplot as uplt + +fig = uplt.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 +# ----------------------- +# +# UltraPlot automatically adjusts the figure width and height by default to +# respect the physical size of a "reference" subplot and the geometry of the +# :func:`~ultraplot.figure.Figure.gridspec`. The "reference" subplot is the subplot whose +# :func:`~ultraplot.axes.Axes.number` matches the `refnum` that was passed to +# :class:`~ultraplot.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 :func:`~ultraplot.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 :class:`~ultraplot.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 +# :func:`~matplotlib.axes.Axes.set_aspect` or filling the subplot with a +# :ref:`geographic projection `, :func:`~ultraplot.axes.PlotAxes.imshow` +# plot, or :func:`~ultraplot.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 +# :func:`~ultraplot.figure.Figure.add_subplot` or passing `ncols` or `nrows` to +# :func:`~ultraplot.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 UltraPlot :func:`~ultraplot.axes.Axes.colorbar`\ s and +# :func:`~ultraplot.axes.Axes.panel`\ s are always independent of the figure size. +# :class:`~ultraplot.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 ultraplot as uplt +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 = uplt.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 = uplt.subplots(ncols=2, nrows=3, proj="robin", share=0) +axs.format(land=True, landcolor="k") +fig.format(suptitle="Auto figure dimensions for grid of cartopy projections") + + +# %% +import ultraplot as uplt + +uplt.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 = uplt.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 = uplt.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 = uplt.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 = uplt.subplots([[1, 2], [1, 3]], refnum=ref, refwidth=1.8) + axs[ref - 1].format(title="reference", suptitle=suptitle) + +uplt.rc.reset() + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_tight: +# +# Spacing and tight layout +# ------------------------ +# +# UltraPlot automatically adjusts the spacing between subplots +# by default to accomadate labels using its own `"tight layout" algorithm +# `__. +# In contrast to matplotlib's algorithm, UltraPlot's algorithm can :ref:`change the +# figure size ` and permits variable spacing between each subplot +# row and column (see :class:`ultraplot.gridspec.GridSpec` for details). +# This algorithm can be disabled entirely by passing ``tight=False`` to +# :class:`~ultraplot.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 :class:`~ultraplot.figure.Figure` or +# :class:`~ultraplot.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 :class:`~ultraplot.figure.Figure` or :class:`~ultraplot.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 :func:`~ultraplot.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 ultraplot as uplt + +# Stress test of the tight layout algorithm +# This time override the algorithm between selected subplot rows/columns +fig, axs = uplt.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 ultraplot as uplt + +# 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 = uplt.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. UltraPlot builds on this feature by: +# +# #. Automatically sharing axes between subplots and :ref:`panels ` +# occupying the same rows or columns of the :class:`~ultraplot.gridspec.GridSpec`. This +# works for :ref:`aribtrarily complex subplot grids `. It also works +# for subplots generated one-by-one with :func:`~ultraplot.figure.Figure.add_subplot` +# rather than :func:`~ultraplot.figure.Figure.subplots`. It is controlled by the `sharex` +# and `sharey` :class:`~ultraplot.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 :class:`~ultraplot.gridspec.GridSpec`. +# This also works for complex subplot grids and subplots generated one-by-one. +# It is controlled by the `spanx` and `spany` :class:`~ultraplot.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 outermost plots. This implies that for left and bottom +# labels (default), the labels will appear on the leftmost and bottommost subplots. +# Note that labels will be shared only for plots that are immediately adjacent +# in the same row or column of the :class:`~ultraplot.gridspec.GridSpec`; a space +# or empty plot will add the labels, but not break the limit sharing. See below +# for a more complex example. +# +# The below examples demonstrate the effect of various axis and label sharing +# settings on the appearance of several subplot grids. + +# %% +import ultraplot as uplt +import numpy as np + +N = 50 +M = 40 +state = np.random.RandomState(51423) +cycle = uplt.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 = uplt.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 ultraplot as uplt +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 = uplt.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" +# When subplots are arranged on a grid, UltraPlot will +# automatically share axis labels where appropriate. For more +# complex layouts, UltraPlot will add the labels when the subplot +# is facing and "edge" which is defined as not immediately having a subplot next to it. For example: +# %% +import ultraplot as uplt, numpy as np + +layout = [[1, 0, 2], [0, 3, 0], [4, 0, 6]] +fig, ax = uplt.subplots(layout) +ax.format(xtickloc="top", ytickloc="right") +# plot data to indicate that limits are still shared +x = y = np.linspace(0, 1, 10) +for axi in ax: + axi.plot(axi.number * x, axi.number * y) + +# %% [raw] raw_mimetype="text/restructuredtext" +# Notice how the top and right labels here are added since no +# subplot is immediately adjacent to another, the limits however, are shared. + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_units: +# +# Physical units +# -------------- +# +# UltraPlot 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 :func:`~ultraplot.axes.Axes.colorbar` and :func:`~ultraplot.axes.Axes.panel` widths; +# various :meth:`~ultraplot.axes.Axes.legend` spacing and padding arguments; various +# :func:`~ultraplot.axes.Axes.format` font size and padding arguments; the line width and +# marker size arguments passed to :class:`~ultraplot.axes.PlotAxes` commands; and all +# applicable :func:`~ultraplot.config.rc` settings, e.g. :rcraw:`subplots.refwidth`, +# :rcraw:`legend.columnspacing`, and :rcraw:`axes.labelpad`. This feature is +# powered by the physical units engine :func:`~ultraplot.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 ultraplot as uplt +import numpy as np + +with uplt.rc.context(fontsize="12px"): # depends on rc['figure.dpi'] + fig, axs = uplt.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/ultraplotrc b/docs/ultraplotrc new file mode 100644 index 000000000..695703999 --- /dev/null +++ b/docs/ultraplotrc @@ -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/usage.rst b/docs/usage.rst new file mode 100644 index 000000000..3af7593f8 --- /dev/null +++ b/docs/usage.rst @@ -0,0 +1,203 @@ +.. _cartopy: https://cartopy.readthedocs.io/stable/ + +.. _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 UltraPlot +============= + +This page offers a condensed overview of UltraPlot'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 UltraPlot?`. + +.. _usage_background: + +Background +========== + +UltraPlot is an object-oriented matplotlib wrapper. The "wrapper" part means +that UltraPlot's features are largely a *superset* of matplotlib. You can use +plotting commands like :func:`~matplotlib.axes.Axes.plot`, :func:`~matplotlib.axes.Axes.scatter`, +:func:`~matplotlib.axes.Axes.contour`, and :func:`~matplotlib.axes.Axes.pcolor` like you always +have. The "object-oriented" part means that UltraPlot's features are implemented with +*subclasses* of the :class:`~matplotlib.figure.Figure` and :class:`~matplotlib.axes.Axes` classes. + +If you tend to use :obj:`~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 +:obj:`~matplotlib.pyplot`, makes things easier when working with multiple figures and axes, +and is certainly more "`pythonic `__". +Therefore, although many UltraPlot features may still work, we do not officially +support the :obj:`~matplotlib.pyplot` interface. + +.. _usage_import: + +Importing UltraPlot +================= + +Importing UltraPlot 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 UltraPlot at the top of your script and do nothing else! +We recommend importing UltraPlot as follows: + +.. code-block:: python + + import ultraplot as uplt + +This differentiates UltraPlot from the usual ``plt`` abbreviation reserved for +the :obj:`~matplotlib.pyplot` module. + +.. _usage_classes: + +Figure and axes classes +======================= + +Creating figures with UltraPlot is very similar to +matplotlib. You can either create the figure and +all of its subplots at once: + +.. code-block:: python + + fig, axs = uplt.subplots(...) + +or create an empty figure +then fill it with subplots: + +.. code-block:: python + + fig = uplt.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 :func:`~ultraplot.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, UltraPlot uses the :class:`~ultraplot.figure.Figure`, :class:`~ultraplot.axes.Axes`, and +:class:`~ultraplot.axes.PlotAxes` subclasses. UltraPlot figures are saved with +:func:`~ultraplot.figure.Figure.save` or `~matplotlib.figure.Figure.savefig`, +and UltraPlot axes belong to one of the following three child classes: + +* :class:`~ultraplot.axes.CartesianAxes`: + For ordinary plots with *x* and *y* coordinates. +* :class:`~ultraplot.axes.GeoAxes`: + For geographic plots with *longitude* and *latitude* coordinates. +* :class:`~ultraplot.axes.PolarAxes`: + For polar plots with *azimuth* and *radius* coordinates. + +Most of UltraPlot'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 :func:`~ultraplot.axes.Axes.format` and :func:`~ultraplot.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 :func:`~ultraplot.axes.Axes.colorbar` and :meth:`~ultraplot.axes.Axes.legend` commands + draw colorbars and legends inside of subplots or along the outside edges of + subplots. The :func:`~ultraplot.figure.Figure.colorbar` and :meth:`~ultraplot.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 :class:`~ultraplot.axes.PlotAxes` subclass (used for all UltraPlot axes) + adds many, many useful features to virtually every plotting command + (including :func:`~ultraplot.axes.PlotAxes.plot`, :func:`~ultraplot.axes.PlotAxes.scatter`, + :func:`~ultraplot.axes.PlotAxes.bar`, :func:`~ultraplot.axes.PlotAxes.area`, + :func:`~ultraplot.axes.PlotAxes.box`, :func:`~ultraplot.axes.PlotAxes.violin`, + :func:`~ultraplot.axes.PlotAxes.contour`, :func:`~ultraplot.axes.PlotAxes.pcolor`, + and :func:`~ultraplot.axes.PlotAxes.imshow`). See the :ref:`1D plotting ` + and :ref:`2D plotting ` sections for details. + +.. _usage_integration: + +Integration features +==================== + +UltraPlot 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 :class:`~ultraplot.axes.GeoAxes` class uses the `cartopy`_ or + `basemap`_ packages to :ref:`plot geophysical data `, + :ref:`add geographic features `, and + :ref:`format projections `. :class:`~ultraplot.axes.GeoAxes` provides + provides a simpler, cleaner interface than the original `cartopy`_ and `basemap`_ + interfaces. Figures can be filled with :class:`~ultraplot.axes.GeoAxes` by passing the + `proj` keyword to :func:`~ultraplot.ui.subplots`. +* If you pass a :class:`~pandas.Series`, :class:`~pandas.DataFrame`, or :class:`~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 :func:`~xarray.DataArray.plot` and + :func:`~pandas.DataFrame.plot` commands. See the sections on :ref:`1D plotting + ` and :ref:`2D plotting ` for a demonstration. + +Since these features are optional, +UltraPlot can be used without installing any of these packages. + +.. _usage_features: + +Additional features +=================== + +Outside of the features provided by the :class:`~ultraplot.figure.Figure` and +:class:`~ultraplot.axes.Axes` subclasses, UltraPlot includes several useful +classes and :ref:`constructor functions `. + +* The :class:`~ultraplot.constructor.Colormap` and :class:`~ultraplot.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 :class:`~ultraplot.colors.ContinuousColormap` and + :class:`~ultraplot.colors.DiscreteColormap` subclasses replace the default matplotlib + colormap classes and add several methods. The new + :class:`~ultraplot.colors.PerceptualColormap` class is used to make + colormaps with :ref:`perceptually uniform transitions `. +* The :func:`~ultraplot.demos.show_cmaps`, :func:`~ultraplot.demos.show_cycles`, + :func:`~ultraplot.demos.show_colors`, :func:`~ultraplot.demos.show_fonts`, + :func:`~ultraplot.demos.show_channels`, and :func:`~ultraplot.demos.show_colorspaces` + functions are used to visualize your :ref:`color scheme ` + and :ref:`font options ` and + :ref:`inspect individual colormaps `. +* The :class:`~ultraplot.constructor.Norm` constructor function generates colormap + normalizers from shorthand names. The new + :class:`~ultraplot.colors.SegmentedNorm` normalizer scales colors evenly + w.r.t. index for arbitrarily spaced monotonic levels, and the new + :class:`~ultraplot.colors.DiscreteNorm` meta-normalizer is used to + :ref:`break up colormap colors into discrete levels `. +* The :class:`~ultraplot.constructor.Locator`, :class:`~ultraplot.constructor.Formatter`, and + :class:`~ultraplot.constructor.Scale` constructor functions return corresponding class + instances from flexible input types. These are used to interpret keyword + arguments passed to :func:`~ultraplot.axes.Axes.format`, and can be used to quickly + and easily modify :ref:`x and y axis settings `. +* The :func:`~ultraplot.config.rc` object, an instance of + :class:`~ultraplot.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 :func:`~ultraplot.config.inline_backend_fmt` + so that your inline figures look the same as your saved figures. diff --git a/docs/why.rst b/docs/why.rst new file mode 100644 index 000000000..74fc644c4 --- /dev/null +++ b/docs/why.rst @@ -0,0 +1,924 @@ +.. _cartopy: https://cartopy.readthedocs.io/stable/ + +.. _basemap: https://matplotlib.org/basemap/index.html + +.. _seaborn: https://seaborn.pydata.org + +.. _pandas: https://pandas.pydata.org + +.. _xarray: http://xarray.pydata.org/en/stable/ + +.. _rainbow: https://doi.org/10.1175/BAMS-D-13-00155.1 + +.. _xkcd: https://blog.xkcd.com/2010/05/03/color-survey-results/ + +.. _opencolor: https://yeun.github.io/open-color/ + +.. _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 + +.. _seacolor: https://seaborn.pydata.org/tutorial/color_palettes.html + +.. _texgyre: https://frommindtotype.wordpress.com/2018/04/23/the-tex-gyre-font-family/ + +.. _why: + +============ +Why UltraPlot? +============ + +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... + +* Make highly complex figures with many subplots. +* Want to finely tune their annotations and aesthetics. +* Need to make new figures nearly every day. + +UltraPlot'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 `. UltraPlot +makes changes that would be hard to justify or difficult to incorporate +into matplotlib itself, owing to differing design choices and backwards +compatibility considerations. + +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 `. + +.. _why_less_typing: + +Less typing, more plotting +========================== + +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 :class:`~matplotlib.axes.Axes` +setter (e.g. :func:`~matplotlib.axes.Axes.set_xlabel` and +:func:`~matplotlib.axes.Axes.set_xticks`), an :class:`~matplotlib.axis.XAxis` or +:class:`~matplotlib.axis.YAxis` setter (e.g. +:func:`~matplotlib.axis.Axis.set_major_locator` and +:func:`~matplotlib.axis.Axis.set_major_formatter`), a :class:`~matplotlib.spines.Spine` +setter (e.g. :func:`~matplotlib.spines.Spine.set_bounds`), or a "bulk" property +setter (e.g. :func:`~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 +------- + +UltraPlot includes the :func:`~ultraplot.axes.Axes.format` command to resolve this. +Think of this as an expanded and thoroughly documented version of the +:func:`~matplotlib.artist.Artist.update` command. :func:`~ultraplot.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 :func:`~ultraplot.figure.Figure.format` +and :func:`~ultraplot.gridspec.SubplotGrid.format` commands can be used to +:func:`~ultraplot.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... + +.. code-block:: python + + import ultraplot as uplt + fig, axs = uplt.subplots(ncols=2) + axs.format(color='gray', linewidth=1) + axs.format(xlim=(0, 100), xticks=10, xtickminor=True, xlabel='foo', ylabel='bar') + +is much more succinct than... + +.. code-block:: python + + 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 :class:`~ultraplot.axes.CartesianAxes` formatting, + see :ref:`this page `. +* For :class:`~ultraplot.axes.PolarAxes` formatting, + see :ref:`this page `. +* For :class:`~ultraplot.axes.GeoAxes` formatting, + see :ref:`this page `. + +.. _why_constructor: + +Class constructor functions +=========================== + +Limitation +---------- + +Matplotlib and `cartopy`_ define several classes with verbose names like +:class:`~matplotlib.ticker.MultipleLocator`, :class:`~matplotlib.ticker.FormatStrFormatter`, +and :class:`~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. + +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? + +Changes +------- + +In UltraPlot, 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 :class:`~ultraplot.constructor.Locator` +returns a :class:`~matplotlib.ticker.MultipleLocator`, a +lists of strings passed to :class:`~ultraplot.constructor.Formatter` returns a +:class:`~matplotlib.ticker.FixedFormatter`, and :class:`~ultraplot.constructor.Cycle` +and :class:`~ultraplot.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. +``uplt.MultipleLocator(...)`` or ``uplt.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) +================================ ============================================================ ============================================================================== ================================================================================================================================================================================================ +:class:`~ultraplot.constructor.Proj` :class:`~cartopy.crs.Projection` or :class:`~mpl_toolkits.basemap.Basemap` :func:`~ultraplot.figure.Figure.add_subplot` and :func:`~ultraplot.figure.Figure.add_subplots` ``proj=`` +:class:`~ultraplot.constructor.Locator` :class:`~matplotlib.ticker.Locator` :func:`~ultraplot.axes.Axes.format` and :func:`~ultraplot.axes.Axes.colorbar` ``locator=``, ``xlocator=``, ``ylocator=``, ``minorlocator=``, ``xminorlocator=``, ``yminorlocator=``, ``ticks=``, ``xticks=``, ``yticks=``, ``minorticks=``, ``xminorticks=``, ``yminorticks=`` +:class:`~ultraplot.constructor.Formatter` :class:`~matplotlib.ticker.Formatter` :func:`~ultraplot.axes.Axes.format` and :func:`~ultraplot.axes.Axes.colorbar` ``formatter=``, ``xformatter=``, ``yformatter=``, ``ticklabels=``, ``xticklabels=``, ``yticklabels=`` +:class:`~ultraplot.constructor.Scale` :class:`~matplotlib.scale.ScaleBase` :func:`~ultraplot.axes.Axes.format` ``xscale=``, ``yscale=`` +:class:`~ultraplot.constructor.Colormap` :class:`~matplotlib.colors.Colormap` 2D :class:`~ultraplot.axes.PlotAxes` commands ``cmap=`` +:class:`~ultraplot.constructor.Norm` :class:`~matplotlib.colors.Normalize` 2D :class:`~ultraplot.axes.PlotAxes` commands ``norm=`` +:class:`~ultraplot.constructor.Cycle` :class:`~cycler.Cycler` 1D :class:`~ultraplot.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: + +Automatic dimensions and spacing +================================ + +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 :func:`~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 +:class:`~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, UltraPlot fixes the physical dimensions of a *reference subplot* rather +than the figure. The reference subplot dimensions are controlled with the `refwidth`, +`refheight`, and `refaspect` :class:`~ultraplot.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 `, :func:`~matplotlib.axes.Axes.imshow`, and +:func:`~ultraplot.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, UltraPlot also uses :ref:`its own tight layout algorithm ` -- +preventing text labels from overlapping with subplots. This algorithm works with the +:class:`~ultraplot.gridspec.GridSpec` subclass rather than :class:`~matplotlib.gridspec.GridSpec`, which +provides the following advantages: + +* The :class:`~ultraplot.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 :class:`~ultraplot.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 + :func:`~ultraplot.figure.Figure.add_subplots` fixes the right margin + at 1 font size-width while the others are adjusted automatically. +* Only one :class:`~ultraplot.gridspec.GridSpec` is permitted per figure, + considerably simplifying the tight layout algorithm calculations. + This restriction is enforced by requiring successive + :func:`~ultraplot.figure.Figure.add_subplot` calls to imply the same geometry and + include only subplot specs generated from the same :class:`~ultraplot.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 +============================== + +Limitation +---------- + +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 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 +------- + +UltraPlot 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 + :class:`~ultraplot.gridspec.GridSpec` row or column. This is controlled by the `sharex`, + `sharey`, `spanx`, `spany`, `alignx`, and `aligny` figure keywords. +* The figure :func:`~ultraplot.figure.Figure.colorbar` and :meth:`~ultraplot.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, ``uplt.rc['abc'] = 'A.'`` or ``axs.format(abc='A.')``. + This is possible because :func:`~ultraplot.figure.Figure.add_subplot` assigns a unique + :func:`~ultraplot.axes.Axes.number` to every new subplot. +* The :func:`~ultraplot.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 :class:`~ultraplot.gridspec.SubplotGrid` is returned by + :func:`~ultraplot.figure.Figure.subplots`, and can be indexed like a list or a 2D array. +* The :func:`~ultraplot.axes.Axes.panel_axes` (shorthand :func:`~ultraplot.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 :class:`~ultraplot.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 +============================= + +Limitation +---------- + +In matplotlib, it can be difficult to draw :func:`~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, :func:`~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 +------- + +UltraPlot 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 :func:`~ultraplot.axes.Axes.colorbar` or :meth:`~ultraplot.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 :func:`~ultraplot.axes.Axes.colorbar` or :meth:`~ultraplot.axes.Axes.legend`. +* To draw a colorbar or legend along the edge of the figure, use + :func:`~ultraplot.figure.Figure.colorbar` and :class:`~ultraplot.figure.Figure.legend`. + The `col`, `row`, and `span` keywords control which + :class:`~ultraplot.gridspec.GridSpec` rows and columns are spanned + by the colorbar or legend. + +Since :class:`~ultraplot.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, :class:`~ultraplot.figure.Figure` and :class:`~ultraplot.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 +========================== + +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 +------- + +UltraPlot uses the :class:`~ultraplot.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" :class:`~ultraplot.axes.PlotAxes` commands +like :func:`~ultraplot.axes.PlotAxes.line` (equivalent to :func:`~ultraplot.axes.PlotAxes.plot`) +and :func:`~ultraplot.axes.PlotAxes.scatter`: + +* The treatment of data arguments passed to the 1D :class:`~ultraplot.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 :class:`~ultraplot.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 :class:`~ultraplot.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 :class:`~ultraplot.axes.PlotAxes` commands. +* The :func:`~ultraplot.axes.PlotAxes.bar` and :func:`~ultraplot.axes.PlotAxes.barh` commands accept 2D + arrays and can :ref:`stack or group ` successive columns. Likewise, the + :func:`~ultraplot.axes.PlotAxes.area` and :func:`~ultraplot.axes.PlotAxes.areax` commands (shorthands + for :func:`~ultraplot.axes.PlotAxes.fill_between` and :func:`~ultraplot.axes.PlotAxes.fill_betweenx`) + accept 2D arrays and can :ref:`stack or overlay ` successive columns. +* The :func:`~ultraplot.axes.PlotAxes.bar`, :func:`~ultraplot.axes.PlotAxes.barh`, + :func:`~ultraplot.axes.PlotAxes.vlines`, :func:`~ultraplot.axes.PlotAxes.hlines`, + :func:`~ultraplot.axes.PlotAxes.area`, and :func:`~ultraplot.axes.PlotAxes.areax` + commands accept a `negpos` keyword argument that :ref:`assigns different + colors ` to "negative" and "positive" regions. +* The :func:`~ultraplot.axes.PlotAxes.linex` and :func:`~ultraplot.axes.PlotAxes.scatterx` commands + are just like :func:`~ultraplot.axes.PlotAxes.line` and :func:`~ultraplot.axes.PlotAxes.scatter`, + but positional arguments are interpreted as *x* coordinates or (*y*, *x*) pairs. + There are also the related commands :func:`~ultraplot.axes.PlotAxes.stemx`, + :func:`~ultraplot.axes.PlotAxes.stepx`, :func:`~ultraplot.axes.PlotAxes.boxh` (shorthand for + :func:`~ultraplot.axes.PlotAxes.boxploth`), and :func:`~ultraplot.axes.PlotAxes.violinh` (shorthand + for :func:`~ultraplot.axes.PlotAxes.violinploth`). +* The :func:`~ultraplot.axes.PlotAxes.line`, :func:`~ultraplot.axes.PlotAxes.linex`, + :func:`~ultraplot.axes.PlotAxes.scatter`, :func:`~ultraplot.axes.PlotAxes.scatterx`, + :func:`~ultraplot.axes.PlotAxes.bar`, and :func:`~ultraplot.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 :func:`~matplotlib.axes.Axes.errorbar` or :func:`~matplotlib.axes.Axes.fill_between`. +* The :func:`~ultraplot.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" :class:`~ultraplot.axes.PlotAxes` commands +like :func:`~ultraplot.axes.PlotAxes.pcolor` and :func:`~ultraplot.axes.PlotAxes.contour`: + +* The treatment of data arguments passed to the 2D :class:`~ultraplot.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 :class:`~ultraplot.constructor.Colormap` and :class:`~ultraplot.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 :func:`~ultraplot.axes.PlotAxes.contour`, :func:`~ultraplot.axes.PlotAxes.contourf`, + :func:`~ultraplot.axes.PlotAxes.pcolormesh`, and :func:`~ultraplot.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 :class:`~ultraplot.axes.PlotAxes` commands. +* The :class:`~ultraplot.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 :class:`~ultraplot.axes.PlotAxes` commands. +* The :class:`~ultraplot.colors.DivergingNorm` normalizer is perfect for data with a + :ref:`natural midpoint ` and offers both "fair" and "unfair" scaling. + The :class:`~ultraplot.colors.SegmentedNorm` normalizer can generate + uneven color gradations useful for :ref:`unusual data distributions `. +* The :func:`~ultraplot.axes.PlotAxes.heatmap` command invokes + :func:`~ultraplot.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 :func:`~ultraplot.axes.PlotAxes.pcolor` are + automatically translated to "edges", and coordinate edges passed to commands like + :func:`~ultraplot.axes.PlotAxes.contour` are automatically translated to "centers". In + matplotlib, ``pcolor`` simply truncates and offsets the data when it receives centers. +* Commands like :func:`~ultraplot.axes.PlotAxes.pcolor`, :func:`~ultraplot.axes.PlotAxes.contourf` + and :func:`~ultraplot.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 :class:`~ultraplot.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 +=============================== + +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 :class:`~mpl_toolkits.basemap.Basemap` object rather than the +:class:`~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 +------- + +UltraPlot 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 = uplt.subplots(proj='pcarree')`` +or ``fig.add_subplot(proj='pcarree')``. Alternatively, use the +:class:`~ultraplot.constructor.Proj` constructor function to quickly generate +a :class:`~cartopy.crs.Projection` or :class:`~mpl_toolkits.basemap.Basemap` instance. + +Requesting geographic projections creates a :class:`~ultraplot.axes.GeoAxes` +with unified support for `cartopy`_ and `basemap`_ features via the +:func:`~ultraplot.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 :func:`~ultraplot.axes.CartesianAxes.format` and :func:`~ultraplot.axes.PolarAxes.format`. + +The :class:`~ultraplot.axes.GeoAxes` subclass also makes longitude-latitude coordinates +the "default" coordinate system by passing ``transform=ccrs.PlateCarree()`` +or ``latlon=True`` to :class:`~ultraplot.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 :class:`~ultraplot.axes.PlotAxes` commands +like :func:`~ultraplot.axes.PlotAxes.contour` and :func:`~ultraplot.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 :class:`~ultraplot.axes.GeoAxes`, + see :ref:`this page `. +* For formatting :class:`~ultraplot.axes.GeoAxes`, + see :ref:`this page `. +* For changing the :class:`~ultraplot.axes.GeoAxes` bounds, + see :ref:`this page `. + +.. _why_xarray_pandas: + +Pandas and xarray integration +============================= + +Limitation +---------- + +Scientific data is commonly stored in array-like containers +that include metadata -- namely, :class:`~xarray.DataArray`\ s, :class:`~pandas.DataFrame`\ s, +and :class:`~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 :func:`~xarray.DataArray.plot`, :func:`~pandas.DataFrame.plot`, +and :func:`~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 +:obj:`~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 +------- + +UltraPlot reproduces many of the :func:`~xarray.DataArray.plot`, +:func:`~pandas.DataFrame.plot`, and :func:`~pandas.Series.plot` +features directly on the :class:`~ultraplot.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. + +UltraPlot also handles metadata associated with :class:`~xarray.DataArray`, :class:`~pandas.DataFrame`, +:class:`~pandas.Series`, and :class:`~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 :class:`~pint.Quantity` arrays (including +:class:`~pint.Quantity` those stored inside :class:`~xarray.DataArray` containers), a unit string +is generated from the `pint.Unit` according to the :rcraw:`unitformat` setting +(note UltraPlot also automatically calls :func:`~pint.UnitRegistry.setup_matplotlib` +whenever a :class:`~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 :class:`~ultraplot.axes.PlotAxes` commands, + see :ref:`this page `. +* For integration with 2D :class:`~ultraplot.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 +========================== + +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 +------- + +UltraPlot adds new colormaps, colors, and fonts to help you make more +aesthetically pleasing figures. + +* UltraPlot 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 :class:`~ultraplot.colors.PerceptualColormap` class for generating + new ones. A :ref:`table of colormap ` and + :ref:`color cycles ` can be shown using + :func:`~ultraplot.demos.show_cmaps` and :func:`~ultraplot.demos.show_cycles`. + Colormaps like ``'jet'`` can still be accessed, but this is discouraged. +* UltraPlot 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 :func:`~ultraplot.demos.show_colors`. +* UltraPlot 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 :func:`~ultraplot.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 +:class:`~matplotlib.colors.LinearSegmentedColormap` class (representing "smooth" +color gradations) and the :class:`~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 :class:`~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 +------- + +UltraPlot tries to make it easy to manipulate colormaps and property cycles. + +* All colormaps in UltraPlot are replaced with the :class:`~ultraplot.colors.ContinuousColormap` + and :class:`~ultraplot.colors.DiscreteColormap` subclasses of + :class:`~matplotlib.colors.LinearSegmentedColormap` and :class:`~matplotlib.colors.ListedColormap`. + These classes include several useful features leveraged by the + :ref:`constructor functions ` + :class:`~ultraplot.constructor.Colormap` and :class:`~ultraplot.constructor.Cycle`. +* The :class:`~ultraplot.constructor.Colormap` function can merge, truncate, and + modify existing colormaps or generate brand new colormaps. It can also + create new :class:`~ultraplot.colors.PerceptualColormap`\ s -- a type of + :class:`~ultraplot.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 :class:`~ultraplot.constructor.Cycle` function can make property cycles from + scratch or retrieve "registered" color cycles from their associated + :class:`~ultraplot.colors.DiscreteColormap` instances. It can also make property + cycles by splitting up the colors from registered or on-the-fly + :class:`~ultraplot.colors.ContinuousColormap`\ s and :class:`~ultraplot.colors.PerceptualColormap`\ s. + +UltraPlot 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 +:class:`~ultraplot.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 +===================== + +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 +------- + +UltraPlot uses physical units for the :class:`~ultraplot.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. UltraPlot also permits arbitrary +string units for these keywords, for the :class:`~ultraplot.figure.Figure` keywords +`figsize`, `figwidth`, `figheight`, `refwidth`, and `refheight`, and in a +few other places. This is powered by the physical units engine :func:`~ultraplot.utils.units`. +Acceptable units include inches, centimeters, millimeters, +pixels, `points `__, and `picas +`__ (a table of acceptable +units is found :ref:`here `). Note the :func:`~ultraplot.utils.units` engine +also translates rc settings assigned to :func:`~ultraplot.config.rc_matplotlib` and +:obj:`~ultraplot.config.rc_UltraPlot`, 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 :class:`~ultraplot.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 :obj:`~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 UltraPlot, you can use the :obj:`~ultraplot.config.rc` object to change both native +matplotlib settings (found in :obj:`~ultraplot.config.rc_matplotlib`) and added UltraPlot +settings (found in :obj:`~ultraplot.config.rc_UltraPlot`). 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 ``uplt.rc.key = value``, ``uplt.rc[key] = value``, +``uplt.rc.update(key=value)``, using :func:`~ultraplot.axes.Axes.format`, or using +:func:`~ultraplot.config.Configurator.context`. Settings that have changed during the +python session can be saved to a file with :func:`~ultraplot.config.Configurator.save` +(see :func:`~ultraplot.config.Configurator.changed`), and settings can be loaded from +files with :func:`~ultraplot.config.Configurator.load`. + +Links +----- + +* For an introduction, + see :ref:`this page `. +* For more on changing settings, + see :ref:`this page `. +* For more on UltraPlot 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_dotUltraPlot: + +Loading stuff +============= + +Limitation +---------- + +Matplotlib :obj:`~matplotlib.rcParams` can be changed persistently by placing +ref:`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 +------- + +UltraPlot settings can be changed persistently by editing the default ``ultraplotrc`` +file in the location given by :func:`~ultraplot.config.Configurator.user_file` (this is +usually ``$HOME/.ultraplot/ultraplotrc``) or by adding loose ``ultraplotrc`` 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. + +UltraPlot also automatically registers colormaps, color cycles, colors, and font +files stored in subfolders named ``cmaps``, ``cycles``, ``colors``, and ``fonts`` +in the location given by :func:`~ultraplot.config.Configurator.user_folder` (this is usually +``$HOME/.ultraplot``), as well as loose subfolders named ``ultraplot_cmaps``, +``ultraplot_cycles``, ``ultraplot_colors``, and ``ultraplot_fonts`` in the current +directory or an arbitrary parent directory. You can save colormaps and color cycles to +:func:`~ultraplot.config.Configurator.user_folder` simply by passing ``save=True`` to +:class:`~ultraplot.constructor.Colormap` and :class:`~ultraplot.constructor.Cycle`. To re-register +these files during an active python session, or to register arbitrary input arguments, +you can use :func:`~ultraplot.config.register_cmaps`, :func:`~ultraplot.config.register_cycles`, +:func:`~ultraplot.config.register_colors`, or :func:`~ultraplot.config.register_fonts`. + +Links +----- + +* For the ``ultraplotrc`` 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/environment.yml b/environment.yml new file mode 100644 index 000000000..2e9519a3c --- /dev/null +++ b/environment.yml @@ -0,0 +1,33 @@ +name: ultraplot-dev +channels: + - conda-forge +dependencies: + - python>=3.10,<3.14 + - numpy + - matplotlib>=3.9 + - cartopy + - xarray + - seaborn + - pandas + - pytest + - pytest-mpl + - pytest-cov + - jupyter + - pip + - pint + - sphinx + - nbsphinx + - jupytext + - sphinx-copybutton + - sphinx-autoapi + - sphinx-automodapi + - sphinx-rtd-theme + - typing-extensions + - basemap >=1.4.1 + - pre-commit + - sphinx-design + - networkx + - pyarrow + - cftime + - pip: + - git+https://github.com/ultraplot/UltraTheme.git diff --git a/logo/PermanentMarker-Regular.ttf b/logo/PermanentMarker-Regular.ttf new file mode 100644 index 000000000..3218fc5b3 Binary files /dev/null and b/logo/PermanentMarker-Regular.ttf differ diff --git a/logo/environment-logo.yml b/logo/environment-logo.yml new file mode 100644 index 000000000..0e0aa1c1b --- /dev/null +++ b/logo/environment-logo.yml @@ -0,0 +1,38 @@ +# Hard requirements for running tests +# See docs/environment.yml for dependency notes +# WARNING: Keep this up-to-date with ci/environment.yml +name: ultraplot-dev +channels: + - conda-forge +dependencies: + - python==3.11 + - numpy>=1.26.0 + - pandas + - xarray + - matplotlib>=3.9.1 + - cartopy + - ipykernel + - pandoc + - python-build + - setuptools==72.1.0 + - setuptools_scm + - setuptools_scm_git_archive + - wheel + - 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 + - pip: + - git+https://github.com/ultraplot-dev/sphinx-automodapi@ultraplot-mods diff --git a/logo/logo.py b/logo/logo.py new file mode 100644 index 000000000..019d4074f --- /dev/null +++ b/logo/logo.py @@ -0,0 +1,213 @@ +# %% +import ultraplot as plt, numpy as np + +from matplotlib.font_manager import FontProperties +from matplotlib import patheffects as pe +from matplotlib.patches import Rectangle +from scipy.ndimage import gaussian_filter + +font = FontProperties(fname="./PermanentMarker-Regular.ttf") + + +fs = 38 +left = 0.575 +sw = 3 +fig, ax = plt.subplots(figsize=(3, 1.25)) +ax.text( + left, + 0.5, + "Ultra", + fontsize=fs, + fontproperties=font, + va="center", + ha="right", + color="steelblue", + path_effects=[ + pe.Stroke(linewidth=sw, foreground="white"), + pe.Normal(), + ], + transform=ax.transAxes, +) +ax.text( + left, + 0.5, + "Plot", + fontsize=fs, + # fontproperties = font, + va="center", + ha="left", + color="white", + path_effects=[ + pe.Stroke(linewidth=sw, foreground="steelblue"), + pe.Normal(), + ], + transform=ax.transAxes, +) + +shift = 0.033 +import colorengine as ce + +colors = np.linspace(0, 1, 4, 0) +# colors = plt.colormaps.get_cmap("viko")(colors) +colors = ce.vivid(colors) +for idx, color in enumerate(colors): + s = idx * shift + ax.axhline(0.275 - 2.3 * s, 0.59 + s, 0.9 - s, color=color, ls="-", lw=3) +ax.axis(False) +# fig.set_facecolor("lightgray") + + +# Create checkerboard pattern +n_squares_x, n_squares_y = 20, 8 # Adjust number of squares +square_size = 0.1 # Size of each square + + +fig_aspect = fig.get_figwidth() / fig.get_figheight() +square_size_y = 0.1 # Base square size in y direction +square_size_x = square_size_y / fig_aspect # Adjust x size to maintain square shape + +n_squares_x = ( + int(1.0 / square_size_x) + 1 +) # Calculate number of squares needed in x direction + +# Create alpha mask with Gaussian fade +x = np.linspace(-2, 2, n_squares_x) +y = np.linspace(-2, 2, n_squares_y) +X, Y = np.meshgrid(x, y) +R = np.sqrt(X**2 + Y**2) +alpha = np.exp(-(R**2) / 0.75) # Adjust the 1.0 to control fade rate +alpha = gaussian_filter(alpha, sigma=0.5) # Adjust sigma for smoothness + +# Create colormap gradient +cmap = plt.colormaps.get_cmap("viko") # Choose your colormap + +for i in range(n_squares_x): + for j in range(n_squares_y): + if (i + j) % 2 == 0: # Checkerboard pattern + color = cmap(i / n_squares_x) # Color varies along x-axis + rect = Rectangle( + (i * square_size_x - 0.3, j * square_size_y + 0.075), + square_size_x, + square_size_y, + facecolor=color, + alpha=alpha[j, i], + transform=ax.transAxes, + ) + ax.add_patch(rect) + + +fig.savefig( + "UltraPlotLogo.svg", + transparent=True, + bbox_inches="tight", +) + +fig.savefig( + "UltraPlotLogo.png", + transparent=True, + bbox_inches="tight", +) +fig.show() + + +# %% +import ultraplot as plt, numpy as np + +from matplotlib.font_manager import FontProperties +from matplotlib import patheffects as pe +from matplotlib.patches import Rectangle +from scipy.ndimage import gaussian_filter + +font = FontProperties(fname="./PermanentMarker-Regular.ttf") + + +fs = 38 +left = 0.5 +sw = 3 +fig, ax = plt.subplots(figsize=(2, 2)) +ax.text( + left, + 0.52, + "Ultra", + fontsize=fs, + fontproperties=font, + va="bottom", + ha="center", + color="steelblue", + path_effects=[ + pe.Stroke(linewidth=sw, foreground="white"), + pe.Normal(), + ], + transform=ax.transAxes, +) +ax.text( + left, + 0.48, + "Plot", + fontsize=fs, + # fontproperties = font, + va="top", + ha="center", + color="white", + path_effects=[ + pe.Stroke(linewidth=sw, foreground="steelblue"), + pe.Normal(), + ], + transform=ax.transAxes, +) + +shift = 0.033 +import colorengine as ce + +colors = np.linspace(0, 1, 4, 0) +# colors = plt.colormaps.get_cmap("viko")(colors) +ax.axis(False) +ax.axis("square") +# fig.set_facecolor("lightgray") + + +# Create checkerboard pattern +n_squares_x, n_squares_y = 20, 8 # Adjust number of squares +square_size = 0.1 # Size of each square + + +fig_aspect = fig.get_figwidth() / fig.get_figheight() +square_size_y = 0.1 # Base square size in y direction +square_size_x = square_size_y / fig_aspect # Adjust x size to maintain square shape + +n_squares_x = ( + int(1.0 / square_size_x) + 1 +) # Calculate number of squares needed in x direction + +# Create alpha mask with Gaussian fade +x = np.linspace(-2, 2, n_squares_x) +y = np.linspace(-2, 2, n_squares_y) +X, Y = np.meshgrid(x, y) +R = np.sqrt(X**2 + Y**2) +alpha = np.exp(-(R**2) / 0.75) # Adjust the 1.0 to control fade rate +alpha = gaussian_filter(alpha, sigma=0.5) # Adjust sigma for smoothness + +# Create colormap gradient +cmap = plt.colormaps.get_cmap("viko") # Choose your colormap + +for i in range(n_squares_x): + for j in range(n_squares_y): + if (i + j) % 2 == 0: # Checkerboard pattern + color = cmap(i / n_squares_x) # Color varies along x-axis + rect = Rectangle( + (i * square_size_x, j * square_size_y + 0.075), + square_size_x, + square_size_y, + facecolor=color, + alpha=alpha[j, i], + transform=ax.transAxes, + ) + ax.add_patch(rect) + + +fig.savefig( + "UltraPlotLogoSquare.png", + transparent=True, + bbox_inches="tight", +) +fig.show() diff --git a/logo/run-linter.sh b/logo/run-linter.sh new file mode 100755 index 000000000..8815dbdf5 --- /dev/null +++ b/logo/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 ultraplot 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 ultraplot + +# echo '[black]' +# black --check -S ultraplot + +# echo '[doc8]' +# doc8 ./*.rst docs/*.rst --ignore D001 # ignore line-too-long due to RST tables diff --git a/logo/webicon.py b/logo/webicon.py new file mode 100644 index 000000000..48b855b58 --- /dev/null +++ b/logo/webicon.py @@ -0,0 +1,64 @@ +# Generate square logo for website icon +import ultraplot as uplt, numpy as np +from matplotlib import patheffects as pe +from matplotlib.font_manager import FontProperties +from matplotlib import patches + +font = FontProperties(fname="PermanentMarker-Regular.ttf") +fs = 33 +sw = 3 + + +# Set the figure with a polar projection +fig, ax = uplt.subplots() +n = 30 +for idx, (color, rad) in enumerate(zip(np.linspace(0, 1, n), np.linspace(0, 0.5, n))): + color = uplt.colormaps.get("viko")(color) + circle = patches.Circle( + (0.5, 0.5), radius=rad, facecolor=color, zorder=n - idx, alpha=(n - idx) / n + ) + ax.add_artist(circle) + + +# Remove grid and labels +ax.set_xticks([]) +ax.set_yticks([]) +ax.grid(False) +ax.set_frame_on(False) + +# Overlay text +left = 0.575 +middle = 0.52 +ax.text( + left, + middle, + "Ultra", + fontsize=fs, + fontproperties=font, + va="center", + ha="right", + color="steelblue", + path_effects=[ + pe.Stroke(linewidth=sw, foreground="white"), + pe.Normal(), + ], + transform=ax.transAxes, + zorder=n + 1, +) +ax.text( + left, + middle, + "Plot", + fontsize=fs, + # fontproperties = font, + va="center", + ha="left", + color="white", + path_effects=[ + pe.Stroke(linewidth=sw, foreground="steelblue"), + pe.Normal(), + ], + transform=ax.transAxes, + zorder=n + 1, +) +fig.savefig("../docs/_static/logo_blank.svg") diff --git a/logo/whyUltraPlot.svg b/logo/whyUltraPlot.svg new file mode 100644 index 000000000..f77f065fd --- /dev/null +++ b/logo/whyUltraPlot.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/proplot/__init__.py b/proplot/__init__.py deleted file mode 100644 index f0c0ad23c..000000000 --- a/proplot/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env python3 -#------------------------------------------------------------------------------# -# Import everything in this folder into a giant module -# Files are segretated by function, so we don't end up with -# giant 5,000-line single file -#------------------------------------------------------------------------------# -# First set up notebook -from .notebook import * -name = 'ProPlot' -# Then import stuff -from .utils import * # misc stuff -from .rcmod import * # custom configuration implementation -from .base import * # basic tools -from .subplots import * -from .gridspec import * -from .colortools import * # color tools -from .fonttools import * # fonts -from .axistools import * # locators, normalizers, and formatters -from .proj import * # cartopy projections and whatnot -from .demos import * # demonstrations diff --git a/proplot/axistools.py b/proplot/axistools.py deleted file mode 100644 index 5729925d6..000000000 --- a/proplot/axistools.py +++ /dev/null @@ -1,833 +0,0 @@ -#!/usr/bin/env python3 -""" -Define various axis scales, locators, and formatters. Also define normalizers -generally used for colormap scaling. Below is rough overview of API. -General Notes: - * Want to try to avoid **using the Formatter to scale/transform values, and - passing the locator an array of scaled/transformed values**. Makes more sense - to instead define separate **axis transforms**, then can use locators and - formatters like normal, as they were intended to be used. This way, if e.g. - matching frequency-axis with wavelength-axis, just conver the **axis limits** - so they match, then you're good. -Scales: - * These are complicated. See: https://matplotlib.org/_modules/matplotlib/scale.html#ScaleBase - Use existing ones as inspiration -- e.g. InverseScale modeled after LogScale. - * Way to think of these is that *every single value you see on an axes first - gets secretly converted through some equation*, e.g. logarithm, and plotted - linearly in that transformation space. - * Methods include: - - get_transform(), which should return an mtransforms.Transform instance - - set_default_locators_and_formatters(), which should return - default locators and formatters - - limit_range_for_scale(), which can be used to raise errors/clip - stuff not within that range. From Mercator example: unlike the - autoscaling provided by the tick locators, this range limiting will - always be adhered to, whether the axis range is set manually, - determined automatically or changed through panning and zooming. - Important notes on methods: - - When you set_xlim or set_ylim, the minpos used is actually the *data - limits* minpos (i.e. minimum coordinate for plotted data). So don't - try to e.g. clip data < 0, that is job for transform, if you use minpos - in limit_range_for_scale will get wrong and weird results. - - Common to use set_smart_bounds(True) in set_default_locators_and_formatters() - call -- but this only draws ticks where **data exists**. Often this may - not be what we want. Check out source code, see if we can develop own - version smarter than this, that still prevents these hanging ticks. - * Also, have to be 'registered' unlike locators and formatters, which - can be passed to the 'set' methods. Or maybe not? -Transforms: - * These are complicted. See: https://matplotlib.org/_modules/matplotlib/transforms.html#Transform - * Attributes: - - input_dims, output_dims, is_separable, and has_inverse; the dims are because - transforms can be N-D, but for *scales* are always 1, 1. Note is_separable is - true if transform is separable in x/y dimensions. - * Methods: - - transform(): transforms N-D coordinates, given M x N array of values. Can also - just declare transform_affine or transform_non_affine. - - inverted(): if has_inverse True, performs inverse transform. -Locators: - * These are complicated. See: https://matplotlib.org/_modules/matplotlib/ticker.html#Locator - * Special: - - __init__() not defined on base class but *must* be defined for subclass. - * Methods include: - - tick_values(), which accepts vmin/vmax and returns values of located ticks - - __call__(), which can return data limits, view limits, or - other stuff; not sure how this works or when it's invoked. - - view_limits(), which changes the *view* limits from default vmin, vmax - to prevent singularities (uses mtransforms.nonsingular method; for - more info on this see: https://matplotlib.org/_modules/matplotlib/transforms.html#nonsingular) - * Methods that usually can be left alone: - - raise_if_exceeds(), which just tests if ticks exceed MAXTICKS number - - autoscale(), which calls the internal locator 'view_limits' with - result of axis.get_view_interval() - - pan() and zoom() for interactive purposes -Formatters: - * Easy to construct: just build with FuncFormatter a function that accepts - the number and a 'position', which maybe is used for offset or something - but almost always don't touch it, leave it default. -Normalizers: - * Generally these are used for colormaps, easy to construct: require - only an __init__ method and a __call__ method. - * The init method takes vmin, vmax, and clip, and can define custom - attributes. The call method just returns a *masked array* to handle NaNs, - and call transforms data from physical units to *normalized* units - from 0-1, representing position in colormap. -""" -#------------------------------------------------------------------------------# -# Imports -#------------------------------------------------------------------------------# -import re -from . import utils -from .utils import ic -from fractions import Fraction -from types import FunctionType -import numpy as np -import numpy.ma as ma -import matplotlib.dates as mdates -import matplotlib.colors as mcolors -import matplotlib.ticker as mticker -import matplotlib.scale as mscale -import matplotlib.transforms as mtransforms - -#------------------------------------------------------------------------------# -# Tick scales -#------------------------------------------------------------------------------# -scales = ['linear','log','symlog','logit', # builtin - 'pressure', 'height', - 'exp','sine','mercator','inverse'] # custom -def scale(scale, **kwargs): - """ - Generate arbitrary scale object. - """ - args = [] - if utils.isvector(scale): - scale, args = scale[0], scale[1:] - if scale in scales and not args: - pass # already registered - elif scale=='cutoff': - scale = CutoffScaleFactory(*args, **kwargs) - elif scale in ('exp', 'height', 'pressure'): # note here args is non-zero - if scale=='height': - if len(args)!=1: - raise ValueError('Only one non-keyword arg allowed.') - args = [*args, True] - if scale=='pressure': - if len(args)!=1: - raise ValueError('Only one non-keyword arg allowed.') - args = [*args, False] - scale = ExpScaleFactory(*args, **kwargs) - else: - raise ValueError(f'Unknown scale {scale}.') - return scale - -class ExpTransform(mtransforms.Transform): - # Create transform object - input_dims = 1 - output_dims = 1 - has_inverse = True - is_separable = True - def __init__(self, scale, thresh): - mtransforms.Transform.__init__(self) - self.thresh = thresh - self.scale = scale - def transform(self, a): - return np.exp(self.scale*np.array(a)) - def transform_non_affine(self, a): - return self.transform(a) - def inverted(self): - return InvertedExpTransform(self.scale, self.thresh) - -class InvertedExpTransform(mtransforms.Transform): - input_dims = 1 - output_dims = 1 - has_inverse = True - is_separable = True - def __init__(self, scale, thresh): - mtransforms.Transform.__init__(self) - self.thresh = thresh - self.scale = scale - def transform(self, a): - a = np.array(a) - aa = a.copy() - aa[a<=self.thresh] = self.thresh - return np.log(aa)/self.scale - def transform_non_affine(self, a): - return self.transform(a) - def inverted(self): - return ExpTransform(self.scale, self.thresh) - -def ExpScaleFactory(scale, to_exp=True, name='exp'): - """ - Exponential scale, useful for plotting height and pressure e.g. - """ - scale_num = scale - scale_name = name # must make a copy - class ExpScale(mscale.ScaleBase): - name = scale_name # assigns as attribute - scale = scale_num - forward = to_exp - # Declare name - def __init__(self, axis, thresh=1e-300, **kwargs): - # Initialize - mscale.ScaleBase.__init__(self) - self.thresh = thresh - - def limit_range_for_scale(self, vmin, vmax, minpos): - # Prevent conversion from inverting axis scale, which - # happens when scale is less than zero - # 99% of time this is want user will want I think - # NOTE: Do not try to limit data to range above zero here, that - # is transform's job; see header for info. - if self.scale < 0: - vmax, vmin = vmin, vmax - return vmin, vmax - - def set_default_locators_and_formatters(self, axis): - # Consider changing this - # axis.set_smart_bounds(True) # may prevent ticks from extending off sides - axis.set_major_formatter(formatter('custom')) - axis.set_minor_formatter(formatter('null')) - - def get_transform(self): - # Either sub into e(scale*z), the default, or invert - # the exponential - if self.forward: - return ExpTransform(self.scale, self.thresh) - else: - return InvertedExpTransform(self.scale, self.thresh) - - # Register and return - mscale.register_scale(ExpScale) - # print(f'Registered scale "{scale_name}".') - return scale_name - -def CutoffScaleFactory(scale, lower, upper=None, name='cutoff'): - """ - Constructer for scale with custom cutoffs. Three options here: - 1. Put a 'cliff' between two numbers (default). - 2. Accelerate the scale gradient between two numbers (scale>1). - 3. Deccelerate the scale gradient between two numbers (scale<1). So - scale is fast on edges but slow in middle. - Todo: - * Alongside this, create method for drawing those cutoff diagonal marks - with white space between. - See: https://stackoverflow.com/a/5669301/4970632 for multi-axis solution - and for this class-based solution. Note the space between 1-9 in Paul's answer - is because actual cutoffs were 0.1 away (and tick locs are 0.2 apart). - """ - scale_name = name # have to copy to different name - if scale<0: - raise ValueError('Scale must be a positive float.') - if upper is None: - if scale==np.inf: - raise ValueError('For infinite scale (i.e. discrete cutoff), need both lower and upper bounds.') - - class CutoffTransform(mtransforms.Transform): - # Create transform object - input_dims = 1 - output_dims = 1 - has_inverse = True - is_separable = True - def __init__(self): - mtransforms.Transform.__init__(self) - def transform(self, a): - a = np.array(a) # very numpy array - aa = a.copy() - if upper is None: # just scale between 2 segments - m = (a > lower) - aa[m] = a[m] - (a[m] - lower)*(1 - 1/scale) - elif lower is None: - m = (a < upper) - aa[m] = a[m] - (upper - a[m])*(1 - 1/scale) - else: - m1 = (a > lower) - m2 = (a > upper) - m3 = (a > lower) & (a < upper) - if scale==np.inf: - aa[m1] = a[m1] - (upper - lower) - aa[m3] = lower - else: - aa[m2] = a[m2] - (upper - lower)*(1 - 1/scale) - aa[m3] = a[m3] - (a[m3] - lower)*(1 - 1/scale) - return aa - def transform_non_affine(self, a): - return self.transform(a) - def inverted(self): - return InvertedCutoffTransform() - - class InvertedCutoffTransform(mtransforms.Transform): - input_dims = 1 - output_dims = 1 - has_inverse = True - is_separable = True - def __init__(self): - mtransforms.Transform.__init__(self) - def transform(self, a): - a = np.array(a) - aa = a.copy() - if upper is None: - m = (a > lower) - aa[m] = a[m] + (a[m] - lower)*(1 - 1/scale) - elif lower is None: - m = (a < upper) - aa[m] = a[m] + (upper - a[m])*(1 - 1/scale) - else: - n = (upper-lower)*(1 - 1/scale) - m1 = (a > lower) - m2 = (a > upper - n) - m3 = (a > lower) & (a < (upper - n)) - if scale==np.inf: - aa[m1] = a[m1] + (upper - lower) - else: - aa[m2] = a[m2] + n - aa[m3] = a[m3] + (a[m3] - lower)*(1 - 1/scale) - return aa - def transform_non_affine(self, a): - return self.transform(a) - def inverted(self): - return CutoffTransform() - - class CutoffScale(mscale.ScaleBase): - # Declare name - name = scale_name - def __init__(self, axis, **kwargs): - mscale.ScaleBase.__init__(self) - self.name = scale_name - - def get_transform(self): - return CutoffTransform() - - def set_default_locators_and_formatters(self, axis): - axis.set_major_formatter(formatter('custom')) - axis.set_minor_formatter(formatter('null')) - axis.set_smart_bounds(True) # may prevent ticks from extending off sides - - # Register and return - mscale.register_scale(CutoffScale) - # print(f'Registered scale "{scale_name}".') - return scale_name - -class MercatorLatitudeTransform(mtransforms.Transform): - # Default attributes - input_dims = 1 - output_dims = 1 - is_separable = True - has_inverse = True - def __init__(self, thresh): - # Initialize, declare attribute - mtransforms.Transform.__init__(self) - self.thresh = thresh - def transform_non_affine(self, a): - # For M N-dimensional transform, transform MxN into result - # So numbers stay the same, but data will then be linear in the - # result of the math below. - a = np.radians(a) # convert to radians - m = ma.masked_where((a < -self.thresh) | (a > self.thresh), a) - # m[m.mask] = np.nan - # a[m.mask] = np.nan - if m.mask.any(): - return ma.log(np.abs(ma.tan(m) + 1.0 / ma.cos(m))) - else: - return np.log(np.abs(np.tan(a) + 1.0 / np.cos(a))) - def inverted(self): - # Just call inverse transform class - return InvertedMercatorLatitudeTransform(self.thresh) - -class InvertedMercatorLatitudeTransform(mtransforms.Transform): - # As above, but for the inverse transform - input_dims = 1 - output_dims = 1 - is_separable = True - has_inverse = True - def __init__(self, thresh): - mtransforms.Transform.__init__(self) - self.thresh = thresh - def transform_non_affine(self, a): - # m = ma.masked_where((a < -self.thresh) | (a > self.thresh), a) - return np.degrees(np.arctan2(1, np.sinh(a))) # always assume in first/fourth quadrant, i.e. go from -pi/2 to pi/2 - def inverted(self): - return MercatorLatitudeTransform(self.thresh) - -class MercatorLatitudeScale(mscale.ScaleBase): - """ - See: https://matplotlib.org/examples/api/custom_scale_example.html - The scale function: - ln(tan(y) + sec(y)) - The inverse scale function: - atan(sinh(y)) - Applies user-defined threshold below +/-90 degrees above and below which nothing - will be plotted. See: http://en.wikipedia.org/wiki/Mercator_projection - Mercator can actually be useful in some scientific contexts; one of Libby's - papers uses it I think. - """ - name = 'mercator' - def __init__(self, axis, *, thresh=85.0, **kwargs): - # Initialize - mscale.ScaleBase.__init__(self) - if thresh >= 90.0: - raise ValueError('Threshold "thresh" must be <=90.') - self.thresh = thresh - - def get_transform(self): - # Return special transform object - return MercatorLatitudeTransform(self.thresh) - - def limit_range_for_scale(self, vmin, vmax, minpos): - # *Hard* limit on axis boundaries - return max(vmin, -self.thresh), min(vmax, self.thresh) - - def set_default_locators_and_formatters(self, axis): - # Apply these - axis.set_smart_bounds(True) - axis.set_major_locator(Locator(20)) # every 20 degrees - axis.set_major_formatter(formatter('deg')) - axis.set_minor_formatter(formatter('null')) - -class SineLatitudeTransform(mtransforms.Transform): - # Default attributes - input_dims = 1 - output_dims = 1 - is_separable = True - has_inverse = True - def __init__(self): - # Initialize, declare attribute - mtransforms.Transform.__init__(self) - def transform_non_affine(self, a): - # Transformation - 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)) - def inverted(self): - # Just call inverse transform class - return InvertedSineLatitudeTransform() - -class InvertedSineLatitudeTransform(mtransforms.Transform): - # As above, but for the inverse transform - input_dims = 1 - output_dims = 1 - is_separable = True - has_inverse = True - def __init__(self): - mtransforms.Transform.__init__(self) - 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)) - def inverted(self): - return SineLatitudeTransform() - -class SineLatitudeScale(mscale.ScaleBase): - """ - The scale function: - sin(rad(y)) - The inverse scale function: - deg(arcsin(y)) - """ - name = 'sine' - def __init__(self, axis, **kwargs): - # Initialize - mscale.ScaleBase.__init__(self) - - def get_transform(self): - # Return special transform object - return SineLatitudeTransform() - - def limit_range_for_scale(self, vmin, vmax, minpos): - # *Hard* limit on axis boundaries - return vmin, vmax - # return max(vmin, -90), min(vmax, 90) - - def set_default_locators_and_formatters(self, axis): - # Apply these - axis.set_smart_bounds(True) - axis.set_major_locator(locator(20)) # every 20 degrees - axis.set_major_formatter(formatter('deg')) - axis.set_minor_formatter(formatter('null')) - -class InverseTransform(mtransforms.Transform): - # Create transform object - input_dims = 1 - output_dims = 1 - is_separable = True - has_inverse = True - def __init__(self, minpos): - mtransforms.Transform.__init__(self) - self.minpos = minpos - def transform(self, a): - a = np.array(a) - aa = a.copy() - aa[a<=0] = self.minpos - # aa[a<=0] = np.nan # minpos - return 1.0/aa - def transform_non_affine(self, a): - return self.transform(a) - def inverted(self): - return InvertedInverseTransform(self.minpos) - -class InvertedInverseTransform(mtransforms.Transform): - input_dims = 1 - output_dims = 1 - is_separable = True - has_inverse = True - def __init__(self, minpos): - mtransforms.Transform.__init__(self) - self.minpos = minpos - def transform(self, a): - a = np.array(a) - aa = a.copy() - aa[a<=0] = self.minpos - # aa[a<=0] = np.nan # messes up automatic ylim setting - return 1.0/aa - def transform_non_affine(self, a): - return self.transform(a) - def inverted(self): - return InverseTransform(self.minpos) - -class InverseScale(mscale.ScaleBase): - """ - Similar to LogScale, but this scales to be linear in *inverse* of x. Very - useful e.g. to plot wavelengths on twin axis with wavenumbers. - - Important note: - Unlike log-scale, we can't just warp the space between - the axis limits -- have to actually change axis limits. This scale will - invert and swap the limits you provide. Weird! But works great! - """ - # Declare name - name = 'inverse' - def __init__(self, axis, minpos=1e-2, **kwargs): - # Initialize (note thresh is always needed) - mscale.ScaleBase.__init__(self) - self.minpos = minpos - - def get_transform(self): - # Return transform class - return InverseTransform(self.minpos) - - def limit_range_for_scale(self, vmin, vmax, minpos): - # *Hard* limit on axis boundaries - if not np.isfinite(minpos): - minpos = 1e-300 - return (minpos if vmin <= 0 else vmin, - minpos if vmax <= 0 else vmax) - - def set_default_locators_and_formatters(self, axis): - # TODO: fix minor locator issue - # NOTE: log formatter can ignore certain major ticks! why is that? - axis.set_smart_bounds(True) # may prevent ticks from extending off sides - axis.set_major_locator(mticker.LogLocator(base=10, subs=[1, 2, 5])) - axis.set_minor_locator(mticker.LogLocator(base=10, subs='auto')) - axis.set_major_formatter(formatter('custom')) - axis.set_minor_formatter(formatter('null')) - # axis.set_major_formatter(mticker.LogFormatter()) - -# Register hard-coded scale names, so user can set_xscale and set_yscale with strings -mscale.register_scale(InverseScale) -mscale.register_scale(SineLatitudeScale) -mscale.register_scale(MercatorLatitudeScale) -ExpScaleFactory(-1.0/7, False, 'pressure') # scale pressure so it matches a height axis -ExpScaleFactory(-1.0/7, True, 'height') # scale height so it matches a pressure axis - -#------------------------------------------------------------------------------# -# Helper functions for instantiating arbitrary Locator and Formatter classes -# When calling these functions, the format() method should automatically -# detect presence of date axis by testing if unit converter is on axis is -# DateConverter instance -# 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 -# The axis_date() method just sets the converter to the date one -#------------------------------------------------------------------------------# -def locator(loc, *args, minor=False, time=False, **kwargs): - """ - Construct a locator object. - Argument: - Can be number (specify multiples along which ticks - are drawn), list (tick these positions), or string for dictionary - lookup of possible locators. - Optional: - time: whether we want 'datetime' locators - kwargs: passed to locator when instantiated - Note: Default Locator includes 'nbins' option to subsample - the points passed so that no more than 'nbins' ticks are selected. - """ - # Do nothing, and return None if locator is None - if isinstance(loc, mticker.Locator): - return loc - # Decipher user input - if loc is None: - if time: - loc = mticker.AutoDateLocator(*args, **kwargs) - elif minor: - loc = mticker.AutoMinorLocator(*args, **kwargs) - else: - loc = mticker.AutoLocator(*args, **kwargs) - elif type(loc) is str: # dictionary lookup - if loc=='logminor': - loc = 'log' - kwargs.update({'subs':np.arange(0,10)}) - elif loc not in locators: - raise ValueError(f'Unknown locator "{loc}". Options are {", ".join(locators.keys())}.') - loc = locators[loc](*args, **kwargs) - elif utils.isnumber(loc): # scalar variable - loc = mticker.MultipleLocator(loc, *args, **kwargs) - else: - loc = mticker.FixedLocator(np.sort(loc), *args, **kwargs) # not necessary - return loc - -def formatter(form, *args, time=False, tickrange=None, **kwargs): - """ - As above, auto-interpret user input. - Includes option for %-formatting of numbers and dates, passing a list of strings - for explicitly overwriting the text. - Argument: - can be number (specify max precision of output), list (set - the strings on integers of axis), string (for .format() or percent - formatting), string for dictionary lookup of possible - formatters, Formatter instance, or function. - Optional: - time: whether we want 'datetime' formatters - kwargs: passed to locator when instantiated - """ - # Already have a formatter object - if isinstance(form, mticker.Formatter): # formatter object - return form - if utils.isvector(form) and form[0]=='frac': - args.append(form[1]) # the number - form = form[0] - # Interpret user input - if form is None: # by default use my special super cool formatter, better than original - if time: - form = mdates.AutoDateFormatter(*args, **kwargs) - else: - form = CustomFormatter(*args, tickrange=tickrange, **kwargs) - elif isinstance(form, FunctionType): - form = mticker.FuncFormatter(form, *args, **kwargs) - elif type(form) is str: # assumption is list of strings - if '{x}' in form: - form = mticker.StrMethodFormatter(form, *args, **kwargs) # new-style .format() form - elif '%' in form: - if time: - form = mdates.DateFormatter(form, *args, **kwargs) # %-style, dates - else: - form = mticker.FormatStrFormatter(form, *args, **kwargs) # %-style, numbers - else: - if form not in formatters: - raise ValueError(f'Unknown formatter "{form}". Options are {", ".join(formatters.keys())}.') - if form in ['deg','deglon','deglat','lon','lat']: - kwargs.update({'deg':('deg' in form)}) - form = formatters[form](*args, **kwargs) - elif utils.isnumber(form): # interpret scalar number as *precision* - form = CustomFormatter(form, *args, tickrange=tickrange, **kwargs) - else: - form = mticker.FixedFormatter(form) # list of strings on the major ticks, wherever they may be - return form - -#------------------------------------------------------------------------------- -# Formatting classes for mapping numbers (axis ticks) to formatted strings -# Create pseudo-class functions that actually return auto-generated formatting -# classes by passing function references to Funcformatter -#------------------------------------------------------------------------------- -# First the default formatter -def CustomFormatter(precision=2, tickrange=[-np.inf, np.inf]): - """ - Format as a number, with N sigfigs, and trimming trailing zeros. - Recall, must pass function in terms of n (number) and loc. - Arguments: - precision: max number of digits after decimal place (default 3) - tickrange: range [min,max] in which we draw tick labels (allows removing - tick labels but keeping ticks in certain region; default [-np.inf,np.inf]) - For minus sign scaling, see: https://tex.stackexchange.com/a/79158/73149 - """ - # Format definition - if tickrange is None: - tickrange = [-np.inf, np.inf] - elif utils.isnumber(tickrange): # use e.g. -1 for no ticks - tickrange = [-tickrange, tickrange] - def f(value, location): - # Exit if not in tickrange - eps = abs(value)/1000 - if (value+eps)tickrange[1]: - return '' # avoid some ticks - # Return special string - # * Note *cannot* use 'g' because 'g' precision operator specifies count of - # significant digits, not places after decimal place. - # * There is no format that specifies digits after decimal place AND trims trailing zeros. - string = f'{{{0}:.{precision:d}f}}'.format(value) # f-string compiled, then format run - if '.' in string: # g-style trimming - string = string.rstrip('0').rstrip('.') - if string=='-0': # special case - string = '0' - if value>0 and string=='0': - # raise RuntimeError('Tried to round tick position label to zero. Add precision or use an exponential formatter.') - # print('Warning: Tried to round tick position label to zero. Add precision or use an exponential formatter.') - pass - # Use unicode minus instead of ASCII hyphen (which is default) - string = re.sub('-', '−', string) # pure unicode minus - # string = re.sub('-', '${-}$', string) # latex version - # string = re.sub('-', u'\u002d', string) # unicode hyphen minus, looks same as hyphen - # string = re.sub('-', r'\scalebox{0.75}[1.0]{$-$}', string) - return string - # And create object - return mticker.FuncFormatter(f) - -#------------------------------------------------------------------------------# -# Formatting with prefixes -#------------------------------------------------------------------------------# -def PrefixSuffixFormatter(*args, prefix=None, suffix=None, **kwargs): - """ - Arbitrary prefix and suffix in front of values. - """ - prefix = prefix or '' - suffix = suffix or '' - def f(value, location): - # Finally use default formatter - func = CustomFormatter(*args, **kwargs) - return prefix + func(value, location) + suffix - # And create object - return mticker.FuncFormatter(f) - -def MoneyFormatter(*args, **kwargs): - """ - Arbitrary prefix and suffix in front of values. - """ - # And create object - return PrefixSuffixFormatter(*args, prefix='$', **kwargs) - -#------------------------------------------------------------------------------# -# Formatters for dealing with one axis in geographic coordinates -#------------------------------------------------------------------------------# -def CoordinateFormatter(*args, cardinal=None, deg=True, **kwargs): - """ - Generalized function for making LatFormatter and LonFormatter. - Requires only which string to use for points left/right of zero (e.g. W/E, S/N). - """ - def f(value, location): - # Optional degree symbol - suffix = '' - if deg: - suffix = '\N{DEGREE SIGN}' # Unicode lookup by name - # Apply suffix if not on equator/prime meridian - if isinstance(cardinal,str): - if value<0: - value *= -1 - suffix += cardinal[0] - elif value>0: - suffix += cardinal[1] - # Finally use default formatter - func = CustomFormatter(*args, **kwargs) - return func(value, location) + suffix - # And create object - return mticker.FuncFormatter(f) - -def LatFormatter(*args, **kwargs): - """ - Just calls CoordinateFormatter. Note the strings are only used if - we set cardinal=True, otherwise just prints negative/positive degrees. - """ - return CoordinateFormatter(*args, cardinal='SN', **kwargs) - -def LonFormatter(*args, **kwargs): - """ - Just calls CoordinateFormatter. Note the strings are only used if - we set cardinal=True, otherwise just prints negative/positive degrees. - """ - return CoordinateFormatter(*args, cardinal='WE', **kwargs) - -#------------------------------------------------------------------------------# -# Formatters with fractions -#------------------------------------------------------------------------------# -def FracFormatter(symbol, number): - """ - Format as fractions, multiples of some value, e.g. a physical constant. - """ - def f(n, loc): # must accept location argument - frac = Fraction(n/number).limit_denominator() - if n==0: # zero - string = '0' - elif frac.denominator==1: # denominator is one - if frac.numerator==1: - string = f'${symbol}$' - elif frac.numerator==-1: - string = f'${{-}}{symbol:s}$' - else: - string = f'${frac.numerator:d}{symbol:s}$' - elif frac.numerator==1: # numerator is +/-1 - string = f'${symbol:s}/{frac.denominator:d}$' - elif frac.numerator==-1: - 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 = re.sub('-', '−', string) # minus will be converted to unicode version since it's inside LaTeX math - return string - # And create FuncFormatter class - return mticker.FuncFormatter(f) - -def PiFormatter(): - """ - Return FracFormatter, where the number is np.pi and - symbol is $\pi$. - """ - return FracFormatter(r'\pi', np.pi) - -def eFormatter(): - """ - Return FracFormatter, where the number is np.exp(1) and - symbol is $e$. - """ - return FracFormatter('e', np.exp(1)) - -# Declare dictionaries -# Includes some custom classes, so has to go at end -locators = { - 'none': mticker.NullLocator, - 'null': mticker.NullLocator, - 'log': mticker.LogLocator, - 'maxn': mticker.MaxNLocator, - 'linear': mticker.LinearLocator, - 'log': mticker.LogLocator, - 'multiple': mticker.MultipleLocator, - 'fixed': mticker.FixedLocator, - 'index': mticker.IndexLocator, - 'symmetric': mticker.SymmetricalLogLocator, - 'logit': mticker.LogitLocator, - 'minor': mticker.AutoMinorLocator, - 'microsecond': mdates.MicrosecondLocator, - 'second': mdates.SecondLocator, - 'minute': mdates.MinuteLocator, - 'hour': mdates.HourLocator, - 'day': mdates.DayLocator, - 'weekday': mdates.WeekdayLocator, - 'month': mdates.MonthLocator, - 'year': mdates.YearLocator, - } -formatters = { # note default LogFormatter uses ugly e+00 notation - 'none': mticker.NullFormatter, - 'null': mticker.NullFormatter, - 'strmethod': mticker.StrMethodFormatter, - 'formatstr': mticker.FormatStrFormatter, - 'scalar': mticker.ScalarFormatter, - 'log': mticker.LogFormatterSciNotation, - 'eng': mticker.LogFormatterMathtext, - 'sci': mticker.LogFormatterSciNotation, - 'logit': mticker.LogitFormatter, - 'eng': mticker.EngFormatter, - 'percent': mticker.PercentFormatter, - 'index': mticker.IndexFormatter, - 'default': CustomFormatter, - 'custom': CustomFormatter, - 'proplot': CustomFormatter, - '$': MoneyFormatter, - 'pi': PiFormatter, - 'e': eFormatter, - 'deg': CoordinateFormatter, - 'lat': LatFormatter, - 'lon': LonFormatter, - 'deglat': LatFormatter, - 'deglon': LonFormatter, - } diff --git a/proplot/base.py b/proplot/base.py deleted file mode 100644 index f1c459ef6..000000000 --- a/proplot/base.py +++ /dev/null @@ -1,2904 +0,0 @@ -#!/usr/bin/env python3 -#------------------------------------------------------------------------------ -# Figure subclass and axes subclasses central to this library -#------------------------------------------------------------------------------# -# Decorators used a lot here; below is very simple example that demonstrates -# how simple decorator decorators work -# def decorator1(func): -# def decorator(): -# print('decorator 1 called') -# func() -# print('decorator 1 finished') -# return decorator -# def decorator2(func): -# def decorator(): -# print('decorator 2 called') -# func() -# print('decorator 2 finished') -# return decorator -# @decorator1 -# @decorator2 -# def hello(): -# print('hello world!') -# hello() -#------------------------------------------------------------------------------ -# Recommended using functools.wraps from comment: -# https://stackoverflow.com/a/739665/4970632 -# This tool preserve __name__ metadata. -# Builtin module requirements -# Note that even if not in IPython notebook, io capture output still works; -# seems to return some other module in that case -import os -import numpy as np -import warnings -from IPython.utils import io -from matplotlib.cbook import mplDeprecation -from matplotlib.projections import register_projection, PolarAxes -# from matplotlib.lines import _get_dash_pattern, _scale_dashes -from functools import wraps -import matplotlib.figure as mfigure -import matplotlib.axes as maxes -import matplotlib.scale as mscale -import matplotlib.contour as mcontour -import matplotlib.patheffects as mpatheffects -import matplotlib.dates as mdates -import matplotlib.colors as mcolors -import matplotlib.text as mtext -import matplotlib.ticker as mticker -import matplotlib.artist as martist -import matplotlib.gridspec as mgridspec -import matplotlib.transforms as mtransforms -import matplotlib.collections as mcollections - -# Local modules, projection sand formatters and stuff -from .gridspec import _gridspec_kwargs, FlexibleGridSpecFromSubplotSpec -# from .rcmod import rc, rcParams -from .rcmod import rc -from .proj import Aitoff, Hammer, KavrayskiyVII, WinkelTripel, Circle -from . import colortools, fonttools, axistools, utils -from .utils import _dot_dict, _fill, ic, timer, counter, docstring_fix - -# Silly recursive function, returns a...z...aa...zz...aaa...zzz -# God help you if you ever need that many labels -_abc = 'abcdefghijklmnopqrstuvwxyz' -def _ascii(i, prefix=''): - if i < 26: - return prefix + _abc[i] - else: - return _ascii(i - 26, prefix) + _abc[i % 26] - -# Filter warnings, seems to be necessary before drawing stuff for first time, -# otherwise this has no effect (e.g. if you stick it in a function) -warnings.filterwarnings('ignore', category=mplDeprecation) -# Optionally import mapping toolboxes -# Main conda distro says they are incompatible, so make sure not required! -try: - from cartopy.mpl.geoaxes import GeoAxes - from cartopy.crs import PlateCarree -except ModuleNotFoundError: - GeoAxes = PlateCarree = object - -#------------------------------------------------------------------------------# -# Decorators -# To bulk decorate lists of methods, instead of wrapping methods explicitly, -# we do some hacky bullshit with __getattribute__ and apply 'wrapper' there -# Do this because it's cleaner, not really any performance issues since even -# if we look up attributes thousands of times, testing membership in a length-4 -# list is nanoseconds level -#------------------------------------------------------------------------------# -# First distinguish plot types -_line_methods = ( # basemap methods you want to wrap that aren't 2D grids - 'plot', 'scatter', 'tripcolor', 'tricontour', 'tricontourf' - ) -_contour_methods = ( - 'contour', 'tricontour', - ) -_pcolor_methods = ( - 'pcolor', 'pcolormesh', 'pcolorpoly', 'tripcolor' - ) -_contourf_methods = ( - 'contourf', 'tricontourf', - ) -_show_methods = ( - 'imshow', 'matshow', 'spy', 'hist2d', - ) -_center_methods = ( - 'contour', 'contourf', 'quiver', 'streamplot', 'barbs' - ) -_edge_methods = ( - 'pcolor', 'pcolormesh', 'pcolorpoly', - ) - -# Next distinguish plots by more broad properties -_cycle_methods = ( - 'plot', 'scatter', 'bar', 'barh', 'hist', 'boxplot', 'errorbar' - ) -_cmap_methods = ( - 'cmapline', - 'contour', 'contourf', 'pcolor', 'pcolormesh', - 'matshow', 'imshow', 'spy', 'hist2d', - 'tripcolor', 'tricontour', 'tricontourf', - ) -_nolevels_methods = ( - 'pcolor', 'pcolormesh', 'pcolorpoly', 'tripcolor', - 'imshow', 'matshow', 'spy' - ) - -# Finally disable some stuff for all axes, and just for map projection axes -# The keys in below dictionary are error messages -_disabled_methods = { - "Unsupported plotting function {}.": - ('pie', 'table', 'hexbin', 'eventplot', - 'xcorr', 'acorr', 'psd', 'csd', 'magnitude_spectrum', - 'angle_spectrum', 'phase_spectrum', 'cohere', 'specgram'), - "Redundant function {} has been disabled. Control axis scale with format(xscale='scale', yscale='scale'). Date formatters will be used automatically when x/y coordinates are python datetime or numpy datetime64.": - ('plot_date', 'semilogx', 'semilogy', 'loglog'), - "Redundant function {} has been disabled. Use proj='polar' in subplots() call, then use angle as 'x' and radius as 'y'.": - ('polar',) - } -_map_disabled_methods = ( - 'matshow', 'imshow', 'spy', 'bar', 'barh', - # 'triplot', 'tricontour', 'tricontourf', 'tripcolor', - 'hist', 'hist2d', 'errorbar', 'boxplot', 'violinplot', 'step', 'stem', - 'hlines', 'vlines', 'axhline', 'axvline', 'axhspan', 'axvspan', - 'fill_between', 'fill_betweenx', 'fill', 'stackplot') - -# Map projections -_map_pseudocyl = ['moll','robin','eck4','kav7','sinu','mbtfpq','vandg','hammer'] - -#------------------------------------------------------------------------------ -# Helper functions for plot overrides -# List of stuff in pcolor/contourf that need to be fixed: -# * White lines between the edges; cover them by changing edgecolors to 'face'. -# * Determination of whether we are using graticule edges/centers; not sure -# what default behavior is but harder to debug. My decorator is nicer. -# * Pcolor can't take an extend argument, and colorbar can take an extend argument -# but it is ignored when the mappable is a contourf. Make our pcolor decorator -# add an "extend" attribute on the mappable that our colorbar decorator detects. -# * Extend used in contourf causes color-change between in-range values and -# out-of-range values, while extend used in colorbar on pcolor has no such -# color change. Standardize by messing with the colormap. -#------------------------------------------------------------------------------ -def _parse_args(args, rowmajor): - """ - Parse arguments for checking 2D data centers/edges. - """ - if len(args)>2: - Zs = args[2:] - else: - Zs = args - Zs = [np.array(Z) for Z in Zs] # ensure array - if rowmajor: # input has shape 'y-by-x' instead of 'x-by-y' - Zs = [Z.T for Z in Zs] - if len(args)>2: - x, y = args[:2] - else: - x = np.arange(Zs[0].shape[0]) - y = np.arange(Zs[0].shape[1]) - return np.array(x), np.array(y), Zs - -def _check_centers(func): - """ - Check shape of arguments passed to contour, and fix result. - Optional numbers of arguments: - * Z - * U, V - * x, y, Z - * x, y, U, V - """ - @wraps(func) - def decorator(*args, rowmajor=False, **kwargs): - # Checks whether sizes match up, checks whether graticule was input - x, y, Zs = _parse_args(args, rowmajor) - xlen, ylen = x.shape[0], y.shape[-1] - for Z in Zs: - if Z.ndim!=2: - raise ValueError(f'Input arrays must be 2D, instead got shape {Z.shape}.') - elif Z.shape[0]==xlen-1 and Z.shape[1]==ylen-1: - x, y = (x[1:]+x[:-1])/2, (y[1:]+y[:-1])/2 # get centers, given edges - elif Z.shape[0]!=xlen or Z.shape[1]!=ylen: - raise ValueError(f'X ({"x".join(str(i) for i in x.shape)}) ' - f'and Y ({"x".join(str(i) for i in y.shape)}) must correspond to ' - f'nrows ({Z.shape[0]}) and ncolumns ({Z.shape[1]}) of Z, or its borders.') - Zs = [Z.T for Z in Zs] - result = func(x, y, *Zs, **kwargs) - return result - return decorator - -def _check_edges(func): - """ - Check shape of arguments passed to pcolor, and fix result. - """ - @wraps(func) - def decorator(*args, rowmajor=False, **kwargs): - # Checks that sizes match up, checks whether graticule was input - x, y, Zs = _parse_args(args, rowmajor) - xlen, ylen = x.shape[0], y.shape[-1] - for Z in Zs: - if Z.ndim!=2: - raise ValueError(f'Input arrays must be 2D, instead got shape {Z.shape}.') - elif Z.shape[0]==xlen and Z.shape[1]==ylen: - x, y = utils.edges(x), utils.edges(y) - elif Z.shape[0]!=xlen-1 or Z.shape[1]!=ylen-1: - raise ValueError(f'X ({"x".join(str(i) for i in x.shape)}) ' - f'and Y ({"x".join(str(i) for i in y.shape)}) must correspond to ' - f'nrows ({Z.shape[0]}) and ncolumns ({Z.shape[1]}) of Z, or its borders.') - Zs = [Z.T for Z in Zs] - result = func(x, y, *Zs, **kwargs) - return result - # return func(self, x, y, *Zs, **kwargs) - return decorator - -def _cycle_features(self, func): - """ - Allow specification of color cycler at plot-time. Will simply set the axes - property cycler, and if it differs from user input, update it. - See: https://github.com/matplotlib/matplotlib/blob/master/lib/matplotlib/axes/_base.py - The set_prop_cycle command modifies underlying _get_lines and _get_patches_for_fill. - """ - @wraps(func) - def decorator(*args, cycle=None, cycle_kw={}, **kwargs): - # Determine and temporarily set cycler - if cycle is not None: - if not utils.isvector(cycle): - cycle = cycle, - cycle = colortools.cycle(*cycle, **cycle_kw) - self.set_prop_cycle(color=cycle) - return func(*args, **kwargs) - return decorator - -def _cmap_features(self, func): - """ - Manage output of contour and pcolor functions. - New features: - * Create new colormaps on the fly, and merge arbitrary named - or created colormaps. - * Always use full range of colormap, whether you are extending - max, min, neither, or both. For the first three, will reconstruct - colormap so 'out-of-bounds' have same color as edge colors - from 'in-bounds' region. - Also see: https://stackoverflow.com/a/48614231/4970632 - - Notes - ----- - The 'bins' argument lets you choose between: - 1) (True) Use a *discrete* normalizer with a *continuous* (i.e. very - high resolution) color table. - 2) (False) Use a *continuous* normalizer with a *discrete* (containing - the number of colors you want) color table. - """ - @wraps(func) - def decorator(*args, cmap=None, cmap_kw={}, - bins=True, # use *discrete* normalizer with 'continuous' color table - values=None, levels=None, norm=None, - values_as_levels=True, # if values are passed, treat them as levels? or just use them for e.g. cmapline, then do whatever? - extend='neither', **kwargs): - # First get normalizer (i.e. a callable with an .inverse attribute - # that inverts the call) and levels. If user provided one, and also - # specified *values* (bin centers), make sure you get the bin levels - # (halfway points) in *transformed space*, e.g. log space. - name = func.__name__ - norm = colortools.norm(norm, levels=levels) # if None, returns None; for my custom colormaps, we will need the levels - if kwargs.get('interp', 0): # e.g. for cmapline, we want to *interpolate* - values_as_levels = False # get levels later down the line - if utils.isvector(values) and values_as_levels: - if norm: # is not None - levels = norm.inverse(utils.edges(norm(values))) - else: - levels = utils.edges(values) - levels = _fill(levels, 11) # e.g. pcolormesh can auto-determine levels if you input a number - - # Call function with custom stuff - # NOTE: For contouring, colors discretized automatically. But we also - # do it with a BinNorm. Redundant? So far no harm so seriosuly leave it alone. - if name in _contour_methods or name in _contourf_methods: # only valid kwargs for contouring - kwargs.update({'levels': levels, 'extend': extend}) - if name == 'cmapline': - kwargs.update({'values': values}) # implement this directly - if name in _show_methods: # *do not* auto-adjust aspect ratio! messes up subplots! - kwargs.update({'aspect': 'auto'}) - result = func(*args, **kwargs) - if name in _nolevels_methods: - result.extend = extend - - # Get levels automatically determined by contourf, or make them - # from the automatically chosen pcolor/imshow clims - # the normalizers will ***prefer*** this over levels - if not utils.isvector(levels): # i.e. was an integer - if hasattr(result, 'levels'): - levels = result.levels - else: - levels = np.linspace(*result.get_clim(), levels) - result.levels = levels # make sure they are on there! - - if name in _contour_methods and cmap is None: - # Contour *lines* can be colormapped, but this should not be - # default if user did not input a cmap - N = None - else: - # Choose to either: - # 1) Use lookup table values and a smooth normalizer - # TODO: Figure out how extend stuff works, a bit confused again. - if not bins: - offset = {'neither':-1, 'max':0, 'min':0, 'both':1} - N = len(levels) + offset[extend] - 1 - norm = colortools.LinearSegmentedNorm(norm=norm, levels=levels) - # 2) Use a high-resolution lookup table with a discrete normalizer - # NOTE: Unclear which is better/more accurate? Intuition is this one. - else: - N = None # will be ignored - norm = colortools.BinNorm(norm=norm, levels=levels, extend=extend) - result.set_norm(norm) - - # Specify colormap - cmap = cmap or rc['image.cmap'] - if isinstance(cmap, (str, dict, mcolors.Colormap)): - cmap = cmap, # make a tuple - cmap = colortools.colormap(*cmap, N=N, extend=extend, **cmap_kw) - if not cmap._isinit: - cmap._init() - result.set_cmap(cmap) - - # Fix white lines between filled contours/mesh - linewidth = 0.4 # seems to be lowest threshold where white lines disappear - if name in _contourf_methods: - for contour in result.collections: - contour.set_edgecolor('face') - contour.set_linewidth(linewidth) - if name in _pcolor_methods: - result.set_edgecolor('face') - result.set_linewidth(linewidth) # seems to do the trick, without dots in corner being visible - - return result - - return decorator - -#------------------------------------------------------------------------------# -# Helper functions for basemap and cartopy plot overrides -# NOTE: These wrappers should be invoked *after* _check_centers and _check_edges, -# which perform basic shape checking and permute the data array, so the data -# will now be y by x (or lat by lon) instead of lon by lat. -#------------------------------------------------------------------------------# -# Normally we *cannot* modify the underlying *axes* pcolormesh etc. because this -# this will cause basemap's self.m.pcolormesh etc. to use my *custom* version and -# cause a suite of weird errors. Prevent this recursion with the below decorator. -def _m_call(self, func): - """ - Call the basemap version of the function of the same name. - """ - name = func.__name__ - @wraps(func) - def decorator(*args, **kwargs): - return self.m.__getattribute__(name)(ax=self, *args, **kwargs) - return decorator - -def _no_recurse(self, func): - """ - Decorator to prevent recursion in Basemap method overrides. - See: https://stackoverflow.com/a/37675810/4970632 - """ - @wraps(func) - # def decorator(self, *args, **kwargs): - def decorator(*args, **kwargs): - name = getattr(func, '__name__') - if self._recurred: - # Don't call func again, now we want to call the parent function - # Note this time 'self' is repeated in position args[0] - self._recurred = False - result = super(BasemapAxes, self).__getattribute__(name)(*args, **kwargs) - else: - # Actually return the basemap version - self._recurred = True - # result = self.m.__getattribute__(name)(ax=self, *args, **kwargs) - result = func(*args, **kwargs) - self._recurred = False # cleanup, in case recursion never occurred - return result - return decorator - -def _linefix_basemap(self, func): - """ - Simply add an additional kwarg. Needs whole function because we - want to @wrap it to preserve documentation. - """ - @wraps(func) - # def decorator(self, *args, **kwargs): - def decorator(*args, **kwargs): - kwargs.update(latlon=True) - return func(*args, **kwargs) - # return func(self, *args, **kwargs) - return decorator - -def _gridfix_basemap(self, func): - """ - Interpret coordinates and fix discontinuities in grid. - """ - @wraps(func) - def decorator(lon, lat, Z, fix_poles=True, **kwargs): - # def decorator(self, lon, lat, Z, **kwargs): - # Raise errors - # print('lon', lon, 'lat', lat, 'Z', Z) - lonmin, lonmax = self.m.lonmin, self.m.lonmax - if lon.max()>lon.min()+360: - raise ValueError(f'Longitudes span {lon.min()} to {lon.max()}. Can only span 360 degrees at most.') - if lon.min()<-360 or lon.max()>360: - raise ValueError(f'Longitudes span {lon.min()} to {lon.max()}. Must fall in range [-360, 360].') - if lonmin<-360 or lonmin>0: - print(f'Warning: Minimum longitude is {lonmin}, not in range [-360,0].') - # raise ValueError('Minimum longitude must fall in range [-360, 0].') - # 1) Establish 360-degree range - lon -= 720 - while True: - filter_ = lon,359,0 (borders), returns id of first zero - roll = -np.argmin(lon) # always returns *first* value - if lon[0]==lon[-1]: - lon = np.roll(lon[:-1], roll) - lon = np.append(lon, lon[0]+360) - else: - lon = np.roll(lon, roll) - Z = np.roll(Z, roll, axis=1) - # 3) Roll in same direction some more, if some points on right-edge - # extend more than 360 above the minimum longitude; THEY should be the - # ones on west/left-hand-side of map - lonroll = np.where(lon>lonmin+360)[0] # tuple of ids - if lonroll: # non-empty - roll = lon.size-min(lonroll) # e.g. if 10 lons, lonmax id is 9, we want to roll once - lon = np.roll(lon, roll) # need to roll foreward - Z = np.roll(Z, roll, axis=1) # roll again - lon[:roll] -= 360 # retains monotonicity - # 4) Set NaN where data not in range lonmin, lonmax - # This needs to be done for some regional smaller projections or otherwise - # might get weird side-effects due to having valid data way outside of the - # map boundaries -- e.g. strange polygons inside an NaN region - Z = Z.copy() - if lon.size-1==Z.shape[1]: # test western/eastern grid cell edges - # remove data where east boundary is east of min longitude or west - # boundary is west of max longitude - Z[:,(lon[1:]lonmax)] = np.nan - elif lon.size==Z.shape[1]: # test the centers - # this just tests centers and pads by one for safety - # remember that a *slice* with no valid range just returns empty array - where = np.where((lonlonmax))[0] - Z[:,where[1:-1]] = np.nan - # 5) Fix holes over poles by interpolating there (equivalent to - # simple mean of highest/lowest latitude points) - # if self.m.projection[:4] != 'merc': # did not fix the problem where Mercator goes way too far - if fix_poles: - Z_south = np.repeat(Z[0,:].mean(), Z.shape[1])[None,:] - Z_north = np.repeat(Z[-1,:].mean(), Z.shape[1])[None,:] - lat = np.concatenate(([-90], lat, [90])) - Z = np.concatenate((Z_south, Z, Z_north), axis=0) - # 6) Fix seams at map boundary; 3 scenarios here: - # Have edges (e.g. for pcolor), and they fit perfectly against basemap seams - # this does not augment size - if lon[0]==lonmin and lon.size-1==Z.shape[1]: # borders fit perfectly - pass # do nothing - # Have edges (e.g. for pcolor), and the projection edge is in-between grid cell boundaries - # this augments size by 1 - elif lon.size-1==Z.shape[1]: # no interpolation necessary; just make a new grid cell - lon = np.append(lonmin, lon) # append way easier than concatenate - lon[-1] = lonmin + 360 # we've added a new tiny cell to the end - Z = np.concatenate((Z[:,-1:], Z), axis=1) # don't use pad; it messes up masked arrays - # Have centers (e.g. for contourf), and we need to interpolate to the - # left/right edges of the map boundary - # this augments size by 2 - elif lon.size==Z.shape[1]: # linearly interpolate to the edges - x = np.array([lon[-1], lon[0]+360]) # x - if x[0] != x[1]: - y = np.concatenate((Z[:,-1:], Z[:,:1]), axis=1) - xq = lonmin+360 - yq = (y[:,:1]*(x[1]-xq) + y[:,1:]*(xq-x[0]))/(x[1]-x[0]) # simple linear interp formula - Z = np.concatenate((yq, Z, yq), axis=1) - lon = np.append(np.append(lonmin, lon), lonmin+360) - else: - raise ValueError() - # Finally get grid of x/y map projection coordinates - lat[lat>90], lat[lat<-90] = 90, -90 # otherwise, weird stuff happens - x, y = self.m(*np.meshgrid(lon, lat)) - # Prevent error where old boundary, drawn on a different axes, remains - # to the Basemap instance, which means it is not in self.patches, which - # means Basemap tries to draw it again so it can clip the contours by the - # resulting path, which raises error because you can't draw on Artist on multiple axes - self.m._mapboundarydrawn = self.boundary # stored the axes-specific boundary here - # Call function - return func(x, y, Z, **kwargs) - return decorator - -def _linefix_cartopy(func): - """ - Simply add an additional kwarg. Needs whole function because we - want to @wrap it to preserve documentation. - """ - @wraps(func) - def decorator(*args, transform=PlateCarree, **kwargs): - if isinstance(transform, type): - transform = transform() # instantiate - return func(*args, transform=transform, **kwargs) - return decorator - -def _gridfix_cartopy(func): - """ - Apply default transform and fix discontinuities in grid. - Note for cartopy, we don't have to worry about meridian at which longitude - wraps around; projection handles all that. - - Todo - ---- - Contouring methods for some reason have issues with circularly wrapped - data. Triggers annoying TopologyException statements, which we suppress - with IPython capture_output() tool, like in nbsetup(). - See: https://github.com/SciTools/cartopy/issues/946 - """ - @wraps(func) - def decorator(lon, lat, Z, transform=PlateCarree, fix_poles=True, **kwargs): - # 1) Fix holes over poles by *interpolating* there (equivalent to - # simple mean of highest/lowest latitude points) - if fix_poles: - Z_south = np.repeat(Z[0,:].mean(), Z.shape[1])[None,:] - Z_north = np.repeat(Z[-1,:].mean(), Z.shape[1])[None,:] - lat = np.concatenate(([-90], lat, [90])) - Z = np.concatenate((Z_south, Z, Z_north), axis=0) - # 2) Fix seams at map boundary; by ensuring circular coverage - if (lon[0] % 360) != ((lon[-1] + 360) % 360): - lon = np.array((*lon, lon[0] + 360)) # make longitudes circular - Z = np.concatenate((Z, Z[:,:1]), axis=1) # make data circular - # Call function - if isinstance(transform, type): - transform = transform() # instantiate - with io.capture_output() as captured: - result = func(lon, lat, Z, transform=transform, **kwargs) - # Call function - return result - return decorator - -#------------------------------------------------------------------------------ -# Custom figure class -#------------------------------------------------------------------------------ -class EmptyPanel(object): - """ - Dummy object to put in place when an axes or figure panel does not exist. - Makes nicer error message than if we just put 'None' or nothing there. - Remember: __getattr__ is invoked only when __getattribute__ fails, i.e. - when user requests anything that isn't a hidden object() method. - """ - def __bool__(self): - return False # it's empty, so this is 'falsey' - - def __getattr__(self, attr, *args): - raise AttributeError('Panel does not exist.') - -@docstring_fix -class Figure(mfigure.Figure): - # Subclass adding some super cool features - def __init__(self, figsize, - gridspec=None, subplots_kw=None, - rcreset=True, auto_adjust=True, pad=0.1, - **kwargs): - """ - Matplotlib figure with some pizzazz. - Requires: - figsize: - figure size (width, height) in inches - subplots_kw: - dictionary-like container of the keyword arguments used to - initialize - Optional: - rcreset (True): - when figure is drawn, reset rc settings to defaults? - auto_adjust (True): - when figure is drawn, trim the gridspec edges without messing - up axes aspect ratios and internal spacing? - """ - # Initialize figure with some custom attributes. - # Whether to reset rcParams wheenver a figure is drawn (e.g. after - # ipython notebook finishes executing) - self._rcreset = rcreset - self._smart_pad = pad - self._smart_tight = auto_adjust # note name _tight already taken! - self._smart_tight_init = True # is figure in its initial state? - self._span_labels = [] # add axis instances to this, and label position will be updated - # Gridspec information - self._gridspec = gridspec # gridspec encompassing drawing area - self._subplots_kw = _dot_dict(subplots_kw) # extra special settings - # Figure dimensions - self.width = figsize[0] # dimensions - self.height = figsize[1] - # Panels, initiate as empty - self.leftpanel = EmptyPanel() - self.bottompanel = EmptyPanel() - self.rightpanel = EmptyPanel() - self.toppanel = EmptyPanel() - # Proceed - super().__init__(figsize=figsize, **kwargs) # python 3 only - # Initialize suptitle, adds _suptitle attribute - self.suptitle('') - - def _rowlabels(self, labels, **kwargs): - # Assign rowlabels - axs = [] - for ax in self.axes: - if isinstance(ax, BaseAxes) and not isinstance(ax, PanelAxes) and ax._col_span[0]==0: - axs.append(ax) - if isinstance(labels,str): # common during testing - labels = [labels]*len(axs) - if len(labels)!=len(axs): - raise ValueError(f'Got {len(labels)} labels, but there are {len(axs)} rows.') - axs = [ax for _,ax in sorted(zip([ax._row_span[0] for ax in axs],axs))] - for ax,label in zip(axs,labels): - if label and not ax.rowlabel.get_text(): - # Create a CompositeTransform that converts coordinates to - # universal dots, then back to axes - label_to_ax = ax.yaxis.label.get_transform() + ax.transAxes.inverted() - x, _ = label_to_ax.transform(ax.yaxis.label.get_position()) - ax.rowlabel.set_visible(True) - # Add text - ax.rowlabel.update({'text':label, - 'position':[x,0.5], - 'ha':'right', 'va':'center', **kwargs}) - - def _collabels(self, labels, **kwargs): - # Assign collabels - axs = [] - for ax in self.axes: - if isinstance(ax, BaseAxes) and not isinstance(ax, PanelAxes) and ax._row_span[0]==0: - axs.append(ax) - if isinstance(labels,str): - labels = [labels]*len(axs) - if len(labels)!=len(axs): - raise ValueError(f'Got {len(labels)} labels, but there are {len(axs)} columns.') - axs = [ax for _,ax in sorted(zip([ax._col_span[0] for ax in axs],axs))] - for ax,label in zip(axs,labels): - if label and not ax.collabel.get_text(): - ax.collabel.update({'text':label, **kwargs}) - - def _suptitle_setup(self, renderer=None, offset=False, **kwargs): - # Intelligently determine supertitle position: - # Determine x by the underlying gridspec structure, where main axes lie. - left = self._subplots_kw.left - right = self._subplots_kw.right - if self.leftpanel: - left += (self._subplots_kw.lwidth + self._subplots_kw.lspace) - if self.rightpanel: - right += (self._subplots_kw.rwidth + self._subplots_kw.rspace) - xpos = left/self.width + 0.5*(self.width - left - right)/self.width - - if not offset or not kwargs.get('text', self._suptitle.get_text()): - # Simple offset, not using the automatically determined - # title position for guidance - base = rc['axes.titlepad']/72 + self._gridspec.top*self.height - ypos = base/self.height - transform = self.transFigure - else: - # Figure out which title on the top-row axes will be offset the most - # NOTE: Have to use private API to figure out whether axis has - # tick labels or not! Seems to be no other way to do it. - # See: https://matplotlib.org/_modules/matplotlib/axis.html#Axis.set_tick_params - title_lev1, title_lev2, title_lev3 = None, None, None - for ax in self.axes: - # TODO: Need to ensure we do not test *bottom* axes panels - if not isinstance(ax, BaseAxes) or not ax._row_span[0]==0 or \ - (isinstance(ax, PanelAxes) and ax.panel_side=='bottom'): - continue - title_lev1 = ax.title # always will be non-None - if ((ax.title.get_text() and not ax._title_inside) or ax.collabel.get_text()): - title_lev2 = ax.title - if ax.xaxis.get_ticks_position() == 'top': - test = 'label1On' not in ax.xaxis._major_tick_kw \ - or ax.xaxis._major_tick_kw['label1On'] \ - or ax.xaxis._major_tick_kw['label2On'] - if test: - title_lev3 = ax.title - - # Hacky bugfixes: - # 1) If no title, fill with spaces. Does nothing in most cases, but - # if tick labels are on top, without this step matplotlib tight subplots - # will not see the suptitle; now suptitle will just occupy empty title space. - # 2) If title and tick labels on top, offset the suptitle and get - # matplotlib to adjust tight_subplot by prepending newlines to title. - # 3) Otherwise, offset suptitle, and matplotlib will recognize the - # suptitle during tight_subplot adjustment. - # ic(title_lev2, title_lev1, title_lev3) - if not title_lev2: # no title present - line = 0 - title = title_lev1 - if not title.get_text(): - # if not title.axes._title_inside: - title.set_text('\n ') # dummy spaces, so subplots adjust will work properly - elif title_lev3: # upper axes present - line = 1.0 # looks best empirically - title = title_lev3 - text = title.get_text() - title.set_text('\n\n' + text) - else: - line = 1.2 # default line spacing; see: https://matplotlib.org/api/text_api.html#matplotlib.text.Text.set_linespacing - title = title_lev1 # most common one - - # First idea: Create blended transform, end with newline - # ypos = title.get_position()[1] - # transform = mtransforms.blended_transform_factory( - # self.transFigure, title.get_transform()) - # text = kwargs.pop('text', self._suptitle.get_text()) - # if text[-1:] != '\n': - # text += '\n' - # kwargs['text'] = text - # New idea: Get the transformed position - # NOTE: Seems draw() is called more than once, and the last times - # are when title positions are appropriately offset. - # NOTE: Default linespacing is 1.2; it has no get, only a setter; see - # https://matplotlib.org/api/text_api.html#matplotlib.text.Text.set_linespacing - transform = title.get_transform() + self.transFigure.inverted() - ypos = transform.transform(title.get_position())[1] - line = line*(rc['axes.titlesize']/72)/self.height - ypos = ypos + line - transform = self.transFigure - - # Update settings - self._suptitle.update({'position':(xpos, ypos), - 'transform':transform, - 'ha':'center', 'va':'bottom', **kwargs}) - - # @counter - def draw(self, renderer, *args, **kwargs): - # Special: Figure out if other titles are present, and if not - # bring suptitle close to center - ref_ax = None - self._suptitle_setup(renderer, offset=True) # just applies the spacing - self._auto_smart_tight_layout(renderer) - # If rc settings have been changed, reset them when the figure is - # displayed (usually means we have finished executing a notebook cell). - if not rc._init and self._rcreset: - print('Resetting rcparams.') - rc.reset() - return super().draw(renderer, *args, **kwargs) - - def panel_factory(self, subspec, whichpanels=None, - hspace=None, wspace=None, - hwidth=None, wwidth=None, - sharex=None, sharey=None, # external sharing - sharex_level=3, sharey_level=3, - sharex_panels=True, sharey_panels=True, # by default share main x/y axes with panel x/y axes - **kwargs): - # Helper function for creating paneled axes. - width, height = self.width, self.height - translate = {'bottom':'b', 'top':'t', 'right':'r', 'left':'l'} - whichpanels = translate.get(whichpanels, whichpanels) - whichpanels = whichpanels or 'r' - hspace = _fill(hspace, 0.13) # teeny tiny space - wspace = _fill(wspace, 0.13) - hwidth = _fill(hwidth, 0.45) # default is panels for plotting stuff, not colorbars - wwidth = _fill(wwidth, 0.45) - if any(s.lower() not in 'lrbt' for s in whichpanels): - raise ValueError(f'Whichpanels argument can contain characters l (left), r (right), b (bottom), or t (top), instead got "{whichpanels}".') - - # Determine rows/columns and indices - nrows = 1 + sum(1 for i in whichpanels if i in 'bt') - ncols = 1 + sum(1 for i in whichpanels if i in 'lr') - sides_lr = [l for l in ['l',None,'r'] if not l or l in whichpanels] - sides_tb = [l for l in ['t',None,'b'] if not l or l in whichpanels] - # Detect empty positions and main axes position - main_pos = (int('t' in whichpanels), int('l' in whichpanels)) - corners = {'tl':(0,0), 'tr':(0,main_pos[1]+1), - 'bl':(main_pos[0]+1,0), 'br':(main_pos[0]+1,main_pos[1]+1)} - empty_pos = [position for corner,position in corners.items() if - corner[0] in whichpanels and corner[1] in whichpanels] - - # Fix wspace/hspace in inches, using the Bbox from get_postition - # on the subspec object to determine physical width of axes to be created - # * Consider writing some convenience funcs to automate this unit conversion - bbox = subspec.get_position(self) # valid since axes not drawn yet - if hspace is not None: - hspace = np.atleast_1d(hspace) - if hspace.size==1: - hspace = np.repeat(hspace, (nrows-1,)) - boxheight = np.diff(bbox.intervaly)[0]*height - height = boxheight - hspace.sum() - hspace = hspace/(height/nrows) - if wspace is not None: - wspace = np.atleast_1d(wspace) - if wspace.size==1: - wspace = np.repeat(wspace, (ncols-1,)) - boxwidth = np.diff(bbox.intervalx)[0]*width - width = boxwidth - wspace.sum() - wspace = wspace/(width/ncols) - - # Figure out hratios/wratios - # Will enforce (main_width + panel_width)/total_width = 1 - wwidth_ratios = [width - wwidth*(ncols-1)]*ncols - if wwidth_ratios[0]<0: - raise ValueError(f'Panel wwidth {wwidth} is too large. Must be less than {width/(nrows-1):.3f}.') - for i in range(ncols): - if i!=main_pos[1]: # this is a panel entry - wwidth_ratios[i] = wwidth - hwidth_ratios = [height-hwidth*(nrows-1)]*nrows - if hwidth_ratios[0]<0: - raise ValueError(f'Panel hwidth {hwidth} is too large. Must be less than {height/(ncols-1):.3f}.') - for i in range(nrows): - if i!=main_pos[0]: # this is a panel entry - hwidth_ratios[i] = hwidth - - # Create subplotspec and draw the axes - # Will create axes in order of rows/columns so that the "base" axes - # are always built before the axes to be "shared" with them - panels = [] - gs = FlexibleGridSpecFromSubplotSpec( - nrows = nrows, - ncols = ncols, - subplot_spec = subspec, - wspace = wspace, - hspace = hspace, - width_ratios = wwidth_ratios, - height_ratios = hwidth_ratios, - ) - # Draw main axes - ax = self.add_subplot(gs[main_pos[0], main_pos[1]], **kwargs) - axmain = ax - # Draw axes - panels = {} - kwpanels = {**kwargs, 'projection':'panel'} # override projection - kwpanels.pop('number', None) # don't want numbering on panels - translate = {'b':'bottom', 't':'top', 'l':'left', 'r':'right'} # inverse - for r,side_tb in enumerate(sides_tb): # iterate top-bottom - for c,side_lr in enumerate(sides_lr): # iterate left-right - if (r,c) in empty_pos or (r,c)==main_pos: - continue - side = translate.get(side_tb or side_lr, None) - ax = self.add_subplot(gs[r,c], panel_side=side, panel_parent=axmain, **kwpanels) - panels[side] = ax - - # Finally add as attributes, and set up axes sharing - axmain.bottompanel = panels.get('bottom', EmptyPanel()) - axmain.toppanel = panels.get('top', EmptyPanel()) - axmain.leftpanel = panels.get('left', EmptyPanel()) - axmain.rightpanel = panels.get('right', EmptyPanel()) - if sharex_panels: - axmain._sharex_panels() - if sharey_panels: - axmain._sharey_panels() - axmain._sharex_setup(sharex, sharex_level) - axmain._sharey_setup(sharey, sharey_level) - return axmain - - def smart_tight_layout(self, renderer=None, pad=None): - """ - Get arguments necessary passed to subplots() to create a tight figure - bounding box without screwing aspect ratios, widths/heights, and such. - """ - # Get bounding box that encompasses *all artists*, compare to bounding - # box used for saving *figure* - if pad is None: - pad = self._smart_pad - if self._subplots_kw is None or self._gridspec is None: - raise ValueError("Initialize figure with 'subplots_kw' and 'gridspec' to draw tight grid.") - obbox = self.bbox_inches # original bbox - if not renderer: # cannot use the below on figure save! figure becomes a special FigurePDF class or something - renderer = self.canvas.get_renderer() - bbox = self.get_tightbbox(renderer) - ox, oy, x, y = obbox.intervalx, obbox.intervaly, bbox.intervalx, bbox.intervaly - x1, y1, x2, y2 = x[0], y[0], ox[1]-x[1], oy[1]-y[1] # deltas - - # Apply new settings - lname = 'lspace' if self.leftpanel else 'left' - rname = 'rspace' if self.rightpanel else 'right' - bname = 'bspace' if self.bottompanel else 'bottom' - tname = 'top' - subplots_kw = self._subplots_kw - left = getattr(subplots_kw, lname) - x1 + pad - right = getattr(subplots_kw, rname) - x2 + pad - bottom = getattr(subplots_kw, bname) - y1 + pad - top = getattr(subplots_kw, tname) - y2 + pad - subplots_kw.update({lname:left, rname:right, bname:bottom, tname:top}) - figsize, *_, gridspec_kw = _gridspec_kwargs(**subplots_kw) - self._smart_tight_init = False - self._gridspec.update(**gridspec_kw) - self.set_size_inches(figsize) - - # Fix any spanning labels that we've added to _span_labels - # These need figure-relative height coordinates - for axis in self._span_labels: - axis.axes._share_span_label(axis) - - def _auto_smart_tight_layout(self, renderer=None): - # If we haven't already, compress edges - if not self._smart_tight_init or not self._smart_tight: - return - # Cartopy sucks at labels! Bounding box identified will be wrong. - # 1) If you used set_bounds to zoom into part of a cartopy projection, - # this can erroneously identify invisible edges of map as being part of boundary - # 2) If you have gridliner text labels, matplotlib won't detect them. - if any(isinstance(ax, CartopyAxes) for ax in self.axes): - return - # Adjust if none of not done already - print('Adjusting gridspec.') - self.smart_tight_layout(renderer) - - # @timer - def save(self, filename, silent=False, auto_adjust=True, pad=0.1, **kwargs): - # Notes: - # * Gridspec object must be updated before figure is printed to - # screen in interactive environment; will fail to update after that. - # Seems to be glitch, should open thread on GitHub. - # * To color axes patches, you may have to explicitly pass the - # transparent=False kwarg. - # Some kwarg translations, to pass to savefig - if 'alpha' in kwargs: - kwargs['transparent'] = not bool(kwargs.pop('alpha')) # 1 is non-transparent - if 'color' in kwargs: - kwargs['facecolor'] = kwargs.pop('color') # the color - kwargs['transparent'] = True - # Finally, save - self._suptitle_setup(offset=True) # just applies the spacing - self._auto_smart_tight_layout() - if not silent: - print(f'Saving to "{filename}".') - return super().savefig(os.path.expanduser(filename), **kwargs) # specify DPI for embedded raster objects - - def savefig(self, *args, **kwargs): - # Alias for save. - return self.save(*args, **kwargs) - -#------------------------------------------------------------------------------# -# Generalized custom axes class -#------------------------------------------------------------------------------# -@docstring_fix -class BaseAxes(maxes.Axes): - """ - Subclass the default Axes class. Then register it as the 'base' projection, - and you will get a subclass Subplot by calling fig.add_subplot(projection='base'). - Notes: - * You cannot subclass SubplotBase directly, should only be done with - maxes.subplot_class_factory, which is called automatically when using add_subplot. - * Cartopy projections should use same methods as for ordinary 'cartesian' - plot, so we put a bunch of definition overrides in here. - """ - # Initial stuff - name = 'base' - def __init__(self, *args, number=None, - sharex=None, sharey=None, spanx=None, spany=None, - sharex_level=0, sharey_level=0, - map_name=None, - panel_parent=None, panel_side=None, - **kwargs): - # Initialize - self._spanx = spanx # boolean toggles, whether we want to span axes labels - self._spany = spany - self._title_inside = True # toggle this to figure out whether we need to push 'super title' up - self._zoom = None # if non-empty, will make invisible - self._inset_parent = None # change this later - self._insets = [] # add to these later - self._map_name = map_name # consider conditionally allowing 'shared axes' for certain projections - super().__init__(*args, **kwargs) - - # Panels - if panel_side not in (None, 'left','right','bottom','top'): - raise ValueError(f'Invalid panel side "{panel_side}".') - self.panel_side = panel_side - self.panel_parent = panel_parent # used when declaring parent - self.bottompanel = EmptyPanel() - self.toppanel = EmptyPanel() - self.leftpanel = EmptyPanel() - self.rightpanel = EmptyPanel() - - # Number and size - if isinstance(self, maxes.SubplotBase): - nrows, ncols, subspec = self._topmost_subspec() - self._row_span = ((subspec.num1 // ncols) // 2, (subspec.num2 // ncols) // 2) - self._col_span = ((subspec.num1 % ncols) // 2, (subspec.num2 % ncols) // 2) - else: - self._row_span = None - self._col_span = None - self.number = number # for abc numbering - self.width = np.diff(self._position.intervalx)*self.figure.width # position is in figure units - self.height = np.diff(self._position.intervaly)*self.figure.height - - # Turn off tick labels and axis label for shared axes - # Want to do this ***manually*** because want to have the ability to - # add shared axes ***after the fact in general***. If the API changes, - # will modify the below methods. - self._sharex_setup(sharex, sharex_level) - self._sharey_setup(sharey, sharey_level) - - # Add extra text properties for abc labeling, rows/columns labels - # (can only be filled with text if axes is on leftmost column/topmost row) - self.abc = self.text(0, 0, '') # position tbd - self.collabel = self.text(*self.title.get_position(), '', - va='baseline', ha='center', transform=self.title.get_transform()) - self.rowlabel = self.text(*self.yaxis.label.get_position(), '', - va='center', ha='right', transform=self.transAxes) - - # Enforce custom rc settings! And only look for rcSpecial settings. - with rc._context(mode=1): - self._rcupdate() - - # Apply some simple featueres, and disable spectral and triangular features - # See: https://stackoverflow.com/a/23126260/4970632 - # Also see: https://github.com/matplotlib/matplotlib/blob/master/lib/matplotlib/axes/_axes.py - # for all Axes methods ordered logically in class declaration. - def __getattribute__(self, attr, *args): - for message,attrs in _disabled_methods.items(): - if attr in attrs: - raise NotImplementedError(message.format(attr)) - if attr=='pcolorpoly': - attr = 'pcolor' # use alias so don't run into recursion issues due to internal pcolormesh calls to pcolor() - obj = super().__getattribute__(attr, *args) - if attr in _cmap_methods: - obj = _cmap_features(self, obj) - elif attr in _cycle_methods: - obj = _cycle_features(self, obj) - return obj - - def _topmost_subspec(self): - # Needed for e.g. getting the top-level SubplotSpec (i.e. the one - # encompassed by an axes and all its panels, if any are present) - subspec = self.get_subplotspec() - gridspec = subspec.get_gridspec() - while isinstance(gridspec, mgridspec.GridSpecFromSubplotSpec): - try: - subspec = gridspec._subplot_spec - except AttributeError: - raise ValueError('The _subplot_spec attribute is missing from this GridSpecFromSubplotSpec. Cannot determine the parent GridSpec rows/columns occupied by this slot.') - gridspec = subspec.get_gridspec() - nrows, ncols = gridspec.get_geometry() - return nrows, ncols, subspec - - def _sharex_setup(self, sharex, level): - if sharex is None: - return - if self is sharex: - return - if isinstance(self, MapAxes) or isinstance(sharex, MapAxes): - return - if level not in range(4): - raise ValueError('Level can be 1 (do not share limits, just hide axis labels), 2 (share limits, but do not hide tick labels), or 3 (share limits and hide tick labels).') - # Share vertical panel x-axes with *eachother* - if self.leftpanel and sharex.leftpanel: - self.leftpanel._sharex_setup(sharex.leftpanel, level) - if self.rightpanel and sharex.rightpanel: - self.rightpanel._sharex_setup(sharex.rightpanel, level) - # Share horizontal panel x-axes with *sharex* - if self.bottompanel and sharex is not self.bottompanel: - self.bottompanel._sharex_setup(sharex, level) - if self.toppanel and sharex is not self.toppanel: - self.toppanel._sharex_setup(sharex, level) - # Builtin features - self._sharex = sharex - if level>1: - self._shared_x_axes.join(self, sharex) - # Simple method for setting up shared axes - # WARNING: It turned out setting *another axes' axis label* as - # this attribute caused error, because matplotlib tried to add - # the same artist instance twice. Can only make it invisible. - if level>2: - for t in self.xaxis.get_ticklabels(): - t.set_visible(False) - self.xaxis.label.set_visible(False) - - def _sharey_setup(self, sharey, level): - if sharey is None: - return - if self is sharey: - return - if isinstance(self, MapAxes) or isinstance(sharey, MapAxes): - return - if level not in range(4): - raise ValueError('Level can be 1 (do not share limits, just hide axis labels), 2 (share limits, but do not hide tick labels), or 3 (share limits and hide tick labels).') - # Share horizontal panel y-axes with *eachother* - if self.bottompanel and sharey.bottompanel: - self.bottompanel._sharey_setup(sharey.bottompanel, level) - if self.toppanel and sharey.toppanel: - self.toppanel._sharey_setup(sharey.toppanel, level) - # Share vertical panel y-axes with *sharey* - if self.leftpanel: - self.leftpanel._sharey_setup(sharey, level) - if self.rightpanel: - self.rightpanel._sharey_setup(sharey, level) - # sharey = self.leftpanel._sharey or self.leftpanel - # Builtin features - self._sharey = sharey - if level>1: - self._shared_y_axes.join(self, sharey) - # Simple method for setting up shared axes - if level>2: - for t in self.yaxis.get_ticklabels(): - t.set_visible(False) - self.yaxis.label.set_visible(False) - - def _sharex_panels(self): - # Call this once panels are all declared - if self.bottompanel: - self._sharex_setup(self.bottompanel, 3) - bottom = self.bottompanel or self - if self.toppanel: - self.toppanel._sharex_setup(bottom, 3) - - def _sharey_panels(self): - # Same but for y - if self.leftpanel: - self._sharey_setup(self.leftpanel, 3) - left = self.leftpanel or self - if self.rightpanel: - self.rightpanel._sharey_setup(left, 3) - - def _rcupdate(self): - # Figure patch (for some reason needs to be re-asserted even if declared before figure drawn) - kw = rc.fill({'facecolor':'figure.facecolor'}) - self.figure.patch.update(kw) - # Axes, figure title (builtin settings) - kw = rc.fill({'fontsize':'axes.titlesize', 'weight':'axes.titleweight', 'fontname':'fontname'}) - self.title.update(kw) - kw = rc.fill({'fontsize':'figure.titlesize', 'weight':'figure.titleweight', 'fontname':'fontname'}) - self.figure._suptitle.update(kw) - # Row and column labels, ABC labels - kw = rc.fill({'fontsize':'abc.fontsize', 'weight':'abc.weight', 'color':'abc.color', 'fontname':'fontname'}) - self.abc.update(kw) - kw = rc.fill({'fontsize':'rowlabel.fontsize', 'weight':'rowlabel.weight', 'color':'rowlabel.color', 'fontname':'fontname'}) - self.rowlabel.update(kw) - kw = rc.fill({'fontsize':'collabel.fontsize', 'weight':'collabel.weight', 'color':'collabel.color', 'fontname':'fontname'}) - self.collabel.update(kw) - - def _text_update(self, obj, kwargs): - # Allow updating properties introduced by the BaseAxes.text() override. - # Don't really want to subclass mtext.Text; only have a few features - # NOTE: Don't use kwargs because want this to look like standard - # artist self.update. - try: - obj.update(kwargs) - except Exception: - obj.set_visible(False) - text = kwargs.pop('text', obj.get_text()) - color = kwargs.pop('color', obj.get_color()) - weight = kwargs.pop('weight', obj.get_weight()) - fontsize = kwargs.pop('fontsize', obj.get_fontsize()) - x, y = None, None - pos = obj.get_position() - if 'position' in kwargs: - x, y = kwargs.pop('position') - x = kwargs.pop('x', x) - y = kwargs.pop('y', y) - if x is None: - x = pos[0] - if y is None: - y = pos[1] - try: - x = x[0] - except TypeError: - pass - try: - y = y[0] - except TypeError: - pass - obj = self.text(x, y, text, color=color, weight=weight, fontsize=fontsize, **kwargs) - return obj - - def _title_pos(self, pos, **kwargs): - # Position arbitrary text to left/middle/right either inside or outside - # of axes (default is center, outside) - ypad = (rc['axes.titlepad']/72)/self.height # to inches --> to axes relative - xpad = (rc['axes.titlepad']/72)/self.width # why not use the same for x? - xpad_i, ypad_i = xpad*1.5, ypad*1.5 # inside labels need a bit more room - pos = pos or 'oc' - extra = {} - if not isinstance(pos, str): - ha = va = 'center' - x, y = pos - transform = self.transAxes - else: - # Get horizontal position - if not any(c in pos for c in 'lcr'): - pos += 'c' - if not any(c in pos for c in 'oi'): - pos += 'o' - if 'c' in pos: - x = 0.5 - ha = 'center' - elif 'l' in pos: - x = 0 + xpad_i*('i' in pos) - ha = 'left' - elif 'r' in pos: - x = 1 - xpad_i*('i' in pos) - ha = 'right' - # Record _title_inside so we can automatically deflect suptitle - # If *any* object is outside (title or abc), want to deflect it up - if 'o' in pos: - y = 1 # 1 + ypad # leave it alone, may be adjusted during draw-time to account for axis label (fails to adjust for tick labels; see notebook) - va = 'baseline' - self._title_inside = False - transform = self.title.get_transform() - elif 'i' in pos: - y = 1 - ypad_i - va = 'top' - transform = self.transAxes - extra['border'] = _fill(kwargs.pop('border', None), True) # by default - return {'x':x, 'y':y, 'transform':transform, 'ha':ha, 'va':va, **extra} - - # Make axes invisible - def invisible(self): - # Make axes invisible - 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) - - # New convenience feature - # The title position can be a mix of 'l/c/r' and 'i/o' - def format(self, - suptitle=None, suptitle_kw={}, - collabels=None, collabels_kw={}, - rowlabels=None, rowlabels_kw={}, # label rows and columns - title=None, titlepos=None, title_kw={}, - abc=None, abcpos=None, abcformat=None, abc_kw={}, - rc_kw={}, **kwargs, - ): - """ - Function for formatting axes of all kinds; some arguments are only relevant to special axes, like - colorbar or basemap axes. By default, simply applies the dictionary values from settings() above, - but can supply many kwargs to further modify things. - - Todo - ---- - * Add options for datetime handling; note possible date axes handles are TimeStamp (pandas), - np.datetime64, DateTimeIndex; can fix with fig.autofmt_xdate() or manually set options; uses - ax.is_last_row() or ax.is_first_column(), so should look that up. - * Problem is there is no autofmt_ydate(), so really should implement my own - version of this. - """ - # NOTE: These next two are actually *figure-wide* settings, but that - # line seems to get 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. - # Create figure title - fig = self.figure # the figure - if suptitle is not None: - fig._suptitle_setup(text=suptitle, **suptitle_kw) - if rowlabels is not None: - fig._rowlabels(rowlabels, **rowlabels_kw) - if collabels is not None: - fig._collabels(collabels, **collabels_kw) - - # Create axes title - # Input needs to be emptys string - if title is not None: - pos_kw = self._title_pos(titlepos or 'oc', **title_kw) - self.title = self._text_update(self.title, {'text':title, 'visible':True, **pos_kw, **title_kw}) - - # Create axes numbering - if self.number is not None and abc: - # Get text - abcformat = abcformat or 'a' - if 'a' not in abcformat: - raise ValueError(f'Invalid abcformat {abcformat}.') - abcedges = abcformat.split('a') - text = abcedges[0] + _ascii(self.number-1) + abcedges[-1] - pos_kw = self._title_pos(abcpos or 'il') - self.abc = self._text_update(self.abc, {'text':text, **abc_kw, **pos_kw}) - elif hasattr(self, 'abc') and abc is not None and not abc: - # Hide - self.abc.set_visible(False) - - # First update (note that this will call _rcupdate overridden by child - # classes, which can in turn call the parent class version, so we only - # need to call this from the base class, and all settings will be applied) - with rc._context(rc_kw, mode=2, **kwargs): - self._rcupdate() - - # Create legend creation method - def legend(self, *args, **kwargs): - # Call custom legend() function. - return legend_factory(self, *args, **kwargs) - - # Fill entire axes with colorbar - def colorbar(self, *args, **kwargs): - # Call colorbar() function. - return colorbar_factory(self, *args, **kwargs) - - # Fancy wrappers - def text(self, x, y, text, - transform=None, border=False, invert=False, - linewidth=2, lw=None, **kwargs): # linewidth is for the border - """ - Wrapper around original text method. Adds feature for easily drawing - text with white border around black text, or vice-versa with invert==True. - - Warning - ------- - Basemap gridlining methods call text, so if you change the default - transform, will not be able to draw lat/lon labels! - """ - # Get default transform by string name - linewidth = lw or linewidth - if not transform: - transform = self.transData - elif isinstance(transform, mtransforms.Transform): - pass # do nothing - elif transform=='figure': - transform = self.figure.transFigure - elif transform=='axes': - transform = self.transAxes - elif transform=='data': - transform = self.transData - else: - raise ValueError(f"Unknown transform {transform}. Use string \"axes\" or \"data\".") - # Raise more helpful error message if font unavailable - name = kwargs.pop('fontname', rc['fontname']) # is actually font.sans-serif - if name not in fonttools.fonts: - suffix = '' - if name not in fonttools._missing_fonts: - suffix = f' Available fonts are: {", ".join(fonttools.fonts)}.' - print(f'Warning: Font "{name}" unavailable, falling back to DejaVu Sans.' + suffix) - fonttools._missing_fonts.append(name) - name = 'DejaVu Sans' - # Call parent, with custom rc settings - # These seem to sometimes not get used by default - size = kwargs.pop('fontsize', rc['font.size']) - color = kwargs.pop('color', rc['text.color']) - weight = kwargs.pop('font', rc['font.weight']) - t = super().text(x, y, text, transform=transform, fontname=name, - fontsize=size, color=color, fontweight=weight, **kwargs) - # Optionally draw border around text - if border: - facecolor, bgcolor = ('wk' if invert else 'kw') - t.update({'color':facecolor, 'zorder':1e10, # have to update after-the-fact for path effects - 'path_effects': [mpatheffects.Stroke(linewidth=linewidth, foreground=bgcolor), mpatheffects.Normal()]}) - return t - - # @_cycle_features - def plot(self, *args, cmap=None, values=None, **kwargs): - """ - Expand functionality of plot to also make LineCollection lines, i.e. lines - whose colors change as a function of some key/indicator. - """ - if cmap is None and values is None: - # Make normal boring lines - lines = super().plot(*args, **kwargs) - elif cmap is not None and values is not None: - # Make special colormap lines - lines = self.cmapline(*args, cmap=cmap, values=values, **kwargs) - else: - # Error - raise ValueError('To draw colormap line, must provide kwargs "values" and "cmap".') - return lines - - # @_cycle_features - def scatter(self, *args, **kwargs): - """ - Just add some more consistent keyword argument options. - """ - # Manage input arguments - if len(args)>4: - raise ValueError(f'Function accepts up to 4 args, received {len(args)}.') - args = [*args] - if len(args)>3: - kwargs['c'] = args.pop(3) - if len(args)>2: - kwargs['s'] = args.pop(2) - # Apply some aliases for keyword arguments - aliases = {'c': ['markercolor', 'color'], - 's': ['markersize', 'size'], - 'linewidths': ['lw','linewidth','markeredgewidth', 'markeredgewidths'], - 'edgecolors': ['markeredgecolor', 'markeredgecolors']} - for name,options in aliases.items(): - for option in options: - if option in kwargs: - kwargs[name] = kwargs.pop(option) - return super().scatter(*args, **kwargs) - - # @_cmap_features - def cmapline(self, *args, cmap=None, norm=None, - values=None, interp=0, **kwargs): - """ - Create lines with colormap. - See: https://matplotlib.org/gallery/lines_bars_and_markers/multicolored_line.html - Will manage input more strictly, this is harder to generalize. - - Optional - -------- - values: - the values to which each (x,y) coordinate corresponds. - bins: - do you want values to be *discretized*, or do you want to - *interpolate* values between points? not yet implemented. - interp: - number of values between each line joint and each *halfway* point - between line joints to which you want to interpolate. for bins, - we don't need any interpolation. - """ - # First error check - if values is None: - raise ValueError('For line with a "colormap", must input values= to which colors will be mapped.') - if len(args) not in (1,2): - raise ValueError(f'Function 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'Input 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'Got {len(x)} xs, {len(y)} ys, but {len(values)} colormap values.') - - # Next draw the line - # Interpolate values to optionally allow for smooth gradations between - # values (bins=False) or color switchover halfway between points (bins=True) - # Next optionally interpolate the corresponding colormap values - # NOTE: We linearly interpolate here, but user might use a normalizer that - # e.g. performs log before selecting linear color range; don't need to - # implement that here - 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