Skip to content

Palettes (registry)#

The cleopatra.styling.palettes module is the single home for every colour ramp cleopatra knows about. One Palette record — a name, a kind, the colours, and a source (provenance) — describes each palette; one registry looks them up. Adding a colour family is therefore data, not a new code path.

A palette's PaletteKind decides how it becomes a colormap (and, downstream, its natural norm and legend): continuous kinds (sequential/diverging/cyclic) interpolate their colours perceptually in CIELAB (via cleopatra.styling.perceptual); a qualitative palette keeps its exact class swatches as a ListedColormap.

The kind is also what drives Palette.default_norm — pair it with to_colormap to get both the colours and the matching matplotlib norm in one step: a symmetric CenteredNorm for diverging, a BoundaryNorm over the class indices for qualitative, and a linear Normalize otherwise.

The built-in haze / CAMS-AOD / flame families live here and register at import, so the registry is populated whether you import cleopatra.styling.palettes or cleopatra.styling.colors. Their name → Colormap dicts (HAZE_COLORMAPS, CAMS_AOD_COLORMAPS, FLAME_COLORMAPS) are still importable from cleopatra.styling.colors for backward compatibility.

Curated palettes#

A small set of net-new palettes ships pre-registered — generated with this package's own tools (make_diverging / make_categorical), not vendored or copied from any other library:

Name Kind Notes
diverging_blue_red diverging blue ↔ red, lightness-balanced, neutral centre
diverging_purple_green diverging purple ↔ green
diverging_brown_teal diverging brown ↔ teal (moisture/precip anomalies)
category12 qualitative 12 maximally-distinguishable class colours
category20 qualitative 20 class colours (the first 12 match category12)

The diverging maps are built on demand from their two endpoints (so the centre lands exactly on the midpoint); the categorical swatches were generated once with make_categorical (greedy max-min in CIELAB) and frozen for a stable identity. Fetch any of them like the built-ins:

from cleopatra.styling.palettes import get_palette, available_palettes

available_palettes("diverging")     # ['diverging_blue_red', 'diverging_brown_teal', ...]
cmap = get_palette("category12").to_colormap()   # a 12-colour ListedColormap

PaletteKind#

cleopatra.styling.palettes.PaletteKind #

Bases: StrEnum

What a palette is for -- drives colormap construction and default norm.

Members are plain strings (StrEnum), so PaletteKind.SEQUENTIAL == "sequential" and construction is case-insensitive (PaletteKind("Diverging") is PaletteKind.DIVERGING).

Examples:

>>> from cleopatra.styling.palettes import PaletteKind
>>> PaletteKind.DIVERGING == "diverging"
True
>>> PaletteKind("Qualitative") is PaletteKind.QUALITATIVE
True
Source code in src/cleopatra/styling/palettes.py
class PaletteKind(StrEnum):
    """What a palette is for -- drives colormap construction and default norm.

    Members are plain strings (`StrEnum`), so `PaletteKind.SEQUENTIAL ==
    "sequential"` and construction is case-insensitive
    (`PaletteKind("Diverging") is PaletteKind.DIVERGING`).

    Examples:
        ```python
        >>> from cleopatra.styling.palettes import PaletteKind
        >>> PaletteKind.DIVERGING == "diverging"
        True
        >>> PaletteKind("Qualitative") is PaletteKind.QUALITATIVE
        True

        ```
    """

    SEQUENTIAL = "sequential"
    DIVERGING = "diverging"
    CYCLIC = "cyclic"
    QUALITATIVE = "qualitative"

    @classmethod
    def _missing_(cls, value):
        if isinstance(value, str):
            return cls.__members__.get(value.upper().replace("-", "_"))
        return None

Palette#

cleopatra.styling.palettes.Palette dataclass #

One colour palette: name, kind, colours, and provenance.

Parameters:

Name Type Description Default
name str

Unique registry key / colormap name.

required
kind PaletteKind

A PaletteKind (or its string value). Coerced to the enum.

required
colors tuple[str, ...]

The palette colours (hex strings or names) -- interpolation anchors for continuous kinds, exact class swatches for qualitative.

required
source str

Free-text provenance (e.g. "cleopatra", "magics"); metadata only. Defaults to "cleopatra".

