thibaud frere commited on
Commit
df95a5c
·
1 Parent(s): f6bb916

update charts

Browse files
app/src/components/Palettes.astro CHANGED
@@ -1,169 +0,0 @@
1
- ---
2
- const rootId = `palettes-${Math.random().toString(36).slice(2)}`;
3
- ---
4
- <div class="palettes" id={rootId} style="width:100%; margin: 10px 0;">
5
- <style is:global>
6
- .palettes { box-sizing: border-box; }
7
- .palettes .palettes__grid { display: grid; grid-template-columns: 1fr; gap: 12px; max-width: 100%; }
8
- .palettes .palette-card { position: relative; display: grid; grid-template-columns: 1fr minmax(0, 220px); align-items: stretch; gap: 12px; border: 1px solid var(--border-color); border-radius: 10px; background: var(--surface-bg); padding: 12px; transition: box-shadow .18s ease, transform .18s ease, border-color .18s ease; min-height: 60px; }
9
- .palettes .palette-card__preview { width: 48px; height: 48px; border-radius: 999px; flex: 0 0 auto; background-size: cover; background-position: center; }
10
- .palettes .palette-card__copy { position: absolute; top: 50%; left: 100%; transform: translateY(-50%); z-index: 3; border-left: none; border-top-left-radius: 0; border-bottom-left-radius: 0; }
11
- .palettes .palette-card__copy svg { width: 18px; height: 18px; fill: currentColor; display: block; color: inherit; }
12
- .palettes .palette-card__swatches { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); grid-auto-rows: 1fr; gap: 2px; margin: 0; min-height: 60px; }
13
- .palettes .palette-card__swatches .sw { width: 100%; min-width: 0; height: auto; border-radius: 0; border: 1px solid var(--border-color); }
14
- .palettes .palette-card__swatches .sw:first-child { border-top-left-radius: 8px; border-bottom-left-radius: 8px; }
15
- .palettes .palette-card__swatches .sw:last-child { border-top-right-radius: 8px; border-bottom-right-radius: 8px; }
16
- .palettes .palette-card__content { display: flex; flex-direction: row; align-items: center; justify-content: flex-start; gap: 12px; min-width: 0; padding-right: 12px; }
17
- .palettes .palette-card__preview { width: 48px; height: 48px; border-radius: 999px; position: relative; flex: 0 0 auto; overflow: hidden; }
18
- .palettes .palette-card__preview .dot { position: absolute; width: 4px; height: 4px; background: #fff; border-radius: 999px; box-shadow: 0 0 0 1px var(--border-color); }
19
- .palettes .palette-card__preview .donut-hole { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); width: 24px; height: 24px; border-radius: 999px; background: var(--surface-bg); box-shadow: 0 0 0 1px var(--border-color) inset; }
20
-
21
- .palettes .palette-card__content__info { display: flex; flex-direction: column; }
22
- .palettes .palette-card__title { text-align: left; font-weight: 800; font-size: 15px; }
23
- .palettes .palette-card__desc { text-align: left; color: var(--muted-color); line-height: 1.5; font-size: 12px; }
24
-
25
- .palettes .palettes__select { width: 100%; max-width: 100%; border: 1px solid var(--border-color); background: var(--surface-bg); color: var(--text-color); padding: 8px 10px; border-radius: 8px; }
26
- .palettes .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 1px, 1px); white-space: nowrap; border: 0; }
27
- .palettes .palettes__controls { display: flex; flex-wrap: wrap; gap: 16px; align-items: center; margin: 8px 0 14px; }
28
- .palettes .palettes__field { display: flex; flex-direction: column; gap: 6px; min-width: 0; flex: 1 1 280px; max-width: 100%; }
29
- .palettes .palettes__label { font-size: 12px; color: var(--muted-color); font-weight: 800; }
30
- .palettes .palettes__label-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
31
- .palettes .ghost-badge { font-size: 11px; padding: 1px 6px; border-radius: 999px; border: 1px solid var(--border-color); color: var(--muted-color); background: transparent; font-variant-numeric: tabular-nums; }
32
- .palettes .palettes__count { display: flex; align-items: center; gap: 8px; max-width: 100%; }
33
- .palettes .palettes__count input[type="range"] { width: 100%; }
34
- .palettes .palettes__count output { min-width: 28px; text-align: center; font-variant-numeric: tabular-nums; font-size: 12px; color: var(--muted-color); }
35
- .palettes input[type="range"] { -webkit-appearance: none; appearance: none; height: 24px; background: transparent; cursor: pointer; accent-color: var(--primary-color); }
36
- .palettes input[type="range"]:focus { outline: none; }
37
- .palettes input[type="range"]::-webkit-slider-runnable-track { height: 6px; background: var(--border-color); border-radius: 999px; }
38
- .palettes input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; margin-top: -6px; width: 18px; height: 18px; background: var(--primary-color); border: 2px solid var(--surface-bg); border-radius: 50%; }
39
- .palettes input[type="range"]::-moz-range-track { height: 6px; background: var(--border-color); border: none; border-radius: 999px; }
40
- .palettes input[type="range"]::-moz-range-progress { height: 6px; background: var(--primary-color); border-radius: 999px; }
41
- .palettes input[type="range"]::-moz-range-thumb { width: 18px; height: 18px; background: var(--primary-color); border: 2px solid var(--surface-bg); border-radius: 50%; }
42
- html.cb-grayscale, body.cb-grayscale { filter: grayscale(1) !important; }
43
- html.cb-protanopia, body.cb-protanopia { filter: url(#cb-protanopia) !important; }
44
- html.cb-deuteranopia, body.cb-deuteranopia { filter: url(#cb-deuteranopia) !important; }
45
- html.cb-tritanopia, body.cb-tritanopia { filter: url(#cb-tritanopia) !important; }
46
- html.cb-achromatopsia, body.cb-achromatopsia { filter: url(#cb-achromatopsia) !important; }
47
- @media (max-width: 1100px) { .palettes .palette-card { grid-template-columns: 1fr; align-items: stretch; gap: 10px; } .palettes .palette-card__swatches { grid-template-columns: repeat(6, minmax(0, 1fr)); } .palettes .palette-card__content { border-right: none; padding-right: 0; } .palettes .palette-card__copy { display: none; } }
48
- </style>
49
- <div class="palettes__controls">
50
- <div class="palettes__field">
51
- <label class="palettes__label" for="cb-select">Color vision simulation</label>
52
- <select id="cb-select" class="palettes__select">
53
- <option value="none">Normal color vision — typical for most people</option>
54
- <option value="achromatopsia">Achromatopsia — no color at all</option>
55
- <option value="protanopia">Protanopia — reduced/absent reds</option>
56
- <option value="deuteranopia">Deuteranopia — reduced/absent greens</option>
57
- <option value="tritanopia">Tritanopia — reduced/absent blues</option>
58
- </select>
59
- </div>
60
- <div class="palettes__field">
61
- <div class="palettes__label-row">
62
- <label class="palettes__label" for="color-count">Number of colors</label>
63
- <output id="color-count-out" for="color-count" class="ghost-badge">8</output>
64
- </div>
65
- <div class="palettes__count">
66
- <input id="color-count" type="range" min="6" max="10" step="1" value="8" aria-label="Number of colors" />
67
- </div>
68
- </div>
69
- </div>
70
- <div class="palettes__grid"></div>
71
- <div class="palettes__simu" role="group" aria-labelledby="cb-sim-title">
72
- <svg aria-hidden="true" focusable="false" width="0" height="0" style="position:absolute; left:-9999px; overflow:hidden;">
73
- <defs>
74
- <filter id="cb-protanopia"><feColorMatrix type="matrix" values="0.567 0.433 0 0 0 0.558 0.442 0 0 0 0 0.242 0.758 0 0 0 0 0 1 0"/></filter>
75
- <filter id="cb-deuteranopia"><feColorMatrix type="matrix" values="0.625 0.375 0 0 0 0.7 0.3 0 0 0 0 0.3 0.7 0 0 0 0 0 1 0"/></filter>
76
- <filter id="cb-tritanopia"><feColorMatrix type="matrix" values="0.95 0.05 0 0 0 0 0.433 0.567 0 0 0 0.475 0.525 0 0 0 0 0 1 0"/></filter>
77
- <filter id="cb-achromatopsia"><feColorMatrix type="matrix" values="0.299 0.587 0.114 0 0 0.299 0.587 0.114 0 0 0.299 0.587 0.114 0 0 0 0 0 1 0"/></filter>
78
- </defs>
79
- </svg>
80
- </div>
81
- </div>
82
- <script type="module" is:inline>
83
- import '/scripts/color-palettes.js';
84
- const ROOT_ID = "{rootId}";
85
- (() => {
86
- const cards = [
87
- { key: 'categorical', title: 'Categorical', desc: 'For <strong>non‑numeric categories</strong>; <strong>visually distinct</strong> colors.' },
88
- { key: 'sequential', title: 'Sequential', desc: 'For <strong>numeric scales</strong>; gradient from <strong>dark to light</strong>. Ideal for <strong>heatmaps</strong>.' },
89
- { key: 'diverging', title: 'Diverging', desc: 'For numeric scales with negative and positive; Opposing extremes with smooth contrast around a neutral midpoint.' }
90
- ];
91
- const getPaletteColors = (key, count) => {
92
- const total=Number(count)||6;
93
- if (window.ColorPalettes && typeof window.ColorPalettes.getColors==='function') {
94
- return window.ColorPalettes.getColors(key,total) || [];
95
- }
96
- return [];
97
- };
98
- const render = () => {
99
- const root = document.getElementById(ROOT_ID) || document.querySelector('.palettes');
100
- if (!root) return;
101
- const grid=root.querySelector('.palettes__grid'); if (!grid) return;
102
- const input=document.getElementById('color-count'); const total=input ? Number(input.value)||6 : 6;
103
- const html = cards.map(c => {
104
- const colors=getPaletteColors(c.key,total);
105
- const swatches=colors.map(col=>`<div class=\"sw\" style=\"background:${col}\"></div>`).join('');
106
- const baseHex = (window.ColorPalettes && typeof window.ColorPalettes.getPrimary==='function') ? window.ColorPalettes.getPrimary() : (colors[0] || '#FF0000');
107
- const hueDeg = (()=>{ try { const s=baseHex.replace('#',''); const v=s.length===3?s.split('').map(ch=>ch+ch).join(''):s; const r=parseInt(v.slice(0,2),16)/255, g=parseInt(v.slice(2,4),16)/255, b=parseInt(v.slice(4,6),16)/255; const M=Math.max(r,g,b), m=Math.min(r,g,b), d=M-m; if (d===0) return 0; let h=0; if (M===r) h=((g-b)/d)%6; else if (M===g) h=(b-r)/d+2; else h=(r-g)/d+4; h*=60; if (h<0) h+=360; return h; } catch { return 0; } })();
108
- const gradient = c.key==='categorical'
109
- ? (() => {
110
- const steps = 60; // smooth hue wheel (fixed orientation)
111
- const wheel = Array.from({ length: steps }, (_, i) => `hsl(${Math.round((i/steps)*360)}, 100%, 50%)`).join(', ');
112
- return `conic-gradient(${wheel})`;
113
- })()
114
- : (colors.length ? `linear-gradient(90deg, ${colors.join(', ')})` : `linear-gradient(90deg, var(--border-color), var(--border-color))`);
115
- const previewInner = (()=>{
116
- if (c.key !== 'categorical' || !colors.length) return '';
117
- const ring = 18; const cx = 24; const cy = 24; const offset = (hueDeg/360) * 2 * Math.PI;
118
- return colors.map((col,i)=>{
119
- const angle = offset + (i/colors.length) * 2 * Math.PI;
120
- const x = cx + ring * Math.cos(angle);
121
- const y = cy + ring * Math.sin(angle);
122
- return `<span class=\"dot\" style=\"left:${x-2}px; top:${y-2}px\"></span>`;
123
- }).join('');
124
- })();
125
- const donutHole = (c.key === 'categorical') ? '<span class=\"donut-hole\"></span>' : '';
126
- return `
127
- <div class="palette-card" data-colors="${colors.join(',')}">
128
- <div class="palette-card__content">
129
- <div class=\"palette-card__preview\" aria-hidden=\"true\" style=\"background:${gradient}\">${previewInner}${donutHole}</div>
130
- <div class="palette-card__content__info">
131
- <div class="palette-card__title">${c.title}</div>
132
- <div class="palette-card__desc">${c.desc}</div>
133
- </div>
134
- </div>
135
- <div class="palette-card__swatches" style="grid-template-columns: repeat(${colors.length}, minmax(0, 1fr));">${swatches}</div>
136
- <button class="palette-card__copy button--ghost" type="button" aria-label="Copy palette">
137
- <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M16 1H4c-1.1 0-2 .9-2 2v12h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>
138
- </button>
139
- </div>`;
140
- }).join('');
141
- grid.innerHTML=html;
142
- };
143
- const MODE_TO_CLASS = { protanopia:'cb-protanopia', deuteranopia:'cb-deuteranopia', tritanopia:'cb-tritanopia', achromatopsia:'cb-achromatopsia' };
144
- const CLEAR_CLASSES = Object.values(MODE_TO_CLASS);
145
- const clearCbClasses = () => { const rootEl=document.documentElement; CLEAR_CLASSES.forEach(cls=>rootEl.classList.remove(cls)); };
146
- const applyCbClass = (mode) => { clearCbClasses(); const cls=MODE_TO_CLASS[mode]; if (cls) document.documentElement.classList.add(cls); };
147
- const currentCbMode = () => { const rootEl=document.documentElement; for (const [mode, cls] of Object.entries(MODE_TO_CLASS)) { if (rootEl.classList.contains(cls)) return mode; } return 'none'; };
148
- const setupCbSim = () => { const select=document.getElementById('cb-select'); if (!select) return; try { select.value=currentCbMode(); } catch{} select.addEventListener('change', () => applyCbClass(select.value)); };
149
- const setupCountControl = () => { const input=document.getElementById('color-count'); const out=document.getElementById('color-count-out'); if (!input) return; const clamp=(n,min,max)=>Math.max(min,Math.min(max,n)); const read=()=>clamp(Number(input.value)||6,6,10); const syncOut=()=>{ if (out) out.textContent=String(read()); }; const onChange=()=>{ syncOut(); render(); }; syncOut(); input.addEventListener('input', onChange); document.addEventListener('palettes:updated', () => { syncOut(); render(); }); };
150
- let copyDelegationSetup=false; const setupCopyDelegation = () => { if (copyDelegationSetup) return; const grid=document.querySelector('.palettes .palettes__grid'); if (!grid) return; grid.addEventListener('click', async (e) => { const btn = e.target.closest ? e.target.closest('.palette-card__copy') : null; if (!btn) return; const card = btn.closest('.palette-card'); if (!card) return; const colors=(card.dataset.colors||'').split(',').filter(Boolean); const json=JSON.stringify(colors,null,2); try { await navigator.clipboard.writeText(json); const old=btn.innerHTML; btn.innerHTML='<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9 16.2l-3.5-3.5-1.4 1.4L9 19 20.3 7.7l-1.4-1.4z"/></svg>'; setTimeout(()=> btn.innerHTML=old, 900); } catch { window.prompt('Copy palette', json); } }); copyDelegationSetup=true; };
151
- const bootstrap = () => {
152
- setupCbSim();
153
- setupCountControl();
154
- setupCopyDelegation();
155
- // Render immediately
156
- render();
157
- // Re-render on palette updates
158
- document.addEventListener('palettes:updated', render);
159
- // Force an immediate notify after listeners are attached (ensures initial render)
160
- try {
161
- if (window.ColorPalettes && typeof window.ColorPalettes.notify === 'function') window.ColorPalettes.notify();
162
- else if (window.ColorPalettes && typeof window.ColorPalettes.refresh === 'function') window.ColorPalettes.refresh();
163
- } catch {}
164
- };
165
- if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', bootstrap, { once: true }); } else { bootstrap(); }
166
- })();
167
- </script>
168
-
169
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/src/content/assets/data/trackio_wandb_demo.csv CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:b63be98ff7b53f4fb9c1db279348ce1d78f901cb2dce51b1c3b10bf5b3faae4d
3
- size 6033
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9c8ae4d27d04319fb18b6b505ac13e4ae96e7bb65e9d42dea6a6b7a69a15c7e5
3
+ size 4742
app/src/content/chapters/vibe-coding-charts.mdx CHANGED
@@ -76,4 +76,10 @@ They can be found in the `app/src/content/embeds` folder and you can also use th
76
  title="Résultats TrackIO"
77
  />
78
  ---
 
 
 
 
 
 
79
 
 
76
  title="Résultats TrackIO"
77
  />
78
  ---
79
+ {/* <HtmlEmbed
80
+ src="d3-trackio-oblivion.html"
81
+ title="Résultats TrackIO"
82
+ frameless
83
+ />
84
+ --- */}
85
 
app/src/content/embeds/d3-trackio-oblivion.html ADDED
@@ -0,0 +1,385 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div class="d3-trackio-oblivion">
2
+ <div class="d3-trackio-oblivion__grid">
3
+ <div class="cell" data-metric="epoch" data-title="epoch"></div>
4
+ <div class="cell" data-metric="train_accuracy" data-title="train_accuracy"></div>
5
+ <div class="cell" data-metric="train_loss" data-title="train_loss"></div>
6
+ <div class="cell" data-metric="val_accuracy" data-title="val_accuracy"></div>
7
+ <div class="cell cell--wide" data-metric="val_loss" data-title="val_loss"></div>
8
+ </div>
9
+ <noscript>JavaScript is required to render this chart.</noscript>
10
+ </div>
11
+ <style>
12
+ @import url('https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@400;600;700&display=swap');
13
+ /* Futuristic "Oblivion"-inspired styling */
14
+ .d3-trackio-oblivion { position: relative;
15
+ --cell-gap: 0px;
16
+ --obl-cyan: #7FF1FF;
17
+ --obl-cyan-dim: rgba(127,241,255,.25);
18
+ --obl-accent: #8BF5FF;
19
+ --obl-bg: rgba(255,255,255,0.06);
20
+ --obl-border: rgba(127,241,255,.25);
21
+ --obl-glow: 0 0 0 1px rgba(127,241,255,.35), 0 8px 40px rgba(127,241,255,.12);
22
+ background: #0f1115;
23
+ --corner-inset: 6px;
24
+ --hud-gap: 10px;
25
+ --hud-corner-size: 8px;
26
+ /* Chart background gradient as a variable for easy theming */
27
+ --hud-bg-gradient: radial-gradient(1200px 200px at 20% -10%, rgba(127,241,255,.03), transparent 80%), radial-gradient(900px 200px at 80% 110%, rgba(127,241,255,.03), transparent 80%);
28
+ /* Tooltip offset (bottom-right of cursor) */
29
+ --tip-offset-x: 0px;
30
+ --tip-offset-y: 0px;
31
+ padding: var(--hud-gap);
32
+ font-family: 'Roboto Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
33
+ }
34
+ .d3-trackio-oblivion * { font-family: inherit;
35
+ }
36
+ [data-theme="dark"] .d3-trackio-oblivion { --obl-bg: rgba(12,18,22,.45); }
37
+
38
+ .d3-trackio-oblivion__grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: --cell-gap; }
39
+ @media (max-width: 980px) { .d3-trackio-oblivion__grid { grid-template-columns: 1fr; } }
40
+ .d3-trackio-oblivion__grid .cell--wide { grid-column: 1 / -1; }
41
+
42
+ .d3-trackio-oblivion .cell {
43
+ /* border: 1px solid var(--obl-border);
44
+ border-radius: 4px;
45
+ background: var(--obl-bg);
46
+ backdrop-filter: saturate(1.1) blur(10px);
47
+ box-shadow: var(--obl-glow); */
48
+ display: flex;
49
+ flex-direction: column;
50
+ position: relative;
51
+ /* Important: allow tooltip to overflow outside cell bounds */
52
+ overflow: visible;
53
+ z-index: 0;
54
+ }
55
+ .d3-trackio-oblivion .cell:hover { z-index: 50; }
56
+ /* Background and corners are explicit elements for maintainability */
57
+ .d3-trackio-oblivion .cell-bg { position: absolute; inset: var(--hud-gap); pointer-events: none; z-index: 1; background: var(--hud-bg-gradient); }
58
+ .d3-trackio-oblivion .cell-corners { position: absolute; inset: var(--corner-inset); pointer-events: none; z-index: 3; background:
59
+ linear-gradient(var(--obl-cyan), var(--obl-cyan)) top left / var(--hud-corner-size) 1px no-repeat,
60
+ linear-gradient(var(--obl-cyan), var(--obl-cyan)) top left / 1px var(--hud-corner-size) no-repeat,
61
+ linear-gradient(var(--obl-cyan), var(--obl-cyan)) top right / var(--hud-corner-size) 1px no-repeat,
62
+ linear-gradient(var(--obl-cyan), var(--obl-cyan)) top right / 1px var(--hud-corner-size) no-repeat,
63
+ linear-gradient(var(--obl-cyan), var(--obl-cyan)) bottom left / var(--hud-corner-size) 1px no-repeat,
64
+ linear-gradient(var(--obl-cyan), var(--obl-cyan)) bottom left / 1px var(--hud-corner-size) no-repeat,
65
+ linear-gradient(var(--obl-cyan), var(--obl-cyan)) bottom right / var(--hud-corner-size) 1px no-repeat,
66
+ linear-gradient(var(--obl-cyan), var(--obl-cyan)) bottom right / 1px var(--hud-corner-size) no-repeat; opacity:.85; }
67
+ .d3-trackio-oblivion .cell-inner { position: relative; z-index: 2; padding: var(--hud-corner-size) 12px 10px var(--hud-gap); display:flex; flex-direction:column; }
68
+ .d3-trackio-oblivion .cell-header { padding: 10px 12px; display: flex; align-items: center; justify-content: space-between; gap: 8px; }
69
+ .d3-trackio-oblivion .cell-title { position: relative; font-size: 12px; font-weight: 800; letter-spacing: 0.12em; text-transform: uppercase; color: var(--obl-cyan); padding-left: 14px; }
70
+ .d3-trackio-oblivion .cell-title:before { content:""; position:absolute; left:0; top:50%; transform:translateY(-50%); width: 6px; height: 6px; background: var(--obl-cyan); border: 1px solid var(--obl-border); box-shadow: 0 0 10px rgba(127,241,255,.25) inset; opacity: .5; }
71
+ .d3-trackio-oblivion .cell-body { position: relative; width: 100%; overflow: hidden; }
72
+ .d3-trackio-oblivion .cell-body svg { max-width: 100%; height: auto; display: block; }
73
+
74
+ /* Axes & grid use cyan tint */
75
+ .d3-trackio-oblivion .axes path, .d3-trackio-oblivion .axes line { stroke: var(--obl-cyan-dim); stroke-opacity: 1; }
76
+ .d3-trackio-oblivion .axes text { fill: var(--obl-cyan); opacity: .6; font-weight: 400; letter-spacing: 0.02em; }
77
+ .d3-trackio-oblivion .grid line { stroke: var(--obl-cyan-dim); stroke-opacity: .5; }
78
+
79
+ /* Legend header */
80
+ .d3-trackio-oblivion__header { display: flex; align-items: flex-start; justify-content: center; gap: 14px; margin: 0 0 10px 0; flex-wrap: wrap; width: 100%; }
81
+ .d3-trackio-oblivion__header .legend-bottom { display: flex; flex-direction: column; align-items: center; gap: 6px; font-size: 12px; color: var(--obl-cyan); text-align: center; }
82
+ .d3-trackio-oblivion__header .legend-bottom .legend-title { font-size: 11px; font-weight: 900; letter-spacing: 0.18em; color: var(--obl-cyan); text-transform: uppercase; }
83
+ .d3-trackio-oblivion__header .legend-bottom .items { display: flex; flex-wrap: wrap; gap: 8px 14px; }
84
+ .d3-trackio-oblivion__header .legend-bottom .item { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; color: var(--obl-cyan); }
85
+ .d3-trackio-oblivion__header .legend-bottom .swatch { width: 14px; height: 14px; border-radius: 2px; border: 1px solid var(--obl-border); display: inline-block; box-shadow: 0 0 12px rgba(127,241,255,.18) inset; }
86
+
87
+ /* Hover ghosting */
88
+ .d3-trackio-oblivion.hovering .lines path.ghost { opacity: .22; }
89
+ .d3-trackio-oblivion.hovering .points circle.ghost { opacity: .22; }
90
+ .d3-trackio-oblivion.hovering .areas path.ghost { opacity: .06; }
91
+ .d3-trackio-oblivion.hovering .legend-bottom .item.ghost { opacity: .35; }
92
+
93
+ /* Tooltip */
94
+ .d3-trackio-oblivion .d3-tooltip {
95
+ position: absolute;
96
+ top: 0;
97
+ left: 0;
98
+ transform: translate(-9999px, -9999px);
99
+ pointer-events: none;
100
+ padding: 10px 12px;
101
+ border-radius: 0;
102
+ font-size: 12px;
103
+ line-height: 1.35;
104
+ background: var(--obl-bg);
105
+ color: var(--obl-cyan);
106
+ box-shadow: 0 8px 32px rgba(127,241,255,.12), 0 2px 8px rgba(0,0,0,.20);
107
+ opacity: .5;
108
+ transition: opacity .12s ease;
109
+ z-index: 1000;
110
+ backdrop-filter: saturate(1.1) blur(10px);
111
+ }
112
+ .d3-trackio-oblivion .d3-tooltip.is-visible { opacity: 1; }
113
+ .d3-trackio-oblivion .d3-tooltip__inner { display: flex; flex-direction: column; gap: 6px; min-width: 220px; text-align: left; }
114
+ .d3-trackio-oblivion .d3-tooltip__inner > div:first-child { font-weight: 900; letter-spacing: .18em; text-transform: uppercase; color: var(--obl-cyan); }
115
+ .d3-trackio-oblivion .d3-tooltip__inner > div:nth-child(2) { font-size: 11px; color: var(--obl-cyan); opacity: .8; display: block; margin-top: -4px; margin-bottom: 2px; letter-spacing: 0.06em; }
116
+ .d3-trackio-oblivion .d3-tooltip__inner > div:nth-child(n+3) { padding-top: 6px; border-top: 1px solid var(--obl-border); }
117
+ .d3-trackio-oblivion .d3-tooltip__color-dot { display: inline-block; width: 12px; height: 12px; border-radius: 2px; border: 1px solid var(--obl-border); box-shadow: 0 0 10px rgba(127,241,255,.2) inset; }
118
+ </style>
119
+ <script>
120
+ (() => {
121
+ const THIS_SCRIPT = document.currentScript;
122
+ const TARGET_METRICS = ['epoch','train_accuracy','train_loss','val_accuracy','val_loss'];
123
+
124
+ const prettyMetricLabel = (key) => {
125
+ if (!key) return '';
126
+ const table = { 'train_accuracy':'Train Accuracy', 'val_accuracy':'Val Accuracy', 'train_loss':'Train Loss', 'val_loss':'Val Loss', 'epoch':'Epoch' };
127
+ if (table[key]) return table[key];
128
+ const cleaned = String(key).replace(/[_-]+/g, ' ').trim();
129
+ return cleaned.split(/\s+/).map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
130
+ };
131
+
132
+ const ensureD3 = (cb) => {
133
+ if (window.d3 && typeof window.d3.select === 'function') return cb();
134
+ let s = document.getElementById('d3-cdn-script');
135
+ if (!s) { s = document.createElement('script'); s.id = 'd3-cdn-script'; s.src = 'https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js'; document.head.appendChild(s); }
136
+ const onReady = () => { if (window.d3 && typeof window.d3.select === 'function') cb(); };
137
+ s.addEventListener('load', onReady, { once: true }); if (window.d3) onReady();
138
+ };
139
+
140
+ function initCell(cell){
141
+ const d3 = window.d3;
142
+ const metricKey = cell.getAttribute('data-metric');
143
+ const titleText = cell.getAttribute('data-title') || metricKey;
144
+ // Structured layers: bg + corners + inner (header + svg)
145
+ const bg = document.createElement('div'); bg.className = 'cell-bg'; cell.appendChild(bg);
146
+ const corners = document.createElement('div'); corners.className = 'cell-corners'; cell.appendChild(corners);
147
+ const inner = document.createElement('div'); inner.className = 'cell-inner'; cell.appendChild(inner);
148
+ const header = document.createElement('div'); header.className = 'cell-header';
149
+ const title = document.createElement('div'); title.className = 'cell-title'; title.textContent = prettyMetricLabel(titleText); header.appendChild(title); inner.appendChild(header);
150
+
151
+ const body = document.createElement('div'); body.className = 'cell-body'; inner.appendChild(body);
152
+ const svg = d3.select(body).append('svg').attr('width','100%').style('display','block');
153
+ const gRoot = svg.append('g');
154
+ const gGrid = gRoot.append('g').attr('class','grid');
155
+ const gAxes = gRoot.append('g').attr('class','axes');
156
+ const gAreas = gRoot.append('g').attr('class','areas');
157
+ const gLines = gRoot.append('g').attr('class','lines');
158
+ const gPoints = gRoot.append('g').attr('class','points');
159
+ const gHover = gRoot.append('g').attr('class','hover');
160
+
161
+ // Tooltip
162
+ cell.style.position = cell.style.position || 'relative';
163
+ let tip = cell.querySelector('.d3-tooltip'); let tipInner; let hideTipTimer = null;
164
+ if (!tip) { tip = document.createElement('div'); tip.className = 'd3-tooltip'; tipInner = document.createElement('div'); tipInner.className = 'd3-tooltip__inner'; tip.appendChild(tipInner); cell.appendChild(tip); } else { tipInner = tip.querySelector('.d3-tooltip__inner') || tip; }
165
+
166
+ // Layout & scales
167
+ let width = 800, height = 180; const margin = { top: 12, right: 20, bottom: 36, left: 44 };
168
+ const xScale = d3.scaleLinear(); const yScale = d3.scaleLinear();
169
+ const lineGen = d3.line().x(d => xScale(d.step)).y(d => yScale(d.value));
170
+
171
+ function updateLayout(axisLabelY){
172
+ const rect = cell.getBoundingClientRect();
173
+ width = Math.max(1, Math.round(rect && rect.width ? rect.width : (cell.clientWidth || 800)));
174
+ height = 180;
175
+ svg.attr('width', width).attr('height', height).attr('viewBox', `0 0 ${width} ${height}`).attr('preserveAspectRatio','xMidYMid meet');
176
+ // Respect HUD gap: keep content inside gradient inset
177
+ const css = getComputedStyle(cell);
178
+ const hudGap = Math.max(0, parseFloat(css.getPropertyValue('--hud-gap')) || 0);
179
+ const innerWidth = Math.max(0, width - margin.left - margin.right - hudGap * 2);
180
+ const innerHeight = Math.max(0, height - margin.top - margin.bottom - hudGap * 2);
181
+ gRoot.attr('transform', `translate(${margin.left + hudGap},${margin.top + hudGap})`);
182
+ xScale.range([0, innerWidth]); yScale.range([innerHeight, 0]);
183
+ // grid cleared; drawn as dot intersections in render()
184
+ gGrid.selectAll('*').remove();
185
+ gAxes.selectAll('*').remove();
186
+ // Build ticks that always include domain edges
187
+ const makeTicks = (scale, approx) => {
188
+ const arr = scale.ticks(approx);
189
+ const dom = scale.domain();
190
+ if (arr.length === 0 || arr[0] !== dom[0]) arr.unshift(dom[0]);
191
+ if (arr[arr.length - 1] !== dom[dom.length - 1]) arr.push(dom[dom.length - 1]);
192
+ // dedupe
193
+ return Array.from(new Set(arr));
194
+ };
195
+ const xTicksForced = makeTicks(xScale, 8);
196
+ const yTicksForced = makeTicks(yScale, 6);
197
+ gAxes
198
+ .append('g')
199
+ .attr('transform', `translate(0,${innerHeight})`)
200
+ .call(
201
+ d3.axisBottom(xScale).tickValues(xTicksForced)
202
+ )
203
+ .call((g) => {
204
+ g.selectAll('path, line')
205
+ .attr('stroke', 'var(--obl-cyan-dim)');
206
+ g.selectAll('text')
207
+ .attr('fill', 'var(--obl-cyan)')
208
+ .style('font-size', '11px');
209
+ });
210
+
211
+ gAxes
212
+ .append('g')
213
+ .call(d3.axisLeft(yScale).tickValues(yTicksForced))
214
+ .call((g) => {
215
+ g.selectAll('path, line')
216
+ .attr('stroke', 'var(--obl-cyan-dim)');
217
+ g.selectAll('text')
218
+ .attr('fill', 'var(--obl-cyan)')
219
+ .style('font-size', '11px');
220
+ });
221
+ gAxes
222
+ .append('text')
223
+ .attr('class','x-axis-label')
224
+ .attr('x', innerWidth/2)
225
+ .attr('y', innerHeight + Math.max(22, Math.min(36, margin.bottom - 2)))
226
+ .attr('fill', 'var(--obl-cyan)')
227
+ .attr('text-anchor', 'middle')
228
+ .style('font-size','9px')
229
+ .style('opacity', '0.3')
230
+ .style('font-weight','500')
231
+ .style('letter-spacing','.12em')
232
+ .style('text-transform','uppercase')
233
+ .text('Steps');
234
+ // Y-axis label removed to gain horizontal space
235
+ return { innerWidth, innerHeight, xTicksForced, yTicksForced };
236
+ }
237
+
238
+ function render(metricData, colorForRun){
239
+ const runs = Object.keys(metricData || {});
240
+ const hasAny = runs.some(r => (metricData[r]||[]).length > 0);
241
+ if (!hasAny) { gRoot.style('display','none'); let msg = body.querySelector('.empty-msg'); if (!msg) { msg = document.createElement('div'); msg.className='empty-msg'; msg.textContent='Metric not found in data.'; Object.assign(msg.style,{ padding:'10px', fontSize:'12px', color:'var(--obl-cyan)', opacity:.6 }); body.appendChild(msg); } return; }
242
+ const msg = body.querySelector('.empty-msg'); if (msg) msg.remove(); gRoot.style('display', null);
243
+
244
+ let minStep = Infinity, maxStep = -Infinity, minVal = Infinity, maxVal = -Infinity;
245
+ runs.forEach(r => { (metricData[r]||[]).forEach(pt => { minStep = Math.min(minStep, pt.step); maxStep = Math.max(maxStep, pt.step); minVal = Math.min(minVal, pt.value); maxVal = Math.max(maxVal, pt.value); }); });
246
+ if (!isFinite(minStep) || !isFinite(maxStep)) return;
247
+ const isAccuracy = /accuracy/i.test(metricKey);
248
+ const axisLabelY = prettyMetricLabel(metricKey);
249
+ xScale.domain([minStep, maxStep]); if (isAccuracy) yScale.domain([0,1]).nice(); else yScale.domain([minVal, maxVal]).nice();
250
+ const { innerWidth, innerHeight, xTicksForced, yTicksForced } = updateLayout(axisLabelY);
251
+
252
+ // Grid as small dots at intersections of y ticks × step positions
253
+ const gridPoints = [];
254
+ xTicksForced.forEach(s => { yTicksForced.forEach(t => { gridPoints.push({ sx: s, ty: t }); }); });
255
+ gGrid.selectAll('circle.grid-dot')
256
+ .data(gridPoints)
257
+ .join('circle')
258
+ .attr('class', 'grid-dot')
259
+ .attr('cx', d => xScale(d.sx))
260
+ .attr('cy', d => yScale(d.ty))
261
+ .attr('r', 1.25)
262
+ .attr('fill', 'var(--obl-cyan-dim)')
263
+ .attr('fill-opacity', 0.5);
264
+
265
+ // No stderr correction shapes for now (kept intentionally minimal)
266
+ gAreas.selectAll('*').remove();
267
+
268
+ // Lines
269
+ const series = runs.map(r => ({ run:r, color: colorForRun(r), values:(metricData[r]||[]).slice().sort((a,b)=>a.step-b.step) }));
270
+ const paths = gLines.selectAll('path.run').data(series, d=>d.run);
271
+ paths.enter().append('path').attr('class','run').attr('fill','none').attr('stroke-width',1.8).attr('opacity',0.95).attr('stroke', d=>d.color).attr('d', d=>lineGen(d.values));
272
+ paths.transition().duration(260).attr('stroke', d=>d.color).attr('opacity',0.95).attr('d', d=>lineGen(d.values));
273
+ paths.exit().remove();
274
+
275
+ // Points
276
+ const allPts = series.flatMap(s => s.values.map(v => ({ run:s.run, color:s.color, step:v.step, value:v.value })));
277
+ const ptsSel = gPoints.selectAll('circle.pt').data(allPts, d=>`${d.run}-${d.step}`);
278
+ ptsSel.enter().append('circle').attr('class','pt').attr('r', 2).attr('fill', d=>d.color).attr('fill-opacity', 0.65).attr('stroke','none').attr('cx', d=>xScale(d.step)).attr('cy', d=>yScale(d.value)).merge(ptsSel).transition().duration(150).attr('cx', d=>xScale(d.step)).attr('cy', d=>yScale(d.value));
279
+ ptsSel.exit().remove();
280
+
281
+ // Hover
282
+ gHover.selectAll('*').remove();
283
+ const overlay = gHover.append('rect').attr('fill','transparent').style('cursor','crosshair').attr('x',0).attr('y',0).attr('width', innerWidth).attr('height', innerHeight);
284
+ const hoverLine = gHover.append('line').style('stroke','var(--obl-cyan)').attr('stroke-opacity', 0.35).attr('stroke-width',1).attr('y1',0).attr('y2',innerHeight).style('display','none');
285
+ function onMove(ev){ if (hideTipTimer) { clearTimeout(hideTipTimer); hideTipTimer = null; } const [mx,my]=d3.pointer(ev, overlay.node()); const nearest = steps.reduce((best,s)=> Math.abs(s - xScale.invert(mx)) < Math.abs(best - xScale.invert(mx)) ? s : best, steps[0]); const xpx = xScale(nearest); hoverLine.attr('x1',xpx).attr('x2',xpx).style('display',null); let html = `<div>${prettyMetricLabel(metricKey)}</div><div>Step ${nearest}</div>`; const entries = series.map(s=>{ const m = new Map(s.values.map(v=>[v.step, v])); const pt = m.get(nearest); return { run:s.run, color:s.color, pt }; }).filter(e => e.pt && e.pt.value!=null); entries.sort((a,b)=> (a.pt.value - b.pt.value)); const fmt = (vv)=> (isAccuracy? (+vv).toFixed(4) : (+vv).toFixed(4)); entries.forEach(e => { const err = (e.pt.stderr!=null && isFinite(e.pt.stderr) && e.pt.stderr>0) ? ` ± ${fmt(e.pt.stderr)}` : ''; html += `<div style=\"display:flex;align-items:center;gap:8px;white-space:nowrap;\"><span class=\"d3-tooltip__color-dot\" style=\"background:${e.color}\"></span><strong>${e.run}</strong><span style=\"margin-left:auto;text-align:right;\">${fmt(e.pt.value)}${err}</span></div>`; }); tipInner.innerHTML = html; const cssVars = getComputedStyle(cell); const offx = Math.max(0, parseFloat(cssVars.getPropertyValue('--tip-offset-x')) || 0); const offy = Math.max(0, parseFloat(cssVars.getPropertyValue('--tip-offset-y')) || 0); const cellRect = cell.getBoundingClientRect(); const cx = (ev && ev.clientX != null) ? ev.clientX : (cellRect.left + mx); const cy = (ev && ev.clientY != null) ? ev.clientY : (cellRect.top + my); const x = cx - cellRect.left + offx; const y = cy - cellRect.top + offy; tip.classList.add('is-visible'); tip.style.transform=`translate(${x}px, ${y}px)`;
286
+ // Animate points at the hovered step to grow slightly
287
+ try {
288
+ gPoints.selectAll('circle.pt')
289
+ .transition().duration(140).ease(d3.easeCubicOut)
290
+ .attr('r', d => (d && d.step === nearest ? 4 : 2));
291
+ } catch(_) {}
292
+ }
293
+ function onLeave(){ hideTipTimer = setTimeout(()=>{ tip.classList.remove('is-visible'); tip.style.transform='translate(-9999px, -9999px)'; hoverLine.style('display','none'); try { gPoints.selectAll('circle.pt').transition().duration(150).ease(d3.easeCubicOut).attr('r', 2); } catch(_) {} }, 100); }
294
+ overlay.on('mousemove', onMove).on('mouseleave', onLeave);
295
+ }
296
+
297
+ return { metricKey, render };
298
+ }
299
+
300
+ const bootstrap = () => {
301
+ const scriptEl = THIS_SCRIPT; let host = null; const header = document.createElement('div'); header.className = 'd3-trackio-oblivion__header'; const legend = document.createElement('div'); legend.className = 'legend-bottom'; legend.innerHTML = '<div class="legend-title">Legend</div><div class="items"></div>'; header.appendChild(legend);
302
+ if (scriptEl && scriptEl.parentElement && scriptEl.parentElement.querySelector) host = scriptEl.parentElement.querySelector('.d3-trackio-oblivion'); if (!host) { let sib = scriptEl && scriptEl.previousElementSibling; while (sib && !(sib.classList && sib.classList.contains('d3-trackio-oblivion'))) { sib = sib.previousElementSibling; } host = sib || null; }
303
+ if (!host) { host = document.querySelector('.d3-trackio-oblivion'); }
304
+ if (!host) return; if (host.dataset && host.dataset.mounted==='true') return; if (host.dataset) host.dataset.mounted='true';
305
+
306
+ // Insert legend header above the grid container
307
+ const gridNode = host.querySelector('.d3-trackio-oblivion__grid');
308
+ if (gridNode && gridNode.parentNode === host) { host.insertBefore(header, gridNode); } else { host.insertBefore(header, host.firstChild); }
309
+ const cells = Array.from(host.querySelectorAll('.cell')); if (!cells.length) return;
310
+ const instances = cells.map(cell => initCell(cell));
311
+
312
+ // Read HtmlEmbed attributes
313
+ let mountEl = host; while (mountEl && !mountEl.getAttribute?.('data-datafiles') && !mountEl.getAttribute?.('data-config')) { mountEl = mountEl.parentElement; }
314
+ let providedData = null; let providedConfig = null;
315
+ try { const attr = mountEl && mountEl.getAttribute ? mountEl.getAttribute('data-datafiles') : null; if (attr && attr.trim()) providedData = attr.trim().startsWith('[') ? JSON.parse(attr) : attr.trim(); } catch(_){ }
316
+ try { const cfg = mountEl && mountEl.getAttribute ? mountEl.getAttribute('data-config') : null; if (cfg && cfg.trim()) providedConfig = cfg.trim().startsWith('{') ? JSON.parse(cfg) : cfg; } catch(_){ }
317
+
318
+ const DEFAULT_CSV = '/data/trackio_wandb_demo.csv';
319
+ const ensureDataPrefix = (p) => { if (typeof p !== 'string' || !p) return p; return p.includes('/') ? p : `/data/${p}`; };
320
+ const normalizeInput = (inp) => Array.isArray(inp) ? inp.map(ensureDataPrefix) : (typeof inp === 'string' ? [ ensureDataPrefix(inp) ] : null);
321
+ const CSV_PATHS = Array.isArray(providedData) ? normalizeInput(providedData) : (typeof providedData === 'string' ? normalizeInput(providedData) || [DEFAULT_CSV] : [ DEFAULT_CSV, './assets/data/trackio_wandb_demo.csv', '../assets/data/trackio_wandb_demo.csv', '../../assets/data/trackio_wandb_demo.csv' ]);
322
+
323
+ const d3 = window.d3;
324
+ (async () => {
325
+ try {
326
+ const texts = await Promise.all(CSV_PATHS.map(p => fetch(p, { cache:'no-cache' }).then(r => r.ok ? r.text() : '').catch(()=>'')));
327
+ const textAll = texts.filter(Boolean).join('\n');
328
+ const rows = d3.csvParse(textAll, d => ({ run:(d.run||'').trim(), step:+d.step, metric:(d.metric||'').trim(), value:+d.value, stderr: (d.stderr!=null && d.stderr!=='') ? +d.stderr : null }));
329
+ // Filter out comments and invalid rows before building lists/maps
330
+ const cleanRows = rows.filter(r => {
331
+ const run = String(r.run||'').trim();
332
+ const metric = String(r.metric||'').trim();
333
+ return run && run.charAt(0) !== '#' && metric && isFinite(+r.step) && !isNaN(+r.step) && isFinite(+r.value) && !isNaN(+r.value);
334
+ });
335
+ const metricsInData = Array.from(new Set(cleanRows.map(r => r.metric)));
336
+ const lcSet = new Set(metricsInData.map(m => m.toLowerCase()));
337
+ const preferIfExists = (cand) => cand.find(c => lcSet.has(String(c).toLowerCase())) || null;
338
+ const resolveMetric = (target) => {
339
+ const override = providedConfig && providedConfig.metricMap && providedConfig.metricMap[target]; if (override && lcSet.has(String(override).toLowerCase())) return metricsInData.find(m => m.toLowerCase() === String(override).toLowerCase());
340
+ const exact = metricsInData.find(m => m.toLowerCase() === target.toLowerCase()); if (exact) return exact;
341
+ const cands = (name) => metricsInData.filter(m => m.toLowerCase().includes(name));
342
+ if (target === 'epoch') return preferIfExists(['epoch']);
343
+ if (target === 'train_accuracy') return preferIfExists(['train_accuracy','training_accuracy','accuracy_train','train_acc','acc_train','train/accuracy','accuracy']) || cands('acc').find(m => /train|trn/i.test(m));
344
+ if (target === 'val_accuracy') return preferIfExists(['val_accuracy','valid_accuracy','validation_accuracy','val_acc','acc_val','val/accuracy']) || cands('acc').find(m => /val|valid/i.test(m));
345
+ if (target === 'train_loss') return preferIfExists(['train_loss','training_loss','loss_train','train/loss','loss']) || cands('loss').find(m => /train|trn/i.test(m));
346
+ if (target === 'val_loss') return preferIfExists(['val_loss','validation_loss','valid_loss','loss_val','val/loss']) || cands('loss').find(m => /val|valid/i.test(m));
347
+ return null;
348
+ };
349
+ const TARGET_TO_DATA = Object.fromEntries(TARGET_METRICS.map(t => [t, resolveMetric(t)]));
350
+ const metricsToDraw = TARGET_METRICS.filter(t => !!TARGET_TO_DATA[t]);
351
+
352
+ const runList = Array.from(new Set(cleanRows.map(r => String(r.run||'').trim()).filter(Boolean))).sort();
353
+ const palette = (() => {
354
+ try { if (window.ColorPalettes && typeof window.ColorPalettes.getColors==='function') return window.ColorPalettes.getColors('categorical', runList.length); } catch(_) {}
355
+ const d = (window.d3 && window.d3.schemeTableau10) ? window.d3.schemeTableau10 : ['#7ff1ff','#5ae0e8','#3cc2da','#289ab8','#1a7e9a','#0f637b','#0a4b5e','#083948','#062a36','#041e27'];
356
+ return d;
357
+ })();
358
+ const colorForRun = (name) => palette[runList.indexOf(name) % palette.length];
359
+
360
+ const legendItemsHost = legend.querySelector('.items');
361
+ legendItemsHost.innerHTML = runList.map((name) => { const color = colorForRun(name); return `<span class="item" data-run="${name}"><span class=\"swatch\" style=\"background:${color}\"></span><span>${name}</span></span>`; }).join('');
362
+
363
+ const dataByMetric = new Map();
364
+ metricsToDraw.forEach(tgt => { const m = TARGET_TO_DATA[tgt]; const map = {}; runList.forEach(r => map[r] = []); cleanRows.filter(r=>r.metric===m).forEach(r => { map[r.run].push({ step:r.step, value:r.value, stderr:r.stderr }); }); dataByMetric.set(tgt, map); });
365
+
366
+ instances.forEach(inst => { const metricMap = dataByMetric.get(inst.metricKey) || {}; inst.render(metricMap, colorForRun); });
367
+
368
+ const rerender = () => { instances.forEach(inst => inst.render(dataByMetric.get(inst.metricKey)||{}, colorForRun)); };
369
+ if (window.ResizeObserver) { const ro = new ResizeObserver(() => rerender()); ro.observe(host); } else { window.addEventListener('resize', rerender); }
370
+
371
+ legendItemsHost.querySelectorAll('.item').forEach(el => {
372
+ el.addEventListener('mouseenter', () => { const run = el.getAttribute('data-run'); if (!run) return; host.classList.add('hovering'); host.querySelectorAll('.cell').forEach(cell => { cell.querySelectorAll('.lines path.run').forEach(p => p.classList.toggle('ghost', (p.__data__ && p.__data__.run) !== run)); cell.querySelectorAll('.points circle.pt').forEach(c => c.classList.toggle('ghost', c.getAttribute('data-run') !== run)); cell.querySelectorAll('.areas path.area').forEach(a => a.classList.toggle('ghost', a.getAttribute('data-run') !== run)); }); legendItemsHost.querySelectorAll('.item').forEach(it => it.classList.toggle('ghost', it.getAttribute('data-run') !== run)); });
373
+ el.addEventListener('mouseleave', () => { host.classList.remove('hovering'); host.querySelectorAll('.cell').forEach(cell => { cell.querySelectorAll('.lines path.run').forEach(p => p.classList.remove('ghost')); cell.querySelectorAll('.points circle.pt').forEach(c => c.classList.remove('ghost')); cell.querySelectorAll('.areas path.area').forEach(a => a.classList.remove('ghost')); }); legendItemsHost.querySelectorAll('.item').forEach(it => it.classList.remove('ghost')); });
374
+ });
375
+ } catch (e) {
376
+ const pre = document.createElement('pre'); pre.textContent = 'CSV load error: ' + (e && e.message ? e.message : e); pre.style.color = 'var(--obl-cyan)'; pre.style.fontSize = '12px'; pre.style.whiteSpace = 'pre-wrap'; host.appendChild(pre);
377
+ }
378
+ })();
379
+ };
380
+
381
+ if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true }); } else { ensureD3(bootstrap); }
382
+ })();
383
+ </script>
384
+
385
+
app/src/pages/index.astro CHANGED
@@ -143,7 +143,7 @@ const licence = (articleFM as any)?.licence ?? (articleFM as any)?.license ?? (a
143
  } catch {}
144
  })();
145
  </script>
146
- <script type="module" src="/scripts/color-palettes.js"></script>
147
 
148
  <!-- TO MANAGE PROPERLY -->
149
  <script src="https://cdn.plot.ly/plotly-3.0.0.min.js" charset="utf-8"></script>
 
143
  } catch {}
144
  })();
