Skip to main content

okpalette

Fast, deterministic categorical color palettes for Python.

Use okpalette when you need distinct, stable colors for labels, plots, dashboards, or reports.

Install

pip install okpalette

With uv:

uv add okpalette

Quickstart

Create stable colors for categories:

from okpalette import create_palette

colors = create_palette(8)
# ["#000058", "#90ff00", "#ff38ff", ...]

Get the same kind of palette from a shell:

okpalette create 8

Works great as a uv tool:

uvx okpalette create 8

CLI success output is JSON only:

{"colors":["#000058","#90ff00","#ff38ff"],"format":"hex"}

Extend colors you already have:

from okpalette import extend_palette

colors = extend_palette(["#0057b8", "#ffd700"], 8)

Rust API

Rust crates can use the generator directly without depending on Python:

[dependencies]
okpalette = "1.1"

The one-shot functions use the same defaults as the Python API and CLI:

use okpalette::{extend_palette, generate_palette, Rgb8};

let colors = generate_palette(8).unwrap();
let brand = [
    "#0057b8".parse::<Rgb8>().unwrap(),
    "#ffd700".parse().unwrap(),
];
let extended = extend_palette(&brand, 8).unwrap();

Use PaletteGenerator for reusable, typed configuration:

use okpalette::{
    BackgroundContrast, CandidateConstraints, ChromaRange, ColorblindMode,
    DistanceWeights, GridSize, LightnessRange, PaletteGenerator, Rgb8,
};

let generator = PaletteGenerator::new()
    .grid_size(GridSize::Fine)
    .constraints(
        CandidateConstraints::new()
            .with_lightness(LightnessRange::new(0.2, 0.9).unwrap())
            .with_chroma(ChromaRange::new(Some(0.04), None).unwrap()),
    )
    .distance_weights(DistanceWeights::new(0.8, 1.2).unwrap())
    .backgrounds(
        [Rgb8::new(255, 255, 255)],
        BackgroundContrast::Normal,
    )
    .unwrap()
    .colorblind_mode(ColorblindMode::All);

let colors = generator.generate(12).unwrap();
let new_colors = generator.generate_extension(&colors[..2], 10).unwrap();

LabelPaletteRequest exposes the position-aware label generator. Typed colors, constraints, parsing, and SVG/PNG rendering are also available from the crate root; candidate search and label-assignment internals remain private.

Use position-aware label colors when nearby labels should be easier to tell apart:

from okpalette import create_label_palette

positions = [(0.0, 0.0), (0.2, 0.0), (5.0, 0.0), (5.2, 0.0)]
labels = ["control", "treated", "control", "outlier"]

label_colors = create_label_palette(positions, labels)

Example

Position Aware Example

Agent Skill

okpalette includes an optional packaged agent skill for simple JSON CLI usage. Install it into a personal Codex or Claude skill directory:

okpalette install-skill --agent codex
okpalette install-skill --agent claude

Use --dry-run to print the target path without writing, and --overwrite to replace an existing installed skill. The Codex installer respects $CODEX_HOME and otherwise writes under ~/.codex; Claude skills are installed under ~/.claude.

Formats

Use RGB tuples when that fits your plotting library better:

rgb = create_palette(5, format="rgb")
# [(8, 0, 80), (224, 8, 0), ...]

rgb01 = create_palette(5, format="rgb01")
# [(0.03137254901960784, 0.0, 0.3137254901960784), ...]

Extend Colors

Use extend_palette() when you already have brand colors or a small palette.

from okpalette import extend_palette

brand = ["#0057b8", "#ffd700"]
colors = extend_palette(brand, 12)

assert colors[:2] == ["#0057b8", "#ffd700"]
assert len(colors) == 12

Use existing colors as anchors without returning them:

new_colors = extend_palette(brand, 10, include_existing=False)

Here target_size=10 still describes the final palette size, so new_colors contains eight generated colors when brand contains two colors.

The same basic workflows are available through the CLI:

okpalette create 10
okpalette create 5 --format rgb
okpalette extend 12 --color "#0057b8" --color "#ffd700"
okpalette extend 10 --color "#0057b8" --generated-only

