Skip to content

Utilities API

The rwthplots.utils module provides four helper functions, all re-exported from the top-level rwthplots namespace:

import rwthplots

rwthplots.save_figure(...)
rwthplots.context(...)
rwthplots.pick_colors(...)
rwthplots.check_accessibility(...)

Figure export

rwthplots.utils.save_figure

save_figure(fig, path, formats=('pdf', 'png'), dpi=None, **savefig_kwargs)

Save a matplotlib figure to multiple file formats in one call.

Parameters

fig : matplotlib.figure.Figure path : str or pathlib.Path Output path without file extension. Parent directories are created automatically. formats : sequence of str File formats to write (e.g. ["pdf", "png", "svg"]). dpi : int or None DPI override for raster formats. Ignored for vector formats (pdf, svg, eps, pgf, ps). If None, the figure's current DPI is used. **savefig_kwargs Forwarded to :meth:~matplotlib.figure.Figure.savefig.

Examples

save_figure(fig, "results/my_plot", formats=["pdf", "png"], dpi=300)

Source code in src/rwthplots/utils.py
def save_figure(fig, path, formats=("pdf", "png"), dpi=None, **savefig_kwargs):
    """
    Save a matplotlib figure to multiple file formats in one call.

    Parameters
    ----------
    fig : matplotlib.figure.Figure
    path : str or pathlib.Path
        Output path *without* file extension.  Parent directories are created
        automatically.
    formats : sequence of str
        File formats to write (e.g. ``["pdf", "png", "svg"]``).
    dpi : int or None
        DPI override for raster formats.  Ignored for vector formats
        (pdf, svg, eps, pgf, ps).  If *None*, the figure's current DPI is used.
    **savefig_kwargs
        Forwarded to :meth:`~matplotlib.figure.Figure.savefig`.

    Examples
    --------
    save_figure(fig, "results/my_plot", formats=["pdf", "png"], dpi=300)
    """
    path = Path(path)
    path.parent.mkdir(parents=True, exist_ok=True)
    for fmt in formats:
        kw = dict(savefig_kwargs)
        kw.setdefault("bbox_inches", "tight")
        if dpi is not None and fmt.lower() not in _VECTOR_FORMATS:
            kw.setdefault("dpi", dpi)
        fig.savefig(path.with_suffix(f".{fmt}"), format=fmt, **kw)

Style context

rwthplots.utils.context

context(*styles: str)

Return a :func:matplotlib.pyplot.style.context manager with automatic rwthplots.styles. prefix expansion.

Short names (without rwthplots.styles.) are expanded automatically:

  • 'rwth-latex''rwthplots.styles.rwth-latex'
  • 'color.blue''rwthplots.styles.color.blue'
  • 'misc.grid''rwthplots.styles.misc.grid'
  • 'size.ieee-column''rwthplots.styles.size.ieee-column'

Full names (already starting with 'rwthplots.') are passed through.

Examples

with rwthplots.context('rwth-latex', 'color.blue', 'misc.grid'): fig, ax = plt.subplots() ax.plot(x, y)

Source code in src/rwthplots/utils.py
def context(*styles: str):
    """
    Return a :func:`matplotlib.pyplot.style.context` manager with automatic
    ``rwthplots.styles.`` prefix expansion.

    Short names (without ``rwthplots.styles.``) are expanded automatically:

    * ``'rwth-latex'``        → ``'rwthplots.styles.rwth-latex'``
    * ``'color.blue'``        → ``'rwthplots.styles.color.blue'``
    * ``'misc.grid'``         → ``'rwthplots.styles.misc.grid'``
    * ``'size.ieee-column'``  → ``'rwthplots.styles.size.ieee-column'``

    Full names (already starting with ``'rwthplots.'``) are passed through.

    Examples
    --------
    with rwthplots.context('rwth-latex', 'color.blue', 'misc.grid'):
        fig, ax = plt.subplots()
        ax.plot(x, y)
    """
    import matplotlib.pyplot as plt
    resolved = [
        s if s.startswith("rwthplots.") else f"rwthplots.styles.{s}"
        for s in styles
    ]
    return plt.style.context(resolved)

Colour selection

rwthplots.utils.pick_colors

pick_colors(n: int, colorset: str = 'rwth_100') -> list[str]

Return the n most perceptually distinct RWTH colours.

Uses a greedy farthest-point algorithm in CIELAB space. The first colour is always RWTH blue; each subsequent pick maximises the minimum CIE 1976 delta-E to the already-selected set.

Parameters

n : int Number of colours to return (1–13). colorset : str RWTH tint level to sample from.

Returns

colors : list of str '#RRGGBB' hex strings in selection order.

Examples

pick_colors(4)

['#00549F', '#FFED00', '#57AB27', '#E30066'] (approximately)