145
  </script>
146
+ <script type="module" src="/src/scripts/color-palettes.js"></script>
147
 
148
  <!-- TO MANAGE PROPERLY -->
149
  <script src="https://cdn.plot.ly/plotly-3.0.0.min.js" charset="utf-8"></script>
app/src/scripts/color-palettes.js ADDED
@@ -0,0 +1,236 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Global color palettes generator and watcher
2
+ // - Observes CSS variable --primary-color and theme changes
3
+ // - Generates categorical, sequential, and diverging palettes (OKLCH/OKLab)
4
+ // - Exposes results as CSS variables on :root
5
+ // - Supports variable color counts per palette via CSS vars
6
+ // - Dispatches a 'palettes:updated' CustomEvent after each update
7
+
8
+ (() => {
9
+ const MODE = { cssRoot: document.documentElement };
10
+
11
+ const getCssVar = (name) => {
12
+ try { return getComputedStyle(MODE.cssRoot).getPropertyValue(name).trim(); } catch { return ''; }
13
+ };
14
+ const getIntFromCssVar = (name, fallback) => {
15
+ const raw = getCssVar(name);
16
+ if (!raw) return fallback;
17
+ const v = parseInt(String(raw), 10);
18
+ if (Number.isNaN(v)) return fallback;
19
+ return v;
20
+ };
21
+ const clamp = (n, min, max) => Math.max(min, Math.min(max, n));
22
+
23
+ // Color math (OKLab/OKLCH)
24
+ const srgbToLinear = (u) => (u <= 0.04045 ? u / 12.92 : Math.pow((u + 0.055) / 1.055, 2.4));
25
+ const linearToSrgb = (u) => (u <= 0.0031308 ? 12.92 * u : 1.055 * Math.pow(Math.max(0, u), 1 / 2.4) - 0.055);
26
+ const rgbToOklab = (r, g, b) => {
27
+ const rl = srgbToLinear(r), gl = srgbToLinear(g), bl = srgbToLinear(b);
28
+ const l = Math.cbrt(0.4122214708 * rl + 0.5363325363 * gl + 0.0514459929 * bl);
29
+ const m = Math.cbrt(0.2119034982 * rl + 0.6806995451 * gl + 0.1073969566 * bl);
30
+ const s = Math.cbrt(0.0883024619 * rl + 0.2817188376 * gl + 0.6299787005 * bl);
31
+ const L = 0.2104542553 * l + 0.7936177850 * m - 0.0040720468 * s;
32
+ const a = 1.9779984951 * l - 2.4285922050 * m + 0.4505937099 * s;
33
+ const b2 = 0.0259040371 * l + 0.7827717662 * m - 0.8086757660 * s;
34
+ return { L, a, b: b2 };
35
+ };
36
+ const oklabToRgb = (L, a, b) => {
37
+ const l_ = L + 0.3963377774 * a + 0.2158037573 * b;
38
+ const m_ = L - 0.1055613458 * a - 0.0638541728 * b;
39
+ const s_ = L - 0.0894841775 * a - 1.2914855480 * b;
40
+ const l = l_ * l_ * l_;
41
+ const m = m_ * m_ * m_;
42
+ const s = s_ * s_ * s_;
43
+ const r = linearToSrgb(+4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s);
44
+ const g = linearToSrgb(-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s);
45
+ const b3 = linearToSrgb(-0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s);
46
+ return { r, g, b: b3 };
47
+ };
48
+ const oklchToOklab = (L, C, hDeg) => { const h = (hDeg * Math.PI) / 180; return { L, a: C * Math.cos(h), b: C * Math.sin(h) }; };
49
+ const oklabToOklch = (L, a, b) => { const C = Math.sqrt(a*a + b*b); let h = Math.atan2(b, a) * 180 / Math.PI; if (h < 0) h += 360; return { L, C, h }; };
50
+ const clamp01 = (x) => Math.min(1, Math.max(0, x));
51
+ const isInGamut = ({ r, g, b }) => r >= 0 && r <= 1 && g >= 0 && g <= 1 && b >= 0 && b <= 1;
52
+ const toHex = ({ r, g, b }) => { const R = Math.round(clamp01(r)*255), G = Math.round(clamp01(g)*255), B = Math.round(clamp01(b)*255); const h = (n) => n.toString(16).padStart(2,'0'); return `#${h(R)}${h(G)}${h(B)}`.toUpperCase(); };
53
+ const oklchToHexSafe = (L, C, h) => { let c = C; for (let i=0;i<12;i++){ const { a, b } = oklchToOklab(L,c,h); const rgb = oklabToRgb(L,a,b); if (isInGamut(rgb)) return toHex(rgb); c = Math.max(0, c-0.02);} return toHex(oklabToRgb(L,0,0)); };
54
+ const parseCssColorToRgb = (css) => { try { const el = document.createElement('span'); el.style.color = css; document.body.appendChild(el); const cs = getComputedStyle(el).color; document.body.removeChild(el); const m = cs.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/i); if (!m) return null; return { r: Number(m[1])/255, g: Number(m[2])/255, b: Number(m[3])/255 }; } catch { return null; } };
55
+
56
+ const getPrimaryHex = () => {
57
+ const css = getCssVar('--primary-color');
58
+ if (!css) return '#E889AB';
59
+ if (/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(css)) return css.toUpperCase();
60
+ const rgb = parseCssColorToRgb(css);
61
+ if (rgb) return toHex(rgb);
62
+ return '#E889AB';
63
+ };
64
+ // No count management via CSS anymore; counts are passed directly to the API
65
+
66
+ const generators = {
67
+ categorical: (baseHex, count) => {
68
+ const parseHex = (h) => { const s = h.replace('#',''); const v = s.length===3 ? s.split('').map(ch=>ch+ch).join('') : s; return { r: parseInt(v.slice(0,2),16)/255, g: parseInt(v.slice(2,4),16)/255, b: parseInt(v.slice(4,6),16)/255 }; };
69
+ const { r, g, b } = parseHex(baseHex);
70
+ const { L, a, b: bb } = rgbToOklab(r,g,b);
71
+ const { C, h } = oklabToOklch(L,a,bb);
72
+ const L0 = Math.min(0.85, Math.max(0.4, L));
73
+ const C0 = Math.min(0.35, Math.max(0.1, C || 0.2));
74
+ const total = Math.max(1, Math.min(12, count || 8));
75
+ const hueStep = 360 / total;
76
+ const results = [];
77
+ for (let i=0;i<total;i++) { const hDeg = (h + i*hueStep) % 360; const lVar = ((i % 3) - 1) * 0.04; results.push(oklchToHexSafe(Math.max(0.4, Math.min(0.85, L0 + lVar)), C0, hDeg)); }
78
+ return results;
79
+ },
80
+ sequential: (baseHex, count) => {
81
+ const parseHex = (h) => { const s = h.replace('#',''); const v = s.length===3 ? s.split('').map(ch=>ch+ch).join('') : s; return { r: parseInt(v.slice(0,2),16)/255, g: parseInt(v.slice(2,4),16)/255, b: parseInt(v.slice(4,6),16)/255 }; };
82
+ const { r, g, b } = parseHex(baseHex);
83
+ const { L, a, b: bb } = rgbToOklab(r,g,b);
84
+ const { C, h } = oklabToOklch(L,a,bb);
85
+ const total = Math.max(1, Math.min(12, count || 8));
86
+ const startL = Math.max(0.25, L - 0.18);
87
+ const endL = Math.min(0.92, L + 0.18);
88
+ const cBase = Math.min(0.33, Math.max(0.08, C * 0.9 + 0.06));
89
+ const out = [];
90
+ for (let i=0;i<total;i++) { const t = total===1 ? 0 : i/(total-1); const lNow = startL*(1-t)+endL*t; const cNow = cBase*(0.85 + 0.15*(1 - Math.abs(0.5 - t)*2)); out.push(oklchToHexSafe(lNow, cNow, h)); }
91
+ return out;
92
+ },
93
+ diverging: (baseHex, count) => {
94
+ const parseHex = (h) => { const s = h.replace('#',''); const v = s.length===3 ? s.split('').map(ch=>ch+ch).join('') : s; return { r: parseInt(v.slice(0,2),16)/255, g: parseInt(v.slice(2,4),16)/255, b: parseInt(v.slice(4,6),16)/255 }; };
95
+ const { r, g, b } = parseHex(baseHex);
96
+ const baseLab = rgbToOklab(r,g,b);
97
+ const baseLch = oklabToOklch(baseLab.L, baseLab.a, baseLab.b);
98
+ const total = Math.max(1, Math.min(12, count || 8));
99
+
100
+ // Left endpoint: EXACT primary color (no darkening)
101
+ const leftLab = baseLab;
102
+ // Right endpoint: complement with same L and similar C (clamped safe)
103
+ const compH = (baseLch.h + 180) % 360;
104
+ const cSafe = Math.min(0.35, Math.max(0.08, baseLch.C));
105
+ const rightLab = oklchToOklab(baseLab.L, cSafe, compH);
106
+ const whiteLab = { L: 0.98, a: 0, b: 0 }; // center near‑white
107
+
108
+ const hexFromOKLab = (L, a, b) => toHex(oklabToRgb(L, a, b));
109
+ const lerp = (a, b, t) => a + (b - a) * t;
110
+ const lerpOKLabHex = (A, B, t) => hexFromOKLab(lerp(A.L, B.L, t), lerp(A.a, B.a, t), lerp(A.b, B.b, t));
111
+
112
+ const out = [];
113
+ if (total % 2 === 1) {
114
+ const nSide = (total - 1) >> 1; // items on each side
115
+ // Left side: include left endpoint exactly at index 0
116
+ for (let i = 0; i < nSide; i++) {
117
+ const t = nSide <= 1 ? 0 : (i / (nSide - 1)); // 0 .. 1
118
+ // Move from leftLab to a value close (but not equal) to white; ensure last before center is lighter
119
+ const tt = t * 0.9; // keep some distance from pure white before center
120
+ out.push(lerpOKLabHex(leftLab, whiteLab, tt));
121
+ }
122
+ // Center
123
+ out.push(hexFromOKLab(whiteLab.L, whiteLab.a, whiteLab.b));
124
+ // Right side: start near white and end EXACTLY at rightLab
125
+ for (let i = 0; i < nSide; i++) {
126
+ const t = nSide <= 1 ? 1 : ((i + 1) / nSide); // (1/n)..1
127
+ const tt = Math.max(0.1, t); // avoid starting at pure white
128
+ out.push(lerpOKLabHex(whiteLab, rightLab, tt));
129
+ }
130
+ // Ensure first and last are exact endpoints
131
+ if (out.length) { out[0] = hexFromOKLab(leftLab.L, leftLab.a, leftLab.b); out[out.length - 1] = hexFromOKLab(rightLab.L, rightLab.a, rightLab.b); }
132
+ } else {
133
+ const nSide = total >> 1;
134
+ // Left half including left endpoint, approaching white but not reaching it
135
+ for (let i = 0; i < nSide; i++) {
136
+ const t = nSide <= 1 ? 0 : (i / (nSide - 1)); // 0 .. 1
137
+ const tt = t * 0.9;
138
+ out.push(lerpOKLabHex(leftLab, whiteLab, tt));
139
+ }
140
+ // Right half: mirror from near white to exact right endpoint
141
+ for (let i = 0; i < nSide; i++) {
142
+ const t = nSide <= 1 ? 1 : ((i + 1) / nSide); // (1/n)..1
143
+ const tt = Math.max(0.1, t);
144
+ out.push(lerpOKLabHex(whiteLab, rightLab, tt));
145
+ }
146
+ if (out.length) { out[0] = hexFromOKLab(leftLab.L, leftLab.a, leftLab.b); out[out.length - 1] = hexFromOKLab(rightLab.L, rightLab.a, rightLab.b); }
147
+ }
148
+ return out;
149
+ }
150
+ };
151
+
152
+ let lastSignature = '';
153
+
154
+ const updatePalettes = () => {
155
+ const primary = getPrimaryHex();
156
+ const signature = `${primary}`;
157
+ if (signature === lastSignature) return;
158
+ lastSignature = signature;
159
+ try { document.dispatchEvent(new CustomEvent('palettes:updated', { detail: { primary } })); } catch {}
160
+ };
161
+
162
+ const bootstrap = () => {
163
+ updatePalettes();
164
+ const mo = new MutationObserver(() => updatePalettes());
165
+ mo.observe(MODE.cssRoot, { attributes: true, attributeFilter: ['style', 'data-theme'] });
166
+ setInterval(updatePalettes, 400);
167
+ // Utility: choose high-contrast (or softened) text style against an arbitrary background color
168
+ const pickTextStyleForBackground = (bgCss, opts = {}) => {
169
+ const cssRoot = document.documentElement;
170
+ const getCssVar = (name) => {
171
+ try { return getComputedStyle(cssRoot).getPropertyValue(name).trim(); } catch { return ''; }
172
+ };
173
+ const resolveCssToRgb01 = (css) => {
174
+ const rgb = parseCssColorToRgb(css);
175
+ if (!rgb) return null;
176
+ return rgb; // already 0..1
177
+ };
178
+ const mixRgb01 = (a, b, t) => ({ r: a.r*(1-t)+b.r*t, g: a.g*(1-t)+b.g*t, b: a.b*(1-t)+b.b*t });
179
+ const relLum = (rgb) => {
180
+ const f = (u) => srgbToLinear(u);
181
+ return 0.2126*f(rgb.r) + 0.7152*f(rgb.g) + 0.0722*f(rgb.b);
182
+ };
183
+ const contrast = (fg, bg) => {
184
+ const L1 = relLum(fg), L2 = relLum(bg); const a = Math.max(L1,L2), b = Math.min(L1,L2);
185
+ return (a + 0.05) / (b + 0.05);
186
+ };
187
+ try {
188
+ const bg = resolveCssToRgb01(bgCss);
189
+ if (!bg) return { fill: getCssVar('--text-color') || '#000', stroke: 'var(--transparent-page-contrast)', strokeWidth: 1 };
190
+ const candidatesCss = [getCssVar('--text-color') || '#111', getCssVar('--on-primary') || '#0f1115', '#000', '#fff'];
191
+ const candidates = candidatesCss
192
+ .map(css => ({ css, rgb: resolveCssToRgb01(css) }))
193
+ .filter(x => !!x.rgb);
194
+ // Pick the max contrast
195
+ let best = candidates[0]; let bestCR = contrast(best.rgb, bg);
196
+ for (let i=1;i<candidates.length;i++){
197
+ const cr = contrast(candidates[i].rgb, bg);
198
+ if (cr > bestCR) { best = candidates[i]; bestCR = cr; }
199
+ }
200
+ // Optional softening via blend factor (0..1), blending towards muted color
201
+ const blend = Math.min(1, Math.max(0, Number(opts.blend || 0)));
202
+ let finalRgb = best.rgb;
203
+ if (blend > 0) {
204
+ const mutedCss = getCssVar('--muted-color') || (getCssVar('--text-color') || '#111');
205
+ const mutedRgb = resolveCssToRgb01(mutedCss) || best.rgb;
206
+ finalRgb = mixRgb01(best.rgb, mutedRgb, blend);
207
+ }
208
+ const haloStrength = Math.min(1, Math.max(0, Number(opts.haloStrength == null ? 0.5 : opts.haloStrength)));
209
+ const stroke = (best.css === '#000' || best.css.toLowerCase() === 'black') ? `rgba(255,255,255,${0.30 + 0.40*haloStrength})` : `rgba(0,0,0,${0.30 + 0.30*haloStrength})`;
210
+ return { fill: toHex(finalRgb), stroke, strokeWidth: (opts.haloWidth == null ? 1 : Number(opts.haloWidth)) };
211
+ } catch {
212
+ return { fill: getCssVar('--text-color') || '#000', stroke: 'var(--transparent-page-contrast)', strokeWidth: 1 };
213
+ }
214
+ };
215
+ window.ColorPalettes = {
216
+ refresh: updatePalettes,
217
+ notify: () => { try { const primary = getPrimaryHex(); document.dispatchEvent(new CustomEvent('palettes:updated', { detail: { primary } })); } catch {} },
218
+ getPrimary: () => getPrimaryHex(),
219
+ getColors: (key, count = 6) => {
220
+ const primary = getPrimaryHex();
221
+ const total = Math.max(1, Math.min(12, Number(count) || 6));
222
+ if (key === 'categorical') return generators.categorical(primary, total);
223
+ if (key === 'sequential') return generators.sequential(primary, total);
224
+ if (key === 'diverging') return generators.diverging(primary, total);
225
+ return [];
226
+ },
227
+ getTextStyleForBackground: (bgCss, opts) => pickTextStyleForBackground(bgCss, opts || {}),
228
+ chooseReadableText: (bgCss, opts) => pickTextStyleForBackground(bgCss, opts || {})
229
+ };
230
+ };
231
+
232
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', bootstrap, { once: true });
233
+ else bootstrap();
234
+ })();
235
+
236
+