Palettes · Science

Scientific figures one dataset, three palettes, the code and the licences

A figure in a paper is printed in grey, read by people with color blindness, and redrawn by people who want your colors. Here one computed dataset is drawn three ways, by plotting code that was run, in palettes chosen by measurement, each with its licence from its source.

Figures, as files
3
Formats
PDF · SVG · PNG
Run with
matplotlib 3.10.8

The figures

One dataset, three figures

The hours from sunrise to sunset, computed for each latitude and day, so anyone can redraw them. Switch the vision to see each figure as a reader with color blindness, or a grey printer, would.

View as

Simulated with Machado, Oliveira and Fernandes (2009); grayscale keeps luminance only. The figures are in English, as most journals ask.

categorical · Okabe–Ito Day length through the year at four latitudes

Line chart of day length through the year at 60°N, 45°N, 30°N and the equator. The equator stays at 12 hours; the further north, the longer the summer days and the shorter the winter ones, from 5.5 to 18.5 hours at 60°N.
Figure 1. Day length through the year at 60°N, 45°N, 30°N and the equator. The first four Okabe–Ito colors, in the order that keeps them furthest apart; each line also has its own marker and a label at its end.

Download PDF, 13 KBSVG, 34 KBPNG, 150 KB

sequential · viridis Day length by latitude and month

Grid of day length by latitude, 60°N to 60°S, and month, January to December, in nine classes from dark purple for short days to yellow for long ones, every value printed: from 5.6 to 18.4 hours.
Figure 2. Day length on the 15th of each month, 60°N to 60°S, in nine viridis classes of 1.5 hours. Every value is printed in its cell, in black or white, whichever reads better.

Download PDF, 15 KBSVG, 101 KBPNG, 306 KB

diverging · PuOr Difference from 12 hours

Grid of the difference between day length and 12 hours by latitude and month, orange where days are shorter than nights and purple where they are longer, every value printed. The northern and southern halves mirror each other.
Figure 3. The same grid as the difference from 12 hours, in nine PuOr classes centred on zero: orange for days shorter than nights, purple for longer, the near-white middle for about equal.

Download PDF, 16 KBSVG, 104 KBPNG, 317 KB

Measured

Why these palettes

Each figure’s palette, and what it was measured for. Distances are CIEDE2000 between simulated colors; lightness is OKLCH L.

The palettes in the figures and what was checked
PaletteJobWhat was checkedResult
Okabe–Ito, first fourTelling the lines apartThe closest pair in typical vision and three color-vision simulations; then in grayscaleΔE 12.3 at worst (Blue and Reddish purple); ΔE 6.0 in grayscale, which is why each line also has a marker and a label
viridisMagnitude, low to highLightness rises at every step, in color and in grayscaleYes: L 0.285 to 0.918; the tightest step is ΔE 4.5
cividisMagnitude, made for color-vision deficiencyThe same, as an alternative to viridisYes: L 0.259 to 0.921; the tightest step is ΔE 6.8
PuOrAbove and below a middle that means somethingLightest in the middle; the two sides apart in every vision that sees hueYes: the closest mirrored pair is ΔE 20.1

Method

What makes a figure survive

  • Name the job first. Categories get distinct hues; a magnitude gets one direction of lightness; a difference gets two hues meeting at a light middle, and only when the middle means something, as 12 hours does here.

  • Give every line a second cue. A marker shape and a label at the line’s end let the figure work in grey and without a legend to look up.

  • Use classes, and print the values when there is room. Nine classes of a fixed width can be read off the colorbar; a printed value cannot be misread at all.

  • Avoid rainbows. jet and RdYlGn change lightness unevenly or not at all, and draw edges the data does not have; the scales page measures both.

  • Label the colorbar with its quantity and unit, and give each figure a caption that names the palette.

The code

The code that drew them

One Python file, matplotlib and NumPy only. It writes each figure as PDF with its fonts embedded, as SVG with the text drawn as outlines so it looks the same wherever it opens, and as a 600 dpi PNG.