Source code in src/rwthplots/utils.py
def pick_colors(n: int, colorset: str = "rwth_100") -> list[str]:
    """
    Return the *n* most perceptually distinct RWTH colours.

    Uses a greedy farthest-point algorithm in CIELAB space.  The first
    colour is always RWTH blue; each subsequent pick maximises the minimum
    CIE 1976 delta-E to the already-selected set.

    Parameters
    ----------
    n : int
        Number of colours to return (1–13).
    colorset : str
        RWTH tint level to sample from.

    Returns
    -------
    colors : list of str
        ``'#RRGGBB'`` hex strings in selection order.

    Examples
    --------
    pick_colors(4)
    # ['#00549F', '#FFED00', '#57AB27', '#E30066']  (approximately)
    """
    from .cmap import rwth_cset

    all_colors = list(rwth_cset(colorset))
    total = len(all_colors)

    if not (1 <= n <= total):
        raise ValueError(f"n must be between 1 and {total}, got {n}.")

    lab = _linear_rgb_to_lab(_hex_colors_to_linear(all_colors))

    selected: list[int] = [0]  # start with blue
    remaining = list(range(1, total))
    while len(selected) < n:
        best_idx = max(
            remaining,
            key=lambda i: min(
                float(np.sqrt(np.sum((lab[i] - lab[j]) ** 2)))
                for j in selected
            ),
        )
        selected.append(best_idx)
        remaining.remove(best_idx)

    return [all_colors[i] for i in selected]

Accessibility

rwthplots.utils.check_accessibility

check_accessibility(colors: Sequence[str], types: Sequence[str] = ('deuteranopia', 'protanopia', 'tritanopia'), threshold: float = 10.0) -> list[dict]

Simulate colour vision deficiencies and report pairs that would be confused.

CVD simulation uses the Viénot-Brettel-Mollon model (1997/1999). Perceptual distance is CIE 1976 delta-E in CIELAB.

Parameters

colors : sequence of str '#RRGGBB' hex colour strings to evaluate. types : sequence of str CVD types to simulate. Supported: 'deuteranopia', 'protanopia', 'tritanopia'. threshold : float Delta-E below which two simulated colours are flagged as confusable. Default 10 (clearly noticeable); use 20 for a stricter check.

Returns

issues : list of dict Each entry: {'cvd_type', 'color_a', 'color_b', 'delta_e'}. Empty list means the palette passes.

Examples

from rwthplots.cmap import rwth_cset issues = check_accessibility(list(rwth_cset('rwth_100'))) for issue in issues: print(issue)

Source code in src/rwthplots/utils.py
def check_accessibility(
    colors: Sequence[str],
    types: Sequence[str] = ("deuteranopia", "protanopia", "tritanopia"),
    threshold: float = 10.0,
) -> list[dict]:
    """
    Simulate colour vision deficiencies and report pairs that would be confused.

    CVD simulation uses the Viénot-Brettel-Mollon model (1997/1999).
    Perceptual distance is CIE 1976 delta-E in CIELAB.

    Parameters
    ----------
    colors : sequence of str
        ``'#RRGGBB'`` hex colour strings to evaluate.
    types : sequence of str
        CVD types to simulate.  Supported: ``'deuteranopia'``,
        ``'protanopia'``, ``'tritanopia'``.
    threshold : float
        Delta-E below which two simulated colours are flagged as confusable.
        Default 10 (clearly noticeable); use 20 for a stricter check.

    Returns
    -------
    issues : list of dict
        Each entry: ``{'cvd_type', 'color_a', 'color_b', 'delta_e'}``.
        Empty list means the palette passes.

    Examples
    --------
    from rwthplots.cmap import rwth_cset
    issues = check_accessibility(list(rwth_cset('rwth_100')))
    for issue in issues:
        print(issue)
    """
    unknown = set(types) - set(_CVD_MATRICES)
    if unknown:
        raise ValueError(
            f"Unknown CVD type(s): {unknown}. Choose from {set(_CVD_MATRICES)}."
        )

    linear = _hex_colors_to_linear(colors)
    n = len(colors)
    issues: list[dict] = []

    for cvd_type in types:
        sim_linear = _simulate_cvd(linear, cvd_type)
        sim_lab = _linear_rgb_to_lab(sim_linear)
        for i in range(n):
            for j in range(i + 1, n):
                delta_e = float(np.sqrt(np.sum((sim_lab[i] - sim_lab[j]) ** 2)))
                if delta_e < threshold:
                    issues.append({
                        "cvd_type": cvd_type,
                        "color_a": colors[i],
                        "color_b": colors[j],
                        "delta_e": round(delta_e, 2),
                    })

    return issues

CVD simulation background

check_accessibility() uses the Viénot-Brettel-Mollon model (1997/1999) to simulate how a palette appears to observers with colour vision deficiency.

The simulation pipeline for each colour:

  1. sRGB → linear RGB — remove gamma correction (IEC 61966-2-1).
  2. Linear RGB → LMS — transform to the three cone types using the Hunt-Pointer-Estévez / Bradford D65 adaptation matrix.
  3. Apply CVD confusion matrix — collapse the affected cone channel(s) to reproduce the confusion experienced by each CVD type.
  4. LMS → linear RGB — invert the cone-space transform.
  5. Linear RGB → CIELAB — convert to a perceptually uniform space.
  6. CIE 1976 ΔE — measure Euclidean distance between simulated pairs. Pairs below the threshold are flagged as confusable.

The confusion matrices and sRGB constants are taken from the daltonize project (MIT license), which implements the same model.

CVD type Cone affected Prevalence (males)
Deuteranopia M (medium) ~6 %
Protanopia L (long) ~2 %
Tritanopia S (short) ~0.003 %