Spaces:
Running
Running
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:
|
| 3 |
-
size
|
|
|
|
| 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 |
+
|