"""Three publication figures from one dataset, in color-blind-safe palettes.

From https://auricartisan.com/library/palette/scientific/ - CC0 1.0.

The data is computed, not measured: the hours from sunrise to sunset at the
centre of the sun, without refraction, from Cooper's (1969) solar declination.
So the figures can be rebuilt anywhere, and checked against the page.

    python figures.py          writes figure-1, figure-2 and figure-3
                               as .pdf, .svg and .png (600 dpi)
    python figures.py --grid   prints the 9 x 12 grid of hours as JSON

Needs matplotlib and NumPy.
"""
import json
import sys

import matplotlib
import numpy as np

matplotlib.use("Agg")
import matplotlib.pyplot as plt  # noqa: E402
from matplotlib.colors import BoundaryNorm, ListedColormap  # noqa: E402

# Okabe and Ito (2008), in the order that keeps the first n colors furthest
# apart in every color vision (see the chart palettes page). One marker each.
OKABE_ITO = ["#0072B2", "#D55E00", "#000000", "#CC79A7"]
MARKERS = ["o", "s", "^", "D"]
# viridis (van der Walt and Smith, CC0) and PuOr (ColorBrewer, Apache 2.0), nine stops each.
VIRIDIS = ["#440154", "#472D7B", "#3B528B", "#2C728E", "#21918C", "#28AE80", "#5EC962", "#ADDC30", "#FDE725"]
PUOR = ["#B35806", "#E08214", "#FDB863", "#FEE0B6", "#F7F7F7", "#D8DAEB", "#B2ABD2", "#8073AC", "#542788"]

LATITUDES = [60, 45, 30, 15, 0, -15, -30, -45, -60]
MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
DAYS = [15, 46, 74, 105, 135, 166, 196, 227, 258, 288, 319, 349]  # the 15th of each month
# Nine classes of 1.5 hours, centred on 12: the middle class is days about as long as nights.
EDGES = 12 + 1.5 * (np.arange(10) - 4.5)

INK = "#000000"
PAPER = "#FFFFFF"


def declination(day):
    """Solar declination in degrees: Cooper (1969)."""
    return 23.45 * np.sin(np.radians(360 * (284 + day) / 365))


def day_length(lat, day):
    """N = (2/15) arccos(-tan(latitude) tan(declination)), in hours."""
    x = -np.tan(np.radians(lat)) * np.tan(np.radians(declination(day)))
    return np.where(x <= -1, 24.0, np.where(x >= 1, 0.0, 2 * np.degrees(np.arccos(np.clip(x, -1, 1))) / 15))


def grid():
    return np.array([[float(day_length(lat, d)) for d in DAYS] for lat in LATITUDES])


def lat_label(lat):
    return "0°" if lat == 0 else f"{abs(lat)}°{'N' if lat > 0 else 'S'}"


def luminance(hex_color):
    c = np.array([int(hex_color[i:i + 2], 16) for i in (1, 3, 5)]) / 255
    c = np.where(c <= 0.04045, c / 12.92, ((c + 0.055) / 1.055) ** 2.4)
    return float(0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2])


def ink_on(hex_color):
    """Black or white, whichever has the higher WCAG contrast on the cell."""
    y = luminance(hex_color)
    return INK if (y + 0.05) / 0.05 >= 1.05 / (y + 0.05) else PAPER


def style():
    plt.rcParams.update({
        "font.family": "DejaVu Sans",
        "font.size": 7,
        "axes.spines.top": False,
        "axes.spines.right": False,
        "axes.linewidth": 0.6,
        "xtick.major.width": 0.6,
        "ytick.major.width": 0.6,
        "pdf.fonttype": 42,          # embed TrueType, as journals ask
        "svg.fonttype": "path",      # draw the SVG's text as outlines: it looks the same anywhere
        "svg.hashsalt": "auric-artisan",
    })


