"""WP44 figure style (matplotlib, vector PDF). Palette = validated reference palette of the dataviz skill (categorical slots in fixed order, validated 2026-11-03 with scripts/validate_palette.js, light surface #fcfcfb). Rules applied everywhere: one y-axis per panel (no dual axes), thin marks, recessive grid, direct labels for <= 4 series, legend when >= 2 series, sequential = single blue hue, diverging = blue/orange with neutral midpoint, text in ink tokens.""" import matplotlib as mpl import matplotlib.pyplot as plt CATEGORICAL = ["#2a78d6", "#eb6834", "#1baf7a", "#eda100", "#e87ba4", "#008300", "#6250d6", "#e34948"] SEQUENTIAL = ["#cde2fb", "#b7d3f6", "#9ec5f4", "#86b6ef", "#6da7ec", "#5598e7", "#3987e5", "#2a78d6", "#256abf", "#1c5cab", "#184f95", "#104281", "#0d366b"] DIVERGING = ["#0d366b", "#2a78d6", "#86b6ef", "#f0efec", "#f7a97f", "#eb6834", "#8a3412"] INK, INK2, MUTED, GRID, AXIS, SURFACE = "#0b0b0b", "#52514e", "#898781", "#e1e0d9", "#c3c2b7", "#fcfcfb" EVENT = "#52514e" # reference-date marker (ChatGPT release), never a series colour def apply(): mpl.rcParams.update({ "figure.dpi": 120, "savefig.dpi": 300, "figure.facecolor": "white", "axes.facecolor": "white", "font.family": "serif", "font.size": 9, "axes.titlesize": 10, "axes.labelsize": 9, "legend.fontsize": 8, "xtick.labelsize": 8, "ytick.labelsize": 8, "axes.edgecolor": AXIS, "axes.linewidth": 0.6, "axes.spines.top": False, "axes.spines.right": False, "axes.grid": True, "grid.color": GRID, "grid.linewidth": 0.5, "axes.axisbelow": True, "xtick.color": MUTED, "ytick.color": MUTED, "axes.labelcolor": INK2, "text.color": INK, "lines.linewidth": 1.4, "lines.markersize": 4, "legend.frameon": False, "axes.prop_cycle": mpl.cycler(color=CATEGORICAL), "pdf.fonttype": 42, "ps.fonttype": 42, }) def seq_cmap(): return mpl.colors.LinearSegmentedColormap.from_list("wp44_seq", SEQUENTIAL) def div_cmap(): return mpl.colors.LinearSegmentedColormap.from_list("wp44_div", DIVERGING) def event_line(ax, x, label="ChatGPT release (2022-11-30)"): ax.axvline(x, color=EVENT, linewidth=0.8, linestyle="--", zorder=1) ax.annotate(label, xy=(x, 1.0), xycoords=("data", "axes fraction"), xytext=(3, -2), textcoords="offset points", fontsize=7, color=INK2, va="top") def save(fig, path): fig.savefig(path, bbox_inches="tight") if path.endswith(".pdf"): fig.savefig(path[:-4] + ".png", bbox_inches="tight") plt.close(fig)