"""Style graphique du traité (Matplotlib). Palette validée : styles/palette.json. Usage : from traite.style import setup, palette, savefig, LARGEUR, DEMI setup(); fig, ax = plt.subplots(figsize=(LARGEUR, 3.2)); ... savefig(fig, "ch16/ch16-fig-x.pdf")""" from __future__ import annotations import json, pathlib import matplotlib as mpl import matplotlib.pyplot as plt from matplotlib import font_manager ROOT = pathlib.Path(__file__).resolve().parents[3] _P = json.loads((ROOT / "styles/palette.json").read_text()) LARGEUR = 135 / 25.4 # pouces (pleine largeur de texte) DEMI = 65 / 25.4 class _Palette: cat = _P["categorical"]; seq = _P["sequential_blue"]; div = _P["diverging"]; status = _P["status"] surface = _P["surface"]; ink = _P["text_primary"]; ink2 = _P["text_secondary"]; grid = _P["grid"] brand = _P["brand_primary"]; brand2 = _P["brand_secondary"] @staticmethod def cmap_seq(): return mpl.colors.LinearSegmentedColormap.from_list("traite_seq", _P["sequential_blue"]) @staticmethod def cmap_div(): d = _P["diverging"]; return mpl.colors.LinearSegmentedColormap.from_list("traite_div", [d["neg"], d["mid"], d["pos"]]) palette = _Palette() def _register_fonts(): import subprocess for name in ("SourceSans3-Regular.otf", "SourceSans3-Bold.otf", "SourceSans3-It.otf", "SourceSans3-Semibold.otf"): try: p = subprocess.run(["kpsewhich", name], capture_output=True, text=True, timeout=10).stdout.strip() if p: font_manager.fontManager.addfont(p) except Exception: pass def setup(): _register_fonts() fam = "Source Sans 3" if any(f.name == "Source Sans 3" for f in font_manager.fontManager.ttflist) else "DejaVu Sans" mpl.rcParams.update({ "font.family": fam, "font.size": 8.5, "axes.titlesize": 9, "axes.labelsize": 8.5, "legend.fontsize": 8, "xtick.labelsize": 8, "ytick.labelsize": 8, "figure.facecolor": palette.surface, "axes.facecolor": palette.surface, "savefig.facecolor": palette.surface, "axes.edgecolor": palette.ink2, "axes.labelcolor": palette.ink, "xtick.color": palette.ink2, "ytick.color": palette.ink2, "text.color": palette.ink, "axes.spines.top": False, "axes.spines.right": False, "axes.grid": True, "grid.color": palette.grid, "grid.linewidth": 0.6, "axes.axisbelow": True, "lines.linewidth": 1.6, "lines.markersize": 5, "axes.prop_cycle": mpl.cycler(color=palette.cat), "legend.frameon": False, "figure.dpi": 150, "pdf.fonttype": 42, "axes.formatter.use_locale": False, "axes.unicode_minus": True, }) def fmt_dollars(x, pos=None): """Formate 452500 → « 452 500 $ » (espace fine insécable).""" s = f"{x:,.0f}".replace(",", " ") return f"{s} $" def fmt_milliers(x, pos=None): return f"{x:,.0f}".replace(",", " ") def fmt_pct(x, pos=None, dec=1): return (f"{x*100:.{dec}f}".replace(".", ",")) + " %" def savefig(fig, rel_path: str, **kw): out = ROOT / "assets/figures" / rel_path out.parent.mkdir(parents=True, exist_ok=True) fig.savefig(out, bbox_inches="tight", **kw) plt.close(fig) return out