def figure_lines():
    """Categorical: day length through the year at four latitudes, labelled at the line ends."""
    fig, ax = plt.subplots(figsize=(3.5, 2.4), layout="constrained")
    days = np.arange(1, 366)
    for lat, color, marker in zip([60, 45, 30, 0], OKABE_ITO, MARKERS):
        hours = day_length(lat, days)
        ax.plot(days, hours, color=color, lw=1.4, marker=marker, markevery=(14, 30), ms=3.6,
                markeredgecolor=PAPER, markeredgewidth=0.5)
        ax.text(371, hours[-1], lat_label(lat), va="center", ha="left", color=INK)
    ax.set_xlim(1, 400)
    ax.set_ylim(0, 24)
    ax.set_xticks([1, 91, 182, 274, 365])
    ax.set_yticks([0, 6, 12, 18, 24])
    ax.set_xlabel("Day of the year")
    ax.set_ylabel("Day length (hours)")
    return fig


def figure_grid(values, colors, edges, label, fmt):
    """A latitude-by-month grid in nine classes, every value printed in its cell."""
    fig, ax = plt.subplots(figsize=(7.2, 3.0), layout="constrained")
    cmap = ListedColormap(colors)
    norm = BoundaryNorm(edges, cmap.N)
    mesh = ax.pcolormesh(values, cmap=cmap, norm=norm, edgecolors=PAPER, linewidth=0.6)
    for i, row in enumerate(values):
        for j, v in enumerate(row):
            cell = colors[int(np.clip(np.searchsorted(edges, v, side="right") - 1, 0, len(colors) - 1))]
            ax.text(j + 0.5, i + 0.5, fmt(v), ha="center", va="center", fontsize=6.5, color=ink_on(cell))
    ax.set_xticks(np.arange(12) + 0.5, MONTHS)
    ax.set_yticks(np.arange(9) + 0.5, [lat_label(lat) for lat in LATITUDES])
    ax.invert_yaxis()
    ax.tick_params(length=0)
    for side in ("left", "bottom"):
        ax.spines[side].set_visible(False)
    bar = fig.colorbar(mesh, ax=ax, ticks=edges, fraction=0.04, pad=0.02)
    bar.ax.set_yticklabels([f"{e:g}" for e in edges])
    bar.set_label(label)
    bar.outline.set_linewidth(0.6)
    return fig


def main():
    hours = grid()
    if "--grid" in sys.argv:
        print(json.dumps(hours.round(9).tolist()))
        return
    style()
    figures = {
        "figure-1": figure_lines(),
        "figure-2": figure_grid(hours, VIRIDIS, EDGES, "Day length (hours)", lambda v: f"{v:.1f}"),
        "figure-3": figure_grid(hours - 12, PUOR, EDGES - 12, "Difference from 12 hours (hours)", lambda v: f"{v:+.1f}"),
    }
    for name, fig in figures.items():
        fig.savefig(f"{name}.pdf", metadata={"CreationDate": None})
        fig.savefig(f"{name}.svg", metadata={"Date": None})
        fig.savefig(f"{name}.png", dpi=600)
        plt.close(fig)
        print(name)


if __name__ == "__main__":
    main()

To run it

pip install matplotlib numpy
python figures.py

How it was run

Run on 15 October 2026 with matplotlib 3.10.8 and NumPy 2.4.6, on Python 3.14.2 under Pyodide 314.0.7. Its grid of hours matched the site’s model at all 108 cells; its SVGs use the palettes’ colors and no others; every printed value reads at 4.86:1 or more on its cell.

Licences

What each palette lets you do

As each source states it. Where a source states no licence, so does this table.

Each palette's source and licence
PaletteFromLicenceWhat it asks
Okabe–ItoMasataka Okabe and Kei Ito, Color Universal Design (2002, revised 2008)None stated for the paletteThe authors ask that use of their teaching files mention Masataka Okabe and Kei Ito. Cite their page.
viridisStéfan van der Walt and Nathaniel Smith (2015), in matplotlibCC0 1.0Nothing.
cividisJamie Nuñez, Christopher Anderton and Ryan Renslow (2018), PLOS ONECC0, as published with the articleNothing.
PuOrColorBrewer: Cynthia Brewer, Mark Harrower and The Pennsylvania State University (2002)Apache License 2.0If you redistribute the scheme’s values, the licence’s conditions apply, including keeping its notice.
These figures and figures.pyAuric ArtisanCC0 1.0Nothing.

Criteria

WCAG criteria it helps with

Found a mistake? Write to us and we will correct it.