okpalette create and okpalette extend always write JSON on success, with the stable shape {"colors":[...],"format":"hex"}. For --format rgb and --format rgb01, tuple colors are serialized as JSON arrays. Validation and generation errors write a short message to stderr and leave stdout empty.

Map Labels To Colors

Use create_label_palette() when positions should influence which label gets which color. Nearby or overlapping labels are assigned more distinct colors.

from okpalette import create_label_palette

positions = [(0.0, 0.0), (0.2, 0.0), (5.0, 0.0), (5.2, 0.0)]
labels = ["control", "treated", "control", "outlier"]

colors = create_label_palette(positions, labels)
# {"control": "#90ff00", "treated": "#000058", "outlier": "#ff38ff"}

Labels may be strings, integers, tuples, or other hashable Python objects. The returned dict preserves first-seen label order.

Keep specific label colors fixed:

colors = create_label_palette(
    positions,
    labels,
    fixed_colors={"control": "#0057b8"},
)

For dataframe-like objects, read position and label columns by duck typing:

colors = create_label_palette_from_columns(
    data,
    positions=["x", "y"],
    label="cluster",
)

Use With Plotting Libraries

okpalette has no required Matplotlib, Altair, or Plotly dependency. The default output is a list of lowercase hex strings, which all three libraries accept directly. Use format="rgb01" for Matplotlib APIs that specifically expect normalized RGB tuples.

Matplotlib color cycles and colormaps:

import matplotlib.pyplot as plt
from matplotlib.colors import ListedColormap
from okpalette import create_palette

colors = create_palette(6)

fig, ax = plt.subplots()
ax.set_prop_cycle(color=colors)
ax.plot(x, y1)
ax.plot(x, y2)

cmap = ListedColormap(create_palette(12), name="okpalette")
ax.scatter(x, y, c=values, cmap=cmap)

Altair categorical scales and raw color columns:

import altair as alt
from okpalette import create_palette

categories = ["control", "treated", "outlier"]
colors = create_palette(len(categories))

chart = alt.Chart(data).mark_point().encode(
    x="x:Q",
    y="y:Q",
    color=alt.Color(
        "group:N",
        scale=alt.Scale(domain=categories, range=colors),
    ),
)

# When a dataframe column already contains okpalette hex strings:
chart = alt.Chart(data).mark_point().encode(
    x="x:Q",
    y="y:Q",
    color=alt.Color("color:N", scale=None),
)

Plotly Express discrete sequences and maps:

import plotly.express as px
from okpalette import create_palette

categories = ["control", "treated", "outlier"]
colors = create_palette(len(categories))
color_map = dict(zip(categories, colors))

fig = px.scatter(
    data,
    x="x",
    y="y",
    color="group",
    color_discrete_map=color_map,
)

Tune Appearance

Background Contrast

By default, palettes are generated without a background constraint. Pass both background and background_contrast when you want colors separated from a known plotting background. Use "normal" for the OKLab background-separation heuristic, or "high" / "wcag" for WCAG 2.2 non-text contrast of at least 3.0:1 against every configured background color.

colors = create_palette(
    32,
    background="#ffffff",
    background_contrast="normal",
)

Avoid other colors:

colors = create_palette(
    16,
    avoid_colors=["#000000"],
    background=["#ffffff", "#f2f2f2"],
    background_contrast="high",
)

background accepts one color or a sequence of colors and filters candidates against those backgrounds. avoid_colors keeps exact colors out of the palette and uses them as distance anchors.

Colorblind-aware generation

Opt into colorblind-aware generation when pairwise palette separability should be tested under selected color vision deficiency simulations:

colors = create_palette(12, colorblind_mode="all")

CLI:

okpalette create 12 --colorblind-mode red-green
okpalette create 12 --colorblind-mode all

colorblind_mode may be None, "protan", "deutan", "tritan", "red-green", or "all". The "red-green" mode optimizes against protan and deutan simulations without including tritan; "daltonism" is accepted as a compatibility alias, but "red-green" is the preferred spelling. The feature uses Machado, Oliveira, and Fernandes 2009 severity-1.0 matrices on linear sRGB and optimizes the worst-case OKLab distance across ordinary vision and the selected simulations. It does not make a palette universally accessible or colorblind-safe. If you also set background_contrast="high" or "wcag", WCAG contrast is still checked against the ordinary sRGB background, not against simulated colors.