'cleopatra'

Examples:

>>> from cleopatra.styling.palettes import Palette, PaletteKind
>>> Palette("d", "diverging", ("#762a83", "#f4f4f4", "#1b7837")).kind
<PaletteKind.DIVERGING: 'diverging'>
Source code in src/cleopatra/styling/palettes.py
@dataclass(frozen=True)
class Palette:
    """One colour palette: name, kind, colours, and provenance.

    Args:
        name: Unique registry key / colormap name.
        kind: A `PaletteKind` (or its string value). Coerced to the enum.
        colors: The palette colours (hex strings or names) -- interpolation
            anchors for continuous kinds, exact class swatches for
            `qualitative`.
        source: Free-text provenance (e.g. `"cleopatra"`, `"magics"`); metadata
            only. Defaults to `"cleopatra"`.

    Examples:
        ```python
        >>> from cleopatra.styling.palettes import Palette, PaletteKind
        >>> Palette("d", "diverging", ("#762a83", "#f4f4f4", "#1b7837")).kind
        <PaletteKind.DIVERGING: 'diverging'>

        ```
    """

    name: str
    kind: PaletteKind
    colors: tuple[str, ...]
    source: str = "cleopatra"

    def __post_init__(self):
        object.__setattr__(self, "kind", PaletteKind(self.kind))
        object.__setattr__(self, "colors", tuple(self.colors))

    def to_colormap(self, n: int = 256) -> Colormap:
        """Build a matplotlib `Colormap` from this palette.

        The colormap is constructed according to `kind`:

        - `qualitative`: the exact swatches as a `ListedColormap` (no interpolation).
        - `diverging`: `make_diverging` from the first and last colours, so the
            neutral centre lands exactly on the midpoint and the arms are
            lightness-balanced. A three-colour diverging palette uses its middle
            colour as the neutral centre; otherwise a near-white default is used.
            A palette with more than three colours has its interior colours
            ignored (only the first/last shape the ramp) and a warning is
            emitted.
        - `sequential` / `cyclic`: the colours interpolated perceptually (CIELAB).

        Args:
            n: Levels for continuous kinds. Defaults to 256. Ignored for
                `qualitative`.

        Returns:
            matplotlib.colors.Colormap: The colormap for this palette.
        """
        if self.kind is PaletteKind.QUALITATIVE:
            return ListedColormap(list(self.colors), name=self.name)
        if self.kind is PaletteKind.DIVERGING:
            if len(self.colors) > 3:
                warnings.warn(
                    f"diverging palette {self.name!r} has {len(self.colors)} "
                    f"colours; to_colormap builds the ramp from only the first "
                    f"and last (a 3-colour palette also uses its middle as the "
                    f"centre), so the interior colours are ignored.",
                    stacklevel=2,
                )
            center = self.colors[1] if len(self.colors) == 3 else "#f4f4f4"
            return make_diverging(
                self.colors[0], self.colors[-1], n, center=center, name=self.name
            )
        return perceptual_colormap(self.name, list(self.colors), n)

    def default_norm(
        self,
        data: np.ndarray | None = None,
        *,
        vmin: float | None = None,
        vmax: float | None = None,
        center: float | None = None,
    ) -> Normalize:
        """Return the matplotlib norm that suits this palette's `kind`.

        The companion to `to_colormap`: pairing a palette's colormap with the norm
        its kind implies gives a sensible default rendering without hand-picking a
        norm every time.

        - `sequential` / `cyclic`: a linear `Normalize` over `[vmin, vmax]`.
        - `diverging`: a `CenteredNorm` symmetric about `center` (default `0.0`),
            so the colormap's neutral midpoint lands on the centre and both ends are
            equidistant.
        - `qualitative`: a `BoundaryNorm` over the `N` discrete class indices, so an
            integer class `k` maps to swatch `k`.

        Concrete bounds are taken from `vmin`/`vmax` when given, else from `data`'s
        finite range; a missing bound left as `None` autoscales at draw time. `data`
        and the bounds are ignored for `qualitative`.

        Args:
            data: Optional array to auto-range from when `vmin`/`vmax` are omitted.
            vmin: Lower bound (continuous kinds).
            vmax: Upper bound (continuous kinds).
            center: Centre for a `diverging` norm. Defaults to `0.0`.

        Returns:
            matplotlib.colors.Normalize: The norm for this palette's kind.

        Examples:
            ```python
            >>> from cleopatra.styling.palettes import Palette
            >>> from matplotlib.colors import BoundaryNorm, CenteredNorm, Normalize
            >>> seq = Palette("s", "sequential", ("#ffffff", "#000000"))
            >>> type(seq.default_norm(vmin=0, vmax=10)) is Normalize
            True
            >>> div = Palette("d", "diverging", ("#0000ff", "#ffffff", "#ff0000"))
            >>> isinstance(div.default_norm(vmin=-5, vmax=8), CenteredNorm)
            True
            >>> Palette("q", "qualitative", ("#f00", "#0f0", "#00f")).default_norm().Ncmap
            3

            ```
        """
        if self.kind is PaletteKind.QUALITATIVE:
            n = len(self.colors)
            return BoundaryNorm(np.arange(n + 1) - 0.5, n)
        vmin, vmax = _auto_bounds(data, vmin, vmax)
        if self.kind is PaletteKind.DIVERGING:
            return _centered_norm(vmin, vmax, center)
        return Normalize(vmin=vmin, vmax=vmax)

