diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
index 6d522dfc9..80a66bb93 100644
--- a/.github/ISSUE_TEMPLATE.md
+++ b/.github/ISSUE_TEMPLATE.md
@@ -28,4 +28,4 @@ import matplotlib.pyplot as plt
### Proplot version
-Paste the results of `import matplotlib; print(matplotlib.__version__); import proplot; print(proplot.version)`here.
+Paste the results of `import matplotlib; print(matplotlib.__version__); import proplot; print(proplot.version)` here.
diff --git a/.gitignore b/.gitignore
index da44adffd..a74ebf9bb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,7 +12,6 @@ dist
# Local docs builds
docs/api
docs/_build
-docs/_static/pygments
docs/_static/proplotrc
docs/_static/rctable.rst
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 7305cc1b6..8a0eb377f 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -22,7 +22,7 @@ repos:
rev: 4.0.1
hooks:
- id: flake8
- args: ['--max-line-length=88', '--ignore=W503,E402,E741']
+ args: ['--max-line-length=88', '--ignore=W503,E402,E731,E741']
# apply once this handles long tables better
# - repo: https://github.com/PyCQA/doc8
diff --git a/.readthedocs.yml b/.readthedocs.yml
index bbb41cdee..22d107f91 100644
--- a/.readthedocs.yml
+++ b/.readthedocs.yml
@@ -1,17 +1,19 @@
-# .readthedocs.yml
# Read the Docs configuration file
+# https://docs.readthedocs.io/en/stable/config-file/v2.html
version: 2
+# Build config
+# https://docs.readthedocs.io/en/stable/guides/conda.html#making-builds-faster-with-mamba
+build:
+ os: "ubuntu-20.04"
+ tools:
+ python: "mambaforge-4.10"
+
# Sphinx config
sphinx:
builder: html
configuration: docs/conf.py
-# Python config
-build:
- image: latest
-python:
- version: 3.6
- system_packages: true
+# Environment config
conda:
environment: docs/environment.yml
diff --git a/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/HOWTOCONTRIBUTE.rst b/CONTRIBUTING.rst
similarity index 100%
rename from HOWTOCONTRIBUTE.rst
rename to CONTRIBUTING.rst
diff --git a/INSTALL.rst b/INSTALL.rst
index 3baef3ff3..1286ff762 100644
--- a/INSTALL.rst
+++ b/INSTALL.rst
@@ -19,7 +19,7 @@ Likewise, an existing installation of proplot can be upgraded to the latest vers
To install a development version of proplot, you can use
-``pip install git+https://github.com/lukelbd/proplot.git``
+``pip install git+https://github.com/proplot-dev/proplot.git``
or clone the repository and run ``pip install -e .`` inside
the ``proplot`` folder.
diff --git a/LICENSE.txt b/LICENSE.txt
index 99e795d9b..96f1555df 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -1,4 +1,4 @@
-Copyright (c) 2018-2019 ProPlot contributors
+Copyright (c) 2018 The Python Packaging Authority
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.rst b/README.rst
index 8eab6d8e0..fe288f961 100644
--- a/README.rst
+++ b/README.rst
@@ -1,4 +1,4 @@
-.. image:: https://github.com/lukelbd/proplot/blob/master/docs/logo_long.png?raw=true
+.. image:: https://github.com/proplot-dev/proplot/blob/master/docs/_static/logo_long.svg?raw=true
:width: 1000px
|build-status| |docs| |pypi| |code-style| |pr-welcome| |license| |gitter| |doi|
@@ -6,6 +6,11 @@
A succinct `matplotlib `__ wrapper for making beautiful,
publication-quality graphics.
+Ultraplot: Proplot's Spiritual Successor
+========================================
+
+Development on ``proplot`` has been indefinitely halted since summer 2023. A spiritual successor in ``ultraplot`` has been launched. It uses the ``proplot`` codebase and has modernized it to support recent versions of ``matplotlib``, ``cartopy``, ``python``, etc. and is adding new features and enhancements. Check out the project over at the `ultraplot repo `__ and `ultraplot docs `__!
+
Documentation
=============
@@ -32,43 +37,43 @@ to the latest version with:
conda upgrade proplot
To install a development version of proplot, you can use
-``pip install git+https://github.com/lukelbd/proplot.git``
+``pip install git+https://github.com/proplot-dev/proplot.git``
or clone the repository and run ``pip install -e .``
inside the ``proplot`` folder.
-.. |code-style| image:: https://img.shields.io/badge/code%20style-black-000000.svg
- :alt: black
- :target: https://github.com/psf/black
-
-.. |build-status| image:: https://travis-ci.com/lukelbd/proplot.svg?branch=master
+.. |build-status| image:: https://travis-ci.com/proplot-dev/proplot.svg?branch=master
:alt: build status
- :target: https://travis-ci.com/lukelbd/proplot
-
-.. |license| image:: https://img.shields.io/github/license/lukelbd/proplot.svg
- :alt: license
- :target: LICENSE.txt
-
-.. |doi| image:: https://zenodo.org/badge/DOI/10.5281/zenodo.3873878.svg
- :alt: doi
- :target: https://doi.org/10.5281/zenodo.3873878
+ :target: https://app.travis-ci.com/proplot-dev/proplot
.. |docs| image:: https://readthedocs.org/projects/proplot/badge/?version=latest
:alt: docs
:target: https://proplot.readthedocs.io/en/latest/?badge=latest
+.. |pypi| image:: https://img.shields.io/pypi/v/proplot?color=83%20197%2052
+ :alt: pypi
+ :target: https://pypi.org/project/proplot/
+
+.. |code-style| image:: https://img.shields.io/badge/code%20style-black-000000.svg
+ :alt: black
+ :target: https://github.com/psf/black
+
.. |pr-welcome| image:: https://img.shields.io/badge/PR-Welcome-green.svg?
:alt: PR welcome
:target: https://git-scm.com/book/en/v2/GitHub-Contributing-to-a-Project
-.. |pypi| image:: https://img.shields.io/pypi/v/proplot?color=83%20197%2052
- :alt: pypi
- :target: https://pypi.org/project/proplot/
+.. |license| image:: https://img.shields.io/github/license/proplot-dev/proplot.svg
+ :alt: license
+ :target: LICENSE.txt
.. |gitter| image:: https://badges.gitter.im/gitterHQ/gitter.svg
:alt: gitter
:target: https://gitter.im/pro-plot/community
+.. |doi| image:: https://zenodo.org/badge/DOI/10.5281/zenodo.3873878.svg
+ :alt: doi
+ :target: https://doi.org/10.5281/zenodo.3873878
+
..
|code-style| image:: https://img.shields.io/badge/code%20style-pep8-green.svg
@@ -76,26 +81,26 @@ inside the ``proplot`` folder.
:target: https://www.python.org/dev/peps/pep-0008/
..
- |coverage| image:: https://codecov.io/gh/lukelbd/proplot.org/branch/master/graph/badge.svg
+ |coverage| image:: https://codecov.io/gh/proplot-dev/proplot/branch/master/graph/badge.svg
:alt: coverage
- :target: https://codecov.io/gh/lukelbd/proplot.org
+ :target: https://codecov.io/gh/proplot-dev/proplot
..
|quality| image:: https://api.codacy.com/project/badge/Grade/931d7467c69c40fbb1e97a11d092f9cd
:alt: quality
- :target: https://www.codacy.com/app/lukelbd/proplot?utm_source=github.com&utm_medium=referral&utm_content=lukelbd/proplot&utm_campaign=Badge_Grade
+ :target: https://www.codacy.com/app/proplot-dev/proplot?utm_source=github.com&utm_medium=referral&utm_content=proplot-dev/proplot&utm_campaign=Badge_Grade
..
- |hits| image:: http://hits.dwyl.io/lukelbd/lukelbd/proplot.svg
+ |hits| image:: http://hits.dwyl.com/proplot-dev/proplot.svg
:alt: hits
- :target: http://hits.dwyl.io/lukelbd/lukelbd/proplot
+ :target: http://hits.dwyl.com/proplot-dev/proplot
..
|contributions| image:: https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat
:alt: contributions
- :target: https://github.com/lukelbd/issues
+ :target: https://github.com/proplot-dev/issues
..
- |issues| image:: https://img.shields.io/github/issues/lukelbd/proplot.svg
- :alt: issues
- :target: https://github.com/lukelbd/issues
+ |issues| image:: https://img.shields.io/github/issues/proplot-dev/proplot.svg
+ :alt: issueks
+ :target: https://github.com/proplot-dev/issues
diff --git a/WHATSNEW.rst b/WHATSNEW.rst
index 6b6df36de..9bfd63ddc 100644
--- a/WHATSNEW.rst
+++ b/WHATSNEW.rst
@@ -223,8 +223,14 @@ Bug fixes
on gridspecs without companion `~proplot.figure.Figure`\ s (:commit:`e69fd041`).
* Fix issues passing pandas datetime coordinates and object-type coordinate
arrays to plotting methods (:issue:`320`).
+* Fix issue where hatching passed to `~proplot.axes.Axes.bar` does nothing unless
+ `edgecolor` is explicitly passed (:issue:`389`).
+* Fix issue where `boxpctiles` is not recognized by e.g. `~proplot.axes.PlotAxes.bar`
+ but `boxpctile` is due to typo (:issue:`382`).
* Fix issue where list-of-string colors passed to `~proplot.axes.Axes.scatter`
are interpreted as data values (:issue:`316`).
+* Fix issue where `~proplot.axes.PlotAxes.step` `where` parameter is ignored due
+ to `drawstyle` conversion (:issue:`359`).
* Fix issue where *x* and *y* axis limits are reversed when passing to
`~proplot.axes.PlotAxes.hexbin` and `~proplot.axes.PlotAxes.hist2d` (:issue:`334`).
* Fix regression where *x* or *y* axis limits are reversed when passing to
@@ -243,6 +249,10 @@ Bug fixes
`~proplot.colors.DiscreteColormap` incorrectly samples the color list (:issue:`299`).
* Fix issue where `~proplot.axes.Axes.legend` ignores the user-input `fontsize`
(:issue:`331`).
+* Fix issue where `~proplot.axes.Axes.legend` ignores the user-input `facecolor`
+ but not the shorthand `fc` (:issue:`402`).
+* Fix issue where passing invalid rc setting to ``.format`` results in persistent
+ invalid `rc` state requiring restarting the session/configurator (:issue:`348`).
* Fix issue where ``proplotrc`` settings are ignored if a subsequent line contains
an overlapping meta-setting (:issue:`333`).
* Fix issue where setting :rcraw:`legend.facecolor` or :rcraw:`legend.edgecolor` to
diff --git a/ci/environment.yml b/ci/environment.yml
index edeff283f..f316c156d 100644
--- a/ci/environment.yml
+++ b/ci/environment.yml
@@ -6,7 +6,7 @@ channels:
- conda-forge
dependencies:
- python==3.8
- - numpy
+ - numpy==1.19.5
- pandas
- xarray
- matplotlib==3.2.2
@@ -36,4 +36,4 @@ dependencies:
- markupsafe==2.0.1
- nbsphinx==0.8.1
- jupytext
- - git+https://github.com/lukelbd/sphinx-automodapi@proplot-mods
+ - git+https://github.com/proplot-dev/sphinx-automodapi@proplot-mods
diff --git a/docs/_static/logo_blank.png b/docs/_static/logo_blank.png
index 7bd6d0c0e..7023ae5af 100644
Binary files a/docs/_static/logo_blank.png and b/docs/_static/logo_blank.png differ
diff --git a/docs/_static/logo_blank.svg b/docs/_static/logo_blank.svg
new file mode 100644
index 000000000..e16a83751
--- /dev/null
+++ b/docs/_static/logo_blank.svg
@@ -0,0 +1,320 @@
+
+
+
diff --git a/docs/_static/logo_long.png b/docs/_static/logo_long.png
index 8f0ae9db1..480c19a76 100644
Binary files a/docs/_static/logo_long.png and b/docs/_static/logo_long.png differ
diff --git a/docs/_static/logo_long.svg b/docs/_static/logo_long.svg
new file mode 100644
index 000000000..0f2a5bfcb
--- /dev/null
+++ b/docs/_static/logo_long.svg
@@ -0,0 +1,518 @@
+
+
+
diff --git a/docs/_static/logo_social.png b/docs/_static/logo_social.png
new file mode 100644
index 000000000..8cf153558
Binary files /dev/null and b/docs/_static/logo_social.png differ
diff --git a/docs/_static/logo_social.svg b/docs/_static/logo_social.svg
new file mode 100644
index 000000000..ad6108a92
--- /dev/null
+++ b/docs/_static/logo_social.svg
@@ -0,0 +1,322 @@
+
+
+
diff --git a/docs/_static/logo_square.png b/docs/_static/logo_square.png
index 8afbf7410..40da64f78 100644
Binary files a/docs/_static/logo_square.png and b/docs/_static/logo_square.png differ
diff --git a/docs/_static/logo_square.svg b/docs/_static/logo_square.svg
new file mode 100644
index 000000000..a8855ee72
--- /dev/null
+++ b/docs/_static/logo_square.svg
@@ -0,0 +1,592 @@
+
+
+
diff --git a/docs/authors.rst b/docs/authors.rst
index 419133f97..98164b0a8 100644
--- a/docs/authors.rst
+++ b/docs/authors.rst
@@ -31,7 +31,7 @@ plotting code. As an undergraduate, he developed a set of
When he switched to python in graduate school, he replicated most of these utilities in
python, learned more about the language, began to rapidly develop them, and decided to
share them with the rest of the scientific community. Luke is also working on a project
-called `climopy `__, a companion to
+called `climopy `__, a companion to
`metpy `__ for carrying out data analysis tasks
related to climate science, and has authored a number of
`vim plugins `__
@@ -40,7 +40,7 @@ as an avid vim user.
`Riley Brady`_ is the next-biggest contributor. He helped Luke set up automatic
testing, deploy this project to PyPi, and improve new user experience. He is
also proplot's earliest user and helped fix `lots of early bugs
-`__.
+`__.
.. _Luke Davis: https://github.com/lukelbd
diff --git a/docs/cartesian.py b/docs/cartesian.py
index 37af9a2ad..eb6640397 100644
--- a/docs/cartesian.py
+++ b/docs/cartesian.py
@@ -513,7 +513,7 @@
# ` 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).
+# `__ for details).
# %%
import proplot as pplt
diff --git a/docs/changelog.html b/docs/changelog.html
deleted file mode 100644
index 356082db4..000000000
--- a/docs/changelog.html
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
diff --git a/docs/conf.py b/docs/conf.py
index 8fdc52773..a9b48620e 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -153,9 +153,9 @@
# Links for What's New github commits, issues, and pull requests
extlinks = {
- 'issue': ('https://github.com/lukelbd/proplot/issues/%s', 'GH#'),
- 'commit': ('https://github.com/lukelbd/proplot/commit/%s', '@'),
- 'pr': ('https://github.com/lukelbd/proplot/pull/%s', 'GH#'),
+ 'issue': ('https://github.com/proplot-dev/proplot/issues/%s', 'GH#'),
+ 'commit': ('https://github.com/proplot-dev/proplot/commit/%s', '@'),
+ 'pr': ('https://github.com/proplot-dev/proplot/pull/%s', 'GH#'),
}
# Set up mapping for other projects' docs
diff --git a/docs/contributing.rst b/docs/contributing.rst
new file mode 100644
index 000000000..3bdd7dc21
--- /dev/null
+++ b/docs/contributing.rst
@@ -0,0 +1 @@
+.. include:: ../CONTRIBUTING.rst
\ No newline at end of file
diff --git a/docs/contributions.rst b/docs/contributions.rst
deleted file mode 100644
index 25b5d4702..000000000
--- a/docs/contributions.rst
+++ /dev/null
@@ -1 +0,0 @@
-.. include:: ../HOWTOCONTRIBUTE.rst
diff --git a/docs/environment.yml b/docs/environment.yml
index 5b946f7fa..0565f0de5 100644
--- a/docs/environment.yml
+++ b/docs/environment.yml
@@ -17,9 +17,9 @@ channels:
- conda-forge
dependencies:
- python==3.8
+ - numpy==1.19.5
- matplotlib==3.2.2
- cartopy==0.20.2
- - numpy
- pandas
- xarray
- ipykernel
@@ -36,4 +36,4 @@ dependencies:
- markupsafe==2.0.1
- nbsphinx==0.8.1
- jupytext
- - git+https://github.com/lukelbd/sphinx-automodapi@proplot-mods
+ - git+https://github.com/proplot-dev/sphinx-automodapi@proplot-mods
diff --git a/docs/index.rst b/docs/index.rst
index 377db2307..14712793f 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -1,8 +1,8 @@
..
ProPlot documentation master file, created by
sphinx-quickstart on Wed Feb 20 01:31:20 2019.
- You can adapt this file completely to your liking, but it should at least
- contain the root `toctree` directive.
+ You can adapt this file completely to your liking, but
+ it should at least contain the root `toctree` directive.
=======
ProPlot
@@ -10,7 +10,7 @@ ProPlot
A succinct `matplotlib `__ wrapper
for making beautiful, publication-quality graphics. This project
-is `published on GitHub `__ and can
+is `published on GitHub `__ and can
be cited using its `Zenodo DOI `__.
.. toctree::
@@ -47,10 +47,9 @@ be cited using its `Zenodo DOI `__.
api
external-links
whatsnew
- contributions
+ contributing
authors
-
Indices and tables
==================
diff --git a/docs/install.rst b/docs/install.rst
index a23a75e3e..b914117c9 100644
--- a/docs/install.rst
+++ b/docs/install.rst
@@ -1 +1 @@
-.. include:: ../INSTALL.rst
+.. include:: ../INSTALL.rst
\ No newline at end of file
diff --git a/docs/logo_long.png b/docs/logo_long.png
new file mode 100644
index 000000000..480c19a76
Binary files /dev/null and b/docs/logo_long.png differ
diff --git a/proplot/axes/base.py b/proplot/axes/base.py
index 3c0da4183..ae55d19d4 100644
--- a/proplot/axes/base.py
+++ b/proplot/axes/base.py
@@ -999,6 +999,12 @@ def _add_colorbar(
ticklenratio = _not_none(ticklenratio, rc['tick.lenratio'])
tickwidthratio = _not_none(tickwidthratio, rc['tick.widthratio'])
rasterized = _not_none(rasterized, rc['colorbar.rasterized'])
+ if extendsize is not None and extendfrac is not None:
+ warnings._warn_proplot(
+ f'You cannot specify both an absolute extendsize={extendsize!r} '
+ f"and a relative extendfrac={extendfrac!r}. Ignoring 'extendfrac'."
+ )
+ extendfrac = None
# Build label and locator keyword argument dicts
# NOTE: This carefully handles the 'maxn' and 'maxn_minor' deprecations
@@ -1037,16 +1043,25 @@ def _add_colorbar(
# Generate and prepare the colorbar axes
# NOTE: The inset axes function needs 'label' to know how to pad the box
# TODO: Use seperate keywords for frame properties vs. colorbar edge properties?
- if loc in ('fill', 'left', 'right', 'top', 'bottom'):
+ # TODO: Have extendsize auto-adjust to the subplot size as it changes
+ fill = loc in ('fill', 'left', 'right', 'top', 'bottom')
+ if not fill:
+ extendsize = _not_none(extendsize, rc['colorbar.insetextend'])
+ kwargs.update({'label': label, 'length': length, 'width': width})
+ cax, kwargs = self._parse_colorbar_inset(loc=loc, pad=pad, **kwargs) # noqa: E501
+ else:
+ extendsize = _not_none(extendsize, rc['colorbar.extend'])
length = _not_none(length, rc['colorbar.length']) # for _add_guide_panel
kwargs.update({'align': align, 'length': length})
- extendsize = _not_none(extendsize, rc['colorbar.extend'])
- ax = self._add_guide_panel(loc, align, length=length, width=width, space=space, pad=pad) # noqa: E501
+ kw = {'width': width, 'space': space, 'pad': pad}
+ ax = self._add_guide_panel(loc, align, length, **kw)
cax, kwargs = ax._parse_colorbar_filled(**kwargs)
- else:
- kwargs.update({'label': label, 'length': length, 'width': width})
- extendsize = _not_none(extendsize, rc['colorbar.insetextend'])
- cax, kwargs = self._parse_colorbar_inset(loc=loc, pad=pad, **kwargs) # noqa: E501
+ vert = kwargs['orientation'] == 'vertical'
+ if extendfrac is None: # compute extendsize
+ width, height = cax._get_size_inches()
+ axsize = height if vert else width
+ extendsize = units(extendsize, 'em', 'in')
+ extendfrac = extendsize / max(axsize - 2 * extendsize, units(1, 'em', 'in'))
# Parse the colorbar mappable
# NOTE: Account for special case where auto colorbar is generated from 1D
@@ -1063,25 +1078,13 @@ def _add_colorbar(
f'Ignoring unused keyword arg(s): {pop}'
)
- # Parse 'extendsize' and 'extendfrac' keywords
- # TODO: Make this auto-adjust to the subplot size
- vert = kwargs['orientation'] == 'vertical'
- if extendsize is not None and extendfrac is not None:
- warnings._warn_proplot(
- f'You cannot specify both an absolute extendsize={extendsize!r} '
- f"and a relative extendfrac={extendfrac!r}. Ignoring 'extendfrac'."
- )
- extendfrac = None
- if extendfrac is None:
- width, height = cax._get_size_inches()
- scale = height if vert else width
- extendsize = units(extendsize, 'em', 'in')
- extendfrac = extendsize / max(scale - 2 * extendsize, units(1, 'em', 'in'))
-
# Parse the tick locators and formatters
# NOTE: In presence of BoundaryNorm or similar handle ticks with special
# DiscreteLocator or else get issues (see mpl #22233).
norm = mappable.norm
+ source = getattr(norm, '_norm', None)
+ vcenter = getattr(source, 'vcenter', None)
+ vcenter = {} if vcenter is None else {'vcenter': vcenter}
formatter = _not_none(formatter, getattr(norm, '_labels', None), 'auto')
formatter = constructor.Formatter(formatter, **formatter_kw)
categorical = isinstance(formatter, mticker.FixedFormatter)
@@ -1093,18 +1096,18 @@ def _add_colorbar(
tickminor = False if categorical else rc['xy'[vert] + 'tick.minor.visible']
if isinstance(norm, mcolors.BoundaryNorm): # DiscreteNorm or BoundaryNorm
ticks = getattr(norm, '_ticks', norm.boundaries)
- segmented = isinstance(getattr(norm, '_norm', None), pcolors.SegmentedNorm)
+ segmented = isinstance(source, pcolors.SegmentedNorm)
if locator is None:
if categorical or segmented:
locator = mticker.FixedLocator(ticks)
else:
- locator = pticker.DiscreteLocator(ticks)
+ locator = pticker.DiscreteLocator(ticks, **vcenter)
if tickminor and minorlocator is None:
- minorlocator = pticker.DiscreteLocator(ticks, minor=True)
+ minorlocator = pticker.DiscreteLocator(ticks, minor=True, **vcenter)
# Special handling for colorbar keyword arguments
# WARNING: Critical to not pass empty major locators in matplotlib < 3.5
- # See this issue: https://github.com/lukelbd/proplot/issues/301
+ # See this issue: https://github.com/proplot-dev/proplot/issues/301
# WARNING: Proplot 'supports' passing one extend to a mappable function
# then overwriting by passing another 'extend' to colobar. But contour
# colorbars break when you try to change its 'extend'. Matplotlib gets
@@ -1602,7 +1605,7 @@ def _parse_frame(guide, fancybox=None, shadow=None, **kwargs):
kw_frame = _pop_kwargs(
kwargs,
alpha=('a', 'framealpha', 'facealpha'),
- facecolor=('fc', 'framecolor', 'facecolor'),
+ facecolor=('fc', 'framecolor'),
edgecolor=('ec',),
edgewidth=('ew',),
)
diff --git a/proplot/axes/cartesian.py b/proplot/axes/cartesian.py
index 1dc88ebf8..521f2dbaa 100644
--- a/proplot/axes/cartesian.py
+++ b/proplot/axes/cartesian.py
@@ -1005,9 +1005,9 @@ def format(
yformatter = _not_none(yformatter=yformatter, yticklabels=yticklabels)
xtickminor_default = ytickminor_default = None
if isinstance(xformatter, mticker.FixedFormatter) or np.iterable(xformatter) and not isinstance(xformatter, str): # noqa: E501
- xtickminor_default = False
+ xtickminor_default = False if xminorlocator is None else None
if isinstance(yformatter, mticker.FixedFormatter) or np.iterable(yformatter) and not isinstance(yformatter, str): # noqa: E501
- ytickminor_default = False
+ ytickminor_default = False if yminorlocator is None else None
xtickminor = _not_none(xtickminor, xtickminor_default, rc.find('xtick.minor.visible', context=True)) # noqa: E501
ytickminor = _not_none(ytickminor, ytickminor_default, rc.find('ytick.minor.visible', context=True)) # noqa: E501
ticklabeldir = kwargs.pop('ticklabeldir', None)
diff --git a/proplot/axes/plot.py b/proplot/axes/plot.py
index e5f2825aa..74de279ec 100644
--- a/proplot/axes/plot.py
+++ b/proplot/axes/plot.py
@@ -1479,7 +1479,7 @@ def _add_error_bars(
): # ugly kludge to check for shading
if all(_ is None for _ in (bardata, barstds, barpctiles)):
barstds, barpctiles = default_barstds, default_barpctiles
- if all(_ is None for _ in (boxdata, boxstds, boxpctile)):
+ if all(_ is None for _ in (boxdata, boxstds, boxpctiles)):
boxstds, boxpctiles = default_boxstds, default_boxpctiles
showbars = any(
_ is not None and _ is not False for _ in (barstds, barpctiles, bardata)
@@ -2101,10 +2101,9 @@ def _parse_color(self, x, y, c, *, apply_cycle=True, infer_rgb=False, **kwargs):
def _parse_cmap(
self, *args,
cmap=None, cmap_kw=None, c=None, color=None, colors=None,
- norm=None, norm_kw=None, extend=None, vmin=None, vmax=None, discrete=None,
- default_cmap=None, default_discrete=True, skip_autolev=False,
- min_levels=None, plot_lines=False, plot_contours=False,
- **kwargs
+ norm=None, norm_kw=None, extend=None, vmin=None, vmax=None, vcenter=None,
+ discrete=None, default_discrete=True, default_cmap=None, skip_autolev=False,
+ min_levels=None, plot_lines=False, plot_contours=False, **kwargs
):
"""
Parse colormap and normalizer arguments.
@@ -2119,8 +2118,10 @@ def _parse_cmap(
Normalize specs.
extend : optional
The colormap extend setting.
- vmin, vmax : flaot, optional
+ vmin, vmax : float, optional
The normalization range.
+ vcenter : float, optional
+ The central value for diverging colormaps.
sequential, diverging, cyclic, qualitative : bool, optional
Toggle various colormap types.
discrete : bool, optional
@@ -2141,16 +2142,22 @@ def _parse_cmap(
norm_kw = norm_kw or {}
vmin = _not_none(vmin=vmin, norm_kw_vmin=norm_kw.pop('vmin', None))
vmax = _not_none(vmax=vmax, norm_kw_vmax=norm_kw.pop('vmax', None))
- extend = _not_none(extend, 'neither')
+ vcenter = _not_none(vcenter=vcenter, norm_kw_vcenter=norm_kw.get('vcenter'))
colors = _not_none(c=c, color=color, colors=colors) # in case untranslated
- modes = {key: kwargs.pop(key, None) for key in ('sequential', 'diverging', 'cyclic', 'qualitative')} # noqa: E501
- trues = {key: b for key, b in modes.items() if b}
- if len(trues) > 1: # noqa: E501
+ extend = 'both' if extend is True else 'neither' if extend is False else extend
+ extend = _not_none(extend, 'neither')
+ modes = ('sequential', 'diverging', 'cyclic', 'qualitative')
+ modes = {mode: kwargs.pop(mode, None) for mode in modes}
+ if vcenter is not None: # shorthand to ensure diverging colormap
+ norm = _not_none(norm, 'div')
+ modes['diverging'] = True
+ norm_kw.setdefault('vcenter', vcenter)
+ if sum(map(bool, modes.values())) > 1: # noqa: E501
warnings._warn_proplot(
- f'Conflicting colormap arguments: {trues!r}. Using the first one.'
+ f'Conflicting colormap arguments: {modes!r}. Using the first one.'
)
- for key in tuple(trues)[1:]:
- del trues[key]
+ keys = tuple(key for key, b in modes.items() if b)
+ for key in keys[1:]:
modes[key] = None
# Create user-input colormap and potentially disable autodiverging
@@ -2166,7 +2173,7 @@ def _parse_cmap(
if colors is not None:
if cmap is not None:
warnings._warn_proplot(
- f'you specified both cmap={cmap!s} and the qualitative-colormap '
+ f'You specified both cmap={cmap!s} and the qualitative-colormap '
f"colors={colors!r}. Ignoring 'colors'. If you meant to specify "
f'the edge color please use e.g. edgecolor={colors!r} instead.'
)
@@ -2186,13 +2193,13 @@ def _parse_cmap(
# Force default options in special cases
# NOTE: Delay application of 'sequential', 'diverging', 'cyclic', 'qualitative'
# until after level generation so 'diverging' can be automatically applied.
- if 'cyclic' in trues or getattr(cmap, '_cyclic', None):
+ if modes['cyclic'] or getattr(cmap, '_cyclic', None):
if extend is not None and extend != 'neither':
warnings._warn_proplot(
f"Cyclic colormaps require extend='neither'. Ignoring extend={extend!r}" # noqa: E501
)
extend = 'neither'
- if 'qualitative' in trues or isinstance(cmap, pcolors.DiscreteColormap):
+ if modes['qualitative'] or isinstance(cmap, pcolors.DiscreteColormap):
if discrete is not None and not discrete: # noqa: E501
warnings._warn_proplot(
'Qualitative colormaps require discrete=True. Ignoring discrete=False.' # noqa: E501
@@ -2231,25 +2238,30 @@ def _parse_cmap(
_, counts = np.unique(np.sign(levels), return_counts=True)
if counts[counts > 1].size > 1:
isdiverging = True
- if not trues and isdiverging and modes['diverging'] is None:
- trues['diverging'] = modes['diverging'] = True
+ if not any(modes.values()) and isdiverging and modes['diverging'] is None:
+ modes['diverging'] = True
# Create the continuous normalizer.
- norm = _not_none(norm, 'div' if 'diverging' in trues else 'linear')
+ isdiverging = modes['diverging']
+ default = 'div' if isdiverging else 'linear'
+ norm = _not_none(norm, default)
+ if isdiverging and isinstance(norm, str) and norm in ('segments', 'segmented'):
+ norm_kw.setdefault('vcenter', 0)
if isinstance(norm, mcolors.Normalize):
norm.vmin, norm.vmax = vmin, vmax
else:
norm = constructor.Norm(norm, vmin=vmin, vmax=vmax, **norm_kw)
- isdiverging = autodiverging and isinstance(norm, pcolors.DivergingNorm)
- if not trues and isdiverging and modes['diverging'] is None:
- trues['diverging'] = modes['diverging'] = True
+ if autodiverging and isinstance(norm, pcolors.DivergingNorm):
+ isdiverging = True
+ if not any(modes.values()) and isdiverging and modes['diverging'] is None:
+ modes['diverging'] = True
# Create the final colormap
if cmap is None:
if default_cmap is not None: # used internally
cmap = default_cmap
- elif trues:
- cmap = rc['cmap.' + tuple(trues)[0]]
+ elif any(modes.values()):
+ cmap = rc['cmap.' + tuple(key for key, b in modes.items() if b)[0]]
else:
cmap = rc['image.cmap']
cmap = constructor.Colormap(cmap, **cmap_kw)
@@ -2344,8 +2356,7 @@ def _parse_cycle(
return kwargs
def _parse_level_lim(
- self, *args,
- vmin=None, vmax=None, robust=None, inbounds=None,
+ self, *args, vmin=None, vmax=None, vcenter=None, robust=None, inbounds=None,
negative=None, positive=None, symmetric=None, to_centers=False, **kwargs
):
"""
@@ -2355,8 +2366,8 @@ def _parse_level_lim(
----------
*args
The sample data.
- vmin, vmax : float, optional
- The user input minimum and maximum.
+ vmin, vmax, vcenter : float, optional
+ The user input minimum, maximum, and center.
robust : bool, optional
Whether to limit the default range to exclude outliers.
inbounds : bool, optional
@@ -2376,6 +2387,7 @@ def _parse_level_lim(
# Parse vmin and vmax
automin = vmin is None
automax = vmax is None
+ vcenter = vcenter or 0.0
if not automin and not automax:
return vmin, vmax, kwargs
@@ -2392,8 +2404,6 @@ def _parse_level_lim(
raise ValueError(f'Unexpected robust={robust!r}. Must be bool, float, or 2-tuple.') # noqa: E501
# Get sample data
- # NOTE: Critical to use _to_numpy_array here because some
- # commands are unstandardized.
# NOTE: Try to get reasonable *count* levels for hexbin/hist2d, but in general
# have no way to select nice ones a priori (why we disable discretenorm).
# NOTE: Currently we only ever use this function with *single* array input
@@ -2409,7 +2419,7 @@ def _parse_level_lim(
continue
if z.ndim > 2: # e.g. imshow data
continue
- z = inputs._to_numpy_array(z)
+ z = inputs._to_numpy_array(z) # critical since not always standardized
if inbounds and x is not None and y is not None: # ignore if None coords
z = self._inbounds_vlim(x, y, z, to_centers=to_centers)
imin, imax = inputs._safe_range(z, pmin, pmax)
@@ -2426,7 +2436,7 @@ def _parse_level_lim(
# NOTE: This is also applied to manual input levels lists in _parse_level_vals
if negative:
if automax:
- vmax = 0
+ vmax = vcenter
else:
warnings._warn_proplot(
f'Incompatible arguments vmax={vmax!r} and negative=True. '
@@ -2434,13 +2444,14 @@ def _parse_level_lim(
)
if positive:
if automin:
- vmin = 0
+ vmin = vcenter
else:
warnings._warn_proplot(
f'Incompatible arguments vmin={vmin!r} and positive=True. '
'Ignoring the latter.'
)
if symmetric:
+ vmin, vmax = vmin - vcenter, vmax - vcenter
if automin and not automax:
vmin = -vmax
elif automax and not automin:
@@ -2452,6 +2463,7 @@ def _parse_level_lim(
f'Incompatible arguments vmin={vmin!r}, vmax={vmax!r}, and '
'symmetric=True. Ignoring the latter.'
)
+ vmin, vmax = vmin + vcenter, vmax + vcenter
return vmin, vmax, kwargs
@@ -2488,16 +2500,13 @@ def _parse_level_num(
Unused arguments.
"""
# Input args
- # NOTE: Some of this is adapted from the hidden contour.ContourSet._autolev
- # NOTE: We use 'symmetric' with MaxNLocator to ensure boundaries include a
- # zero level but may trim many of these below.
+ # NOTE: Some of this is adapted from contour.ContourSet._autolev
+ # NOTE: We use 'symmetric' with MaxNLocator to ensure boundaries
+ # include a zero level but may trim many of these levels below.
norm_kw = norm_kw or {}
locator_kw = locator_kw or {}
extend = _not_none(extend, 'neither')
levels = _not_none(levels, rc['cmap.levels'])
- vmin = _not_none(vmin=vmin, norm_kw_vmin=norm_kw.pop('vmin', None))
- vmax = _not_none(vmax=vmax, norm_kw_vmax=norm_kw.pop('vmax', None))
- norm = constructor.Norm(norm or 'linear', **norm_kw)
symmetric = _not_none(
symmetric=symmetric,
locator_kw_symmetric=locator_kw.pop('symmetric', None),
@@ -2506,7 +2515,8 @@ def _parse_level_num(
# Get default locator from input norm
# NOTE: This normalizer is only temporary for inferring level locs
- norm = constructor.Norm(norm or 'linear', **norm_kw)
+ norm = norm or 'linear'
+ norm = constructor.Norm(norm, **norm_kw)
if locator is not None:
locator = constructor.Locator(locator, **locator_kw)
elif isinstance(norm, mcolors.LogNorm):
@@ -2521,18 +2531,24 @@ def _parse_level_num(
locator = mticker.MaxNLocator(levels, min_n_ticks=1, **locator_kw)
# Get default level locations
+ # NOTE: Critical to adjust ticks with vcenter
nlevs = levels
+ vcenter = getattr(norm, 'vcenter', None)
automin = vmin is None
automax = vmax is None
vmin, vmax, kwargs = self._parse_level_lim(
- *args, vmin=vmin, vmax=vmax, symmetric=symmetric, **kwargs
+ *args, vmin=vmin, vmax=vmax, vcenter=vcenter, symmetric=symmetric, **kwargs
)
+ if vcenter is not None:
+ vmin, vmax = vmin - vcenter, vmax - vcenter
try:
levels = locator.tick_values(vmin, vmax)
except TypeError: # e.g. due to datetime arrays
return None, kwargs
except RuntimeError: # too-many-ticks error
levels = np.linspace(vmin, vmax, levels) # TODO: _autolev used N + 1
+ if vcenter is not None:
+ vmin, vmax, levels = vmin + vcenter, vmax + vcenter, levels + vcenter
# Possibly trim levels far outside of 'vmin' and 'vmax'
# NOTE: This part is mostly copied from matplotlib _autolev
@@ -2578,10 +2594,8 @@ def _parse_level_vals(
Parameters
----------
*args
- The sample data. Passed to `_parse_level_lim`.
- N
- Shorthand for `levels`.
- levels : int or sequence of float, optional
+ The sample data. Passed to `_parse_level_num`.
+ N, levels : int or sequence of float, optional
The levels list or (approximate) number of levels to create.
values : int or sequence of float, optional
The level center list or (approximate) number of level centers to create.
@@ -2589,19 +2603,41 @@ def _parse_level_vals(
Whether to remove out non-positive, non-negative, and zero-valued
levels. The latter is useful for single-color contour plots.
norm, norm_kw : optional
- Passed to `Norm`. Used to possbily infer levels or to convert values.
+ Passed to `Norm`. Used to possibly infer levels or to convert values.
skip_autolev : bool, optional
Whether to skip automatic level generation.
min_levels : int, optional
The minimum number of levels allowed.
+ **kwargs
+ Passed to `_parse_level_num`.
Returns
-------
levels : list of float
The level edges.
+ vmin, vmax : float
+ The minimum and maximum.
+ norm : `matplotlib.colors.Normalize`
+ The normalizer.
**kwargs
Unused arguments.
"""
+ # Generate levels so that ticks will be centered between edges
+ # Solve: (x1 + x2) / 2 = y --> x2 = 2 * y - x1 with arbitrary init x1
+ # NOTE: Used for e.g. parametric plots with logarithmic coordinates
+ def _convert_values(values):
+ descending = values[1] < values[0]
+ if descending: # e.g. [100, 50, 20, 10, 5, 2, 1] successful if reversed
+ values = values[::-1]
+ levels = [1.5 * values[0] - 0.5 * values[1]] # arbitrary starting point
+ for value in values:
+ levels.append(2 * value - levels[-1])
+ if np.any(np.diff(levels) < 0): # never happens for evenly spaced levs
+ levels = utils.edges(values)
+ if descending: # then revert back below
+ levels = levels[::-1]
+ return levels
+
# Helper function that restricts levels
# NOTE: This should have no effect if levels were generated automatically.
# However want to apply these to manual-input levels as well.
@@ -2635,7 +2671,9 @@ def _sanitize_levels(key, array, minsize):
array = norm.boundaries if key == 'levels' else None
return array
- # Parse input arguments and resolve incompatibilities
+ # Parse input arguments and infer edges from centers
+ # NOTE: The only way for user to manually impose BoundaryNorm is by
+ # passing one -- users cannot create one using Norm constructor key.
vmin = vmax = None
levels = _not_none(N=N, levels=levels, norm_kw_levs=norm_kw.pop('levels', None))
if positive and negative:
@@ -2648,10 +2686,6 @@ def _sanitize_levels(key, array, minsize):
f'Incompatible args levels={levels!r} and values={values!r}. Using former.' # noqa: E501
)
values = None
-
- # Infer level edges from level centers if possible
- # NOTE: The only way for user to manually impose BoundaryNorm is by
- # passing one -- users cannot create one using Norm constructor key.
if isinstance(values, Integral):
levels = values + 1
values = None
@@ -2661,27 +2695,13 @@ def _sanitize_levels(key, array, minsize):
else:
values = _sanitize_levels('values', values, 1)
kwargs['discrete_ticks'] = values # passed to _parse_level_norm
- if len(values) == 1:
- levels = [values[0] - 1, values[0] + 1] # weird but why not
+ if len(values) == 1: # special case (see also DiscreteNorm)
+ levels = [values[0] - 1, values[0] + 1]
elif norm is not None and norm not in ('segments', 'segmented'):
- # Generate levels by finding in-between points in the
- # normalized numeric space, e.g. LogNorm space.
- norm_kw = norm_kw or {}
- convert = constructor.Norm(norm, **norm_kw)
+ convert = constructor.Norm(norm, **(norm_kw or {}))
levels = convert.inverse(utils.edges(convert(values)))
else:
- # Generate levels so that ticks will be centered between edges
- # Solve: (x1 + x2) / 2 = y --> x2 = 2 * y - x1 with arbitrary init x1
- descending = values[1] < values[0]
- if descending: # e.g. [100, 50, 20, 10, 5, 2, 1] successful if reversed
- values = values[::-1]
- levels = [1.5 * values[0] - 0.5 * values[1]] # arbitrary starting point
- for value in values:
- levels.append(2 * value - levels[-1])
- if np.any(np.diff(levels) < 0): # never happens for evenly spaced levs
- levels = utils.edges(values)
- if descending: # then revert back below
- levels = levels[::-1]
+ levels = _convert_values(values)
# Process level edges and infer defaults
# NOTE: Matplotlib colorbar algorithm *cannot* handle descending levels so
@@ -2726,7 +2746,7 @@ def _parse_level_norm(
):
"""
Create a `~proplot.colors.DiscreteNorm` or `~proplot.colors.BoundaryNorm`
- from the input colormap and normalizer.
+ from the input levels, normalizer, and colormap.
Parameters
----------
@@ -2812,7 +2832,11 @@ def _parse_level_norm(
# Generate DiscreteNorm and update "child" norm with vmin and vmax from
# levels. This lets the colorbar set tick locations properly!
- if not isinstance(norm, mcolors.BoundaryNorm) and len(levels) > 1:
+ if len(levels) == 1:
+ pass # e.g. contours
+ elif isinstance(norm, mcolors.BoundaryNorm):
+ pass # override with native matplotlib normalizer
+ else:
norm = pcolors.DiscreteNorm(
levels, norm=norm, unique=unique, step=step,
ticks=discrete_ticks, labels=discrete_labels,
@@ -2885,7 +2909,7 @@ def plotx(self, *args, **kwargs):
kwargs = _parse_vert(default_vert=False, **kwargs)
return self._apply_plot(*args, **kwargs)
- def _apply_step(self, *pairs, vert=True, where='pre', **kwargs):
+ def _apply_step(self, *pairs, vert=True, **kwargs):
"""
Plot the steps.
"""
@@ -2894,11 +2918,7 @@ def _apply_step(self, *pairs, vert=True, where='pre', **kwargs):
# approach... but instead repeat _apply_plot internals here so we can
# disable error indications that make no sense for 'step' plots.
kws = kwargs.copy()
- opts = ('pre', 'post', 'mid')
- if where not in opts:
- raise ValueError(f'Invalid where={where!r}. Options are {opts!r}.')
kws.update(_pop_props(kws, 'line'))
- kws.setdefault('drawstyle', 'steps-' + where)
kws, extents = self._inbounds_extent(**kws)
objs = []
for xs, ys, fmt in self._iter_arg_pairs(*pairs):
@@ -4181,10 +4201,8 @@ def _iter_arg_cols(self, *args, label=None, labels=None, values=None, **kwargs):
# Handle cycle args and label lists
# NOTE: Arrays here should have had metadata stripped by _parse_1d_args
# but could still be pint quantities that get processed by axis converter.
- n = max(
- 1 if not inputs._is_array(a) or a.ndim < 2 else a.shape[-1]
- for a in args
- )
+ is_array = lambda data: hasattr(data, 'ndim') and hasattr(data, 'shape') # noqa: E731, E501
+ n = max(1 if not is_array(a) or a.ndim < 2 else a.shape[-1] for a in args)
labels = _not_none(label=label, values=values, labels=labels)
if not np.iterable(labels) or isinstance(labels, str):
labels = n * [labels]
@@ -4202,9 +4220,7 @@ def _iter_arg_cols(self, *args, label=None, labels=None, values=None, **kwargs):
for i in range(n):
kw = kwargs.copy()
kw['label'] = labels[i] or None
- a = tuple(
- a if not inputs._is_array(a) or a.ndim < 2 else a[..., i] for a in args
- )
+ a = tuple(a if not is_array(a) or a.ndim < 2 else a[..., i] for a in args)
yield (i, n, *a, kw)
# Related parsing functions for warnings
diff --git a/proplot/colors.py b/proplot/colors.py
index 531d70f02..dfc39bd80 100644
--- a/proplot/colors.py
+++ b/proplot/colors.py
@@ -339,7 +339,7 @@
docstring._snippet_manager['colors.from_list'] = _from_list_docstring
-def _clip_colors(colors, clip=True, gray=0.2, warn=False):
+def _clip_colors(colors, clip=True, gray=0.2, warn_invalid=False):
"""
Clip impossible colors rendered in an HSL-to-RGB colorspace
conversion. Used by `PerceptualColormap`.
@@ -349,13 +349,11 @@ def _clip_colors(colors, clip=True, gray=0.2, warn=False):
colors : sequence of 3-tuple
The RGB colors.
clip : bool, optional
- If `clip` is ``True`` (the default), RGB channel values >1 are
- clipped to 1. Otherwise, the color is masked out as gray.
+ If `clip` is ``True`` (the default), RGB channel values >1
+ are clipped to 1. Otherwise, the color is masked out as gray.
gray : float, optional
- The identical RGB channel values (gray color) to be used if
- `clip` is ``True``.
- warn : bool, optional
- Whether to issue warning when colors are clipped.
+ The identical RGB channel values (gray color) to be used
+ if `clip` is ``True``.
"""
colors = np.asarray(colors)
under = colors < 0
@@ -364,7 +362,7 @@ def _clip_colors(colors, clip=True, gray=0.2, warn=False):
colors[under], colors[over] = 0, 1
else:
colors[under | over] = gray
- if warn:
+ if warn_invalid:
msg = 'Clipped' if clip else 'Invalid'
for i, name in enumerate('rgb'):
if np.any(under[:, i]) or np.any(over[:, i]):
@@ -372,7 +370,7 @@ def _clip_colors(colors, clip=True, gray=0.2, warn=False):
return colors
-def _get_channel(color, channel, space='hcl'):
+def _color_channel(color, channel, space='hcl'):
"""
Get the hue, saturation, or luminance channel value from the input color. The
color name `color` can optionally be a string with the format ``'color+x'``
@@ -586,7 +584,7 @@ def _load_colors(path, warn_on_failure=True):
If ``True``, issue a warning when loading fails instead of raising an error.
"""
# Warn or raise error (matches Colormap._from_file behavior)
- if not os.path.exists(path):
+ if not os.path.isfile(path):
message = f'Failed to load color data file {path!r}. File not found.'
if warn_on_failure:
warnings._warn_proplot(message)
@@ -782,7 +780,7 @@ def _warn_or_raise(descrip, error=RuntimeError):
warnings._warn_proplot(prefix + ' ' + descrip)
else:
raise error(prefix + ' ' + descrip)
- if not os.path.exists(path):
+ if not os.path.isfile(path):
return _warn_or_raise('File not found.', FileNotFoundError)
# Directly read segmentdata json file
@@ -1944,7 +1942,7 @@ def __init__(
for i, xyy in enumerate(array):
xyy = list(xyy) # make copy!
for j, y in enumerate(xyy[1:]): # modify the y values
- xyy[j + 1] = _get_channel(y, key, space)
+ xyy[j + 1] = _color_channel(y, key, space)
data[key][i] = xyy
# Initialize
super().__init__(name, data, gamma=1.0, N=N, **kwargs)
@@ -2425,10 +2423,10 @@ def __init__(
# Instead user-reversed levels will always get passed here just as
# they are passed to SegmentedNorm inside plot.py
levels, descending = _sanitize_levels(levels)
+ vcenter = getattr(norm, 'vcenter', None)
vmin = norm.vmin = np.min(levels)
vmax = norm.vmax = np.max(levels)
bins, _ = _sanitize_levels(norm(levels))
- vcenter = getattr(norm, 'vcenter', None)
mids = np.zeros((levels.size + 1,))
mids[1:-1] = 0.5 * (levels[1:] + levels[:-1])
mids[0], mids[-1] = mids[1], mids[-2]
@@ -2443,11 +2441,17 @@ def __init__(
# not perfect... get asymmetric color intensity either side of central point.
# So we add special handling for diverging norms below to improve symmetry.
if unique in ('min', 'both'):
- mids[0] += step * (mids[1] - mids[2])
+ scale = levels[0] - levels[1] if len(levels) == 2 else mids[1] - mids[2]
+ mids[0] += step * scale
if unique in ('max', 'both'):
- mids[-1] += step * (mids[-2] - mids[-3])
- mmin, mmax = np.min(mids), np.max(mids)
- if vcenter is None:
+ scale = levels[-1] - levels[-2] if len(levels) == 2 else mids[-2] - mids[-3]
+ mids[-1] += step * scale
+ mmin = np.min(mids)
+ mmax = np.max(mids)
+ if np.isclose(mmin, mmax):
+ mmin = mmin - (mmin or 1) * 1e-10
+ mmax = mmax + (mmax or 1) * 1e-10
+ if vcenter is None: # not diverging norm or centered segmented norm
mids = _interpolate_scalar(mids, mmin, mmax, vmin, vmax)
else:
mask1, mask2 = mids < vcenter, mids >= vcenter
@@ -2538,22 +2542,22 @@ class SegmentedNorm(mcolors.Normalize):
Normalizer that scales data linearly with respect to the
interpolated index in an arbitrary monotonic level sequence.
"""
- def __init__(self, levels, vmin=None, vmax=None, clip=False):
+ def __init__(self, levels, vcenter=None, vmin=None, vmax=None, clip=None, fair=True): # noqa: E501
"""
Parameters
----------
levels : sequence of float
- The level boundaries. Must be monotonically increasing
- or decreasing.
+ The level boundaries. Must be monotonically increasing or decreasing.
+ vcenter : float, default: None
+ The central colormap value. Default is to omit this.
vmin : float, optional
- Ignored but included for consistency with other normalizers.
- Set to the minimum of `levels`.
+ Ignored but included for consistency. Set to ``min(levels)``.
vmax : float, optional
- Ignored but included for consistency with other normalizers.
- Set to the minimum of `levels`.
+ Ignored but included for consistency. Set to ``max(levels)``.
clip : bool, optional
- Whether to clip values falling outside of the minimum
- and maximum of `levels`.
+ Whether to clip values falling outside of `vmin` and `vmax`.
+ fair : bool, optional
+ Whether to use fair scaling. See `DivergingNorm`.
See also
--------
@@ -2587,7 +2591,21 @@ def __init__(self, levels, vmin=None, vmax=None, clip=False):
dest = np.linspace(0, 1, len(levels))
vmin = np.min(levels)
vmax = np.max(levels)
+ if vcenter is not None:
+ center = _interpolate_extrapolate_vector(vcenter, levels, dest)
+ idxs, = np.where(np.isclose(vcenter, levels))
+ if fair:
+ delta = center - 0.5
+ delta = max(-(dest[0] - delta), dest[-1] - delta - 1)
+ dest = (dest - center) / (1 + 2 * delta) + 0.5
+ elif idxs.size and idxs[0] > 0 and idxs[0] < len(levels) - 1:
+ dest1 = np.linspace(0, 0.5, idxs[0] + 1)
+ dest2 = np.linspace(0.5, 1, len(levels) - idxs[0])
+ dest = np.append(dest1, dest2[1:])
+ else:
+ raise ValueError(f'Center {vcenter} not in level list {levels}.')
super().__init__(vmin=vmin, vmax=vmax, clip=clip)
+ self.vcenter = vcenter # used for DiscreteNorm
self._x = self.boundaries = levels # 'boundaries' are used in PlotAxes
self._y = dest
@@ -2636,26 +2654,24 @@ class DivergingNorm(mcolors.Normalize):
def __str__(self):
return type(self).__name__ + f'(center={self.vcenter!r})'
- def __init__(
- self, vcenter=0, vmin=None, vmax=None, fair=True, clip=None
- ):
+ def __init__(self, vcenter=0, vmin=None, vmax=None, clip=None, fair=True):
"""
Parameters
----------
vcenter : float, default: 0
- The data value corresponding to the central colormap position.
+ The central data value.
vmin : float, optional
The minimum data value.
vmax : float, optional
The maximum data value.
+ clip : bool, optional
+ Whether to clip values falling outside of `vmin` and `vmax`.
fair : bool, optional
If ``True`` (default), the speeds of the color gradations on either side
of the center point are equal, but colormap colors may be omitted. If
``False``, all colormap colors are included, but the color gradations on
one side may be faster than the other side. ``False`` should be used with
great care, as it may result in a misleading interpretation of your data.
- clip : bool, optional
- Whether to clip values falling outside of `vmin` and `vmax`.
See also
--------
diff --git a/proplot/config.py b/proplot/config.py
index 79f9e6f7b..1a8ce8df6 100644
--- a/proplot/config.py
+++ b/proplot/config.py
@@ -790,7 +790,11 @@ def __enter__(self):
rc_new = context.rc_new # used for context-based _get_item_context
rc_old = context.rc_old # used to re-apply settings without copying whole dict
for key, value in kwargs.items():
- kw_proplot, kw_matplotlib = self._get_item_dicts(key, value)
+ try: # TODO: consider moving setting validation to .context()
+ kw_proplot, kw_matplotlib = self._get_item_dicts(key, value)
+ except ValueError as error:
+ self.__exit__()
+ raise error
for rc_dict, kw_new in zip(
(rc_proplot, rc_matplotlib),
(kw_proplot, kw_matplotlib),
@@ -1132,20 +1136,20 @@ def _get_label_props(self, native=True, **kwargs):
based on the context.
"""
# Get the label settings
- # NOTE: This permits passing arbitrary additional args to set_[xy]label()
+ # NOTE: This permits passing arbitrary additional args to set_[xy]label().
context = native or self._context_mode == 2
- kw = self.fill(
- {
- 'color': 'axes.labelcolor',
- 'weight': 'axes.labelweight',
- 'size': 'axes.labelsize',
- 'family': 'font.family',
- 'labelpad': 'axes.labelpad', # read by set_xlabel/set_ylabel
- },
- context=context,
- )
+ props = {
+ 'color': 'axes.labelcolor',
+ 'weight': 'axes.labelweight',
+ 'size': 'axes.labelsize',
+ 'family': 'font.family',
+ 'labelpad': 'axes.labelpad', # read by set_xlabel/set_ylabel
+ }
+ kw = self.fill(props, context=context)
for key, value in kwargs.items():
if value is not None: # allow e.g. color=None
+ if key in props:
+ value = self._validate_value(props[key], value)
kw[key] = value
return kw
diff --git a/proplot/internals/__init__.py b/proplot/internals/__init__.py
index 9be36e0f2..4d335ac86 100644
--- a/proplot/internals/__init__.py
+++ b/proplot/internals/__init__.py
@@ -269,6 +269,7 @@ def _pop_kwargs(kwargs, *keys, **aliases):
aliases.update({key: () for key in keys})
for key, aliases in aliases.items():
aliases = (aliases,) if isinstance(aliases, str) else aliases
+ aliases = tuple(alias for alias in aliases if alias != key) # prevent dev errs
opts = {key: kwargs.pop(key, None) for key in (key, *aliases)}
value = _not_none(**opts)
if value is not None:
diff --git a/proplot/internals/docstring.py b/proplot/internals/docstring.py
index aaf68c456..d1589d42e 100644
--- a/proplot/internals/docstring.py
+++ b/proplot/internals/docstring.py
@@ -109,12 +109,12 @@ def _concatenate_inherited(func, prepend_summary=False):
class _SnippetManager(dict):
"""
- A simple database for documentation snippets.
+ A simple database for handling documentation snippets.
"""
def __call__(self, obj):
"""
- Add snippets to the string or object using ``%(name)s`` substitution.
- This lets snippet keys have invalid variable names.
+ Add snippets to the string or object using ``%(name)s`` substitution. Here
+ ``%(name)s`` is used rather than ``.format`` to support invalid identifiers.
"""
if isinstance(obj, str):
obj %= self # add snippets to a string
@@ -126,8 +126,8 @@ def __call__(self, obj):
def __setitem__(self, key, value):
"""
- Populate input strings with other snippets. Developers should take
- care to import modules in the correct order.
+ Populate input strings with other snippets and strip newlines. Developers
+ should take care to import modules in the correct order.
"""
value = self(value)
value = value.strip('\n')
diff --git a/proplot/internals/inputs.py b/proplot/internals/inputs.py
index e5fbc0774..cbbb56235 100644
--- a/proplot/internals/inputs.py
+++ b/proplot/internals/inputs.py
@@ -54,16 +54,6 @@ def _load_objects():
# Type utilities
-def _is_array(data):
- """
- Test whether input is numpy array or pint quantity.
- """
- # NOTE: This is used in _iter_columns to identify 2D matrices that
- # should be iterated over and omit e.g. scalar marker size or marker color.
- _load_objects()
- return isinstance(data, ndarray) or ndarray is not Quantity and isinstance(data, Quantity) # noqa: E501
-
-
def _is_numeric(data):
"""
Test whether input is numeric array rather than datetime or strings.
@@ -141,12 +131,16 @@ def _to_numpy_array(data, strip_units=False):
elif isinstance(data, (DataFrame, Series, Index)):
data = data.values
if Quantity is not ndarray and isinstance(data, Quantity):
- if strip_units:
- return np.atleast_1d(data.magnitude)
- else:
- return np.atleast_1d(data.magnitude) * data.units
+ units = None if strip_units else data.units
+ data = np.atleast_1d(data.magnitude)
else:
- return np.atleast_1d(data) # natively preserves masked arrays
+ units = None
+ data = np.atleast_1d(data) # natively preserves masked arrays
+ if np.issubdtype(data.dtype, bool):
+ data = data.view(np.uint8)
+ if units is not None:
+ data = data * units
+ return data
def _to_masked_array(data, *, copy=False):
diff --git a/proplot/ticker.py b/proplot/ticker.py
index 7dd734890..4779f8dba 100644
--- a/proplot/ticker.py
+++ b/proplot/ticker.py
@@ -148,6 +148,7 @@ class DiscreteLocator(mticker.Locator):
'nbins': None,
'minor': False,
'steps': np.array([1, 2, 3, 4, 5, 6, 8, 10]),
+ 'vcenter': 0.0,
'min_n_ticks': 2
}
@@ -167,6 +168,8 @@ def __init__(self, locs, **kwargs):
steps : array-like of int, default: ``[1 2 3 4 5 6 8]``
Valid integer index steps when selecting from the tick list. Must fall
between 1 and 9. Powers of 10 of these step sizes will also be permitted.
+ vcenter : float, optional
+ The optional non-zero center of the original diverging normalizer.
min_n_ticks : int, default: 1
The minimum number of ticks to select. See also `nbins`.
"""
@@ -180,7 +183,7 @@ def __call__(self):
"""
return self.tick_values(None, None)
- def set_params(self, steps=None, nbins=None, minor=None, min_n_ticks=None):
+ def set_params(self, nbins=None, minor=None, steps=None, vcenter=None, min_n_ticks=None): # noqa: E501
"""
Set the parameters for this locator. See `DiscreteLocator` for details.
"""
@@ -197,6 +200,8 @@ def set_params(self, steps=None, nbins=None, minor=None, min_n_ticks=None):
self._nbins = nbins
if minor is not None:
self._minor = bool(minor) # needed to scale tick space
+ if vcenter is not None:
+ self._vcenter = vcenter
if min_n_ticks is not None:
self._min_n_ticks = int(min_n_ticks) # compare to MaxNLocator
@@ -208,7 +213,7 @@ def tick_values(self, vmin, vmax): # noqa: U100
# interval. Otherwise get misaligned major and minor tick steps.
# NOTE: This tries to select ticks that are integer steps away from zero (like
# AutoLocator). The list minimum is used if this fails (like FixedLocator)
- # NOTE: This avoids awkward steps like '7' or '13' that produce awkward
+ # NOTE: This avoids awkward steps like '7' or '13' that produce strange
# jumps and have no integer divisors (and therefore eliminate minor ticks)
# NOTE: We virtually always want to subsample the level list rather than
# using continuous minor locators (e.g. LogLocator or SymLogLocator) because
@@ -238,10 +243,12 @@ def tick_values(self, vmin, vmax): # noqa: U100
if step % i == 0:
step = step // i
break
+ locs = locs - self._vcenter
diff = np.abs(np.diff(locs[:step + 1:step]))
offset, = np.where(np.isclose(locs % diff if diff.size else 0.0, 0.0))
offset = offset[0] if offset.size else np.argmin(np.abs(locs))
- return locs[offset % step::step] # even multiples from zero or zero-close
+ locs = locs[offset % step::step] # even multiples from zero or zero-close
+ return locs + self._vcenter
class DegreeLocator(mticker.MaxNLocator):
diff --git a/requirements.txt b/requirements.txt
index d490aee8b..bd606d8e4 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1,3 @@
-# Just matplotlib :)
+# Just matplotlib and numpy :)
matplotlib>=3.0.0
+numpy
diff --git a/setup.cfg b/setup.cfg
index 8b972994c..8b422f223 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -22,11 +22,13 @@ classifiers =
project_urls =
Documentation = https://proplot.readthedocs.io
- Issue Tracker = https://github.com/lukelbd/proplot/issues
- Source Code = https://github.com/lukelbd/proplot
+ Issue Tracker = https://github.com/proplot-dev/proplot/issues
+ Source Code = https://github.com/proplot-dev/proplot
[options]
packages = proplot
-install_requires = matplotlib>=3.0.0,<3.6.0
+install_requires =
+ matplotlib>=3.0.0,<3.6.0
+ numpy
include_package_data = True
python_requires = >=3.6.0