Colour and style

Choose a theme, keep colours consistent, and define scales that travel with the plot.

8 min readPlotting

Start with a theme

Use fp.use_light() for a white publication background, fp.use_paper() for warm paper, or fp.use_dark() for a dark background. Call it before creating the figure. These change Matplotlib’s style settings; existing artists keep their values.

Colormap and palette collections

Use a colormap for numerical values and a palette for named categories. Map names are registered with Matplotlib when you import fluxplot, together with their reversed _r forms.

import fluxplot as fp

fp.use_paper()
# These names work in cmap= on fluxplot and Matplotlib calls.
sequential = fp.colors.maps.get("crameri.batlow")
diverging = fp.colors.maps.get("tol.sunset")
category_colours = fp.colors.palettes.get("tol", "bright")
Collection Maps Contents
flexoki 5 Flux’s house maps and colour ramps
mpl 86 Matplotlib’s built-in colormaps, in its documented groups
crameri 60 Fabio Crameri’s Scientific colour maps
tol 20 Paul Tol’s continuous and discrete maps, plus his categorical palettes
cmasher 57 CMasher colormaps
brewer palettes ColorBrewer sets: 9-class sequential, 11-class diverging and the qualitative sets

fp.colors.maps.names("crameri") lists a collection, and fp.colors.maps.info("tol.sunset") describes a map. fp.colors.maps.truncate("batlow", 0.2, 0.8) selects part of a map, and fp.colors.maps.discretize("viridis", [0, 2, 5, 10]) returns a colormap and normalization for bins.

Every fp.use_* theme installs cmasher.rainforest (fp.style.SEQUENTIAL) as Matplotlib’s default image.cmap, so a heatmap without cmap= is in the house style; fp.style.DEFAULT_DIVERGING names the diverging default. You can override either for any plot with cmap=. In a signature plot’s palette=, a colour name such as "red" selects a pale-to-deep ramp of that one hue, while "flexoki.red" selects the 13-step Flexoki ramp.

Map details, derived maps and where the definitions live

Map information. fp.colors.maps.info(name) reports a map’s type (sequential, diverging, cyclic, qualitative or misc), its family, its table size N, whether it is discrete (a listed map of at most fp.colors.DISCRETE_MAX = 32 colours, a set of classes to pick from), and whether it is perceptually uniform (True, False, or None when not assessed). Each definition’s colors is the map’s exact lookup table, so a consumer reproduces it colour for colour. fp.colors.maps.get("batlow") also resolves a bare name, searching the collections in the order of the table above.

House maps. The flexoki collection holds flexoki.sequential, .warm, .diverging, .terrain and .spectrum. They are linear ramps through palette anchors (fp.colors.FLEXOKI_MAP_ANCHORS) and therefore not perceptually uniform (info()["uniform"] is False). Their older bare names, such as flexoki_diverging, are aliases of the same tables.

Derived maps. truncate returns the stretch of a map as a map of its own, named like crameri.batlow[0.2:0.8], a name that fp.colors.maps.get (and therefore a recipe) resolves again. discretize(..., extend="both") returns a (ListedColormap, BoundaryNorm) pair with one colour per bin, which any helper accepts as cmap= / norm=; the colour scale is then recorded as kind: "binned".

Palettes. fp.colors.palettes holds flexoki (the default), brewer and tol. fp.colors.palettes.brewer["Blues"] and fp.colors.palettes.get("tol", "bright") return lists of hex strings.

Definitions. Every colormap and palette is plain data in the package (src/fluxplot/definitions/colormaps.json and palettes.json), so nothing beyond Matplotlib is imported at runtime, and Flux bundles the same files for its pickers. The JSON is generated by tools/build_color_definitions.py from the upstream packages (matplotlib, cmcrameri, tol-colors, cmasher); it is a build-time step, re-run only to refresh a collection (uv run --with cmcrameri --with tol-colors python tools/build_color_definitions.py; a collection whose package is missing keeps its shipped definition). The Flexoki palette has one canonical source, the design-token export src/fluxplot/definitions/flexoki.tokens.json: fp.colors.flex and the flexoki palette collection are both built from it. fluxplot’s “green” is the tokens’ custom GRN hue; Flexoki’s original green is olive.

Theme metadata

The manifest records the theme in force at save time:

// "theme" is "light", "dark", "paper", …; null once an rcParam was changed by hand.
"style": {"theme": "light",
          "tokens": {"ink": "#…", "label": "#…", "tick": "#…", "axis": "#…",
                     "grid": "#…", "plot": "#…", "paper": "#…"}}

The tokens are the scaffold colours by role, as lowercase hex. Every scaffold element painted with one of them carries data-ink-fill or data-ink-stroke naming the token:

Element Token
tick labels and titles ink
axis titles label
ticks tick
spines axis
gridlines grid
the axes background (axes.background) plot
the figure background (figure.background) paper

Overlays that a helper drew in the theme’s ink, such as a significance bracket, are tagged too. Data colours are never tagged, so Flux can restyle a light plot’s furniture for a dark deck and leave the science alone.

fp.save(..., theme_vars=True) additionally rewrites those paints as var(--fx-<token>, <hex>) for a CSS-aware host. It is off by default: browsers and rsvg honour the fallback, but Illustrator and Inkscape have not yet been checked. A rerun with FLUX_PARAMS={"__fluxplot__": {"theme": "dark"}} makes every fp.use_* call apply that theme, and the recipe records the theme in force under __fluxplot__.theme.