default_norm(data=None, *, vmin=None, vmax=None, center=None) #

Return the matplotlib norm that suits this palette's kind.

The companion to to_colormap: pairing a palette's colormap with the norm its kind implies gives a sensible default rendering without hand-picking a norm every time.

  • sequential / cyclic: a linear Normalize over [vmin, vmax].
  • diverging: a CenteredNorm symmetric about center (default 0.0), so the colormap's neutral midpoint lands on the centre and both ends are equidistant.
  • qualitative: a BoundaryNorm over the N discrete class indices, so an integer class k maps to swatch k.

Concrete bounds are taken from vmin/vmax when given, else from data's finite range; a missing bound left as None autoscales at draw time. data and the bounds are ignored for qualitative.

Parameters:

Name Type Description Default
data ndarray | None

Optional array to auto-range from when vmin/vmax are omitted.

None
vmin float | None

Lower bound (continuous kinds).

None
vmax float | None

Upper bound (continuous kinds).

None
center float | None

Centre for a diverging norm. Defaults to 0.0.

None

Returns:

Type Description
Normalize

matplotlib.colors.Normalize: The norm for this palette's kind.

Examples:

>>> from cleopatra.styling.palettes import Palette
>>> from matplotlib.colors import BoundaryNorm, CenteredNorm, Normalize
>>> seq = Palette("s", "sequential", ("#ffffff", "#000000"))
>>> type(seq.default_norm(vmin=0, vmax=10)) is Normalize
True
>>> div = Palette("d", "diverging", ("#0000ff", "#ffffff", "#ff0000"))
>>> isinstance(div.default_norm(vmin=-5, vmax=8), CenteredNorm)
True
>>> Palette("q", "qualitative", ("#f00", "#0f0", "#00f")).default_norm().Ncmap
3
Source code in src/cleopatra/styling/palettes.py
def default_norm(
    self,
    data: np.ndarray | None = None,
    *,
    vmin: float | None = None,
    vmax: float | None = None,
    center: float | None = None,
) -> Normalize:
    """Return the matplotlib norm that suits this palette's `kind`.

    The companion to `to_colormap`: pairing a palette's colormap with the norm
    its kind implies gives a sensible default rendering without hand-picking a
    norm every time.

    - `sequential` / `cyclic`: a linear `Normalize` over `[vmin, vmax]`.
    - `diverging`: a `CenteredNorm` symmetric about `center` (default `0.0`),
        so the colormap's neutral midpoint lands on the centre and both ends are
        equidistant.
    - `qualitative`: a `BoundaryNorm` over the `N` discrete class indices, so an
        integer class `k` maps to swatch `k`.

    Concrete bounds are taken from `vmin`/`vmax` when given, else from `data`'s
    finite range; a missing bound left as `None` autoscales at draw time. `data`
    and the bounds are ignored for `qualitative`.

    Args:
        data: Optional array to auto-range from when `vmin`/`vmax` are omitted.
        vmin: Lower bound (continuous kinds).
        vmax: Upper bound (continuous kinds).
        center: Centre for a `diverging` norm. Defaults to `0.0`.

    Returns:
        matplotlib.colors.Normalize: The norm for this palette's kind.

    Examples:
        ```python
        >>> from cleopatra.styling.palettes import Palette
        >>> from matplotlib.colors import BoundaryNorm, CenteredNorm, Normalize
        >>> seq = Palette("s", "sequential", ("#ffffff", "#000000"))
        >>> type(seq.default_norm(vmin=0, vmax=10)) is Normalize
        True
        >>> div = Palette("d", "diverging", ("#0000ff", "#ffffff", "#ff0000"))
        >>> isinstance(div.default_norm(vmin=-5, vmax=8), CenteredNorm)
        True
        >>> Palette("q", "qualitative", ("#f00", "#0f0", "#00f")).default_norm().Ncmap
        3

        ```
    """
    if self.kind is PaletteKind.QUALITATIVE:
        n = len(self.colors)
        return BoundaryNorm(np.arange(n + 1) - 0.5, n)
    vmin, vmax = _auto_bounds(data, vmin, vmax)
    if self.kind is PaletteKind.DIVERGING:
        return _centered_norm(vmin, vmax, center)
    return Normalize(vmin=vmin, vmax=vmax)

