Skip to main content

Fast, dependency-free OKLab categorical color palettes powered by Rust

Project description

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)
# ["#080050", "#e00800", "#1078ff", ...]

Get the same kind of palette from a shell:

okpalette create 8

CLI success output is JSON only:

{"colors":["#080050","#e00800","#1078ff"],"format":"hex"}

Extend colors you already have:

from okpalette import extend_palette

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

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)

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": "#080050", "treated": "#e00800", "outlier": ...}

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,
    **create_palette_options,
)
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.

Project details


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.0.0.tar.gz (2.0 MB view details)

Uploaded Source

Built Distributions

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

okpalette-1.0.0-cp312-abi3-win_amd64.whl (341.1 kB view details)

Uploaded CPython 3.12+Windows x86-64

okpalette-1.0.0-cp312-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (514.1 kB view details)

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

okpalette-1.0.0-cp312-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (498.2 kB view details)

Uploaded CPython 3.12+manylinux: glibc 2.17+ ARM64

okpalette-1.0.0-cp312-abi3-macosx_11_0_arm64.whl (448.2 kB view details)

Uploaded CPython 3.12+macOS 11.0+ ARM64

File details

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

File metadata

  • Download URL: okpalette-1.0.0.tar.gz
  • Upload date:
  • Size: 2.0 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for okpalette-1.0.0.tar.gz
Algorithm Hash digest
SHA256 1bde9af830ba8dafe9cdc762fc0b54f036f7dbad516a8aa993fca1bb7971c389
MD5 cca7a621874159dad3733b8ad9d20ff8
BLAKE2b-256 0248aee99390b9dfe737da250714a13e3999f8394f44819c345dfa51a03ced6e

See more details on using hashes here.

Provenance

The following attestation bundles were made for okpalette-1.0.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.0.0-cp312-abi3-win_amd64.whl.

File metadata

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

File hashes

Hashes for okpalette-1.0.0-cp312-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 e6912a80754861d334290067d38315ba6a4ece5e7c9cc6e4b4d1bba66efe9acf
MD5 f504aff899b499a24c3b9cf17fdf6c2b
BLAKE2b-256 379811d4128e0b9044780a80e044c41d9e340a33a32d903643a998badd180e31

See more details on using hashes here.

Provenance

The following attestation bundles were made for okpalette-1.0.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.0.0-cp312-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for okpalette-1.0.0-cp312-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 bca1a9029b00c82fa3d08e0a19e4bafef61d49221ac411b5a39f71ef58801078
MD5 615aac30e0e938f69d55c893544a34af
BLAKE2b-256 355f8182b2edf3376ec27d42f169195fab4d721ca4a7c491c3856ec6b9574c60

See more details on using hashes here.

Provenance

The following attestation bundles were made for okpalette-1.0.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.0.0-cp312-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for okpalette-1.0.0-cp312-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 aefd456170a9cdf11a5ccaa6393c3f8b44b4f1cadf3fcdc8563bce59bbc0d5d3
MD5 619ec19d82b3bdc68ffd7a3aae7adbc2
BLAKE2b-256 d2bfde770dab5b8374ea34d304baf489a998e4ffe02886ca34935e34bccf1a34

See more details on using hashes here.

Provenance

The following attestation bundles were made for okpalette-1.0.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.0.0-cp312-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for okpalette-1.0.0-cp312-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 8545f6ba25ddc91b26ea1f943bd44d6a61ce95997ac093f08d8dbd3a8b24e6d7
MD5 c66cd85058fef292393e539609f0b285
BLAKE2b-256 25bcb4394a3dd60514201591fcbc2d0637835741928ac454c93d453020ad7d39

See more details on using hashes here.

Provenance

The following attestation bundles were made for okpalette-1.0.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.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page