Limit hue ranges

warm = create_palette(10, hue=(330, 100))
cool = create_palette(10, hue=(150, 280))

Common constraints:

muted = create_palette(12, chroma=(0.02, 0.12))
bright = create_palette(12, chroma=(0.10, None))
mid_lightness = create_palette(12, lightness=(0.30, 0.80))

lightness is OKLab L in 0..1. hue is OKLCH degrees in 0..360; ranges can wrap around zero.

Preview And Save

from okpalette import create_palette, save_palette, view_palette

colors = create_palette(12)

view_palette(colors)
save_palette(colors, "palette.svg")
save_palette(colors, "palette.png")

view_palette() works in notebooks through _repr_svg_() and _repr_png_().

For raw preview bytes:

from okpalette import palette_png, palette_svg

svg = palette_svg(colors)
png = palette_png(colors)

Grid Size

grid_size controls how many candidate colors are searched.

quick = create_palette(24, grid_size="coarse")  # step 16
default = create_palette(24, grid_size="medium")  # step 8
fine = create_palette(24, grid_size="fine")  # step 4
custom = create_palette(24, grid_size=12)

If constraints leave too few candidates, okpalette raises ValueError with a hint to relax lightness, chroma, hue, or grid_size.

API

create_palette(
    palette_size,
    *,
    seed_colors=(),
    avoid_colors=None,
    background=None,
    background_contrast=None,
    lightness=(0.20, 0.90),
    chroma=(0.04, None),
    hue=None,
    grid_size="medium",
    lightness_weight=1.0,
    chroma_weight=1.0,
    colorblind_mode=None,
    format="hex",
)
extend_palette(
    colors,
    target_size,
    *,
    include_existing=True,
    seed_colors=(),
    avoid_colors=None,
    background=None,
    background_contrast=None,
    lightness=(0.20, 0.90),
    chroma=(0.04, None),
    hue=None,
    grid_size="medium",
    lightness_weight=1.0,
    chroma_weight=1.0,
    colorblind_mode=None,
    format="hex",
)
create_label_palette(
    positions,
    labels,
    *,
    fixed_colors=None,
    seed_colors=(),
    avoid_colors=None,
    background=None,
    background_contrast=None,
    lightness=(0.20, 0.90),
    chroma=(0.04, None),
    hue=None,
    grid_size="medium",
    lightness_weight=1.0,
    chroma_weight=1.0,
    colorblind_mode=None,
    neighbors=8,
    max_points=50_000,
    format="hex",
)
create_label_palette_from_columns(data, *, positions, label, **create_label_palette_options)
view_palette(palette, *, width=1246, height=154)
palette_svg(palette, *, width=1246, height=154)
palette_png(palette, *, width=1246, height=154)
save_palette(palette, path, *, width=1246, height=154)

How It Works

okpalette uses a greedy Glasbey-style algorithm. It starts with anchor colors such as seeds, avoid colors, and the background, then repeatedly chooses the candidate color that is farthest from the nearest anchor or selected color.

Distances are measured in OKLab. Lightness, chroma, and hue constraints are applied through OKLab and OKLCH before colors are selected.

When colorblind_mode is enabled, candidate distances are scored as the minimum of ordinary OKLab distance and the OKLab distance after each selected Machado 2009 severity-1.0 simulation. This is a generation objective for palettes tested under selected CVD simulations, not an accessibility certification.

For label palettes, okpalette builds a weighted label-neighborhood graph from the input positions, then uses that graph while choosing and assigning colors. The default max_points=50_000 bounds graph construction for large datasets; use max_points=None to opt into all-points preprocessing.

The result is deterministic, fast, and stable when extending a palette. It is not a global optimizer.

Python And Wheels

okpalette supports Python 3.12 and newer. Prebuilt wheels use the Python cp312-abi3 stable ABI and are built for Linux x86_64 and aarch64 manylinux2014, macOS aarch64, and Windows x64.

Other platforms install from the source distribution when a wheel is not available, which requires Rust and the normal Python build toolchain. Intel macOS, musllinux, Windows ARM64, and other secondary targets are not prebuilt wheel targets.

References

