Spaces:
Running
Running
thibaud frere
commited on
Commit
·
e2d6261
1
Parent(s):
df95a5c
update charts
Browse files- app/scripts/generate-trackio-data.mjs +196 -0
- app/scripts/jitter-trackio-data.mjs +129 -0
- app/src/components/Palettes.astro +170 -0
- app/src/content/assets/data/trackio_wandb_demo.csv +2 -2
- app/src/content/chapters/vibe-coding-charts.mdx +3 -4
- app/src/content/embeds/d3-trackio-oblivion.html +150 -40
- app/src/content/embeds/d3-trackio.html +282 -73
app/scripts/generate-trackio-data.mjs
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env node
|
| 2 |
+
|
| 3 |
+
// Generate synthetic Trackio-like CSV data with realistic ML curves.
|
| 4 |
+
// - Steps are simple integers (e.g., 1..N)
|
| 5 |
+
// - Metrics: epoch, train_accuracy, val_accuracy, train_loss, val_loss
|
| 6 |
+
// - W&B-like run names (e.g., pleasant-flower-1)
|
| 7 |
+
// - Deterministic with --seed
|
| 8 |
+
//
|
| 9 |
+
// Usage:
|
| 10 |
+
// node app/scripts/generate-trackio-data.mjs \
|
| 11 |
+
// --runs 3 \
|
| 12 |
+
// --steps 10 \
|
| 13 |
+
// --out app/src/content/assets/data/trackio_wandb_synth.csv \
|
| 14 |
+
// [--seed 42] [--epoch-max 3.0] [--amount 1.0] [--start 1]
|
| 15 |
+
//
|
| 16 |
+
// To overwrite the demo file used by the embed:
|
| 17 |
+
// node app/scripts/generate-trackio-data.mjs --runs 3 --steps 10 --out app/src/content/assets/data/trackio_wandb_demo.csv --seed 1337
|
| 18 |
+
|
| 19 |
+
import fs from 'node:fs/promises';
|
| 20 |
+
import path from 'node:path';
|
| 21 |
+
|
| 22 |
+
function parseArgs(argv){
|
| 23 |
+
const args = { runs: 3, steps: 10, out: '', seed: undefined, epochMax: 3.0, amount: 1, start: 1 };
|
| 24 |
+
for (let i = 2; i < argv.length; i++){
|
| 25 |
+
const a = argv[i];
|
| 26 |
+
if (a === '--runs' && argv[i+1]) { args.runs = Math.max(1, parseInt(argv[++i], 10) || 3); continue; }
|
| 27 |
+
if (a === '--steps' && argv[i+1]) { args.steps = Math.max(2, parseInt(argv[++i], 10) || 10); continue; }
|
| 28 |
+
if (a === '--out' && argv[i+1]) { args.out = argv[++i]; continue; }
|
| 29 |
+
if (a === '--seed' && argv[i+1]) { args.seed = Number(argv[++i]); continue; }
|
| 30 |
+
if (a === '--epoch-max' && argv[i+1]) { args.epochMax = Number(argv[++i]) || 3.0; continue; }
|
| 31 |
+
if (a === '--amount' && argv[i+1]) { args.amount = Number(argv[++i]) || 1.0; continue; }
|
| 32 |
+
if (a === '--start' && argv[i+1]) { args.start = parseInt(argv[++i], 10) || 1; continue; }
|
| 33 |
+
}
|
| 34 |
+
if (!args.out) {
|
| 35 |
+
args.out = path.join('app', 'src', 'content', 'assets', 'data', 'trackio_wandb_synth.csv');
|
| 36 |
+
}
|
| 37 |
+
return args;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
function mulberry32(seed){
|
| 41 |
+
let t = seed >>> 0;
|
| 42 |
+
return function(){
|
| 43 |
+
t += 0x6D2B79F5;
|
| 44 |
+
let r = Math.imul(t ^ (t >>> 15), 1 | t);
|
| 45 |
+
r ^= r + Math.imul(r ^ (r >>> 7), 61 | r);
|
| 46 |
+
return ((r ^ (r >>> 14)) >>> 0) / 4294967296;
|
| 47 |
+
};
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
function makeRng(seed){
|
| 51 |
+
if (Number.isFinite(seed)) return mulberry32(seed);
|
| 52 |
+
return Math.random;
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
function randn(rng){
|
| 56 |
+
// Box-Muller transform
|
| 57 |
+
let u = 0, v = 0;
|
| 58 |
+
while (u === 0) u = rng();
|
| 59 |
+
while (v === 0) v = rng();
|
| 60 |
+
return Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
function clamp(x, lo, hi){
|
| 64 |
+
return Math.max(lo, Math.min(hi, x));
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
function logistic(t, k=6, x0=0.5){
|
| 68 |
+
// 1 / (1 + e^{-k (t - x0)}) in [0,1]
|
| 69 |
+
return 1 / (1 + Math.exp(-k * (t - x0)));
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
function expDecay(t, k=3){
|
| 73 |
+
// (1 - e^{-k t}) in [0,1]
|
| 74 |
+
return 1 - Math.exp(-k * t);
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
function pick(array, rng){
|
| 78 |
+
return array[Math.floor(rng() * array.length) % array.length];
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
function buildRunNames(count, rng){
|
| 82 |
+
const adjectives = [
|
| 83 |
+
'pleasant','brisk','silent','ancient','bold','gentle','rapid','shy','curious','lively',
|
| 84 |
+
'fearless','soothing','glossy','hidden','misty','bright','calm','keen','noble','swift'
|
| 85 |
+
];
|
| 86 |
+
const nouns = [
|
| 87 |
+
'flower','glade','sky','river','forest','ember','comet','meadow','harbor','dawn',
|
| 88 |
+
'mountain','prairie','breeze','valley','lagoon','desert','monsoon','reef','thunder','willow'
|
| 89 |
+
];
|
| 90 |
+
const names = new Set();
|
| 91 |
+
let attempts = 0;
|
| 92 |
+
while (names.size < count && attempts < count * 20){
|
| 93 |
+
attempts++;
|
| 94 |
+
const left = pick(adjectives, rng);
|
| 95 |
+
const right = pick(nouns, rng);
|
| 96 |
+
const idx = 1 + Math.floor(rng() * 9);
|
| 97 |
+
names.add(`${left}-${right}-${idx}`);
|
| 98 |
+
}
|
| 99 |
+
return Array.from(names);
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
function formatLike(value, decimals){
|
| 103 |
+
return Number.isFinite(decimals) && decimals >= 0 ? value.toFixed(decimals) : String(value);
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
async function main(){
|
| 107 |
+
const args = parseArgs(process.argv);
|
| 108 |
+
const rng = makeRng(args.seed);
|
| 109 |
+
|
| 110 |
+
// Steps: integers from start .. start+steps-1
|
| 111 |
+
const steps = Array.from({ length: args.steps }, (_, i) => args.start + i);
|
| 112 |
+
const stepNorm = (i) => (i - steps[0]) / (steps[steps.length-1] - steps[0]);
|
| 113 |
+
|
| 114 |
+
const runs = buildRunNames(args.runs, rng);
|
| 115 |
+
|
| 116 |
+
// Per-run slight variations
|
| 117 |
+
const runParams = runs.map((_r, idx) => {
|
| 118 |
+
const r = rng();
|
| 119 |
+
// Final accuracies
|
| 120 |
+
const trainAccFinal = clamp(0.86 + (r - 0.5) * 0.12 * args.amount, 0.78, 0.97);
|
| 121 |
+
const valAccFinal = clamp(trainAccFinal - (0.02 + rng() * 0.05), 0.70, 0.95);
|
| 122 |
+
// Loss plateau
|
| 123 |
+
const lossStart = 7.0 + (rng() - 0.5) * 0.10 * args.amount; // ~7.0 ±0.05
|
| 124 |
+
const lossPlateau = 6.78 + (rng() - 0.5) * 0.04 * args.amount; // ~6.78 ±0.02
|
| 125 |
+
const lossK = 2.0 + rng() * 1.5; // decay speed
|
| 126 |
+
// Acc growth steepness and midpoint
|
| 127 |
+
const kAcc = 4.5 + rng() * 3.0;
|
| 128 |
+
const x0Acc = 0.35 + rng() * 0.25;
|
| 129 |
+
return { trainAccFinal, valAccFinal, lossStart, lossPlateau, lossK, kAcc, x0Acc };
|
| 130 |
+
});
|
| 131 |
+
|
| 132 |
+
const lines = [];
|
| 133 |
+
lines.push('run,step,metric,value,stderr');
|
| 134 |
+
|
| 135 |
+
// EPOCH: linear 0..epochMax across steps
|
| 136 |
+
for (let r = 0; r < runs.length; r++){
|
| 137 |
+
const run = runs[r];
|
| 138 |
+
for (let i = 0; i < steps.length; i++){
|
| 139 |
+
const t = stepNorm(steps[i]);
|
| 140 |
+
const epoch = args.epochMax * t;
|
| 141 |
+
lines.push(`${run},${steps[i]},epoch,${formatLike(epoch, 2)},`);
|
| 142 |
+
}
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
// TRAIN LOSS & VAL LOSS
|
| 146 |
+
for (let r = 0; r < runs.length; r++){
|
| 147 |
+
const run = runs[r];
|
| 148 |
+
const p = runParams[r];
|
| 149 |
+
let prevTrain = null;
|
| 150 |
+
let prevVal = null;
|
| 151 |
+
for (let i = 0; i < steps.length; i++){
|
| 152 |
+
const t = stepNorm(steps[i]);
|
| 153 |
+
const d = expDecay(t, p.lossK); // 0..1
|
| 154 |
+
let trainLoss = p.lossStart - (p.lossStart - p.lossPlateau) * d;
|
| 155 |
+
let valLoss = trainLoss + 0.02 + (rng() * 0.03);
|
| 156 |
+
// Add mild noise
|
| 157 |
+
trainLoss += randn(rng) * 0.01 * args.amount;
|
| 158 |
+
valLoss += randn(rng) * 0.012 * args.amount;
|
| 159 |
+
// Keep reasonable and mostly monotonic (small upward blips allowed)
|
| 160 |
+
if (prevTrain != null) trainLoss = Math.min(prevTrain + 0.01, trainLoss);
|
| 161 |
+
if (prevVal != null) valLoss = Math.min(prevVal + 0.012, valLoss);
|
| 162 |
+
prevTrain = trainLoss; prevVal = valLoss;
|
| 163 |
+
const stderrTrain = clamp(0.03 - 0.02 * t + Math.abs(randn(rng)) * 0.003, 0.006, 0.04);
|
| 164 |
+
const stderrVal = clamp(0.035 - 0.022 * t + Math.abs(randn(rng)) * 0.003, 0.008, 0.045);
|
| 165 |
+
lines.push(`${run},${steps[i]},train_loss,${formatLike(trainLoss, 3)},${formatLike(stderrTrain, 3)}`);
|
| 166 |
+
lines.push(`${run},${steps[i]},val_loss,${formatLike(valLoss, 3)},${formatLike(stderrVal, 3)}`);
|
| 167 |
+
}
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
// TRAIN ACCURACY & VAL ACCURACY (logistic)
|
| 171 |
+
for (let r = 0; r < runs.length; r++){
|
| 172 |
+
const run = runs[r];
|
| 173 |
+
const p = runParams[r];
|
| 174 |
+
for (let i = 0; i < steps.length; i++){
|
| 175 |
+
const t = stepNorm(steps[i]);
|
| 176 |
+
const accBase = logistic(t, p.kAcc, p.x0Acc);
|
| 177 |
+
let trainAcc = clamp(0.55 + accBase * (p.trainAccFinal - 0.55), 0, 1);
|
| 178 |
+
let valAcc = clamp(0.52 + accBase * (p.valAccFinal - 0.52), 0, 1);
|
| 179 |
+
// Gentle noise
|
| 180 |
+
trainAcc = clamp(trainAcc + randn(rng) * 0.005 * args.amount, 0, 1);
|
| 181 |
+
valAcc = clamp(valAcc + randn(rng) * 0.006 * args.amount, 0, 1);
|
| 182 |
+
const stderrTrain = clamp(0.02 - 0.011 * t + Math.abs(randn(rng)) * 0.002, 0.006, 0.03);
|
| 183 |
+
const stderrVal = clamp(0.022 - 0.012 * t + Math.abs(randn(rng)) * 0.002, 0.007, 0.032);
|
| 184 |
+
lines.push(`${run},${steps[i]},train_accuracy,${formatLike(trainAcc, 4)},${formatLike(stderrTrain, 3)}`);
|
| 185 |
+
lines.push(`${run},${steps[i]},val_accuracy,${formatLike(valAcc, 4)},${formatLike(stderrVal, 3)}`);
|
| 186 |
+
}
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
// Ensure directory exists
|
| 190 |
+
await fs.mkdir(path.dirname(args.out), { recursive: true });
|
| 191 |
+
await fs.writeFile(args.out, lines.join('\n') + '\n', 'utf8');
|
| 192 |
+
const relOut = path.relative(process.cwd(), args.out);
|
| 193 |
+
console.log(`Synthetic CSV generated: ${relOut}`);
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
main().catch(err => { console.error(err?.stack || String(err)); process.exit(1); });
|
app/scripts/jitter-trackio-data.mjs
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env node
|
| 2 |
+
|
| 3 |
+
// Jitter Trackio CSV data with small, controlled noise.
|
| 4 |
+
// - Preserves comments (# ...) and blank lines
|
| 5 |
+
// - Leaves 'epoch' values unchanged
|
| 6 |
+
// - Adds mild noise to train/val accuracy (clamped to [0,1])
|
| 7 |
+
// - Adds mild noise to train/val loss (kept >= 0)
|
| 8 |
+
// - Keeps steps untouched
|
| 9 |
+
// Usage:
|
| 10 |
+
// node app/scripts/jitter-trackio-data.mjs \
|
| 11 |
+
// --in app/src/content/assets/data/trackio_wandb_demo.csv \
|
| 12 |
+
// --out app/src/content/assets/data/trackio_wandb_demo.jitter.csv \
|
| 13 |
+
// [--seed 42] [--amount 1.0] [--in-place]
|
| 14 |
+
|
| 15 |
+
import fs from 'node:fs/promises';
|
| 16 |
+
import path from 'node:path';
|
| 17 |
+
|
| 18 |
+
function parseArgs(argv){
|
| 19 |
+
const args = { in: '', out: '', seed: undefined, amount: 1, inPlace: false };
|
| 20 |
+
for (let i = 2; i < argv.length; i++){
|
| 21 |
+
const a = argv[i];
|
| 22 |
+
if (a === '--in' && argv[i+1]) { args.in = argv[++i]; continue; }
|
| 23 |
+
if (a === '--out' && argv[i+1]) { args.out = argv[++i]; continue; }
|
| 24 |
+
if (a === '--seed' && argv[i+1]) { args.seed = Number(argv[++i]); continue; }
|
| 25 |
+
if (a === '--amount' && argv[i+1]) { args.amount = Number(argv[++i]) || 3; continue; }
|
| 26 |
+
if (a === '--in-place') { args.inPlace = true; continue; }
|
| 27 |
+
}
|
| 28 |
+
if (!args.in) throw new Error('--in is required');
|
| 29 |
+
if (args.inPlace) args.out = args.in;
|
| 30 |
+
if (!args.out) {
|
| 31 |
+
const { dir, name, ext } = path.parse(args.in);
|
| 32 |
+
args.out = path.join(dir, `${name}.jitter${ext || '.csv'}`);
|
| 33 |
+
}
|
| 34 |
+
return args;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
function mulberry32(seed){
|
| 38 |
+
let t = seed >>> 0;
|
| 39 |
+
return function(){
|
| 40 |
+
t += 0x6D2B79F5;
|
| 41 |
+
let r = Math.imul(t ^ (t >>> 15), 1 | t);
|
| 42 |
+
r ^= r + Math.imul(r ^ (r >>> 7), 61 | r);
|
| 43 |
+
return ((r ^ (r >>> 14)) >>> 0) / 4294967296;
|
| 44 |
+
};
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
function makeRng(seed){
|
| 48 |
+
if (Number.isFinite(seed)) return mulberry32(seed);
|
| 49 |
+
return Math.random;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
function randn(rng){
|
| 53 |
+
// Box-Muller transform
|
| 54 |
+
let u = 0, v = 0;
|
| 55 |
+
while (u === 0) u = rng();
|
| 56 |
+
while (v === 0) v = rng();
|
| 57 |
+
return Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
function jitterValue(metric, value, amount, rng){
|
| 61 |
+
const m = metric.toLowerCase();
|
| 62 |
+
if (m === 'epoch') return value; // keep as-is
|
| 63 |
+
if (m.includes('accuracy')){
|
| 64 |
+
const n = Math.max(-0.02 * amount, Math.min(0.02 * amount, randn(rng) * 0.01 * amount));
|
| 65 |
+
return Math.max(0, Math.min(1, value + n));
|
| 66 |
+
}
|
| 67 |
+
if (m.includes('loss')){
|
| 68 |
+
const n = Math.max(-0.03 * amount, Math.min(0.03 * amount, randn(rng) * 0.01 * amount));
|
| 69 |
+
return Math.max(0, value + n);
|
| 70 |
+
}
|
| 71 |
+
// default: tiny noise
|
| 72 |
+
const n = Math.max(-0.01 * amount, Math.min(0.01 * amount, randn(rng) * 0.005 * amount));
|
| 73 |
+
return value + n;
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
function formatNumberLike(original, value){
|
| 77 |
+
const s = String(original);
|
| 78 |
+
const dot = s.indexOf('.')
|
| 79 |
+
const decimals = dot >= 0 ? (s.length - dot - 1) : 0;
|
| 80 |
+
if (!Number.isFinite(value)) return s;
|
| 81 |
+
if (decimals <= 0) return String(Math.round(value));
|
| 82 |
+
return value.toFixed(decimals);
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
async function main(){
|
| 86 |
+
const args = parseArgs(process.argv);
|
| 87 |
+
const rng = makeRng(args.seed);
|
| 88 |
+
const raw = await fs.readFile(args.in, 'utf8');
|
| 89 |
+
const lines = raw.split(/\r?\n/);
|
| 90 |
+
const out = new Array(lines.length);
|
| 91 |
+
|
| 92 |
+
for (let i = 0; i < lines.length; i++){
|
| 93 |
+
const line = lines[i];
|
| 94 |
+
if (!line || line.trim().length === 0) { out[i] = line; continue; }
|
| 95 |
+
if (/^\s*#/.test(line)) { out[i] = line; continue; }
|
| 96 |
+
|
| 97 |
+
// Preserve header line unmodified
|
| 98 |
+
if (i === 0 && /^\s*run\s*,\s*step\s*,\s*metric\s*,\s*value\s*,\s*stderr\s*$/i.test(line)) {
|
| 99 |
+
out[i] = line; continue;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
const cols = line.split(',');
|
| 103 |
+
if (cols.length < 4) { out[i] = line; continue; }
|
| 104 |
+
|
| 105 |
+
const [run, stepStr, metric, valueStr, stderrStr = ''] = cols;
|
| 106 |
+
const trimmedMetric = (metric || '').trim();
|
| 107 |
+
const valueNum = Number((valueStr || '').trim());
|
| 108 |
+
|
| 109 |
+
if (!Number.isFinite(valueNum)) { out[i] = line; continue; }
|
| 110 |
+
|
| 111 |
+
const jittered = jitterValue(trimmedMetric, valueNum, args.amount, rng);
|
| 112 |
+
const valueOut = formatNumberLike(valueStr, jittered);
|
| 113 |
+
|
| 114 |
+
// Reassemble with original column count and positions
|
| 115 |
+
const result = [run, stepStr, metric, valueOut, stderrStr].join(',');
|
| 116 |
+
out[i] = result;
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
const finalText = out.join('\n');
|
| 120 |
+
await fs.writeFile(args.out, finalText, 'utf8');
|
| 121 |
+
const relIn = path.relative(process.cwd(), args.in);
|
| 122 |
+
const relOut = path.relative(process.cwd(), args.out);
|
| 123 |
+
console.log(`Jittered data written: ${relOut} (from ${relIn})`);
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
main().catch(err => {
|
| 127 |
+
console.error(err?.stack || String(err));
|
| 128 |
+
process.exit(1);
|
| 129 |
+
});
|
app/src/components/Palettes.astro
CHANGED
|
@@ -0,0 +1,170 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 '/src/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 |
+
|
| 170 |
+
|
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:582f65833ae2138f8f1b57d7a3a07726278550fb5eed589e2c02b3e7bf84a02d
|
| 3 |
+
size 6244
|
app/src/content/chapters/vibe-coding-charts.mdx
CHANGED
|
@@ -73,13 +73,12 @@ They can be found in the `app/src/content/embeds` folder and you can also use th
|
|
| 73 |
---
|
| 74 |
<HtmlEmbed
|
| 75 |
src="d3-trackio.html"
|
| 76 |
-
|
| 77 |
/>
|
| 78 |
---
|
| 79 |
-
|
| 80 |
src="d3-trackio-oblivion.html"
|
| 81 |
-
title="Résultats TrackIO"
|
| 82 |
frameless
|
| 83 |
/>
|
| 84 |
-
---
|
| 85 |
|
|
|
|
| 73 |
---
|
| 74 |
<HtmlEmbed
|
| 75 |
src="d3-trackio.html"
|
| 76 |
+
frameless
|
| 77 |
/>
|
| 78 |
---
|
| 79 |
+
<HtmlEmbed
|
| 80 |
src="d3-trackio-oblivion.html"
|
|
|
|
| 81 |
frameless
|
| 82 |
/>
|
| 83 |
+
---
|
| 84 |
|
app/src/content/embeds/d3-trackio-oblivion.html
CHANGED
|
@@ -13,27 +13,56 @@
|
|
| 13 |
/* Futuristic "Oblivion"-inspired styling */
|
| 14 |
.d3-trackio-oblivion { position: relative;
|
| 15 |
--cell-gap: 0px;
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
--
|
| 30 |
-
--
|
| 31 |
-
|
| 32 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
}
|
| 34 |
.d3-trackio-oblivion * { font-family: inherit;
|
| 35 |
}
|
| 36 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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; } }
|
|
@@ -65,9 +94,14 @@
|
|
| 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 |
|
|
@@ -82,7 +116,7 @@
|
|
| 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; }
|
|
@@ -98,12 +132,12 @@
|
|
| 98 |
transform: translate(-9999px, -9999px);
|
| 99 |
pointer-events: none;
|
| 100 |
padding: 10px 12px;
|
| 101 |
-
border-radius:
|
| 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,.
|
| 107 |
opacity: .5;
|
| 108 |
transition: opacity .12s ease;
|
| 109 |
z-index: 1000;
|
|
@@ -112,8 +146,10 @@
|
|
| 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: .
|
| 116 |
-
.d3-trackio-oblivion .d3-tooltip__inner > div:nth-child(n+3) { padding-top:
|
|
|
|
|
|
|
| 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>
|
|
@@ -146,7 +182,12 @@
|
|
| 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);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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');
|
|
@@ -157,6 +198,7 @@
|
|
| 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';
|
|
@@ -165,10 +207,22 @@
|
|
| 165 |
|
| 166 |
// Layout & scales
|
| 167 |
let width = 800, height = 180; const margin = { top: 12, right: 20, bottom: 36, left: 44 };
|
| 168 |
-
|
| 169 |
-
const lineGen = d3.line().x(d => xScale(
|
| 170 |
-
|
| 171 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 172 |
const rect = cell.getBoundingClientRect();
|
| 173 |
width = Math.max(1, Math.round(rect && rect.width ? rect.width : (cell.clientWidth || 800)));
|
| 174 |
height = 180;
|
|
@@ -192,13 +246,24 @@
|
|
| 192 |
// dedupe
|
| 193 |
return Array.from(new Set(arr));
|
| 194 |
};
|
| 195 |
-
const xTicksForced =
|
| 196 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 197 |
gAxes
|
| 198 |
.append('g')
|
| 199 |
.attr('transform', `translate(0,${innerHeight})`)
|
| 200 |
.call(
|
| 201 |
-
d3.axisBottom(xScale)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 202 |
)
|
| 203 |
.call((g) => {
|
| 204 |
g.selectAll('path, line')
|
|
@@ -210,7 +275,7 @@
|
|
| 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)');
|
|
@@ -246,12 +311,31 @@
|
|
| 246 |
if (!isFinite(minStep) || !isFinite(maxStep)) return;
|
| 247 |
const isAccuracy = /accuracy/i.test(metricKey);
|
| 248 |
const axisLabelY = prettyMetricLabel(metricKey);
|
| 249 |
-
|
| 250 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 251 |
|
| 252 |
// Grid as small dots at intersections of y ticks × step positions
|
|
|
|
| 253 |
const gridPoints = [];
|
| 254 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 255 |
gGrid.selectAll('circle.grid-dot')
|
| 256 |
.data(gridPoints)
|
| 257 |
.join('circle')
|
|
@@ -275,14 +359,40 @@
|
|
| 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')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 286 |
// Animate points at the hovered step to grow slightly
|
| 287 |
try {
|
| 288 |
gPoints.selectAll('circle.pt')
|
|
@@ -290,7 +400,7 @@
|
|
| 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 |
|
|
@@ -298,7 +408,7 @@
|
|
| 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">
|
| 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';
|
|
|
|
| 13 |
/* Futuristic "Oblivion"-inspired styling */
|
| 14 |
.d3-trackio-oblivion { position: relative;
|
| 15 |
--cell-gap: 0px;
|
| 16 |
+
/* Default: light/neutral theme */
|
| 17 |
+
--obl-cyan: #2f343b;
|
| 18 |
+
--obl-cyan-dim: rgba(47,52,59,.28);
|
| 19 |
+
--obl-accent: #1a1d21;
|
| 20 |
+
--obl-bg: rgba(0,0,0,0.04);
|
| 21 |
+
--obl-border: rgba(47,52,59,.22);
|
| 22 |
+
--ghost-obl-border: rgba(47,52,59,.06);
|
| 23 |
+
--obl-glow: 0 0 0 1px rgba(0,0,0,.05), 0 8px 40px rgba(0,0,0,.06);
|
| 24 |
+
/* Engraved separator colors (light theme) */
|
| 25 |
+
--engrave-light: rgba(255,255,255,.15);
|
| 26 |
+
--engrave-dark: rgba(0,0,0,.10);
|
| 27 |
+
background: transparent;
|
| 28 |
+
--corner-inset: 6px;
|
| 29 |
+
--hud-gap: 10px;
|
| 30 |
+
--hud-corner-size: 8px;
|
| 31 |
+
/* Chart background gradient as a variable for easy theming */
|
| 32 |
+
--hud-bg-gradient: radial-gradient(1200px 200px at 20% -10%, rgba(0,0,0,.05), transparent 80%), radial-gradient(900px 200px at 80% 110%, rgba(0,0,0,.05), transparent 80%);
|
| 33 |
+
/* Tooltip offset (bottom-right of cursor) */
|
| 34 |
+
--tip-offset-x: 10px;
|
| 35 |
+
--tip-offset-y: 10px;
|
| 36 |
+
padding: var(--hud-gap);
|
| 37 |
+
--z-tooltip: 50;
|
| 38 |
+
--z-overlay: 99999999;
|
| 39 |
+
z-index: var(--z-tooltip);
|
| 40 |
+
font-family: 'Roboto Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
|
| 41 |
}
|
| 42 |
.d3-trackio-oblivion * { font-family: inherit;
|
| 43 |
}
|
| 44 |
+
/* Dark mode: cyan/Oblivion theme */
|
| 45 |
+
[data-theme="dark"] .d3-trackio-oblivion {
|
| 46 |
+
--obl-cyan: #7FF1FF;
|
| 47 |
+
--obl-cyan-dim: rgba(127,241,255,.25);
|
| 48 |
+
--obl-accent: #8BF5FF;
|
| 49 |
+
--obl-bg: rgba(255,255,255,0.06);
|
| 50 |
+
--obl-border: rgba(127,241,255,.25);
|
| 51 |
+
--ghost-obl-border: rgba(127,241,255,.02);
|
| 52 |
+
--obl-glow: 0 0 0 1px rgba(127,241,255,.35), 0 8px 40px rgba(127,241,255,.12);
|
| 53 |
+
/* Richer cyan gradients for the classic Oblivion blue feel */
|
| 54 |
+
--hud-bg-gradient:
|
| 55 |
+
radial-gradient(1400px 260px at 20% -10%, rgba(127,241,255,.065), transparent 80%),
|
| 56 |
+
radial-gradient(1100px 240px at 80% 110%, rgba(127,241,255,.06), transparent 80%),
|
| 57 |
+
linear-gradient(180deg, rgba(127,241,255,.035), rgba(127,241,255,0) 45%);
|
| 58 |
+
/* Engraved separator colors (dark theme) */
|
| 59 |
+
--engrave-light: rgba(127,241,255,.05);
|
| 60 |
+
--engrave-dark: rgba(0,0,0,.15);
|
| 61 |
+
background: #0f1115;
|
| 62 |
+
}
|
| 63 |
+
/* Tooltip shadows: neutral by default, bluish tint in dark */
|
| 64 |
+
.d3-trackio-oblivion .d3-tooltip { box-shadow: 0 8px 32px rgba(0,0,0,.08), 0 2px 8px rgba(0,0,0,.06); }
|
| 65 |
+
[data-theme="dark"] .d3-trackio-oblivion .d3-tooltip { box-shadow: 0 8px 32px rgba(127,241,255,.05), 0 2px 8px rgba(0,0,0,.10); }
|
| 66 |
|
| 67 |
.d3-trackio-oblivion__grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: --cell-gap; }
|
| 68 |
@media (max-width: 980px) { .d3-trackio-oblivion__grid { grid-template-columns: 1fr; } }
|
|
|
|
| 94 |
linear-gradient(var(--obl-cyan), var(--obl-cyan)) bottom right / var(--hud-corner-size) 1px no-repeat,
|
| 95 |
linear-gradient(var(--obl-cyan), var(--obl-cyan)) bottom right / 1px var(--hud-corner-size) no-repeat; opacity:.85; }
|
| 96 |
.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; }
|
| 97 |
+
.d3-trackio-oblivion .cell-header { padding: 10px 6px 10px 12px; display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
| 98 |
.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; }
|
| 99 |
.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; }
|
| 100 |
+
/* Fullscreen button (icon only, no behavior here) */
|
| 101 |
+
.d3-trackio-oblivion .cell-action { padding: 0 !important; margin-left: auto; display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; border: 0; background: transparent; color: var(--obl-cyan); opacity: .7; cursor: pointer; z-index: 4; }
|
| 102 |
+
.d3-trackio-oblivion .cell-action:hover { opacity: 1; }
|
| 103 |
+
.d3-trackio-oblivion .cell-action svg { width: 22px; height: 22px; opacity: .2; margin-left: 5px; }
|
| 104 |
+
.d3-trackio-oblivion .cell-action svg, .d3-trackio-oblivion .cell-action svg path { fill: currentColor; stroke: none; }
|
| 105 |
.d3-trackio-oblivion .cell-body { position: relative; width: 100%; overflow: hidden; }
|
| 106 |
.d3-trackio-oblivion .cell-body svg { max-width: 100%; height: auto; display: block; }
|
| 107 |
|
|
|
|
| 116 |
.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; }
|
| 117 |
.d3-trackio-oblivion__header .legend-bottom .items { display: flex; flex-wrap: wrap; gap: 8px 14px; }
|
| 118 |
.d3-trackio-oblivion__header .legend-bottom .item { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; color: var(--obl-cyan); }
|
| 119 |
+
.d3-trackio-oblivion__header .legend-bottom .swatch { width: 14px; height: 14px; border-radius: 2px; border: 1px solid var(--ghost-obl-border); display: inline-block; box-shadow: 0 0 12px rgba(127,241,255,.18) inset; }
|
| 120 |
|
| 121 |
/* Hover ghosting */
|
| 122 |
.d3-trackio-oblivion.hovering .lines path.ghost { opacity: .22; }
|
|
|
|
| 132 |
transform: translate(-9999px, -9999px);
|
| 133 |
pointer-events: none;
|
| 134 |
padding: 10px 12px;
|
| 135 |
+
border-radius: 8px;
|
| 136 |
font-size: 12px;
|
| 137 |
line-height: 1.35;
|
| 138 |
background: var(--obl-bg);
|
| 139 |
color: var(--obl-cyan);
|
| 140 |
+
box-shadow: 0 8px 32px rgba(127,241,255,.05), 0 2px 8px rgba(0,0,0,.10);
|
| 141 |
opacity: .5;
|
| 142 |
transition: opacity .12s ease;
|
| 143 |
z-index: 1000;
|
|
|
|
| 146 |
.d3-trackio-oblivion .d3-tooltip.is-visible { opacity: 1; }
|
| 147 |
.d3-trackio-oblivion .d3-tooltip__inner { display: flex; flex-direction: column; gap: 6px; min-width: 220px; text-align: left; }
|
| 148 |
.d3-trackio-oblivion .d3-tooltip__inner > div:first-child { font-weight: 900; letter-spacing: .18em; text-transform: uppercase; color: var(--obl-cyan); }
|
| 149 |
+
.d3-trackio-oblivion .d3-tooltip__inner > div:nth-child(2) { font-size: 11px; color: var(--obl-cyan); opacity: .4; display: block; margin-top: -4px; margin-bottom: 2px; letter-spacing: 0.06em; }
|
| 150 |
+
.d3-trackio-oblivion .d3-tooltip__inner > div:nth-child(n+3) { position: relative; padding-top: 10px; margin-top: 6px; }
|
| 151 |
+
.d3-trackio-oblivion .d3-tooltip__inner > div:nth-child(n+3)::before { content:""; position:absolute; left:0; right:0; top:0; height:1px; background: var(--engrave-dark); }
|
| 152 |
+
.d3-trackio-oblivion .d3-tooltip__inner > div:nth-child(n+3)::after { content:""; position:absolute; left:0; right:0; top:1px; height:1px; background: var(--engrave-light); }
|
| 153 |
.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; }
|
| 154 |
</style>
|
| 155 |
<script>
|
|
|
|
| 182 |
const corners = document.createElement('div'); corners.className = 'cell-corners'; cell.appendChild(corners);
|
| 183 |
const inner = document.createElement('div'); inner.className = 'cell-inner'; cell.appendChild(inner);
|
| 184 |
const header = document.createElement('div'); header.className = 'cell-header';
|
| 185 |
+
const title = document.createElement('div'); title.className = 'cell-title'; title.textContent = prettyMetricLabel(titleText); header.appendChild(title);
|
| 186 |
+
// Fullscreen icon button (no click handler here)
|
| 187 |
+
const fsBtn = document.createElement('button'); fsBtn.className = 'cell-action cell-action--fullscreen'; fsBtn.type = 'button'; fsBtn.title = 'Fullscreen'; fsBtn.setAttribute('aria-label', 'Open fullscreen');
|
| 188 |
+
fsBtn.innerHTML = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 9V4h5v2H6v3H4zm10-5h5v5h-2V6h-3V4zM6 18h3v2H4v-5h2v3zm12-3h2v5h-5v-2h3v-3z"/></svg>';
|
| 189 |
+
header.appendChild(fsBtn);
|
| 190 |
+
inner.appendChild(header);
|
| 191 |
|
| 192 |
const body = document.createElement('div'); body.className = 'cell-body'; inner.appendChild(body);
|
| 193 |
const svg = d3.select(body).append('svg').attr('width','100%').style('display','block');
|
|
|
|
| 198 |
const gLines = gRoot.append('g').attr('class','lines');
|
| 199 |
const gPoints = gRoot.append('g').attr('class','points');
|
| 200 |
const gHover = gRoot.append('g').attr('class','hover');
|
| 201 |
+
const host = cell.closest('.d3-trackio-oblivion');
|
| 202 |
|
| 203 |
// Tooltip
|
| 204 |
cell.style.position = cell.style.position || 'relative';
|
|
|
|
| 207 |
|
| 208 |
// Layout & scales
|
| 209 |
let width = 800, height = 180; const margin = { top: 12, right: 20, bottom: 36, left: 44 };
|
| 210 |
+
let xScale = d3.scaleLinear(); const yScale = d3.scaleLinear();
|
| 211 |
+
const lineGen = d3.line().x(d => xScale(0)).y(d => yScale(d.value));
|
| 212 |
+
|
| 213 |
+
// Generic number abbreviation for axis ticks (K/M/B) with up to 2 decimals
|
| 214 |
+
const formatAbbrev = (value) => {
|
| 215 |
+
const num = Number(value);
|
| 216 |
+
if (!Number.isFinite(num)) return String(value);
|
| 217 |
+
const abs = Math.abs(num);
|
| 218 |
+
const trim2 = (n) => Number(n).toFixed(2).replace(/\.?0+$/, '');
|
| 219 |
+
if (abs >= 1e9) return `${trim2(num / 1e9)}B`;
|
| 220 |
+
if (abs >= 1e6) return `${trim2(num / 1e6)}M`;
|
| 221 |
+
if (abs >= 1e3) return `${trim2(num / 1e3)}K`;
|
| 222 |
+
return trim2(num);
|
| 223 |
+
};
|
| 224 |
+
|
| 225 |
+
function updateLayout(axisLabelY, xTicksArg){
|
| 226 |
const rect = cell.getBoundingClientRect();
|
| 227 |
width = Math.max(1, Math.round(rect && rect.width ? rect.width : (cell.clientWidth || 800)));
|
| 228 |
height = 180;
|
|
|
|
| 246 |
// dedupe
|
| 247 |
return Array.from(new Set(arr));
|
| 248 |
};
|
| 249 |
+
const xTicksForced = (Array.isArray(xTicksArg) && xTicksArg.length)
|
| 250 |
+
? Array.from({ length: xTicksArg.length }, (_, i) => i)
|
| 251 |
+
: makeTicks(xScale, 8);
|
| 252 |
+
const yCount = Math.max(2, Math.min(6, xTicksForced.length));
|
| 253 |
+
const yDom = yScale.domain();
|
| 254 |
+
const yTicksForced = (yCount <= 2)
|
| 255 |
+
? [yDom[0], yDom[1]]
|
| 256 |
+
: Array.from({ length: yCount }, (_, i) => yDom[0] + ((yDom[1] - yDom[0]) * (i / (yCount - 1))));
|
| 257 |
gAxes
|
| 258 |
.append('g')
|
| 259 |
.attr('transform', `translate(0,${innerHeight})`)
|
| 260 |
.call(
|
| 261 |
+
d3.axisBottom(xScale)
|
| 262 |
+
.tickValues(xTicksForced)
|
| 263 |
+
.tickFormat((i) => {
|
| 264 |
+
const val = (Array.isArray(xTicksArg) && xTicksArg[i] != null ? xTicksArg[i] : i);
|
| 265 |
+
return formatAbbrev(val);
|
| 266 |
+
})
|
| 267 |
)
|
| 268 |
.call((g) => {
|
| 269 |
g.selectAll('path, line')
|
|
|
|
| 275 |
|
| 276 |
gAxes
|
| 277 |
.append('g')
|
| 278 |
+
.call(d3.axisLeft(yScale).tickValues(yTicksForced).tickFormat((v) => formatAbbrev(v)))
|
| 279 |
.call((g) => {
|
| 280 |
g.selectAll('path, line')
|
| 281 |
.attr('stroke', 'var(--obl-cyan-dim)');
|
|
|
|
| 311 |
if (!isFinite(minStep) || !isFinite(maxStep)) return;
|
| 312 |
const isAccuracy = /accuracy/i.test(metricKey);
|
| 313 |
const axisLabelY = prettyMetricLabel(metricKey);
|
| 314 |
+
if (isAccuracy) yScale.domain([0,1]).nice(); else yScale.domain([minVal, maxVal]).nice();
|
| 315 |
+
// Compute unique x steps and build index mapping
|
| 316 |
+
const rawSteps = [];
|
| 317 |
+
runs.forEach(r => (metricData[r]||[]).forEach(pt => rawSteps.push(pt.step)));
|
| 318 |
+
const hoverSteps = Array.from(new Set(rawSteps)).sort((a,b)=>a-b);
|
| 319 |
+
const stepIndex = new Map(hoverSteps.map((s,i)=>[s,i]));
|
| 320 |
+
const indices = hoverSteps.map((_, i) => i);
|
| 321 |
+
// Linear index scale → ticks at edges, equal spacing
|
| 322 |
+
xScale = d3.scaleLinear().domain([0, Math.max(0, indices.length - 1)]);
|
| 323 |
+
// Update line generator X accessor to use index directly
|
| 324 |
+
lineGen.x(d => xScale(stepIndex.get(d.step)));
|
| 325 |
+
const { innerWidth, innerHeight, xTicksForced, yTicksForced } = updateLayout(axisLabelY, hoverSteps);
|
| 326 |
|
| 327 |
// Grid as small dots at intersections of y ticks × step positions
|
| 328 |
+
// Exclude dots that fall on the origin axes lines (left Y-axis and bottom X-axis)
|
| 329 |
const gridPoints = [];
|
| 330 |
+
const yDomAll = yScale.domain();
|
| 331 |
+
const yMin = Array.isArray(yDomAll) ? yDomAll[0] : null;
|
| 332 |
+
xTicksForced.forEach(i => {
|
| 333 |
+
yTicksForced.forEach(t => {
|
| 334 |
+
if (i !== 0 && (yMin == null || t !== yMin)) {
|
| 335 |
+
gridPoints.push({ sx: i, ty: t });
|
| 336 |
+
}
|
| 337 |
+
});
|
| 338 |
+
});
|
| 339 |
gGrid.selectAll('circle.grid-dot')
|
| 340 |
.data(gridPoints)
|
| 341 |
.join('circle')
|
|
|
|
| 359 |
// Points
|
| 360 |
const allPts = series.flatMap(s => s.values.map(v => ({ run:s.run, color:s.color, step:v.step, value:v.value })));
|
| 361 |
const ptsSel = gPoints.selectAll('circle.pt').data(allPts, d=>`${d.run}-${d.step}`);
|
| 362 |
+
ptsSel.enter().append('circle')
|
| 363 |
+
.attr('class','pt')
|
| 364 |
+
.attr('r', 2)
|
| 365 |
+
.attr('fill', d=>d.color)
|
| 366 |
+
.attr('stroke','none')
|
| 367 |
+
.attr('cx', d=> xScale(stepIndex.get(d.step)))
|
| 368 |
+
.attr('cy', d=>yScale(d.value))
|
| 369 |
+
.merge(ptsSel)
|
| 370 |
+
.transition().duration(150)
|
| 371 |
+
.attr('cx', d=> xScale(stepIndex.get(d.step)))
|
| 372 |
+
.attr('cy', d=>yScale(d.value));
|
| 373 |
ptsSel.exit().remove();
|
| 374 |
+
// Steps used for hover snapping (unique data steps)
|
| 375 |
+
// already computed above as hoverSteps
|
| 376 |
|
| 377 |
// Hover
|
| 378 |
gHover.selectAll('*').remove();
|
| 379 |
const overlay = gHover.append('rect').attr('fill','transparent').style('cursor','crosshair').attr('x',0).attr('y',0).attr('width', innerWidth).attr('height', innerHeight);
|
| 380 |
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');
|
| 381 |
+
// External hover handlers
|
| 382 |
+
cell.__showExternalStep = (stepVal) => {
|
| 383 |
+
if (stepVal == null) { hoverLine.style('display','none'); return; }
|
| 384 |
+
const idx = stepIndex.get(stepVal);
|
| 385 |
+
if (idx == null) { hoverLine.style('display','none'); return; }
|
| 386 |
+
const xpx = xScale(idx);
|
| 387 |
+
hoverLine.attr('x1',xpx).attr('x2',xpx).style('display',null);
|
| 388 |
+
};
|
| 389 |
+
cell.__clearExternalStep = () => { hoverLine.style('display','none'); };
|
| 390 |
+
if (!cell.__syncAttached && host) {
|
| 391 |
+
host.addEventListener('trackio-hover-step', (ev) => { const d = ev && ev.detail; if (!d) return; if (cell.__showExternalStep) cell.__showExternalStep(d.step); });
|
| 392 |
+
host.addEventListener('trackio-hover-clear', () => { if (cell.__clearExternalStep) cell.__clearExternalStep(); });
|
| 393 |
+
cell.__syncAttached = true;
|
| 394 |
+
}
|
| 395 |
+
function onMove(ev){ if (hideTipTimer) { clearTimeout(hideTipTimer); hideTipTimer = null; } const [mx,my]=d3.pointer(ev, overlay.node()); const idx = Math.round(Math.max(0, Math.min(hoverSteps.length-1, xScale.invert(mx)))); const nearest = hoverSteps[idx]; const xpx = xScale(idx); hoverLine.attr('x1',xpx).attr('x2',xpx).style('display',null); if (host) { try { host.dispatchEvent(new CustomEvent('trackio-hover-step', { detail: { step: nearest } })); } catch(_){} } let html = `<div>Step ${formatAbbrev(nearest)}</div><div>${prettyMetricLabel(metricKey)}</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 => { 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)}</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)`;
|
| 396 |
// Animate points at the hovered step to grow slightly
|
| 397 |
try {
|
| 398 |
gPoints.selectAll('circle.pt')
|
|
|
|
| 400 |
.attr('r', d => (d && d.step === nearest ? 4 : 2));
|
| 401 |
} catch(_) {}
|
| 402 |
}
|
| 403 |
+
function onLeave(){ hideTipTimer = setTimeout(()=>{ tip.classList.remove('is-visible'); tip.style.transform='translate(-9999px, -9999px)'; hoverLine.style('display','none'); if (host) { try { host.dispatchEvent(new CustomEvent('trackio-hover-clear')); } catch(_){} } try { gPoints.selectAll('circle.pt').transition().duration(150).ease(d3.easeCubicOut).attr('r', 2); } catch(_) {} }, 100); }
|
| 404 |
overlay.on('mousemove', onMove).on('mouseleave', onLeave);
|
| 405 |
}
|
| 406 |
|
|
|
|
| 408 |
}
|
| 409 |
|
| 410 |
const bootstrap = () => {
|
| 411 |
+
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">Runs</div><div class="items"></div>'; header.appendChild(legend);
|
| 412 |
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; }
|
| 413 |
if (!host) { host = document.querySelector('.d3-trackio-oblivion'); }
|
| 414 |
if (!host) return; if (host.dataset && host.dataset.mounted==='true') return; if (host.dataset) host.dataset.mounted='true';
|
app/src/content/embeds/d3-trackio.html
CHANGED
|
@@ -9,7 +9,18 @@
|
|
| 9 |
<noscript>JavaScript is required to render this chart.</noscript>
|
| 10 |
</div>
|
| 11 |
<style>
|
| 12 |
-
.d3-trackio { position: relative;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
.d3-trackio__grid {
|
| 14 |
display: grid;
|
| 15 |
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
@@ -30,7 +41,6 @@
|
|
| 30 |
}
|
| 31 |
.d3-trackio .cell-header {
|
| 32 |
padding: 8px 10px;
|
| 33 |
-
border-bottom: 1px solid var(--border-color);
|
| 34 |
display: flex;
|
| 35 |
align-items: center;
|
| 36 |
justify-content: space-between;
|
|
@@ -42,6 +52,67 @@
|
|
| 42 |
color: var(--text-color);
|
| 43 |
text-transform: none;
|
| 44 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
.d3-trackio .cell-body { position: relative; width: 100%; overflow: hidden; }
|
| 46 |
.d3-trackio .cell-body svg { max-width: 100%; height: auto; display: block; }
|
| 47 |
|
|
@@ -51,22 +122,24 @@
|
|
| 51 |
.d3-trackio .axes text { fill: var(--tick-color); }
|
| 52 |
.d3-trackio .grid line { stroke: var(--grid-color); }
|
| 53 |
|
| 54 |
-
/* Global header (legend)
|
| 55 |
.d3-trackio__header {
|
| 56 |
display: flex;
|
| 57 |
align-items: flex-start;
|
| 58 |
-
justify-content:
|
| 59 |
gap: 12px;
|
| 60 |
-
margin:
|
| 61 |
flex-wrap: wrap;
|
|
|
|
| 62 |
}
|
| 63 |
.d3-trackio__header .legend-bottom {
|
| 64 |
display: flex;
|
| 65 |
flex-direction: column;
|
| 66 |
-
align-items:
|
| 67 |
gap: 6px;
|
| 68 |
font-size: 12px;
|
| 69 |
color: var(--text-color);
|
|
|
|
| 70 |
}
|
| 71 |
.d3-trackio__header .legend-bottom .legend-title { font-size: 12px; font-weight: 700; color: var(--text-color); }
|
| 72 |
.d3-trackio__header .legend-bottom .items { display: flex; flex-wrap: wrap; gap: 8px 14px; }
|
|
@@ -81,7 +154,7 @@
|
|
| 81 |
|
| 82 |
/* Tooltip styling aligned with other embeds */
|
| 83 |
.d3-trackio .d3-tooltip {
|
| 84 |
-
z-index:
|
| 85 |
backdrop-filter: saturate(1.12) blur(8px);
|
| 86 |
}
|
| 87 |
.d3-trackio .d3-tooltip__inner { display: flex; flex-direction: column; gap: 6px; min-width: 220px; }
|
|
@@ -129,6 +202,8 @@
|
|
| 129 |
const d3 = window.d3;
|
| 130 |
const metricKey = cell.getAttribute('data-metric');
|
| 131 |
const titleText = cell.getAttribute('data-title') || metricKey;
|
|
|
|
|
|
|
| 132 |
|
| 133 |
// Header
|
| 134 |
const header = document.createElement('div'); header.className = 'cell-header';
|
|
@@ -146,6 +221,9 @@
|
|
| 146 |
const gPoints = gRoot.append('g').attr('class','points');
|
| 147 |
const gHover = gRoot.append('g').attr('class','hover');
|
| 148 |
|
|
|
|
|
|
|
|
|
|
| 149 |
// Tooltip
|
| 150 |
cell.style.position = cell.style.position || 'relative';
|
| 151 |
let tip = cell.querySelector('.d3-tooltip'); let tipInner; let hideTipTimer = null;
|
|
@@ -160,16 +238,16 @@
|
|
| 160 |
} else { tipInner = tip.querySelector('.d3-tooltip__inner') || tip; }
|
| 161 |
|
| 162 |
// Layout & scales
|
| 163 |
-
let width = 800, height = 200; const margin = { top:
|
| 164 |
const xScale = d3.scaleLinear();
|
| 165 |
const yScale = d3.scaleLinear();
|
| 166 |
const lineGen = d3.line().x(d => xScale(d.step)).y(d => yScale(d.value));
|
| 167 |
|
| 168 |
-
function updateLayout(axisLabelY){
|
| 169 |
const rect = cell.getBoundingClientRect();
|
| 170 |
width = Math.max(1, Math.round(rect && rect.width ? rect.width : (cell.clientWidth || 800)));
|
| 171 |
-
// Hauteur fixe
|
| 172 |
-
height = 200;
|
| 173 |
svg.attr('width', width).attr('height', height).attr('viewBox', `0 0 ${width} ${height}`).attr('preserveAspectRatio','xMidYMid meet');
|
| 174 |
const innerWidth = width - margin.left - margin.right; const innerHeight = height - margin.top - margin.bottom;
|
| 175 |
gRoot.attr('transform', `translate(${margin.left},${margin.top})`);
|
|
@@ -183,39 +261,67 @@
|
|
| 183 |
|
| 184 |
// Axes
|
| 185 |
gAxes.selectAll('*').remove();
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
gAxes.append('text')
|
| 193 |
.attr('class', 'x-axis-label')
|
| 194 |
.attr('x', innerWidth / 2)
|
| 195 |
-
.attr('y', innerHeight + Math.max(20, Math.min(36, margin.bottom -
|
| 196 |
.attr('fill', 'var(--text-color)')
|
| 197 |
.attr('text-anchor', 'middle')
|
| 198 |
-
.style('font-size', '
|
| 199 |
-
.style('
|
|
|
|
|
|
|
|
|
|
| 200 |
.text('Steps');
|
| 201 |
-
gAxes.append('text')
|
| 202 |
-
.attr('class', 'y-axis-label')
|
| 203 |
-
.attr('transform', 'rotate(-90)')
|
| 204 |
-
.attr('x', -innerHeight / 2)
|
| 205 |
-
.attr('y', -Math.max(16, Math.min(28, margin.left - 8) + 10))
|
| 206 |
-
.attr('fill', 'var(--text-color)')
|
| 207 |
-
.attr('text-anchor', 'middle')
|
| 208 |
-
.style('font-size', '12px')
|
| 209 |
-
.style('font-weight', '700')
|
| 210 |
-
.text(axisLabelY || 'Value');
|
| 211 |
|
| 212 |
-
return { innerWidth, innerHeight };
|
| 213 |
}
|
| 214 |
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
};
|
| 220 |
|
| 221 |
function render(metricData, colorForRun) {
|
|
@@ -242,17 +348,21 @@
|
|
| 242 |
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); }); });
|
| 243 |
const isAccuracy = /accuracy/i.test(metricKey);
|
| 244 |
const axisLabelY = prettyMetricLabel(metricKey);
|
| 245 |
-
xScale.domain([minStep, maxStep]);
|
| 246 |
if (isAccuracy) yScale.domain([0, 1]).nice(); else yScale.domain([minVal, maxVal]).nice();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 247 |
|
| 248 |
-
const { innerWidth, innerHeight } = updateLayout(axisLabelY);
|
| 249 |
|
| 250 |
-
// Vertical grid lines at each
|
| 251 |
-
const stepSetForGrid = new Set();
|
| 252 |
-
runs.forEach(r => { (metricData[r]||[]).forEach(pt => stepSetForGrid.add(pt.step)); });
|
| 253 |
-
const stepsForGrid = Array.from(stepSetForGrid).sort((a,b)=>a-b);
|
| 254 |
gGrid.selectAll('line.vstep')
|
| 255 |
-
.data(
|
| 256 |
.join(
|
| 257 |
enter => enter.append('line').attr('class','vstep')
|
| 258 |
.attr('y1', 0).attr('y2', innerHeight)
|
|
@@ -264,62 +374,159 @@
|
|
| 264 |
exit => exit.remove()
|
| 265 |
);
|
| 266 |
|
| 267 |
-
//
|
| 268 |
gAreas.selectAll('*').remove();
|
| 269 |
-
runs.forEach(r => {
|
| 270 |
-
const vals = (metricData[r]||[]).slice().sort((a,b)=>a.step-b.step);
|
| 271 |
-
const withErr = vals.filter(v => v && v.stderr != null && isFinite(v.stderr) && v.stderr > 0 && isFinite(v.value));
|
| 272 |
-
if (!withErr.length) return;
|
| 273 |
-
const upper = withErr.map(d => [xScale(d.step), yScale(d.value + d.stderr)]);
|
| 274 |
-
const lower = withErr.slice().reverse().map(d => [xScale(d.step), yScale(d.value - d.stderr)]);
|
| 275 |
-
const coords = upper.concat(lower);
|
| 276 |
-
const pathData = d3.line().x(d=>d[0]).y(d=>d[1]).curve(d3.curveLinearClosed)(coords);
|
| 277 |
-
gAreas.append('path')
|
| 278 |
-
.attr('class','area')
|
| 279 |
-
.attr('data-run', r)
|
| 280 |
-
.attr('d', pathData)
|
| 281 |
-
.attr('fill', colorForRun(r))
|
| 282 |
-
.attr('opacity', 0.15)
|
| 283 |
-
.attr('stroke', 'none');
|
| 284 |
-
});
|
| 285 |
|
| 286 |
// Lines
|
| 287 |
const series = runs.map(r => ({ run: r, color: colorForRun(r), values: (metricData[r]||[]).slice().sort((a,b)=>a.step-b.step) }));
|
| 288 |
const paths = gLines.selectAll('path.run-line').data(series, d=>d.run);
|
| 289 |
paths.enter().append('path').attr('class','run-line').attr('data-run', d=>d.run).attr('fill','none').attr('stroke-width', 1.5).attr('opacity', 0.9)
|
| 290 |
.attr('stroke', d=>d.color).attr('d', d=>lineGen(d.values));
|
| 291 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 292 |
paths.exit().remove();
|
| 293 |
|
| 294 |
// Points
|
| 295 |
const allPoints = series.flatMap(s => s.values.map(v => ({ run:s.run, color:s.color, step:v.step, value:v.value })));
|
| 296 |
const ptsSel = gPoints.selectAll('circle.pt').data(allPoints, d=> `${d.run}-${d.step}`);
|
| 297 |
ptsSel.enter().append('circle').attr('class','pt').attr('data-run', d=>d.run).attr('r', 2).attr('fill', d=>d.color).attr('fill-opacity', 0.6)
|
| 298 |
-
.attr('stroke', 'none').attr('cx', d=>xScale(d.step)).attr('cy', d=>yScale(d.value))
|
| 299 |
.merge(ptsSel)
|
| 300 |
-
.
|
| 301 |
-
.attr('cx', d=>xScale(d.step)).attr('cy', d=>yScale(d.value));
|
|
|
|
|
|
|
|
|
|
| 302 |
ptsSel.exit().remove();
|
| 303 |
|
| 304 |
// Hover
|
| 305 |
gHover.selectAll('*').remove();
|
| 306 |
const overlay = gHover.append('rect').attr('fill','transparent').style('cursor','crosshair').attr('x',0).attr('y',0).attr('width', innerWidth).attr('height', innerHeight);
|
| 307 |
const hoverLine = gHover.append('line').style('stroke','var(--text-color)').attr('stroke-opacity', 0.25).attr('stroke-width',1).attr('y1',0).attr('y2',innerHeight).style('display','none');
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 311 |
const entries = series.map(s=>{ const map = new Map(s.values.map(v=>[v.step, v])); const pt = map.get(nearest); return { run:s.run, color:s.color, pt }; }).filter(e => e.pt && e.pt.value!=null);
|
| 312 |
entries.sort((a,b)=> (a.pt.value - b.pt.value));
|
| 313 |
const fmt = (vv)=> (isAccuracy? (+vv).toFixed(4) : (+vv).toFixed(4));
|
| 314 |
entries.forEach(e => {
|
| 315 |
-
|
| 316 |
-
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>`;
|
| 317 |
});
|
| 318 |
-
tipInner.innerHTML = html; const offsetX=12, offsetY=12; tip.style.opacity='1'; tip.style.transform=`translate(${Math.round(mx+offsetX+margin.left)}px, ${Math.round(my+offsetY+margin.top)}px)`;
|
| 319 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 320 |
overlay.on('mousemove', onMove).on('mouseleave', onLeave);
|
| 321 |
}
|
| 322 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 323 |
return { metricKey, render };
|
| 324 |
}
|
| 325 |
|
|
@@ -339,11 +546,12 @@
|
|
| 339 |
if (!host) return;
|
| 340 |
if (host.dataset && host.dataset.mounted === 'true') return; if (host.dataset) host.dataset.mounted = 'true';
|
| 341 |
|
| 342 |
-
// Build global header (legend)
|
| 343 |
const header = document.createElement('div'); header.className = 'd3-trackio__header';
|
| 344 |
-
const legend = document.createElement('div'); legend.className = 'legend-bottom'; legend.innerHTML = '<div class="legend-title">
|
| 345 |
header.appendChild(legend);
|
| 346 |
-
host.
|
|
|
|
| 347 |
|
| 348 |
const cells = Array.from(host.querySelectorAll('.cell'));
|
| 349 |
if (!cells.length) return;
|
|
@@ -457,6 +665,7 @@
|
|
| 457 |
|
| 458 |
// Resize handling
|
| 459 |
const rerender = () => { instances.forEach(inst => inst.render(dataByMetric.get(inst.metricKey)||{}, colorForRun)); };
|
|
|
|
| 460 |
if (window.ResizeObserver) { const ro = new ResizeObserver(() => rerender()); ro.observe(host); } else { window.addEventListener('resize', rerender); }
|
| 461 |
|
| 462 |
// Legend hover ghosting across all cells
|
|
|
|
| 9 |
<noscript>JavaScript is required to render this chart.</noscript>
|
| 10 |
</div>
|
| 11 |
<style>
|
| 12 |
+
.d3-trackio { position: relative; --z-tooltip: 50; --z-overlay: 99999999;
|
| 13 |
+
/* Softer chart theming (light) */
|
| 14 |
+
--axis-color: rgba(0,0,0,.28);
|
| 15 |
+
--tick-color: rgba(0,0,0,.62);
|
| 16 |
+
--grid-color: rgba(0,0,0,.10);
|
| 17 |
+
}
|
| 18 |
+
/* Softer chart theming (dark) */
|
| 19 |
+
[data-theme="dark"] .d3-trackio {
|
| 20 |
+
--axis-color: rgba(255,255,255,.28);
|
| 21 |
+
--tick-color: rgba(255,255,255,.68);
|
| 22 |
+
--grid-color: rgba(255,255,255,.10);
|
| 23 |
+
}
|
| 24 |
.d3-trackio__grid {
|
| 25 |
display: grid;
|
| 26 |
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
|
|
| 41 |
}
|
| 42 |
.d3-trackio .cell-header {
|
| 43 |
padding: 8px 10px;
|
|
|
|
| 44 |
display: flex;
|
| 45 |
align-items: center;
|
| 46 |
justify-content: space-between;
|
|
|
|
| 52 |
color: var(--text-color);
|
| 53 |
text-transform: none;
|
| 54 |
}
|
| 55 |
+
.d3-trackio .cell-action {
|
| 56 |
+
margin-left: auto;
|
| 57 |
+
display: inline-flex;
|
| 58 |
+
align-items: center;
|
| 59 |
+
justify-content: center;
|
| 60 |
+
width: 36px;
|
| 61 |
+
height: 36px;
|
| 62 |
+
border: 0;
|
| 63 |
+
background: transparent;
|
| 64 |
+
color: var(--text-color);
|
| 65 |
+
opacity: .8;
|
| 66 |
+
cursor: pointer;
|
| 67 |
+
}
|
| 68 |
+
.d3-trackio .cell-action:hover { opacity: 1; }
|
| 69 |
+
.d3-trackio .cell-action svg { width: 28px; height: 28px; }
|
| 70 |
+
.d3-trackio .cell-action svg, .d3-trackio .cell-action svg path { fill: var(--text-color); stroke: none; }
|
| 71 |
+
|
| 72 |
+
/* Fullscreen overlay */
|
| 73 |
+
.d3-trackio__overlay {
|
| 74 |
+
position: fixed;
|
| 75 |
+
inset: 0;
|
| 76 |
+
background: rgba(0,0,0,.48);
|
| 77 |
+
display: flex;
|
| 78 |
+
align-items: center;
|
| 79 |
+
justify-content: center;
|
| 80 |
+
z-index: var(--z-overlay);
|
| 81 |
+
opacity: 0;
|
| 82 |
+
pointer-events: none;
|
| 83 |
+
transition: opacity .2s ease;
|
| 84 |
+
}
|
| 85 |
+
.d3-trackio__overlay.is-open { opacity: 1; pointer-events: auto; }
|
| 86 |
+
.d3-trackio__modal {
|
| 87 |
+
position: relative;
|
| 88 |
+
width: min(92vw, 1200px);
|
| 89 |
+
height: min(92vh, 900px);
|
| 90 |
+
display: flex;
|
| 91 |
+
align-items: stretch;
|
| 92 |
+
justify-content: stretch;
|
| 93 |
+
transform: scale(.96);
|
| 94 |
+
transition: transform .2s ease;
|
| 95 |
+
}
|
| 96 |
+
.d3-trackio__overlay.is-open .d3-trackio__modal { transform: scale(1); }
|
| 97 |
+
.d3-trackio__modal .cell { width: 100%; height: 100%; }
|
| 98 |
+
.d3-trackio__modal .cell .cell-body { height: calc(100% - 44px); }
|
| 99 |
+
/* Conserver le ratio pour éviter les décalages points/ticks lors du zoom */
|
| 100 |
+
.d3-trackio__modal .cell .cell-body svg { width: 100% !important; height: auto !important; }
|
| 101 |
+
.d3-trackio__modal .cell .cell-action { display: none; }
|
| 102 |
+
.d3-trackio__modal-close {
|
| 103 |
+
position: absolute;
|
| 104 |
+
top: 10px;
|
| 105 |
+
right: 10px;
|
| 106 |
+
background: var(--surface-bg);
|
| 107 |
+
border: 1px solid var(--border-color);
|
| 108 |
+
color: var(--text-color);
|
| 109 |
+
width: 28px;
|
| 110 |
+
height: 28px;
|
| 111 |
+
display: flex;
|
| 112 |
+
align-items: center;
|
| 113 |
+
justify-content: center;
|
| 114 |
+
cursor: pointer;
|
| 115 |
+
}
|
| 116 |
.d3-trackio .cell-body { position: relative; width: 100%; overflow: hidden; }
|
| 117 |
.d3-trackio .cell-body svg { max-width: 100%; height: auto; display: block; }
|
| 118 |
|
|
|
|
| 122 |
.d3-trackio .axes text { fill: var(--tick-color); }
|
| 123 |
.d3-trackio .grid line { stroke: var(--grid-color); }
|
| 124 |
|
| 125 |
+
/* Global header (legend) above the grid and centered */
|
| 126 |
.d3-trackio__header {
|
| 127 |
display: flex;
|
| 128 |
align-items: flex-start;
|
| 129 |
+
justify-content: center;
|
| 130 |
gap: 12px;
|
| 131 |
+
margin: 0 0 10px 0;
|
| 132 |
flex-wrap: wrap;
|
| 133 |
+
width: 100%;
|
| 134 |
}
|
| 135 |
.d3-trackio__header .legend-bottom {
|
| 136 |
display: flex;
|
| 137 |
flex-direction: column;
|
| 138 |
+
align-items: center;
|
| 139 |
gap: 6px;
|
| 140 |
font-size: 12px;
|
| 141 |
color: var(--text-color);
|
| 142 |
+
text-align: center;
|
| 143 |
}
|
| 144 |
.d3-trackio__header .legend-bottom .legend-title { font-size: 12px; font-weight: 700; color: var(--text-color); }
|
| 145 |
.d3-trackio__header .legend-bottom .items { display: flex; flex-wrap: wrap; gap: 8px 14px; }
|
|
|
|
| 154 |
|
| 155 |
/* Tooltip styling aligned with other embeds */
|
| 156 |
.d3-trackio .d3-tooltip {
|
| 157 |
+
z-index: var(--z-tooltip);
|
| 158 |
backdrop-filter: saturate(1.12) blur(8px);
|
| 159 |
}
|
| 160 |
.d3-trackio .d3-tooltip__inner { display: flex; flex-direction: column; gap: 6px; min-width: 220px; }
|
|
|
|
| 202 |
const d3 = window.d3;
|
| 203 |
const metricKey = cell.getAttribute('data-metric');
|
| 204 |
const titleText = cell.getAttribute('data-title') || metricKey;
|
| 205 |
+
const host = cell.closest('.d3-trackio');
|
| 206 |
+
const shouldSuppress = () => !!(host && host.dataset && host.dataset.suppressTransitions === '1');
|
| 207 |
|
| 208 |
// Header
|
| 209 |
const header = document.createElement('div'); header.className = 'cell-header';
|
|
|
|
| 221 |
const gPoints = gRoot.append('g').attr('class','points');
|
| 222 |
const gHover = gRoot.append('g').attr('class','hover');
|
| 223 |
|
| 224 |
+
// Legacy flag, kept for compatibility but computed via shouldSuppress()
|
| 225 |
+
let suppressTransitions = false;
|
| 226 |
+
|
| 227 |
// Tooltip
|
| 228 |
cell.style.position = cell.style.position || 'relative';
|
| 229 |
let tip = cell.querySelector('.d3-tooltip'); let tipInner; let hideTipTimer = null;
|
|
|
|
| 238 |
} else { tipInner = tip.querySelector('.d3-tooltip__inner') || tip; }
|
| 239 |
|
| 240 |
// Layout & scales
|
| 241 |
+
let width = 800, height = 200; const margin = { top: 10, right: 20, bottom: 46, left: 44 };
|
| 242 |
const xScale = d3.scaleLinear();
|
| 243 |
const yScale = d3.scaleLinear();
|
| 244 |
const lineGen = d3.line().x(d => xScale(d.step)).y(d => yScale(d.value));
|
| 245 |
|
| 246 |
+
function updateLayout(axisLabelY, xTicksArg){
|
| 247 |
const rect = cell.getBoundingClientRect();
|
| 248 |
width = Math.max(1, Math.round(rect && rect.width ? rect.width : (cell.clientWidth || 800)));
|
| 249 |
+
// Hauteur fixe par défaut, overridable via data-height (pour fullscreen)
|
| 250 |
+
height = Number(cell.getAttribute('data-height')) || 200;
|
| 251 |
svg.attr('width', width).attr('height', height).attr('viewBox', `0 0 ${width} ${height}`).attr('preserveAspectRatio','xMidYMid meet');
|
| 252 |
const innerWidth = width - margin.left - margin.right; const innerHeight = height - margin.top - margin.bottom;
|
| 253 |
gRoot.attr('transform', `translate(${margin.left},${margin.top})`);
|
|
|
|
| 261 |
|
| 262 |
// Axes
|
| 263 |
gAxes.selectAll('*').remove();
|
| 264 |
+
// Ticks: for x, force equal spacing on indices and include edges; for y, equally spaced within domain, count ~ x ticks bounded [2,6]
|
| 265 |
+
const makeTicks = (scale, approx) => {
|
| 266 |
+
const arr = scale.ticks(approx);
|
| 267 |
+
const dom = scale.domain();
|
| 268 |
+
if (arr.length === 0 || arr[0] !== dom[0]) arr.unshift(dom[0]);
|
| 269 |
+
if (arr[arr.length - 1] !== dom[dom.length - 1]) arr.push(dom[dom.length - 1]);
|
| 270 |
+
return Array.from(new Set(arr));
|
| 271 |
+
};
|
| 272 |
+
const xTicksForced = (Array.isArray(xTicksArg) && xTicksArg.length)
|
| 273 |
+
? Array.from({ length: xTicksArg.length }, (_, i) => i)
|
| 274 |
+
: makeTicks(xScale, 8);
|
| 275 |
+
const yCount = Math.max(2, Math.min(6, xTicksForced.length));
|
| 276 |
+
const yDom = yScale.domain();
|
| 277 |
+
const yTicksForced = (yCount <= 2)
|
| 278 |
+
? [yDom[0], yDom[1]]
|
| 279 |
+
: Array.from({ length: yCount }, (_, i) => yDom[0] + ((yDom[1] - yDom[0]) * (i / (yCount - 1))));
|
| 280 |
+
// X axis with forced ticks (indices) and formatted labels mapped to original steps in render()
|
| 281 |
+
gAxes.append('g')
|
| 282 |
+
.attr('transform', `translate(0,${innerHeight})`)
|
| 283 |
+
.call(
|
| 284 |
+
d3.axisBottom(xScale)
|
| 285 |
+
.tickValues(xTicksForced)
|
| 286 |
+
.tickFormat((i) => {
|
| 287 |
+
// Label mapping will be provided by caller via closure over xTicksArg
|
| 288 |
+
const val = (Array.isArray(xTicksArg) && xTicksArg[i] != null ? xTicksArg[i] : i);
|
| 289 |
+
return formatAbbrev(val);
|
| 290 |
+
})
|
| 291 |
+
)
|
| 292 |
+
.call(g=>{ g.selectAll('path, line').attr('stroke','var(--axis-color)'); g.selectAll('text').attr('fill','var(--tick-color)').style('font-size','11px'); });
|
| 293 |
+
// Y axis with forced ticks
|
| 294 |
+
gAxes.append('g')
|
| 295 |
+
.call(d3.axisLeft(yScale).tickValues(yTicksForced).tickFormat((v)=>formatAbbrev(v)))
|
| 296 |
+
.call(g=>{ g.selectAll('path, line').attr('stroke','var(--axis-color)'); g.selectAll('text').attr('fill','var(--tick-color)').style('font-size','11px'); });
|
| 297 |
+
|
| 298 |
+
// Axis labels (only X; Y-label removed to gain space)
|
| 299 |
gAxes.append('text')
|
| 300 |
.attr('class', 'x-axis-label')
|
| 301 |
.attr('x', innerWidth / 2)
|
| 302 |
+
.attr('y', innerHeight + Math.max(20, Math.min(36, margin.bottom - 12)))
|
| 303 |
.attr('fill', 'var(--text-color)')
|
| 304 |
.attr('text-anchor', 'middle')
|
| 305 |
+
.style('font-size', '8px')
|
| 306 |
+
.style('opacity', '.5')
|
| 307 |
+
.style('letter-spacing', '.5px')
|
| 308 |
+
.style('text-transform', 'uppercase')
|
| 309 |
+
.style('font-weight', '500')
|
| 310 |
.text('Steps');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 311 |
|
| 312 |
+
return { innerWidth, innerHeight, xTicksForced, yTicksForced };
|
| 313 |
}
|
| 314 |
|
| 315 |
+
// Generic number abbreviation for ticks (K/M/B) with up to 2 decimals
|
| 316 |
+
const formatAbbrev = (value) => {
|
| 317 |
+
const num = Number(value);
|
| 318 |
+
if (!Number.isFinite(num)) return String(value);
|
| 319 |
+
const abs = Math.abs(num);
|
| 320 |
+
const trim2 = (n) => Number(n).toFixed(2).replace(/\.?0+$/, '');
|
| 321 |
+
if (abs >= 1e9) return `${trim2(num / 1e9)}B`;
|
| 322 |
+
if (abs >= 1e6) return `${trim2(num / 1e6)}M`;
|
| 323 |
+
if (abs >= 1e3) return `${trim2(num / 1e3)}K`;
|
| 324 |
+
return trim2(num);
|
| 325 |
};
|
| 326 |
|
| 327 |
function render(metricData, colorForRun) {
|
|
|
|
| 348 |
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); }); });
|
| 349 |
const isAccuracy = /accuracy/i.test(metricKey);
|
| 350 |
const axisLabelY = prettyMetricLabel(metricKey);
|
|
|
|
| 351 |
if (isAccuracy) yScale.domain([0, 1]).nice(); else yScale.domain([minVal, maxVal]).nice();
|
| 352 |
+
// Build unique steps and index mapping for equal spacing and snapping
|
| 353 |
+
const stepSet = new Set(); runs.forEach(r => (metricData[r]||[]).forEach(v => stepSet.add(v.step)));
|
| 354 |
+
const hoverSteps = Array.from(stepSet).sort((a,b)=>a-b);
|
| 355 |
+
const stepIndex = new Map(hoverSteps.map((s,i)=>[s,i]));
|
| 356 |
+
xScale.domain([0, Math.max(0, hoverSteps.length - 1)]);
|
| 357 |
+
|
| 358 |
+
// Update line generator X accessor to use index directly
|
| 359 |
+
lineGen.x(d => xScale(stepIndex.get(d.step)));
|
| 360 |
|
| 361 |
+
const { innerWidth, innerHeight, xTicksForced } = updateLayout(axisLabelY, hoverSteps);
|
| 362 |
|
| 363 |
+
// Vertical grid lines at each step index (same visibility as horizontal)
|
|
|
|
|
|
|
|
|
|
| 364 |
gGrid.selectAll('line.vstep')
|
| 365 |
+
.data(xTicksForced)
|
| 366 |
.join(
|
| 367 |
enter => enter.append('line').attr('class','vstep')
|
| 368 |
.attr('y1', 0).attr('y2', innerHeight)
|
|
|
|
| 374 |
exit => exit.remove()
|
| 375 |
);
|
| 376 |
|
| 377 |
+
// Remove stderr correction shapes (no uncertainty area)
|
| 378 |
gAreas.selectAll('*').remove();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 379 |
|
| 380 |
// Lines
|
| 381 |
const series = runs.map(r => ({ run: r, color: colorForRun(r), values: (metricData[r]||[]).slice().sort((a,b)=>a.step-b.step) }));
|
| 382 |
const paths = gLines.selectAll('path.run-line').data(series, d=>d.run);
|
| 383 |
paths.enter().append('path').attr('class','run-line').attr('data-run', d=>d.run).attr('fill','none').attr('stroke-width', 1.5).attr('opacity', 0.9)
|
| 384 |
.attr('stroke', d=>d.color).attr('d', d=>lineGen(d.values));
|
| 385 |
+
if (shouldSuppress()) {
|
| 386 |
+
paths.attr('stroke', d=>d.color).attr('opacity',0.9).attr('d', d=>lineGen(d.values));
|
| 387 |
+
} else {
|
| 388 |
+
paths.transition().duration(260).attr('stroke', d=>d.color).attr('opacity',0.9).attr('d', d=>lineGen(d.values));
|
| 389 |
+
}
|
| 390 |
paths.exit().remove();
|
| 391 |
|
| 392 |
// Points
|
| 393 |
const allPoints = series.flatMap(s => s.values.map(v => ({ run:s.run, color:s.color, step:v.step, value:v.value })));
|
| 394 |
const ptsSel = gPoints.selectAll('circle.pt').data(allPoints, d=> `${d.run}-${d.step}`);
|
| 395 |
ptsSel.enter().append('circle').attr('class','pt').attr('data-run', d=>d.run).attr('r', 2).attr('fill', d=>d.color).attr('fill-opacity', 0.6)
|
| 396 |
+
.attr('stroke', 'none').attr('cx', d=>xScale(stepIndex.get(d.step))).attr('cy', d=>yScale(d.value))
|
| 397 |
.merge(ptsSel)
|
| 398 |
+
.each(function(d){ /* placeholder to keep merge chain intact */ })
|
| 399 |
+
.attr('cx', d=>xScale(stepIndex.get(d.step))).attr('cy', d=>yScale(d.value));
|
| 400 |
+
if (!shouldSuppress()) {
|
| 401 |
+
try { gPoints.selectAll('circle.pt').transition().duration(150).attr('cx', d=>xScale(stepIndex.get(d.step))).attr('cy', d=>yScale(d.value)); } catch(_){}
|
| 402 |
+
}
|
| 403 |
ptsSel.exit().remove();
|
| 404 |
|
| 405 |
// Hover
|
| 406 |
gHover.selectAll('*').remove();
|
| 407 |
const overlay = gHover.append('rect').attr('fill','transparent').style('cursor','crosshair').attr('x',0).attr('y',0).attr('width', innerWidth).attr('height', innerHeight);
|
| 408 |
const hoverLine = gHover.append('line').style('stroke','var(--text-color)').attr('stroke-opacity', 0.25).attr('stroke-width',1).attr('y1',0).attr('y2',innerHeight).style('display','none');
|
| 409 |
+
|
| 410 |
+
// Expose external hover handlers for cross-chart sync
|
| 411 |
+
cell.__showExternalStep = (stepVal) => {
|
| 412 |
+
if (stepVal == null) { hoverLine.style('display','none'); return; }
|
| 413 |
+
const idx = stepIndex.get(stepVal);
|
| 414 |
+
if (idx == null) { hoverLine.style('display','none'); return; }
|
| 415 |
+
const xpx = xScale(idx);
|
| 416 |
+
hoverLine.attr('x1',xpx).attr('x2',xpx).style('display',null);
|
| 417 |
+
};
|
| 418 |
+
cell.__clearExternalStep = () => { hoverLine.style('display','none'); };
|
| 419 |
+
if (!cell.__syncAttached && host) {
|
| 420 |
+
host.addEventListener('trackio-hover-step', (ev) => { const d = ev && ev.detail; if (!d) return; if (cell.__showExternalStep) cell.__showExternalStep(d.step); });
|
| 421 |
+
host.addEventListener('trackio-hover-clear', () => { if (cell.__clearExternalStep) cell.__clearExternalStep(); });
|
| 422 |
+
cell.__syncAttached = true;
|
| 423 |
+
}
|
| 424 |
+
function onMove(ev){ if (hideTipTimer) { clearTimeout(hideTipTimer); hideTipTimer = null; } const [mx,my]=d3.pointer(ev, overlay.node()); const idx = Math.round(Math.max(0, Math.min(hoverSteps.length-1, xScale.invert(mx)))); const nearest = hoverSteps[idx]; const xpx = xScale(idx); hoverLine.attr('x1',xpx).attr('x2',xpx).style('display',null); if (host) { try { host.dispatchEvent(new CustomEvent('trackio-hover-step', { detail: { step: nearest } })); } catch(_){} }
|
| 425 |
+
let html = `<div><strong>step</strong> ${formatAbbrev(nearest)}</div><div><strong>${prettyMetricLabel(metricKey)}</strong></div>`;
|
| 426 |
const entries = series.map(s=>{ const map = new Map(s.values.map(v=>[v.step, v])); const pt = map.get(nearest); return { run:s.run, color:s.color, pt }; }).filter(e => e.pt && e.pt.value!=null);
|
| 427 |
entries.sort((a,b)=> (a.pt.value - b.pt.value));
|
| 428 |
const fmt = (vv)=> (isAccuracy? (+vv).toFixed(4) : (+vv).toFixed(4));
|
| 429 |
entries.forEach(e => {
|
| 430 |
+
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)}</span></div>`;
|
|
|
|
| 431 |
});
|
| 432 |
+
tipInner.innerHTML = html; const offsetX=12, offsetY=12; tip.style.opacity='1'; tip.style.transform=`translate(${Math.round(mx+offsetX+margin.left)}px, ${Math.round(my+offsetY+margin.top)}px)`;
|
| 433 |
+
// No animation on chart while scaling up/down
|
| 434 |
+
try {
|
| 435 |
+
const sel = gPoints.selectAll('circle.pt');
|
| 436 |
+
if (shouldSuppress()) { sel.interrupt().attr('r', d => (d && d.step === nearest ? 4 : 2)); }
|
| 437 |
+
else { sel.transition().duration(140).ease(d3.easeCubicOut).attr('r', d => (d && d.step === nearest ? 4 : 2)); }
|
| 438 |
+
} catch(_) {}
|
| 439 |
+
}
|
| 440 |
+
function onLeave(){ hideTipTimer = setTimeout(()=>{ tip.style.opacity='0'; tip.style.transform='translate(-9999px, -9999px)'; hoverLine.style('display','none'); if (host) { try { host.dispatchEvent(new CustomEvent('trackio-hover-clear')); } catch(_){} } try { const sel = gPoints.selectAll('circle.pt'); if (shouldSuppress()) { sel.interrupt().attr('r', 2); } else { sel.transition().duration(150).ease(d3.easeCubicOut).attr('r', 2); } } catch(_) {} }, 100); }
|
| 441 |
overlay.on('mousemove', onMove).on('mouseleave', onLeave);
|
| 442 |
}
|
| 443 |
|
| 444 |
+
// Fullscreen: button + modal behavior
|
| 445 |
+
const fsBtn = document.createElement('button'); fsBtn.className = 'cell-action cell-action--fullscreen'; fsBtn.type = 'button'; fsBtn.title='Fullscreen'; fsBtn.setAttribute('aria-label','Open fullscreen');
|
| 446 |
+
fsBtn.innerHTML = '<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M4 9V4h5v2H6v3H4zm10-5h5v5h-2V6h-3V4zM6 18h3v2H4v-5h2v3zm12-3h2v5h-5v-2h3v-3z"/></svg>';
|
| 447 |
+
header.appendChild(fsBtn);
|
| 448 |
+
|
| 449 |
+
function ensureOverlay(host){
|
| 450 |
+
let overlay = host.querySelector('.d3-trackio__overlay');
|
| 451 |
+
if (!overlay) {
|
| 452 |
+
overlay = document.createElement('div'); overlay.className='d3-trackio__overlay';
|
| 453 |
+
const modal = document.createElement('div'); modal.className='d3-trackio__modal';
|
| 454 |
+
const close = document.createElement('button'); close.className='d3-trackio__modal-close'; close.type='button'; close.innerHTML='✕';
|
| 455 |
+
overlay.appendChild(modal); overlay.appendChild(close);
|
| 456 |
+
host.appendChild(overlay);
|
| 457 |
+
overlay.addEventListener('click', (e)=>{ if (e.target === overlay) close.click(); });
|
| 458 |
+
close.addEventListener('click', ()=>{
|
| 459 |
+
const moving = modal.querySelector('.cell'); if (!moving) { overlay.classList.remove('is-open'); return; }
|
| 460 |
+
const placeholder = moving.__placeholder;
|
| 461 |
+
if (!placeholder) { overlay.classList.remove('is-open'); return; }
|
| 462 |
+
// Global suppression across charts inside this host
|
| 463 |
+
host.dataset.suppressTransitions = '1';
|
| 464 |
+
// Cancel any in-flight D3 transitions
|
| 465 |
+
try { d3.select(host).selectAll('path.run-line').interrupt(); d3.select(host).selectAll('circle.pt').interrupt(); } catch(_){ }
|
| 466 |
+
// FLIP: animate from current (modal) to placeholder position
|
| 467 |
+
const from = moving.getBoundingClientRect();
|
| 468 |
+
const to = placeholder.getBoundingClientRect();
|
| 469 |
+
const dx = to.left - from.left; const dy = to.top - from.top;
|
| 470 |
+
const sx = Math.max(0.0001, to.width / from.width);
|
| 471 |
+
const sy = Math.max(0.0001, to.height / from.height);
|
| 472 |
+
moving.style.transformOrigin = 'top left';
|
| 473 |
+
moving.style.transition = 'transform 220ms cubic-bezier(.2,.8,.2,1)';
|
| 474 |
+
moving.style.transform = `translate(${dx}px, ${dy}px) scale(${sx}, ${sy})`;
|
| 475 |
+
const onEnd = () => {
|
| 476 |
+
moving.removeEventListener('transitionend', onEnd);
|
| 477 |
+
overlay.classList.remove('is-open');
|
| 478 |
+
moving.style.transition = '';
|
| 479 |
+
moving.style.transform = '';
|
| 480 |
+
if (placeholder && placeholder.parentNode) { placeholder.parentNode.insertBefore(moving, placeholder); placeholder.remove(); }
|
| 481 |
+
moving.removeAttribute('data-height');
|
| 482 |
+
// Recompute layout without any line/point animation
|
| 483 |
+
try { const h = host; if (h && h.__rerender) h.__rerender(); } catch {}
|
| 484 |
+
requestAnimationFrame(()=>{ delete host.dataset.suppressTransitions; });
|
| 485 |
+
};
|
| 486 |
+
moving.addEventListener('transitionend', onEnd);
|
| 487 |
+
});
|
| 488 |
+
window.addEventListener('keydown', (e)=>{ if (e.key === 'Escape' && overlay.classList.contains('is-open')) { const btn = overlay.querySelector('.d3-trackio__modal-close'); btn && btn.click(); }});
|
| 489 |
+
}
|
| 490 |
+
return overlay;
|
| 491 |
+
}
|
| 492 |
+
|
| 493 |
+
fsBtn.addEventListener('click', ()=>{
|
| 494 |
+
const hostNode = cell.closest('.d3-trackio'); if (!hostNode) return;
|
| 495 |
+
const overlay = ensureOverlay(hostNode); const modal = overlay.querySelector('.d3-trackio__modal');
|
| 496 |
+
// If another chart is open, close it first then proceed
|
| 497 |
+
const existing = modal.querySelector('.cell');
|
| 498 |
+
if (overlay.classList.contains('is-open') && existing && existing !== cell) {
|
| 499 |
+
const btn = overlay.querySelector('.d3-trackio__modal-close');
|
| 500 |
+
if (btn) { btn.dispatchEvent(new Event('click')); }
|
| 501 |
+
const waitEnd = () => {
|
| 502 |
+
if (!overlay.classList.contains('is-open')) { fsBtn.click(); return; }
|
| 503 |
+
requestAnimationFrame(waitEnd);
|
| 504 |
+
};
|
| 505 |
+
waitEnd();
|
| 506 |
+
return;
|
| 507 |
+
}
|
| 508 |
+
// Global suppression across charts inside this host
|
| 509 |
+
hostNode.dataset.suppressTransitions = '1';
|
| 510 |
+
// Cancel any in-flight D3 transitions
|
| 511 |
+
try { d3.select(hostNode).selectAll('path.run-line').interrupt(); d3.select(hostNode).selectAll('circle.pt').interrupt(); } catch(_){ }
|
| 512 |
+
const before = cell.getBoundingClientRect();
|
| 513 |
+
const placeholder = document.createElement('div'); placeholder.style.width = cell.offsetWidth + 'px'; placeholder.style.height = cell.offsetHeight + 'px';
|
| 514 |
+
cell.__placeholder = placeholder; cell.parentNode.insertBefore(placeholder, cell);
|
| 515 |
+
modal.appendChild(cell);
|
| 516 |
+
// Do NOT re-render or change SVG internals; scale via CSS only
|
| 517 |
+
const after = cell.getBoundingClientRect();
|
| 518 |
+
const dx = before.left - after.left; const dy = before.top - after.top;
|
| 519 |
+
const sx = Math.max(0.0001, before.width / after.width);
|
| 520 |
+
const sy = Math.max(0.0001, before.height / after.height);
|
| 521 |
+
cell.style.transformOrigin = 'top left';
|
| 522 |
+
cell.style.transition = 'transform 220ms cubic-bezier(.2,.8,.2,1)';
|
| 523 |
+
cell.style.transform = `translate(${dx}px, ${dy}px) scale(${sx}, ${sy})`;
|
| 524 |
+
overlay.classList.add('is-open');
|
| 525 |
+
requestAnimationFrame(()=>{ requestAnimationFrame(()=>{ cell.style.transform = 'none'; }); });
|
| 526 |
+
const onEnd = () => { cell.removeEventListener('transitionend', onEnd); cell.style.transition = ''; cell.style.transform = ''; requestAnimationFrame(()=>{ /* keep suppress until settled or close */ }); };
|
| 527 |
+
cell.addEventListener('transitionend', onEnd);
|
| 528 |
+
});
|
| 529 |
+
|
| 530 |
return { metricKey, render };
|
| 531 |
}
|
| 532 |
|
|
|
|
| 546 |
if (!host) return;
|
| 547 |
if (host.dataset && host.dataset.mounted === 'true') return; if (host.dataset) host.dataset.mounted = 'true';
|
| 548 |
|
| 549 |
+
// Build global header (legend) and insert ABOVE the grid
|
| 550 |
const header = document.createElement('div'); header.className = 'd3-trackio__header';
|
| 551 |
+
const legend = document.createElement('div'); legend.className = 'legend-bottom'; legend.innerHTML = '<div class="legend-title">Runs</div><div class="items"></div>';
|
| 552 |
header.appendChild(legend);
|
| 553 |
+
const gridNode = host.querySelector('.d3-trackio__grid');
|
| 554 |
+
if (gridNode && gridNode.parentNode === host) { host.insertBefore(header, gridNode); } else { host.insertBefore(header, host.firstChild); }
|
| 555 |
|
| 556 |
const cells = Array.from(host.querySelectorAll('.cell'));
|
| 557 |
if (!cells.length) return;
|
|
|
|
| 665 |
|
| 666 |
// Resize handling
|
| 667 |
const rerender = () => { instances.forEach(inst => inst.render(dataByMetric.get(inst.metricKey)||{}, colorForRun)); };
|
| 668 |
+
host.__rerender = rerender;
|
| 669 |
if (window.ResizeObserver) { const ro = new ResizeObserver(() => rerender()); ro.observe(host); } else { window.addEventListener('resize', rerender); }
|
| 670 |
|
| 671 |
// Legend hover ghosting across all cells
|