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.

Metadata

Release files for okpalette 1.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for okpalette 1.1.1
File Size Uploaded Provenance
okpalette-1.1.1.tar.gz 56.2 kB With provenance Details

Built distributions (wheels)

Table of built distributions (wheels) for okpalette 1.1.1
File
okpalette-1.1.1-cp312-abi3-win_amd64.whl CPython 3.12 abi3 Windows x86-64 Details
okpalette-1.1.1-cp312-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl CPython 3.12 abi3 Linux glibc 2.17+ x86-64 Details
okpalette-1.1.1-cp312-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl CPython 3.12 abi3 Linux glibc 2.17+ ARM64 Details
okpalette-1.1.1-cp312-abi3-macosx_11_0_arm64.whl CPython 3.12 abi3 macOS 11.0+ ARM64 Details

Total release size: 2.0 MB

Release files / okpalette-1.1.1.tar.gz

Download URL okpalette-1.1.1.tar.gz
Size 56.2 kB
Tags Source
SHA-256 checksum
How to use checksums
ba83d5a5ae598585534584c989e9dc0857d983ce5c9aa9ff2aff4b853a9971a8
BLAKE2b-256 checksum
How to use checksums
40d121137eeaf2b5618ae243690c1117e7744f917f42aa9ad0855c7b69974619
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 5, 2026.

Transparency log

Release files / okpalette-1.1.1-cp312-abi3-win_amd64.whl

Download URL okpalette-1.1.1-cp312-abi3-win_amd64.whl
Size 374.0 kB
Tags CPython 3.12 Windows x86-64 abi3
SHA-256 checksum
How to use checksums
c11aab4702cd94a390bdf3f43f415a8c9e6599d1daf5996a65dcfc807c0edbfe
BLAKE2b-256 checksum
How to use checksums
f289389a9d07999e86de3534047133f3e075ce621bb7dcd9685452f61d8141cd
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 5, 2026.

Transparency log

Release files / okpalette-1.1.1-cp312-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Download URL okpalette-1.1.1-cp312-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Size 553.7 kB
Tags CPython 3.12 Linux glibc 2.17+ x86-64 abi3
SHA-256 checksum
How to use checksums
d397aac3b6ff2ca1fea3a844e6257e4cd672ca276eae7a264c6d35d955e9c380
BLAKE2b-256 checksum
How to use checksums
564acf20ec029289d94917296d753396d0328d6f2f033b206ffe39ca497fa07e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 5, 2026.

Transparency log

Release files / okpalette-1.1.1-cp312-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl

Download URL okpalette-1.1.1-cp312-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Size 542.0 kB
Tags CPython 3.12 Linux glibc 2.17+ ARM64 abi3
SHA-256 checksum
How to use checksums
ca51e8b891a8401a4ab08616c7dcbe357121463acee327cfc3b25dfc0b503008
BLAKE2b-256 checksum
How to use checksums
8ce13998a534cd91989cd96fa22fbafdb2bac8126ef46382cc84913a11c93310
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 5, 2026.

Transparency log

Release files / okpalette-1.1.1-cp312-abi3-macosx_11_0_arm64.whl

Download URL okpalette-1.1.1-cp312-abi3-macosx_11_0_arm64.whl
Size 488.1 kB
Tags CPython 3.12 abi3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
8d51630cecb62de0d8acec83e2e54ac6d871a1f512e36cb5eabed8513607d761
BLAKE2b-256 checksum
How to use checksums
3fc7c8e18c2f4920c122de66cfbea75e25f4414e71aaa621329d31b8a2d808df
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 5, 2026.

Transparency log

Security for okpalette 1.1.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 5 files in this release have been uploaded with provenance metadata.

Select from 5 files

Attestation for okpalette-1.1.1.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 5, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

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