to_colormap(n=256) #

Build a matplotlib Colormap from this palette.

The colormap is constructed according to kind:

  • qualitative: the exact swatches as a ListedColormap (no interpolation).
  • diverging: make_diverging from the first and last colours, so the neutral centre lands exactly on the midpoint and the arms are lightness-balanced. A three-colour diverging palette uses its middle colour as the neutral centre; otherwise a near-white default is used. A palette with more than three colours has its interior colours ignored (only the first/last shape the ramp) and a warning is emitted.
  • sequential / cyclic: the colours interpolated perceptually (CIELAB).

Parameters:

Name Type Description Default
n int

Levels for continuous kinds. Defaults to 256. Ignored for qualitative.

256

Returns:

Type Description
Colormap

matplotlib.colors.Colormap: The colormap for this palette.

Source code in src/cleopatra/styling/palettes.py
def to_colormap(self, n: int = 256) -> Colormap:
    """Build a matplotlib `Colormap` from this palette.

    The colormap is constructed according to `kind`:

    - `qualitative`: the exact swatches as a `ListedColormap` (no interpolation).
    - `diverging`: `make_diverging` from the first and last colours, so the
        neutral centre lands exactly on the midpoint and the arms are
        lightness-balanced. A three-colour diverging palette uses its middle
        colour as the neutral centre; otherwise a near-white default is used.
        A palette with more than three colours has its interior colours
        ignored (only the first/last shape the ramp) and a warning is
        emitted.
    - `sequential` / `cyclic`: the colours interpolated perceptually (CIELAB).

    Args:
        n: Levels for continuous kinds. Defaults to 256. Ignored for
            `qualitative`.

    Returns:
        matplotlib.colors.Colormap: The colormap for this palette.
    """
    if self.kind is PaletteKind.QUALITATIVE:
        return ListedColormap(list(self.colors), name=self.name)
    if self.kind is PaletteKind.DIVERGING:
        if len(self.colors) > 3:
            warnings.warn(
                f"diverging palette {self.name!r} has {len(self.colors)} "
                f"colours; to_colormap builds the ramp from only the first "
                f"and last (a 3-colour palette also uses its middle as the "
                f"centre), so the interior colours are ignored.",
                stacklevel=2,
            )
        center = self.colors[1] if len(self.colors) == 3 else "#f4f4f4"
        return make_diverging(
            self.colors[0], self.colors[-1], n, center=center, name=self.name
        )
    return perceptual_colormap(self.name, list(self.colors), n)

Registry#

Register a palette, look one up, or list what's available (optionally filtered by kind). PALETTES is the underlying name → Palette mapping.

cleopatra.styling.palettes.register(palette) #

Add (or replace) a palette in the registry and return it.

Parameters:

Name Type Description Default
palette Palette

The Palette to register under its name.

required

Returns:

