Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions docs/colormaps.md
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,30 @@ external visualization libraries. To that end, `cmap.Colormap` provides
Returns an instance of
[`pyqtgraph.ColorMap`](https://pyqtgraph.readthedocs.io/en/latest/api_reference/colormap.html#pyqtgraph.ColorMap)

### Using cmap names as matplotlib strings

`cmap` does not register its colormaps with matplotlib on import. To use catalog
names as plain strings anywhere matplotlib accepts a colormap name, call [`cmap.register_mpl`][]:

```python
import cmap
import matplotlib.pyplot as plt

cmap.register_mpl("tol:*", "batlow") # names or fnmatch patterns
plt.imshow(data, cmap="tol:sunset")
plt.imshow(data, cmap="crameri:batlow_r") # reversed versions are registered too
```

Colormaps are registered under their canonical, fully-qualified name (lowercase, with
`_` in place of `-` or spaces: `Colormap(name).name`), so they never collide with
matplotlib's builtin names.

!!!note "For library authors"
Please don't call `cmap.register_mpl("*")` on behalf of your users: it adds
1300+ entries to matplotlib's global registry, visible to all code in the
session, and takes a second or more. Prefer registering the specific name(s)
you need.

## Usage with pydantic

`Colormap` can be used as a field type in [pydantic](https://docs.pydantic.dev/)
Expand Down
3 changes: 2 additions & 1 deletion src/cmap/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

from ._color import HSLA, HSVA, RGBA, RGBA8, Color, ColorLike
from ._colormap import Colormap, ColormapLike, ColorStop, ColorStops
from ._util import to_mpl
from ._util import register_mpl, to_mpl

if TYPE_CHECKING:
from collections.abc import Iterator, Mapping
Expand Down Expand Up @@ -74,6 +74,7 @@ def resolve(self, name: str) -> str:
"ColorStop",
"Catalog",
"CatalogItem",
"register_mpl",
"to_mpl",
"RGBA",
"RGBA8",
Expand Down
63 changes: 63 additions & 0 deletions src/cmap/_util.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import sys
from fnmatch import fnmatchcase
from typing import TYPE_CHECKING, TypedDict, cast

import numpy as np
Expand Down Expand Up @@ -593,6 +594,68 @@ def to_mpl(
return cm.to_matplotlib(N=N, gamma=gamma)


def register_mpl(*patterns: str) -> list[str]:
"""Register colormaps from the cmap catalog with `matplotlib.colormaps`.

Call this explicitly to make catalog names usable as strings anywhere
matplotlib accepts a colormap name.

Colormaps are registered under their canonical, fully-qualified cmap name
(lowercase, with `_` instead of `-` or spaces; see `Colormap(name).name`), along
with a reversed `_r` version. Short names like "viridis" are never registered, so
as not to collide with matplotlib's colormaps. Names that are already registered
are left untouched, so it is safe to call this multiple times.

Parameters
----------
*patterns : str
Colormap names or `fnmatch`-style patterns (e.g. `"tol:*"`) to register.
Any name accepted by `Colormap` works (including short names and aliases).
`"*"` registers the entire catalog, but be aware that matplotlib's
registration time grows with the size of its registry: this may take many
seconds.

Returns
-------
list[str]
The canonical names of the matched colormaps.

Examples
--------
>>> import cmap
>>> cmap.register_mpl("tol:*", "batlow") # doctest: +SKIP
>>> plt.imshow(data, cmap="crameri:batlow_r") # doctest: +SKIP
"""
import matplotlib as mpl

from cmap import Colormap

if not patterns:
raise TypeError("register_mpl() requires at least one name or pattern.")
catalog = Colormap.catalog()
norm = catalog._norm_name
resolved: set[str] = set()
for pattern in patterns:
matches = {
catalog[catalog.resolve(key)].qualified_name
for key in catalog
if fnmatchcase(norm(key), norm(pattern))
}
if not matches:
raise KeyError(f"No colormaps in the cmap catalog match {pattern!r}.")
resolved |= matches

# look up `mpl.colormaps` at call time: downstream libraries (e.g. UltraPlot)
# may have replaced it with their own registry.
existing = set(mpl.colormaps)
for name in resolved - existing:
mpl_cm = Colormap(name).to_mpl()
for cm in (mpl_cm, mpl_cm.reversed()):
if cm.name not in existing:
mpl.colormaps.register(cm)
return sorted(resolved)


if __name__ == "__main__": # pragma: no cover
import matplotlib.pyplot as plt

Expand Down
25 changes: 25 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
except ImportError:
MplFigure = None

import cmap
from cmap import Colormap, _util

CMAP_NAME = "viridis"
Expand Down Expand Up @@ -78,3 +79,27 @@ def test_report() -> None:

report2 = _util.report(Colormap("red"))
assert isinstance(report2, dict)


def test_register_mpl() -> None:
mpl = pytest.importorskip("matplotlib")
before = set(mpl.colormaps)
try:
names = cmap.register_mpl("tol:*", "batlow", "colorbrewer:Blues")
assert "tol:bright" in names
assert {"crameri:batlow", "colorbrewer:blues"} <= set(names)
for name in names:
assert mpl.colormaps[name].name == name
assert f"{name}_r" in mpl.colormaps
assert "batlow" not in mpl.colormaps

# idempotent
added = set(mpl.colormaps) - before
assert cmap.register_mpl("tol:*") == [n for n in names if n.startswith("tol")]
assert set(mpl.colormaps) - before == added

with pytest.raises(KeyError, match="No colormaps"):
cmap.register_mpl("nope:*")
finally:
for name in set(mpl.colormaps) - before:
mpl.colormaps.unregister(name)
Loading