Skip to content

Themes

theme

Themes: the single bundle of style choices a figure is drawn with.

A Theme is an immutable dataclass holding the genuinely style-varying knobs (palette, type scale, spine/grid/background, default line weights). There is no global "current theme" - a theme is passed to pyplotrs.subplots (or pyplotrs.Figure) and flows to its axes, matching the library's no-global-state philosophy.

Built-in presets are module attributes, so pyplotrs.themes.dark etc. work::

fig, ax = pyplotrs.subplots(theme=pyplotrs.themes.dark)

Derive your own with Theme.with_::

mine = pyplotrs.themes.default.with_(grid=True, line_width=2.0)

Theme dataclass

Theme(
    palette: tuple[RGBA, ...] = _OKABE_ITO,
    text_color: RGBA = (0, 0, 0, 255),
    spine_color: RGBA = (0, 0, 0, 255),
    spines: tuple[str, ...] = ("left", "bottom"),
    spine_width: float = 0.8,
    spine_join: str = "miter",
    tick_label_size: float = 8.0,
    axis_label_size: float = 9.0,
    title_size: float = 10.0,
    suptitle_size: float = 11.0,
    legend_size: float = 8.0,
    title_weight: str = "normal",
    suptitle_weight: str = "normal",
    axis_label_weight: str = "normal",
    line_width: float = 1.2,
    grid: bool = False,
    grid_color: RGBA = (221, 221, 221, 255),
    grid_width: float = 0.6,
    figure_facecolor: RGBA | None = None,
    axes_facecolor: RGBA | None = None,
    legend_facecolor: RGBA = (255, 255, 255, 255),
    legend_edgecolor: RGBA = (179, 179, 179, 255),
)

An immutable set of style choices. Use with_ to derive variants.

separator_color property

separator_color: RGBA

The hairline drawn between adjacent filled shapes - histogram bins, pie wedges.

The intent is "the plot background showing through", so it follows axes_facecolor, then the page behind it, and only reaches white when neither is stated. Hardcoding white here is what made histogram bins grow white outlines under a dark theme; stopping the chain at axes_facecolor brought the same outlines back for a theme that darkens the page and lets it show through the plot area - which is exactly what dark does.

__repr__

__repr__() -> str

A one-line summary, not the whole field list.

The dataclass-generated repr is ~1000 characters of raw RGBA tuples - it fills a notebook cell and answers nothing. What a reader wants from a theme at a glance is which one it is and the two things most likely to have been derived: the base font size and the palette length.

Source code in python/pyplotrs/theme.py
def __repr__(self) -> str:
    """A one-line summary, not the whole field list.

    The dataclass-generated repr is ~1000 characters of raw RGBA tuples -
    it fills a notebook cell and answers nothing. What a reader wants from
    a theme at a glance is which one it is and the two things most likely
    to have been derived: the base font size and the palette length.
    """
    for name, preset in _PRESETS.items():
        if self == preset:
            return f"<Theme {name!r}>"
    return (f"<Theme derived: title_size={self.title_size:g}, "
            f"{len(self.palette)} palette colors>")

resolve

resolve(color) -> RGBA

Resolve a color spec against this theme's palette (see parse_color).

Source code in python/pyplotrs/theme.py
def resolve(self, color) -> RGBA:
    """Resolve a color spec against this theme's palette (see
    ``parse_color``)."""
    return parse_color(color, self.palette)

with_

with_(**changes) -> 'Theme'

A copy of this theme with changes applied.

Source code in python/pyplotrs/theme.py
def with_(self, **changes) -> "Theme":
    """A copy of this theme with ``changes`` applied."""
    return replace(self, **changes)

parse_color

parse_color(color, palette: tuple[RGBA, ...]) -> RGBA

Resolve a color spec to RGBA against palette.

Accepts, in order:

  • "C0".."Cn" - index palette (cycling). This is the one place color strings are interpreted, so "C3" means this theme's fourth color, not a fixed global one.
  • "#rgb", "#rgba", "#rrggbb", "#rrggbbaa" hex.
  • A CSS color name ("red", "steelblue"), case-insensitive.
  • A 3- or 4-component tuple. All-float tuples in 0-1 are treated as matplotlib-style fractions and scaled to bytes; anything else is taken as literal 0-255 bytes. Alpha follows the same convention as its tuple.
Source code in python/pyplotrs/theme.py
def parse_color(color, palette: tuple[RGBA, ...]) -> RGBA:
    """Resolve a color spec to RGBA against ``palette``.

    Accepts, in order:

    * ``"C0".."Cn"`` - index ``palette`` (cycling). This is the one place color
      strings are interpreted, so ``"C3"`` means *this theme's* fourth color,
      not a fixed global one.
    * ``"#rgb"``, ``"#rgba"``, ``"#rrggbb"``, ``"#rrggbbaa"`` hex.
    * A CSS color name (``"red"``, ``"steelblue"``), case-insensitive.
    * A 3- or 4-component tuple. **All-float tuples in 0-1 are treated as
      matplotlib-style fractions** and scaled to bytes; anything else is taken as
      literal 0-255 bytes. Alpha follows the same convention as its tuple.
    """
    if isinstance(color, str):
        if len(color) >= 2 and color[0] == "C" and color[1:].isdigit():
            return palette[int(color[1:]) % len(palette)]
        if color.startswith("#"):
            parsed = _parse_hex(color)
            if parsed is not None:
                return parsed
            raise ValueError(f"malformed hex color {color!r}")
        named = _CSS_COLORS.get(color.lower())
        if named is not None:
            return (*named, 255)
        raise ValueError(f"unknown color string {color!r}")

    values = tuple(color)
    if len(values) not in (3, 4):
        raise ValueError(f"color tuple must have 3 or 4 components, got {len(values)}")
    if _is_unit_float_tuple(values):
        scaled = [_channel(v * 255.0) for v in values]
        return (scaled[0], scaled[1], scaled[2], scaled[3] if len(scaled) == 4 else 255)
    r, g, b = (_channel(v) for v in values[:3])
    return (r, g, b, _channel(values[3]) if len(values) == 4 else 255)

get

get(theme) -> Theme

Coerce theme (a Theme, a preset name, or None) to a Theme. None -> default.

Source code in python/pyplotrs/theme.py
def get(theme) -> Theme:
    """Coerce ``theme`` (a ``Theme``, a preset name, or ``None``) to a
    ``Theme``. ``None`` -> ``default``."""
    if theme is None:
        return default
    if isinstance(theme, Theme):
        return theme
    if isinstance(theme, str) and theme in _PRESETS:
        return _PRESETS[theme]
    raise ValueError(f"unknown theme {theme!r}; choose from {sorted(_PRESETS)}")