Name Type Description
Palette Palette

The same palette, for convenient chaining.

Source code in src/cleopatra/styling/palettes.py
def register(palette: Palette) -> Palette:
    """Add (or replace) a palette in the registry and return it.

    Args:
        palette: The `Palette` to register under its `name`.

    Returns:
        Palette: The same palette, for convenient chaining.
    """
    PALETTES[palette.name] = palette
    return palette

cleopatra.styling.palettes.get_palette(name) #

Look up a registered palette by name.

Parameters:

Name Type Description Default
name str

The palette's registry key.

required

Returns:

Name Type Description
Palette Palette

The registered palette.

Raises:

Type Description
KeyError

If no palette is registered under name.

Source code in src/cleopatra/styling/palettes.py
def get_palette(name: str) -> Palette:
    """Look up a registered palette by name.

    Args:
        name: The palette's registry key.

    Returns:
        Palette: The registered palette.

    Raises:
        KeyError: If no palette is registered under `name`.
    """
    try:
        return PALETTES[name]
    except KeyError:
        raise KeyError(
            f"unknown palette {name!r}; registered: {available_palettes()}"
        ) from None

cleopatra.styling.palettes.available_palettes(kind=None) #

List registered palette names, optionally filtered by kind.

Parameters:

Name Type Description Default
kind PaletteKind | str | None

If given, return only palettes of this PaletteKind (or its string value). Defaults to None (all palettes).

None

Returns:

Type Description
list[str]

list[str]: Sorted palette names.

Examples:

>>> from cleopatra.styling.palettes import available_palettes
>>> isinstance(available_palettes("sequential"), list)
True
Source code in src/cleopatra/styling/palettes.py
def available_palettes(kind: PaletteKind | str | None = None) -> list[str]:
    """List registered palette names, optionally filtered by kind.

    Args:
        kind: If given, return only palettes of this `PaletteKind` (or its
            string value). Defaults to `None` (all palettes).

    Returns:
        list[str]: Sorted palette names.

    Examples:
        ```python
        >>> from cleopatra.styling.palettes import available_palettes
        >>> isinstance(available_palettes("sequential"), list)
        True

        ```
    """
    if kind is None:
        return sorted(PALETTES)
    kind = PaletteKind(kind)
    return sorted(n for n, p in PALETTES.items() if p.kind is kind)

Preview#

Browse the registry as a grouped swatch grid — a quick way to see every palette (or just one kind, or an explicit list) at a glance. Returns the matplotlib Figure.

The registered cleopatra palettes, grouped by kind

from cleopatra.styling.palettes import preview_palettes

fig = preview_palettes()                 # all registered palettes, grouped by kind
fig = preview_palettes("diverging")      # just the diverging maps
fig.savefig("palettes.png", dpi=130, bbox_inches="tight")

cleopatra.styling.palettes.preview_palettes(kind=None, *, names=None, n=256) #

Render registered palettes as a grouped swatch grid.

Each palette is drawn as a horizontal strip of its colormap -- continuous kinds show a smooth ramp, qualitative shows its discrete class swatches -- labelled with its name and source, under a bold heading per PaletteKind. A quick way to browse the registry (including anything you have registered).

Parameters:

Name Type Description Default
kind PaletteKind | str | None

Show only this kind (a PaletteKind or its string value). Ignored when names is given. Defaults to None (every registered palette).

None
names Sequence[str] | None

An explicit list of palette names to show instead of filtering by kind; still grouped by kind in the grid.

None
n int

Levels used to build each continuous colormap. Defaults to 256.

256

Returns:

Type Description
Figure

matplotlib.figure.Figure: The swatch-grid figure (save or show it

Figure

yourself; cleopatra never changes the active backend).

Raises:

Type Description
KeyError

If a name in names is not registered.

ValueError

If no palettes match the selection.

Examples:

