"""Shared figure style for WP36 (vector-first, print-safe, validated palette). Categorical slots are assigned in FIXED order (never cycled): series-1 blue, series-2 orange, series-3 aqua, series-4 yellow, series-5 magenta, series-6 green, series-7 violet, series-8 red. Sequential = blue ramp; diverging = blue <-> red with neutral grey midpoint. Validated with the dataviz palette validator (light mode, adjacent pairs: worst CVD dE 9.1, normal-vision 19.6). """ from __future__ import annotations import matplotlib as mpl import matplotlib.pyplot as plt from matplotlib.colors import LinearSegmentedColormap, ListedColormap CAT = ["#2a78d6", "#eb6834", "#1baf7a", "#eda100", "#e87ba4", "#008300", "#6250d6", "#e34948"] SEQ = ["#cde2fb", "#b7d3f6", "#9ec5f4", "#86b6ef", "#6da7ec", "#5598e7", "#3987e5", "#2a78d6", "#256abf", "#1c5cab", "#184f95", "#104281", "#0d366b"] DIV = ["#0d366b", "#2a78d6", "#86b6ef", "#f0efec", "#f0a3a2", "#e34948", "#8a1f1f"] SURFACE = "#fcfcfb" TEXT = "#0b0b0b" TEXT2 = "#52514e" GRID = "#e6e5e1" NEUTRAL = "#9a9892" cmap_seq = LinearSegmentedColormap.from_list("ucml_seq", SEQ) cmap_div = LinearSegmentedColormap.from_list("ucml_div", DIV) cmap_cat = ListedColormap(CAT) # region fixed colour order (identity follows the entity, never its rank) REGIONS = ["Montréal", "Laval", "Montérégie", "Laurentides", "Lanaudière", "Capitale-Nationale", "Chaudière-Appalaches", "Estrie", "Mauricie", "Centre-du-Québec", "Outaouais", "Bas-Saint-Laurent", "Saguenay–Lac-Saint-Jean", "Abitibi-Témiscamingue", "Côte-Nord", "Gaspésie–Îles-de-la-Madeleine", "Nord-du-Québec"] POWERTRAIN_ORDER = ["ICE", "HEV", "PHEV", "BEV", "Diesel", "unknown"] BODY_ORDER = ["SUV", "Sedan", "Pickup", "Hatchback", "Van", "Coupe", "Convertible", "Wagon"] CLASS_ORDER = ["direct_dealer", "full_portal", "banner", "sampled_portal", "private_marketplace", "small_portal"] CLASS_LABEL = {"direct_dealer": "Dealer websites", "full_portal": "Full-inventory portals", "banner": "Dealer groups", "sampled_portal": "Sampled portals (excluded)", "private_marketplace": "Private sellers (Kijiji)", "small_portal": "Small portals"} def color_for(order: list[str], key: str) -> str: """Fixed slot per entity; entities beyond slot 8 fold into neutral grey.""" try: i = order.index(key) except ValueError: return NEUTRAL return CAT[i] if i < len(CAT) else NEUTRAL def setup() -> None: mpl.rcParams.update({ "figure.facecolor": SURFACE, "axes.facecolor": SURFACE, "savefig.facecolor": SURFACE, "font.family": "serif", "font.serif": ["Times New Roman", "Times", "Nimbus Roman", "DejaVu Serif"], "font.size": 9, "axes.edgecolor": GRID, "axes.linewidth": 0.6, "axes.labelcolor": TEXT, "axes.titlesize": 9.5, "axes.titleweight": "bold", "axes.grid": True, "grid.color": GRID, "grid.linewidth": 0.5, "axes.axisbelow": True, "axes.spines.top": False, "axes.spines.right": False, "xtick.color": TEXT2, "ytick.color": TEXT2, "xtick.labelsize": 8, "ytick.labelsize": 8, "legend.frameon": False, "legend.fontsize": 8, "lines.linewidth": 1.6, "lines.markersize": 4, "patch.linewidth": 0.0, "pdf.fonttype": 42, "ps.fonttype": 42, "figure.dpi": 150, "savefig.dpi": 300, "figure.constrained_layout.use": True, }) def save(fig, stem: str, figdir=None) -> None: """Save vector PDF (paper) + PNG (preview) under figures/ and paper/figures/.""" from . import config as C import shutil figdir = figdir or C.FIGURES figdir.mkdir(parents=True, exist_ok=True) fig.savefig(figdir / f"{stem}.pdf") fig.savefig(figdir / f"{stem}.png") (C.ROOT / "paper" / "figures").mkdir(parents=True, exist_ok=True) shutil.copy(figdir / f"{stem}.pdf", C.ROOT / "paper" / "figures" / f"{stem}.pdf") plt.close(fig)