Spaces:
Sleeping
Sleeping
| """Project visual system. Single source of truth for matplotlib + Plotly + graphviz. | |
| **Linear / Vercel anchor (dark · clinical · projection-sized).** | |
| Anchor decided 2026-05-09 in design.md §1. Previous Stripe Press tokens are | |
| preserved as ``src/plot_style_stripe.py.bak`` and can be restored by | |
| ``cp src/plot_style_stripe.py.bak src/plot_style.py``. | |
| Public surface unchanged — every name imported by the rest of the project is | |
| still exported with the same identifier, only the *values* changed: | |
| BG, SURFACE, RULE, INK, INK_MUTED, INK_FAINT, ACCENT | |
| CLUSTER_FILLS, CLUSTER_MARKERS, CENTROID_COLOR, INK_RAMP | |
| FONT_DISPLAY, FONT_BODY, FONT_MONO | |
| apply_style, use_mono_ticks, plotly_template, GRAPHVIZ_DEFAULTS | |
| The discipline that defines this anchor: | |
| 1. Single accent (``ACCENT``) reserved for SIGNIFICANT outcomes only — | |
| p ≤ 0.05 cells, the winning configuration row, the live-predict | |
| assigned cluster, active selections. Everything else is grayscale. | |
| 2. Type sized for ~3-5 m projection: tick fonts ≥ 14px, body ≥ 16px, | |
| headlines 28-40px. No 12px text anywhere. | |
| 3. Backgrounds are warm-near-black (#08090A), not pure #000 — pure | |
| black on a projector reads as a void; a hint of warmth keeps the | |
| surface feeling like a real product. | |
| """ | |
| from __future__ import annotations | |
| import warnings | |
| from pathlib import Path | |
| import matplotlib as mpl | |
| import matplotlib.font_manager as fm | |
| import plotly.graph_objects as go | |
| # === Linear / Vercel dark tokens (locked 2026-05-09) =================== | |
| BG = "#08090A" # canvas — near-black, slightly warm | |
| SURFACE = "#101113" # cards, sidebar | |
| SURFACE_RAISED = "#16171A" # hover / active row | |
| RULE = "#1F2023" # dividers, axis lines, table borders | |
| INK = "#F4F4F5" # primary text | |
| INK_MUTED = "#A1A1AA" # secondary text, axis labels | |
| INK_FAINT = "#71717A" # tertiary text, captions | |
| ACCENT = "#22D3EE" # single accent — cyan, used only for SIGNIFICANCE | |
| ACCENT_DIM = "#0E7490" # accent backgrounds, hover states for accent elements | |
| POSITIVE = "#34D399" # tertiary signal — gain / improvement (sparingly) | |
| NEGATIVE = "#F87171" # tertiary signal — loss / regression (sparingly) | |
| # === Cluster mapping =================================================== | |
| # On a dark canvas the contrast logic flips: the "primary" cluster gets the | |
| # brightest fill (INK ≈ white), with monotone steps down. The accent (cyan) | |
| # is reserved for centroids and active-selection halo, NOT for cluster | |
| # membership — so a single cyan hit on a chart always means "this is the one | |
| # you should look at" (winning config, predicted assignment, p ≤ 0.05). | |
| CLUSTER_FILLS = { | |
| 0: INK, # primary cluster (post-canonical-remap) | |
| 1: INK_MUTED, # secondary | |
| 2: INK_FAINT, # tertiary | |
| -1: "#3F3F46", # noise (HDBSCAN) | |
| } | |
| CLUSTER_MARKERS = {0: "o", 1: "s", 2: "^", -1: "x"} | |
| CENTROID_COLOR = ACCENT | |
| INK_RAMP = [INK, INK_MUTED, INK_FAINT, ACCENT_DIM] | |
| # === Font stacks ======================================================= | |
| # Inter throughout — Linear-faithful, projection-legible. JetBrains Mono | |
| # for tabular numerics so silhouette / ARI / p-values align column-wise. | |
| FONT_DISPLAY = ["Inter", "system-ui", "-apple-system", "Segoe UI", "Helvetica Neue", "sans-serif"] | |
| FONT_BODY = ["Inter", "system-ui", "-apple-system", "Segoe UI", "Helvetica Neue", "sans-serif"] | |
| FONT_MONO = ["JetBrains Mono", "SF Mono", "Menlo", "Consolas", "monospace"] | |
| # === Bundled-font registration ========================================= | |
| def _register_bundled_fonts() -> None: | |
| """Load bundled .ttf fonts so PNG output is deterministic across machines.""" | |
| fonts_dir = Path(__file__).resolve().parent.parent / "assets" / "fonts" | |
| if not fonts_dir.exists(): | |
| return | |
| for ttf in fonts_dir.glob("*.ttf"): | |
| try: | |
| fm.fontManager.addfont(str(ttf)) | |
| except Exception: # pragma: no cover | |
| pass | |
| def _check_fonts() -> None: | |
| """Warn once if Inter / JetBrains Mono aren't available after registration.""" | |
| available = {f.name for f in fm.fontManager.ttflist} | |
| required = {"Inter", "JetBrains Mono"} | |
| missing = required - available | |
| if missing: | |
| warnings.warn( | |
| f"Required fonts missing — figures will fall back: {missing}. " | |
| f"Install via assets/fonts/*.ttf or system font manager.", | |
| stacklevel=2, | |
| ) | |
| _register_bundled_fonts() | |
| _check_fonts() | |
| # === matplotlib rcParams =============================================== | |
| def apply_style() -> None: | |
| """Apply project rcParams. Idempotent — call once per script entrypoint. | |
| NOTE on tabular figures: rcParams cannot enforce monospace digit alignment | |
| in matplotlib because the text renderer doesn't activate OpenType ``tnum`` | |
| features. The discipline is :func:`use_mono_ticks` on any axes showing | |
| decimals — that's what actually delivers tabular alignment. | |
| """ | |
| mpl.rcParams.update({ | |
| # --- Type ramp (sized for 3-5 m projection) --------------------- | |
| "font.family": "sans-serif", | |
| "font.sans-serif": FONT_DISPLAY, | |
| "font.size": 14, | |
| "axes.titlesize": 18, | |
| "axes.titleweight": 600, | |
| "axes.labelsize": 14, | |
| "xtick.labelsize": 13, | |
| "ytick.labelsize": 13, | |
| "legend.fontsize": 13, | |
| "figure.titlesize": 22, | |
| "figure.titleweight": 600, | |
| # --- Backgrounds ------------------------------------------------ | |
| "figure.facecolor": BG, | |
| "axes.facecolor": BG, | |
| "savefig.facecolor": BG, | |
| "savefig.edgecolor": "none", | |
| "savefig.bbox": "tight", | |
| "savefig.dpi": 200, | |
| # --- Spines (explicit; do not inherit user matplotlibrc) -------- | |
| "axes.spines.top": False, | |
| "axes.spines.right": False, | |
| "axes.spines.left": True, | |
| "axes.spines.bottom": True, | |
| "axes.edgecolor": RULE, | |
| "axes.linewidth": 0.8, | |
| "axes.labelcolor": INK_MUTED, | |
| "axes.titlecolor": INK, | |
| "xtick.color": INK_MUTED, | |
| "ytick.color": INK_MUTED, | |
| "xtick.direction": "out", | |
| "ytick.direction": "out", | |
| # --- Grid (subtle) ---------------------------------------------- | |
| "axes.grid": True, | |
| "axes.grid.axis": "y", | |
| "grid.color": RULE, | |
| "grid.linewidth": 0.5, | |
| # --- Color cycle + legend frame --------------------------------- | |
| "axes.prop_cycle": mpl.cycler(color=INK_RAMP), | |
| "legend.frameon": False, | |
| "text.color": INK, | |
| }) | |
| def use_mono_ticks(ax) -> None: | |
| """Switch numeric tick labels to JetBrains Mono so decimals align.""" | |
| for label in ax.get_xticklabels() + ax.get_yticklabels(): | |
| label.set_family(FONT_MONO) | |
| # === Plotly template =================================================== | |
| def plotly_template() -> go.layout.Template: | |
| """Return a Plotly template matching the matplotlib system. Apply via: | |
| import plotly.io as pio | |
| pio.templates["int396"] = plotly_template() | |
| pio.templates.default = "int396" | |
| """ | |
| sans = "Inter, system-ui, sans-serif" | |
| mono = "JetBrains Mono, SF Mono, Menlo, monospace" | |
| return go.layout.Template( | |
| layout=go.Layout( | |
| font=dict(family=sans, size=14, color=INK), | |
| title=dict( | |
| font=dict(family=sans, size=20, color=INK, weight=600), | |
| x=0.0, | |
| xanchor="left", | |
| ), | |
| paper_bgcolor=BG, | |
| plot_bgcolor=BG, | |
| colorway=INK_RAMP, | |
| xaxis=dict( | |
| gridcolor=RULE, | |
| gridwidth=0.5, | |
| linecolor=RULE, | |
| tickcolor=RULE, | |
| tickfont=dict(family=mono, size=13, color=INK_MUTED), | |
| title=dict(font=dict(family=sans, size=14, color=INK_MUTED)), | |
| zeroline=False, | |
| ), | |
| yaxis=dict( | |
| gridcolor=RULE, | |
| gridwidth=0.5, | |
| linecolor=RULE, | |
| tickcolor=RULE, | |
| tickfont=dict(family=mono, size=13, color=INK_MUTED), | |
| title=dict(font=dict(family=sans, size=14, color=INK_MUTED)), | |
| zeroline=False, | |
| ), | |
| legend=dict( | |
| bgcolor="rgba(0,0,0,0)", | |
| bordercolor="rgba(0,0,0,0)", | |
| font=dict(size=13, color=INK_MUTED), | |
| ), | |
| margin=dict(l=56, r=24, t=56, b=56), | |
| hoverlabel=dict( | |
| bgcolor=SURFACE_RAISED, | |
| bordercolor=RULE, | |
| font=dict(family=sans, size=13, color=INK), | |
| ), | |
| ) | |
| ) | |
| # === Graphviz defaults ================================================= | |
| GRAPHVIZ_DEFAULTS = dict( | |
| graph_attr={ | |
| "bgcolor": BG, | |
| "fontname": "Inter", | |
| "fontsize": "13", | |
| "rankdir": "LR", | |
| "nodesep": "0.4", | |
| "ranksep": "0.6", | |
| "pad": "0.4", | |
| "fontcolor": INK, | |
| }, | |
| node_attr={ | |
| "shape": "box", | |
| "style": "filled,rounded", | |
| "fillcolor": SURFACE, | |
| "color": RULE, | |
| "fontname": "Inter", | |
| "fontsize": "12", | |
| "fontcolor": INK, | |
| "penwidth": "0.8", | |
| "margin": "0.2,0.12", | |
| }, | |
| edge_attr={ | |
| "color": INK_FAINT, | |
| "penwidth": "0.7", | |
| "arrowsize": "0.6", | |
| "fontname": "JetBrains Mono", | |
| "fontsize": "10", | |
| "fontcolor": INK_MUTED, | |
| }, | |
| ) | |
| __all__ = [ | |
| "BG", "SURFACE", "SURFACE_RAISED", "RULE", | |
| "INK", "INK_MUTED", "INK_FAINT", "ACCENT", "ACCENT_DIM", | |
| "POSITIVE", "NEGATIVE", | |
| "CLUSTER_FILLS", "CLUSTER_MARKERS", "CENTROID_COLOR", "INK_RAMP", | |
| "FONT_DISPLAY", "FONT_BODY", "FONT_MONO", | |
| "apply_style", "use_mono_ticks", "plotly_template", | |
| "GRAPHVIZ_DEFAULTS", | |
| ] | |