>>> import matplotlib
>>> matplotlib.use("Agg")
>>> import matplotlib.pyplot as plt
>>> from cleopatra.styling.palettes import preview_palettes
>>> fig = preview_palettes("diverging")
>>> len(fig.axes) > 0
True
>>> plt.close(fig)
Source code in src/cleopatra/styling/palettes.py
def preview_palettes(
    kind: PaletteKind | str | None = None,
    *,
    names: Sequence[str] | None = None,
    n: int = 256,
) -> Figure:
    """Render registered palettes as a grouped swatch grid.

    Each palette is drawn as a horizontal strip of its colormap -- continuous
    kinds show a smooth ramp, `qualitative` shows its discrete class swatches --
    labelled with its name and `source`, under a bold heading per `PaletteKind`.
    A quick way to browse the registry (including anything you have `register`ed).

    Args:
        kind: Show only this kind (a `PaletteKind` or its string value). Ignored
            when `names` is given. Defaults to `None` (every registered palette).
        names: An explicit list of palette names to show instead of filtering by
            `kind`; still grouped by kind in the grid.
        n: Levels used to build each continuous colormap. Defaults to 256.

    Returns:
        matplotlib.figure.Figure: The swatch-grid figure (save or show it
        yourself; cleopatra never changes the active backend).

    Raises:
        KeyError: If a name in `names` is not registered.
        ValueError: If no palettes match the selection.

    Examples:
        ```python
        >>> import matplotlib
        >>> matplotlib.use("Agg")
        >>> import matplotlib.pyplot as plt
        >>> from cleopatra.styling.palettes import preview_palettes
        >>> fig = preview_palettes("diverging")
        >>> len(fig.axes) > 0
        True
        >>> plt.close(fig)

        ```
    """
    if names is not None:
        selected = [get_palette(name) for name in names]
    elif kind is not None:
        selected = [PALETTES[name] for name in available_palettes(kind)]
    else:
        selected = [PALETTES[name] for name in available_palettes()]
    if not selected:
        raise ValueError("no palettes to preview for the given selection")

    # A header row per non-empty kind group, then that group's palette rows.
    rows: list[tuple[str, object]] = []
    for group in _KIND_ORDER:
        members = [p for p in selected if p.kind is group]
        if not members:
            continue
        rows.append(("header", f"{group.value.title()}  ({len(members)})"))
        rows.extend(("palette", p) for p in members)

    grad = np.linspace(0, 1, n).reshape(1, -1)
    fig, axes = plt.subplots(len(rows), 1, figsize=(8.5, 0.42 * len(rows)))
    axes = np.atleast_1d(axes)
    fig.subplots_adjust(left=0.28, right=0.985, top=0.99, bottom=0.02, hspace=0.55)
    for ax, (row_type, payload) in zip(axes, rows):
        ax.set_xticks([])
        ax.set_yticks([])
        if row_type == "header":
            for spine in ax.spines.values():
                spine.set_visible(False)
            ax.text(0.0, 0.5, payload, fontsize=12, fontweight="bold",
                    va="center", ha="left", transform=ax.transAxes)
            continue
        ax.imshow(grad, aspect="auto", cmap=payload.to_colormap(n), extent=(0, 1, 0, 1))
        for spine in ax.spines.values():
            spine.set_edgecolor("0.6")
            spine.set_linewidth(0.5)
        ax.text(-0.02, 0.5, payload.name, fontsize=9, family="monospace",
                va="center", ha="right", transform=ax.transAxes)
        ax.text(1.008, 0.5, payload.source, fontsize=6.5, color="0.45",
                va="center", ha="left", transform=ax.transAxes)
    return fig

Examples#

Register and use a palette#

from cleopatra.styling.palettes import Palette, PaletteKind, register, get_palette, available_palettes

# register a diverging palette (interpolated perceptually when built)
register(Palette("temp_anomaly", PaletteKind.DIVERGING, ("#762a83", "#f4f4f4", "#1b7837")))

p = get_palette("temp_anomaly")
cmap = p.to_colormap()                # a LinearSegmentedColormap
norm = p.default_norm(vmin=-4, vmax=6)  # a CenteredNorm symmetric about 0
print(available_palettes("diverging"))  # ['temp_anomaly', ...]
# ... then: ax.imshow(data, cmap=cmap, norm=norm)

Discover the built-in families#

from cleopatra.styling.palettes import available_palettes, get_palette

print(available_palettes("sequential"))            # includes 'haze_dust', 'cams_aod_blue_red', ...
print(get_palette("cams_aod_blue_red").source)     # 'magics'