Source code for vane.viz.palette

# SPDX-License-Identifier: Apache-2.0
# Copyright (c) 2026 Jae Hoon Seo, SMI Lab, Inha University
"""Colour palettes of the figures.

Every VANE figure uses one palette, the standard engineering-paper line colours
(:data:`STANDARD_COLORS`: black, red, blue, green, magenta, orange, cyan), defined once
with the rest of the figure style (see :func:`vane.viz.apply_style`) and re-exported
here. :func:`apply_standard_palette` sets it as a Plotly figure's trace colour cycle.

For readers with a colour-vision deficiency, the Okabe-Ito qualitative palette [1]_
(:data:`OKABE_ITO`), an eight-colour set designed to stay distinguishable across the
common colour-vision deficiencies, is available as an opt-in alternative:
:func:`apply_colorblind_palette` replaces each standard colour of a finished figure by
the Okabe-Ito colour of the nearest hue (black stays black, red becomes vermillion,
blue stays blue, green becomes bluish green, and so on), so that the mode families
stay distinct.

References
----------
.. [1] Okabe, M. and Ito, K. (2008). Color Universal Design (CUD): how to make figures
   and presentations that are friendly to colorblind people.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

from vane.viz._style import STANDARD_COLORS, standard_colorway

if TYPE_CHECKING:
    import plotly.graph_objects as go

__all__ = [
    "OKABE_ITO",
    "STANDARD_COLORS",
    "apply_colorblind_palette",
    "apply_standard_palette",
    "colorblind_colorway",
    "standard_colorway",
]

# The Okabe-Ito colourblind-safe qualitative palette (hex). Black is the canonical
# eighth colour and is ordered last so the chromatic hues cycle first, keeping it clear
# of the black axes/annotations on a white background.
OKABE_ITO: tuple[str, ...] = (
    "#E69F00",  # orange
    "#56B4E9",  # sky blue
    "#009E73",  # bluish green
    "#F0E442",  # yellow
    "#0072B2",  # blue
    "#D55E00",  # vermillion
    "#CC79A7",  # reddish purple
    "#000000",  # black
)

# Standard colour (lower-case hex) -> the Okabe-Ito colour of the nearest hue.
_TO_OKABE_ITO = dict(
    zip(
        (color.lower() for color in STANDARD_COLORS),
        (
            OKABE_ITO[7],  # black -> black
            OKABE_ITO[5],  # red -> vermillion
            OKABE_ITO[4],  # blue -> blue
            OKABE_ITO[2],  # green -> bluish green
            OKABE_ITO[6],  # magenta -> reddish purple
            OKABE_ITO[0],  # orange -> orange
            OKABE_ITO[1],  # cyan -> sky blue
        ),
        strict=True,
    )
)


[docs] def colorblind_colorway() -> list[str]: """Return the Okabe-Ito palette as a Plotly ``colorway`` list. Returns ------- list[str] The eight colours of :data:`OKABE_ITO`. """ return list(OKABE_ITO)
[docs] def apply_standard_palette(figure: go.Figure) -> go.Figure: """Set a figure's trace colour cycle to the standard line colours. Parameters ---------- figure : plotly.graph_objects.Figure The figure; its ``layout.colorway`` is set so that traces added without an explicit colour cycle through :data:`STANDARD_COLORS`. Returns ------- plotly.graph_objects.Figure The same figure, for chaining. """ figure.update_layout(colorway=standard_colorway()) return figure
def _swap(color: object) -> object: """Return the Okabe-Ito colour for a standard colour; other values are unchanged.""" if isinstance(color, str): return _TO_OKABE_ITO.get(color.lower(), color) if isinstance(color, (list, tuple)): return [_swap(item) for item in color] return color
[docs] def apply_colorblind_palette(figure: go.Figure) -> go.Figure: """Recolour a figure with the colourblind-safe Okabe-Ito palette. The figure's ``layout.colorway`` becomes :data:`OKABE_ITO`, and every explicit line or marker colour that is one of :data:`STANDARD_COLORS` (such as the mode-family colours of the Campbell diagram) is replaced by the Okabe-Ito colour of the nearest hue. Other explicit colours (greys, colour scales) are left unchanged. Parameters ---------- figure : plotly.graph_objects.Figure The figure to recolour, in place. Returns ------- plotly.graph_objects.Figure The same figure, for chaining. """ figure.update_layout(colorway=colorblind_colorway()) for trace in figure.data: for part in ("line", "marker"): if part not in trace: continue style = trace[part] if "color" in style and style.color is not None: style.color = _swap(style.color) return figure