Skip to content

Cookbook

Short, copy-pasteable recipes. Each is self-contained — import melanopy as mp and go. For the functions they draw on, see the API reference.

Rank a set of colormaps

melanopic_ratio is the M/P meanwhere a map sits (display white = 1.0; under 1 protective, over 1 alerting); mp_spread is the M/P spread (σ)how tightly it sits.

import matplotlib.pyplot as plt
import numpy as np

import melanopy as mp

for name in ["copper", "magma", "viridis", "cool", "gray"]:
    c = plt.get_cmap(name)(np.linspace(0, 1, 256))[:, :3]
    s = mp.rate_colormap(c)
    print(f"{name:8s} M/P={s['melanopic_ratio']:.2f}  spread σ={s['mp_spread']:.2f}")
copper   M/P=0.49  spread σ=0.03
magma    M/P=0.72  spread σ=0.53
viridis  M/P=0.83  spread σ=0.56
cool     M/P=2.06  spread σ=0.58
gray     M/P=1.00  spread σ=0.00

Read a colormap's melanopic profile

The mean and spread summarize a curve. Ask for it with profile=True to see where a map dumps its blue — viridis spikes at the dark end (high σ, "smeared") while copper and gray stay flat (low σ, "pure").

import matplotlib.pyplot as plt
import numpy as np

import melanopy as mp

for name in ["viridis", "copper", "gray"]:
    c = plt.get_cmap(name)(np.linspace(0, 1, 256))[:, :3]
    p = mp.rate_colormap(c, profile=True)  # adds positions / ratios / luminance
    print(f"{name:8s} σ={p['mp_spread']:.2f}  dark-end M/P={p['ratios'][10]:.2f}  "
          f"mid M/P={p['ratios'][128]:.2f}")
viridis  σ=0.56  dark-end M/P=3.06  mid M/P=1.21
copper   σ=0.03  dark-end M/P=0.70  mid M/P=0.47
gray     σ=0.00  dark-end M/P=1.00  mid M/P=1.00

To plot the full curve, use p["positions"] (the [0, 1] data grid) against p["ratios"]:

fig, ax = plt.subplots()
for name in ["viridis", "copper", "gray"]:
    c = plt.get_cmap(name)(np.linspace(0, 1, 256))[:, :3]
    p = mp.rate_colormap(c, profile=True)
    ax.plot(p["positions"], p["ratios"], label=f"{name} (σ={p['mp_spread']:.2f})")
ax.axhline(1.0, ls=":", color="grey")  # display white = 1
ax.set(xlabel="data value", ylabel="melanopic ratio (M/P)", ylim=(0, 2))
ax.legend()
plt.show()

Sweep the Circadia family

mp.circadia(alpha) walks the axis from protective (alpha=0) to alerting (alpha=1) while holding lightness uniform. Rating each step shows the melanopic ratio climb monotonically — the dial is an emergent property of the OKLab geometry, not a knob the generator sets.

import numpy as np

import melanopy as mp

for a in np.linspace(0, 1, 5):
    ratio, spread = mp.circadia_rating(a)
    print(f"alpha={a:.2f}  M/P={ratio:.2f}  spread={spread:.2f}")
alpha=0.00  M/P=0.29  spread=0.07
alpha=0.25  M/P=0.58  spread=0.02
alpha=0.50  M/P=0.92  spread=0.13
alpha=0.75  M/P=1.33  spread=0.29
alpha=1.00  M/P=1.73  spread=0.42

The Circadia family swept from protective to alerting

Label a live α-slider with its rated M/P

α is a control — a geometric position on the OKLab morph — not the melanopic ratio the viewer receives, and that ratio is panel-dependent. mp.circadia_rating(α, panel=...) composes the generator and the rater in one call, so a slider can label itself with the physical number for its configured panel. Drag the slider, call im.set_cmap — never recompute the data.

import matplotlib.pyplot as plt
import numpy as np
from matplotlib.widgets import Slider

import melanopy as mp

PANEL = "representative"  # prefer a measured panel for research use — see the API reference

z = np.add.outer(np.sin(np.linspace(0, 6, 200)), np.cos(np.linspace(0, 6, 300)))
z = (z - z.min()) / (z.max() - z.min())

fig, ax = plt.subplots()
fig.subplots_adjust(bottom=0.2)
im = ax.imshow(z, cmap=mp.circadia(0.0, as_cmap=True), aspect="auto")


def relabel(a):
    ratio, spread = mp.circadia_rating(a, panel=PANEL)  # the rated, panel-aware M/P
    ax.set_title(f"α = {a:.2f}  →  M/P = {ratio:.2f}  (σ = {spread:.2f}, {PANEL})")


def on_change(a):
    im.set_cmap(mp.circadia(a, as_cmap=True))  # recolour the fill; never recompute the data
    relabel(a)
    fig.canvas.draw_idle()


sax = fig.add_axes([0.2, 0.06, 0.6, 0.04])
slider = Slider(sax, "α", 0.0, 1.0, valinit=0.0)
relabel(0.0)
slider.on_changed(on_change)
plt.show()

The initial title reads α = 0.00 → M/P = 0.29 (σ = 0.07, representative). The SMACC reference app does the same through the pyqtgraph adapter (melanopy.adapters.pyqtgraph).

Match the map to the data

The melanopic axis can carry the data's meaning, not just score it. When the data is itself circadian, let the alerting end mark wakefulness and the protective end mark sleep, and the map's melanopic axis is the sleep–wake axis. Reach for the sequential circadia_sweep for an unsigned state (asleep → awake), and the diverging circadia_diverging for a signed quantity (sleep-promoting ↔ alerting, neutral at the zero crossing).

import matplotlib.pyplot as plt
import numpy as np

import melanopy as mp

# Wakefulness 0 (asleep) .. 1 (awake) over a noon-to-noon day, for two weeks.
hours = np.linspace(12, 36, 144)
days = np.arange(14)[:, None]
rise = 1 / (1 + np.exp(-2.6 * (hours - (23.2 + 0.11 * days))))  # asleep in the evening
fall = 1 / (1 + np.exp(-2.6 * ((31.4 + 0.04 * days) - hours)))  # awake the next morning
wake = np.clip(1 - rise * fall, 0, 1)

# Sequential: cool = awake (alerting), warm = asleep (protective).
plt.imshow(wake, aspect="auto", cmap=mp.circadia_sweep(as_cmap=True), vmin=0, vmax=1)
plt.show()

A sleep–wake raster coloured by circadia_sweep, and an alerting-drive curve by circadia_diverging

The full two-panel figure — including the circadia_diverging alerting-drive curve — is reproducible with uv run scripts/build_sleep_wake_demo.py.