These references describe the methods and standards that inform okpalette. They do not make generated palettes WCAG-compliant, colorblind-safe, or globally optimal.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

okpalette-1.1.0.tar.gz (55.7 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

okpalette-1.1.0-cp312-abi3-win_amd64.whl (375.5 kB view details)

Uploaded CPython 3.12+Windows x86-64

okpalette-1.1.0-cp312-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (553.0 kB view details)

Uploaded CPython 3.12+manylinux: glibc 2.17+ x86-64

okpalette-1.1.0-cp312-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (540.3 kB view details)

Uploaded CPython 3.12+manylinux: glibc 2.17+ ARM64

okpalette-1.1.0-cp312-abi3-macosx_11_0_arm64.whl (492.4 kB view details)

Uploaded CPython 3.12+macOS 11.0+ ARM64

File details

Details for the file okpalette-1.1.0.tar.gz.

File metadata

  • Download URL: okpalette-1.1.0.tar.gz
  • Upload date:
  • Size: 55.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for okpalette-1.1.0.tar.gz
Algorithm Hash digest
SHA256 0ba454fdba677f69e2edc7631fda6d9e7039ac43122015c2e27b1996bbcf1aa2
MD5 8d4b46de5a1df83cb9265888b1f0da3e
BLAKE2b-256 f7d0f90571061b7392cf447ee716b3c368e97d3582d7863c0cd78797c1b4a252

See more details on using hashes here.

Provenance

The following attestation bundles were made for okpalette-1.1.0.tar.gz:

Publisher: release.yml on pmbaumgartner/okpalette

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file okpalette-1.1.0-cp312-abi3-win_amd64.whl.

File metadata

  • Download URL: okpalette-1.1.0-cp312-abi3-win_amd64.whl
  • Upload date:
  • Size: 375.5 kB
  • Tags: CPython 3.12+, Windows x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for okpalette-1.1.0-cp312-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 caa7f99aca05f0fd5fbc295fb320e8da41ef05f07d76a7a5bac143dd486cbd57
MD5 db59ef3c7ed14bad8387ca9cf321285a
BLAKE2b-256 8c3c6d945d0c2aa658188da3633c002c4ce7656c55838e53371b7824ea98c092

See more details on using hashes here.

Provenance

The following attestation bundles were made for okpalette-1.1.0-cp312-abi3-win_amd64.whl:

Publisher: release.yml on pmbaumgartner/okpalette

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file okpalette-1.1.0-cp312-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for okpalette-1.1.0-cp312-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 fd7102bc422877cf22d5ec657a656068cb799d279307ee445965bdc91dcad1de
MD5 4422d5468657bc7952090e77f8a25f61
BLAKE2b-256 3d76f31c8a818b80299d56cec89206fdfba680a4dec94d4e93e3b47de6147c67

See more details on using hashes here.

Provenance

The following attestation bundles were made for okpalette-1.1.0-cp312-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:

Publisher: release.yml on pmbaumgartner/okpalette

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file okpalette-1.1.0-cp312-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for okpalette-1.1.0-cp312-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 5d4e8fd6d0fe4a407c7c432132809b098b78c438016de17c435695ebabc7e2d0
MD5 c6f7d9303fc8bc5e9437138684a201d5
BLAKE2b-256 e4f95addbdcee155b2a615a7b43c4ce8812d37b498d1aa7f3b4a41383d7ebf33

See more details on using hashes here.

Provenance

The following attestation bundles were made for okpalette-1.1.0-cp312-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl:

Publisher: release.yml on pmbaumgartner/okpalette

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file okpalette-1.1.0-cp312-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for okpalette-1.1.0-cp312-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 d693332f3e8b45db66e5449cd516df039867ce7ef5a58a48ab33ba31864de711
MD5 c1c99ca8fdd3dbf7bf2e6345e589c1ec
BLAKE2b-256 429cfca420b2977569f117dc29787c907c30121e9601c4f7b5715fe7b55da822

See more details on using hashes here.

Provenance

The following attestation bundles were made for okpalette-1.1.0-cp312-abi3-macosx_11_0_arm64.whl:

Publisher: release.yml on pmbaumgartner/okpalette

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

1.1.0 This release

5 files

1.0.0

5 files

0.2.0

5 files

0.1.0

5 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page