Consistent category colours

Every series records its primary paint in the manifest, as series[].color:

"color": {"hex": "#35ab49", "alpha": 1.0, "token": "flexoki.green-400",
          "palette": {"name": "flexoki.light", "index": 2}}

token is the exact palette token when one names the colour, and palette gives its slot in the active cycle when it came from there. A colour-mapped collection records "varies", and its scale names the colour scale. The glowbar and fluxbox payloads record the palette each category’s shades came from.

fp.colors.categories keeps category colours consistent across figures:

# Pin a colour:
fp.colors.categories.assign({"SD": "#bc5215"})
# The pinned colour, or the next unused palette slot:
colour = fp.colors.categories.get("sleep")
# Write the pinned colours to the project file:
fp.colors.categories.save()

get() returns the colour pinned to a category; otherwise it assigns the next unused slot of the palette (by default the theme’s cycle) in first-request order and remembers it. A category therefore keeps its colour whether or not the others are on the plot. Surface maps (categorical_colors), glowbar and fluxbox group colours, and tag_seaborn’s hue levels all consult it.

save() and load() write and read the pinned colours in the project file fluxplot.colors.json ({"spec": "fluxplot/colors", "version": 1, "categories": {"SD": "#bc5215"}, "palette": "flexoki"}). The first use loads it automatically from $FLUXPLOT_COLORS, or from the nearest such file between the working directory and the Git root; nothing is ever written implicitly. Setting categories.auto_series = True colours fp.line and fp.scatter series by their name when the call gives no colour (off by default).

Two recipe controls apply on a rerun: __fluxplot__.palette = "tol.bright" makes every fp.use_* install that palette as the colour cycle (glowbar categories draw from it too), and __fluxplot__.series = {"<series id>": {"color": "#…"}} recolours a series. The helpers and the signature plots check both before drawing.

Signature plots on dark grounds

The glowbar, fluxbox and hexmatrix judge every ink against the ground they are drawn on. On a dark theme:

fluxplot’s test suite checks a WCAG contrast of at least 1.5 for every mark, and at least 3 for bracket and identity lines, under the light, paper and dark themes.

Value × confidence

A colour-mapped mark can carry a second, opacity channel: alpha_by= on fp.hexmatrix ("count", a column, a per-point or per-hexagon array, or a matrix in matrix mode), fp.heatmap (a matrix of the same shape — p-values, n) and fp.scatter (one value per point), with alpha_range=(0.25, 1.0) and alpha_norm="linear"|"log". Each element’s alpha runs over the range with its value (a missing value takes the low end), set as per-element alpha so matplotlib renders it; the manifest records the channel as colorScales[].alpha = {source, range, norm} and every element carries data-alpha-value, so a consumer can recompute fill-opacity live.

For example, use observation counts as alpha_by to make sparsely sampled regions lighter. Higher channel values receive higher opacity, so transform an uncertainty measure explicitly if lower values should be more prominent.

Accessibility lint

fp.colorcheck asks what a plot’s colours do for the reader:

fp.save(..., lint="warn") runs check_figure on the tagged series and text inks. Findings go to SaveResult.warnings and the manifest’s quality.color; lint="error" refuses the save. Linting is off by default.

The house colour cycles are ordered so that no two adjacent colours collapse for a colour-deficient reader: fp.CYCLE_ORDER is blue, orange, purple, green, magenta, yellow, cyan, red. The smallest adjacent distance under any deficiency is ΔE 20 in the light cycle and 15 in the dark one. This order was introduced on 30 September 2026 (cyan used to sit next to magenta, at ΔE 5.6), so plots that let the cycle pick their colours changed from the third series on when regenerated. Order alone cannot guarantee greyscale separation; vary the marker or line style for a figure that must print in black and white.

Colour controls in the recipe

Every colour scale is a recipe control: fp.save writes its complete state under recipe.params.__fluxplot__[<key>], and Flux’s Colour scales editor edits it and regenerates the plot. A control has this shape (| separates the alternatives):

{"cmap": "magma" | {"lut": ["#…"], "under": "#…", "over": "#…", "bad": "#…"},
 "reversed": false,
 "vmin": 1, "vmax": 50,
 "norm": {"kind": "linear" | "log" | "symlog" | "power" | "twoslope" | "centered",
          "vcenter": 0, "gamma": 0.5, "linthresh": 1, "linscale": 1},
 "extend": "neither" | "min" | "max" | "both"}

The helpers read the controls from FLUX_PARAMS automatically. A control that merely restates what the script asked for (the replay of a recorded save) keeps the script’s own objects — a ListedColormap with no resolvable name included — so regeneration never fails on its own output; a control that differs is applied, and an impossible one (an unknown map, a log norm with vmin <= 0, a twoslope centre outside the limits) raises a ValueError naming the key. extend reaches the colour key drawn with fp.colorbar. The old flat {cmap, vmin, vmax} is still understood.

The key names the series (rates), never the axes’ position, so adding a subplot cannot orphan an edit. A second colour-mapped series with the same name gets panel.<name>.rates or axes.<n>.rates, and key= names it explicitly. Controls saved by an older Flux under the positional key are still honoured. Other recipe options are described in Recipes and parameters.

Share a scale across panels

Use fp.color_scale to give multiple plots a common range and colour map. The matrices and colour scales guide gives the pattern and explains how limits are combined.

Search the docs

Search guides, workspaces, and reference topics.

↑ ↓ to move · Enter to open · Esc to close