thibaud frere commited on
Commit
99af53b
·
1 Parent(s): 8bb224c
app/.astro/settings.json CHANGED
@@ -1,5 +1,3 @@
1
- {
2
- "_variables": {
3
- "lastUpdateCheck": 1756115195405
4
- }
5
- }
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:585e9065073b0b2528747f99b78ccc382984e713ac7e6a5c9f99a47956e3f42e
3
+ size 58
 
 
app/astro.config.mjs CHANGED
@@ -8,7 +8,7 @@ import remarkFootnotes from 'remark-footnotes';
8
  import rehypeSlug from 'rehype-slug';
9
  import rehypeAutolinkHeadings from 'rehype-autolink-headings';
10
  import rehypeCitation from 'rehype-citation';
11
- import rehypeCodeCopyAndLabel from './plugins/rehype/code-copy-and-label.mjs';
12
  import rehypeReferencesAndFootnotes from './plugins/rehype/post-citation.mjs';
13
  import remarkIgnoreCitationsInCode from './plugins/remark/ignore-citations-in-code.mjs';
14
  import rehypeRestoreAtInCode from './plugins/rehype/restore-at-in-code.mjs';
@@ -53,11 +53,11 @@ export default defineConfig({
53
  [rehypeCitation, {
54
  bibliography: 'src/content/bibliography.bib',
55
  linkCitations: true,
56
- csl: 'vancouver'
57
  }],
58
  rehypeReferencesAndFootnotes,
59
  rehypeRestoreAtInCode,
60
- rehypeCodeCopyAndLabel,
61
  rehypeWrapTables
62
  ]
63
  }
 
8
  import rehypeSlug from 'rehype-slug';
9
  import rehypeAutolinkHeadings from 'rehype-autolink-headings';
10
  import rehypeCitation from 'rehype-citation';
11
+ import rehypeCodeCopy from './plugins/rehype/code-copy.mjs';
12
  import rehypeReferencesAndFootnotes from './plugins/rehype/post-citation.mjs';
13
  import remarkIgnoreCitationsInCode from './plugins/remark/ignore-citations-in-code.mjs';
14
  import rehypeRestoreAtInCode from './plugins/rehype/restore-at-in-code.mjs';
 
53
  [rehypeCitation, {
54
  bibliography: 'src/content/bibliography.bib',
55
  linkCitations: true,
56
+ csl: "vancouver"
57
  }],
58
  rehypeReferencesAndFootnotes,
59
  rehypeRestoreAtInCode,
60
+ rehypeCodeCopy,
61
  rehypeWrapTables
62
  ]
63
  }
app/package.json CHANGED
Binary files a/app/package.json and b/app/package.json differ
 
app/plugins/rehype/{code-copy-and-label.mjs → code-copy.mjs} RENAMED
@@ -1,6 +1,6 @@
1
- // Minimal rehype plugin to wrap code blocks with a copy button and a language label
2
  // Exported as a standalone module to keep astro.config.mjs lean
3
- export default function rehypeCodeCopyAndLabel() {
4
  return (tree) => {
5
  // Walk the tree; lightweight visitor to find <pre><code>
6
  const visit = (node, parent) => {
@@ -90,3 +90,5 @@ export default function rehypeCodeCopyAndLabel() {
90
  }
91
 
92
 
 
 
 
1
+ // Minimal rehype plugin to wrap code blocks with a copy button
2
  // Exported as a standalone module to keep astro.config.mjs lean
3
+ export default function rehypeCodeCopy() {
4
  return (tree) => {
5
  // Walk the tree; lightweight visitor to find <pre><code>
6
  const visit = (node, parent) => {
 
90
  }
91
 
92
 
93
+
94
+
app/public/scripts/color-palettes.js ADDED
@@ -0,0 +1,217 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Global color palettes generator and watcher
2
+ // - Observes CSS variable --primary-color and theme changes
3
+ // - Generates categorical, sequential, and diverging palettes (OKLCH/OKLab)
4
+ // - Exposes results as CSS variables on :root
5
+ // - Supports variable color counts per palette via CSS vars
6
+ // - Dispatches a 'palettes:updated' CustomEvent after each update
7
+
8
+ (() => {
9
+ const MODE = { cssRoot: document.documentElement };
10
+
11
+ const getCssVar = (name) => {
12
+ try { return getComputedStyle(MODE.cssRoot).getPropertyValue(name).trim(); } catch { return ''; }
13
+ };
14
+ const getIntFromCssVar = (name, fallback) => {
15
+ const raw = getCssVar(name);
16
+ if (!raw) return fallback;
17
+ const v = parseInt(String(raw), 10);
18
+ if (Number.isNaN(v)) return fallback;
19
+ return v;
20
+ };
21
+ const clamp = (n, min, max) => Math.max(min, Math.min(max, n));
22
+
23
+ // Color math (OKLab/OKLCH)
24
+ const srgbToLinear = (u) => (u <= 0.04045 ? u / 12.92 : Math.pow((u + 0.055) / 1.055, 2.4));
25
+ const linearToSrgb = (u) => (u <= 0.0031308 ? 12.92 * u : 1.055 * Math.pow(Math.max(0, u), 1 / 2.4) - 0.055);
26
+ const rgbToOklab = (r, g, b) => {
27
+ const rl = srgbToLinear(r), gl = srgbToLinear(g), bl = srgbToLinear(b);
28
+ const l = Math.cbrt(0.4122214708 * rl + 0.5363325363 * gl + 0.0514459929 * bl);
29
+ const m = Math.cbrt(0.2119034982 * rl + 0.6806995451 * gl + 0.1073969566 * bl);
30
+ const s = Math.cbrt(0.0883024619 * rl + 0.2817188376 * gl + 0.6299787005 * bl);
31
+ const L = 0.2104542553 * l + 0.7936177850 * m - 0.0040720468 * s;
32
+ const a = 1.9779984951 * l - 2.4285922050 * m + 0.4505937099 * s;
33
+ const b2 = 0.0259040371 * l + 0.7827717662 * m - 0.8086757660 * s;
34
+ return { L, a, b: b2 };
35
+ };
36
+ const oklabToRgb = (L, a, b) => {
37
+ const l_ = L + 0.3963377774 * a + 0.2158037573 * b;
38
+ const m_ = L - 0.1055613458 * a - 0.0638541728 * b;
39
+ const s_ = L - 0.0894841775 * a - 1.2914855480 * b;
40
+ const l = l_ * l_ * l_;
41
+ const m = m_ * m_ * m_;
42
+ const s = s_ * s_ * s_;
43
+ const r = linearToSrgb(+4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s);
44
+ const g = linearToSrgb(-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s);
45
+ const b3 = linearToSrgb(-0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s);
46
+ return { r, g, b: b3 };
47
+ };
48
+ const oklchToOklab = (L, C, hDeg) => { const h = (hDeg * Math.PI) / 180; return { L, a: C * Math.cos(h), b: C * Math.sin(h) }; };
49
+ const oklabToOklch = (L, a, b) => { const C = Math.sqrt(a*a + b*b); let h = Math.atan2(b, a) * 180 / Math.PI; if (h < 0) h += 360; return { L, C, h }; };
50
+ const clamp01 = (x) => Math.min(1, Math.max(0, x));
51
+ const isInGamut = ({ r, g, b }) => r >= 0 && r <= 1 && g >= 0 && g <= 1 && b >= 0 && b <= 1;
52
+ const toHex = ({ r, g, b }) => { const R = Math.round(clamp01(r)*255), G = Math.round(clamp01(g)*255), B = Math.round(clamp01(b)*255); const h = (n) => n.toString(16).padStart(2,'0'); return `#${h(R)}${h(G)}${h(B)}`.toUpperCase(); };
53
+ const oklchToHexSafe = (L, C, h) => { let c = C; for (let i=0;i<12;i++){ const { a, b } = oklchToOklab(L,c,h); const rgb = oklabToRgb(L,a,b); if (isInGamut(rgb)) return toHex(rgb); c = Math.max(0, c-0.02);} return toHex(oklabToRgb(L,0,0)); };
54
+ const parseCssColorToRgb = (css) => { try { const el = document.createElement('span'); el.style.color = css; document.body.appendChild(el); const cs = getComputedStyle(el).color; document.body.removeChild(el); const m = cs.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/i); if (!m) return null; return { r: Number(m[1])/255, g: Number(m[2])/255, b: Number(m[3])/255 }; } catch { return null; } };
55
+
56
+ const getPrimaryHex = () => {
57
+ const css = getCssVar('--primary-color');
58
+ if (!css) return '#E889AB';
59
+ if (/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(css)) return css.toUpperCase();
60
+ const rgb = parseCssColorToRgb(css);
61
+ if (rgb) return toHex(rgb);
62
+ return '#E889AB';
63
+ };
64
+ const getCounts = () => {
65
+ const Fallback = 6;
66
+ const globalCount = clamp(getIntFromCssVar('--palette-count', Fallback), 1, 12);
67
+ return {
68
+ categorical: clamp(getIntFromCssVar('--palette-categorical-count', globalCount), 1, 12),
69
+ sequential: clamp(getIntFromCssVar('--palette-sequential-count', globalCount), 1, 12),
70
+ diverging: clamp(getIntFromCssVar('--palette-diverging-count', globalCount), 1, 12),
71
+ };
72
+ };
73
+
74
+ const generators = {
75
+ categorical: (baseHex, count) => {
76
+ const parseHex = (h) => { const s = h.replace('#',''); const v = s.length===3 ? s.split('').map(ch=>ch+ch).join('') : s; return { r: parseInt(v.slice(0,2),16)/255, g: parseInt(v.slice(2,4),16)/255, b: parseInt(v.slice(4,6),16)/255 }; };
77
+ const { r, g, b } = parseHex(baseHex);
78
+ const { L, a, b: bb } = rgbToOklab(r,g,b);
79
+ const { C, h } = oklabToOklch(L,a,bb);
80
+ const L0 = Math.min(0.85, Math.max(0.4, L));
81
+ const C0 = Math.min(0.35, Math.max(0.1, C || 0.2));
82
+ const total = Math.max(1, Math.min(12, count || 6));
83
+ const hueStep = 360 / total;
84
+ const results = [];
85
+ for (let i=0;i<total;i++) { const hDeg = (h + i*hueStep) % 360; const lVar = ((i % 3) - 1) * 0.04; results.push(oklchToHexSafe(Math.max(0.4, Math.min(0.85, L0 + lVar)), C0, hDeg)); }
86
+ return results;
87
+ },
88
+ sequential: (baseHex, count) => {
89
+ const parseHex = (h) => { const s = h.replace('#',''); const v = s.length===3 ? s.split('').map(ch=>ch+ch).join('') : s; return { r: parseInt(v.slice(0,2),16)/255, g: parseInt(v.slice(2,4),16)/255, b: parseInt(v.slice(4,6),16)/255 }; };
90
+ const { r, g, b } = parseHex(baseHex);
91
+ const { L, a, b: bb } = rgbToOklab(r,g,b);
92
+ const { C, h } = oklabToOklch(L,a,bb);
93
+ const total = Math.max(1, Math.min(12, count || 6));
94
+ const startL = Math.max(0.25, L - 0.18);
95
+ const endL = Math.min(0.92, L + 0.18);
96
+ const cBase = Math.min(0.33, Math.max(0.08, C * 0.9 + 0.06));
97
+ const out = [];
98
+ for (let i=0;i<total;i++) { const t = total===1 ? 0 : i/(total-1); const lNow = startL*(1-t)+endL*t; const cNow = cBase*(0.85 + 0.15*(1 - Math.abs(0.5 - t)*2)); out.push(oklchToHexSafe(lNow, cNow, h)); }
99
+ return out;
100
+ },
101
+ diverging: (baseHex, count) => {
102
+ const parseHex = (h) => { const s = h.replace('#',''); const v = s.length===3 ? s.split('').map(ch=>ch+ch).join('') : s; return { r: parseInt(v.slice(0,2),16)/255, g: parseInt(v.slice(2,4),16)/255, b: parseInt(v.slice(4,6),16)/255 }; };
103
+ const { r, g, b } = parseHex(baseHex);
104
+ const baseLab = rgbToOklab(r,g,b);
105
+ const baseLch = oklabToOklch(baseLab.L, baseLab.a, baseLab.b);
106
+ const total = Math.max(1, Math.min(12, count || 6));
107
+
108
+ // Left endpoint: EXACT primary color (no darkening)
109
+ const leftLab = baseLab;
110
+ // Right endpoint: complement with same L and similar C (clamped safe)
111
+ const compH = (baseLch.h + 180) % 360;
112
+ const cSafe = Math.min(0.35, Math.max(0.08, baseLch.C));
113
+ const rightLab = oklchToOklab(baseLab.L, cSafe, compH);
114
+ const whiteLab = { L: 0.98, a: 0, b: 0 }; // center near‑white
115
+
116
+ const hexFromOKLab = (L, a, b) => toHex(oklabToRgb(L, a, b));
117
+ const lerp = (a, b, t) => a + (b - a) * t;
118
+ const lerpOKLabHex = (A, B, t) => hexFromOKLab(lerp(A.L, B.L, t), lerp(A.a, B.a, t), lerp(A.b, B.b, t));
119
+
120
+ const out = [];
121
+ if (total % 2 === 1) {
122
+ const nSide = (total - 1) >> 1; // items on each side
123
+ // Left side: include left endpoint exactly at index 0
124
+ for (let i = 0; i < nSide; i++) {
125
+ const t = nSide <= 1 ? 0 : (i / (nSide - 1)); // 0 .. 1
126
+ // Move from leftLab to a value close (but not equal) to white; ensure last before center is lighter
127
+ const tt = t * 0.9; // keep some distance from pure white before center
128
+ out.push(lerpOKLabHex(leftLab, whiteLab, tt));
129
+ }
130
+ // Center
131
+ out.push(hexFromOKLab(whiteLab.L, whiteLab.a, whiteLab.b));
132
+ // Right side: start near white and end EXACTLY at rightLab
133
+ for (let i = 0; i < nSide; i++) {
134
+ const t = nSide <= 1 ? 1 : ((i + 1) / nSide); // (1/n)..1
135
+ const tt = Math.max(0.1, t); // avoid starting at pure white
136
+ out.push(lerpOKLabHex(whiteLab, rightLab, tt));
137
+ }
138
+ // Ensure first and last are exact endpoints
139
+ if (out.length) { out[0] = hexFromOKLab(leftLab.L, leftLab.a, leftLab.b); out[out.length - 1] = hexFromOKLab(rightLab.L, rightLab.a, rightLab.b); }
140
+ } else {
141
+ const nSide = total >> 1;
142
+ // Left half including left endpoint, approaching white but not reaching it
143
+ for (let i = 0; i < nSide; i++) {
144
+ const t = nSide <= 1 ? 0 : (i / (nSide - 1)); // 0 .. 1
145
+ const tt = t * 0.9;
146
+ out.push(lerpOKLabHex(leftLab, whiteLab, tt));
147
+ }
148
+ // Right half: mirror from near white to exact right endpoint
149
+ for (let i = 0; i < nSide; i++) {
150
+ const t = nSide <= 1 ? 1 : ((i + 1) / nSide); // (1/n)..1
151
+ const tt = Math.max(0.1, t);
152
+ out.push(lerpOKLabHex(whiteLab, rightLab, tt));
153
+ }
154
+ if (out.length) { out[0] = hexFromOKLab(leftLab.L, leftLab.a, leftLab.b); out[out.length - 1] = hexFromOKLab(rightLab.L, rightLab.a, rightLab.b); }
155
+ }
156
+ return out;
157
+ }
158
+ };
159
+
160
+ const setCssVar = (name, value) => { try { MODE.cssRoot.style.setProperty(name, value); } catch {} };
161
+ const removeCssVar = (name) => { try { MODE.cssRoot.style.removeProperty(name); } catch {} };
162
+
163
+ let lastSignature = '';
164
+ let lastCounts = { categorical: 0, sequential: 0, diverging: 0 };
165
+
166
+ const updatePalettes = () => {
167
+ const primary = getPrimaryHex();
168
+ const counts = getCounts();
169
+ const signature = `${primary}|${counts.categorical}|${counts.sequential}|${counts.diverging}`;
170
+ if (signature === lastSignature) return;
171
+
172
+ const out = {};
173
+ out.categorical = generators.categorical(primary, counts.categorical);
174
+ out.sequential = generators.sequential(primary, counts.sequential);
175
+ out.diverging = generators.diverging(primary, counts.diverging);
176
+
177
+ setCssVar('--primary-hex', primary);
178
+ setCssVar('--palette-categorical-count-current', String(out.categorical.length));
179
+ setCssVar('--palette-sequential-count-current', String(out.sequential.length));
180
+ setCssVar('--palette-diverging-count-current', String(out.diverging.length));
181
+
182
+ const applyList = (key, list, prevCount) => {
183
+ for (let i=0;i<list.length;i++) setCssVar(`--palette-${key}-${i+1}`, list[i]);
184
+ for (let i=list.length;i<prevCount;i++) removeCssVar(`--palette-${key}-${i+1}`);
185
+ setCssVar(`--palette-${key}-json`, JSON.stringify(list));
186
+ };
187
+ applyList('categorical', out.categorical, lastCounts.categorical);
188
+ applyList('sequential', out.sequential, lastCounts.sequential);
189
+ applyList('diverging', out.diverging, lastCounts.diverging);
190
+
191
+ lastCounts = { categorical: out.categorical.length, sequential: out.sequential.length, diverging: out.diverging.length };
192
+ lastSignature = signature;
193
+
194
+ try { document.dispatchEvent(new CustomEvent('palettes:updated', { detail: { primary, counts, palettes: out } })); } catch {}
195
+ };
196
+
197
+ const bootstrap = () => {
198
+ updatePalettes();
199
+ const mo = new MutationObserver(() => updatePalettes());
200
+ mo.observe(MODE.cssRoot, { attributes: true, attributeFilter: ['style', 'data-theme'] });
201
+ setInterval(updatePalettes, 400);
202
+ window.ColorPalettes = {
203
+ refresh: updatePalettes,
204
+ getColors: (key) => {
205
+ const count = Number(getCssVar(`--palette-${key}-count-current`)) || 0;
206
+ const arr = [];
207
+ for (let i=0;i<count;i++) arr.push(getCssVar(`--palette-${key}-${i+1}`));
208
+ return arr.filter(Boolean);
209
+ }
210
+ };
211
+ };
212
+
213
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', bootstrap, { once: true });
214
+ else bootstrap();
215
+ })();
216
+
217
+
app/scripts/export-pdf.mjs CHANGED
@@ -168,8 +168,19 @@ async function main() {
168
  // filename can be provided, else computed from DOM (button) or page title later
169
  let outFileBase = (args.filename && String(args.filename).replace(/\.pdf$/i, '')) || 'article';
170
 
171
- console.log('> Building Astro site…');
172
- await run('npm', ['run', 'build']);
 
 
 
 
 
 
 
 
 
 
 
173
 
174
  console.log('> Starting Astro preview…');
175
  // Start preview in its own process group so we can terminate all children reliably
@@ -263,6 +274,8 @@ async function main() {
263
  }
264
  function fixSvg(svg){
265
  if (!svg) return;
 
 
266
  if (isSmallSvg(svg)) { lockSmallSvgSize(svg); return; }
267
  try { svg.removeAttribute('width'); } catch {}
268
  try { svg.removeAttribute('height'); } catch {}
@@ -309,7 +322,6 @@ async function main() {
309
  // - Ensure an SVG background (CSS background on svg element)
310
  const cssHandle = await page.addStyleTag({ content: `
311
  .hero .points { mix-blend-mode: normal !important; }
312
- .d3-galaxy svg { background: var(--surface-bg); }
313
  ` });
314
  const thumbPath = resolve(cwd, 'dist', 'thumb.jpg');
315
  await page.screenshot({ path: thumbPath, type: 'jpeg', quality: 85, fullPage: false });
@@ -360,6 +372,8 @@ async function main() {
360
  }
361
  function fixSvg(svg){
362
  if (!svg) return;
 
 
363
  if (isSmallSvg(svg)) { lockSmallSvgSize(svg); return; }
364
  try { svg.removeAttribute('width'); } catch {}
365
  try { svg.removeAttribute('height'); } catch {}
@@ -412,8 +426,9 @@ async function main() {
412
 
413
  /* Banner centering & visibility */
414
  .hero .points { mix-blend-mode: normal !important; }
415
- .d3-galaxy { width: 100% !important; height: 300px; max-width: 980px !important; margin-left: auto !important; margin-right: auto !important; }
416
- .d3-galaxy svg { background: var(--surface-bg); width: 100% !important; height: auto !important; }
 
417
  ` });
418
  } catch {}
419
  await page.pdf({
 
168
  // filename can be provided, else computed from DOM (button) or page title later
169
  let outFileBase = (args.filename && String(args.filename).replace(/\.pdf$/i, '')) || 'article';
170
 
171
+ // Build only if dist/ does not exist
172
+ const distDir = resolve(cwd, 'dist');
173
+ let hasDist = false;
174
+ try {
175
+ const st = await fs.stat(distDir);
176
+ hasDist = st && st.isDirectory();
177
+ } catch {}
178
+ if (!hasDist) {
179
+ console.log('> Building Astro site…');
180
+ await run('npm', ['run', 'build']);
181
+ } else {
182
+ console.log('> Skipping build (dist/ exists)…');
183
+ }
184
 
185
  console.log('> Starting Astro preview…');
186
  // Start preview in its own process group so we can terminate all children reliably
 
274
  }
275
  function fixSvg(svg){
276
  if (!svg) return;
277
+ // Do not alter banner galaxy SVG sizing; it relies on explicit width/height
278
+ try { if (svg.closest && svg.closest('.d3-galaxy')) return; } catch {}
279
  if (isSmallSvg(svg)) { lockSmallSvgSize(svg); return; }
280
  try { svg.removeAttribute('width'); } catch {}
281
  try { svg.removeAttribute('height'); } catch {}
 
322
  // - Ensure an SVG background (CSS background on svg element)
323
  const cssHandle = await page.addStyleTag({ content: `
324
  .hero .points { mix-blend-mode: normal !important; }
 
325
  ` });
326
  const thumbPath = resolve(cwd, 'dist', 'thumb.jpg');
327
  await page.screenshot({ path: thumbPath, type: 'jpeg', quality: 85, fullPage: false });
 
372
  }
373
  function fixSvg(svg){
374
  if (!svg) return;
375
+ // Do not alter banner galaxy SVG sizing; it relies on explicit width/height
376
+ try { if (svg.closest && svg.closest('.d3-galaxy')) return; } catch {}
377
  if (isSmallSvg(svg)) { lockSmallSvgSize(svg); return; }
378
  try { svg.removeAttribute('width'); } catch {}
379
  try { svg.removeAttribute('height'); } catch {}
 
426
 
427
  /* Banner centering & visibility */
428
  .hero .points { mix-blend-mode: normal !important; }
429
+ /* Do NOT force a fixed height to avoid clipping in PDF */
430
+ .d3-galaxy { width: 100% !important; max-width: 980px !important; margin-left: auto !important; margin-right: auto !important; }
431
+ .d3-galaxy svg { width: 100% !important; height: auto !important; }
432
  ` });
433
  } catch {}
434
  await page.pdf({
app/src/components/Accordion.astro CHANGED
@@ -89,23 +89,18 @@ const wrapperClass = ["accordion", className].filter(Boolean).join(" ");
89
  }
90
 
91
  .accordion__summary {
 
92
  list-style: none;
93
  display: flex;
94
  align-items: center;
95
- justify-content: center;
96
  gap: 4px;
97
- padding: 4px;
98
  cursor: pointer;
99
  color: var(--text-color);
100
  user-select: none;
101
- position: relative;
102
  }
103
 
104
- .accordion[size="big"] .accordion__summary {
105
- padding: 16px;
106
- }
107
-
108
-
109
  /* Remove conditional padding to avoid jump on close */
110
 
111
  /* Remove native marker */
@@ -116,13 +111,16 @@ const wrapperClass = ["accordion", className].filter(Boolean).join(" ");
116
  content: "";
117
  }
118
 
 
 
 
 
119
  .accordion__title {
120
  font-weight: 600;
121
  }
122
 
123
  .accordion__chevron {
124
- position: absolute;
125
- right: 8px;
126
  transition: transform 220ms ease;
127
  opacity: .85;
128
  }
@@ -165,7 +163,6 @@ const wrapperClass = ["accordion", className].filter(Boolean).join(" ");
165
  padding: 0;
166
  }
167
 
168
-
169
  /* Separator between header and content when open (edge-to-edge) */
170
  .accordion[open] .accordion__content-wrapper::before {
171
  content: "";
@@ -184,7 +181,6 @@ const wrapperClass = ["accordion", className].filter(Boolean).join(" ");
184
  outline-offset: 3px;
185
  border-radius: 8px;
186
  }
187
-
188
 
189
 
190
  </style>
 
89
  }
90
 
91
  .accordion__summary {
92
+ margin: 0;
93
  list-style: none;
94
  display: flex;
95
  align-items: center;
96
+ justify-content: space-between;
97
  gap: 4px;
98
+ padding: var(--spacing-2) var(--spacing-3);
99
  cursor: pointer;
100
  color: var(--text-color);
101
  user-select: none;
 
102
  }
103
 
 
 
 
 
 
104
  /* Remove conditional padding to avoid jump on close */
105
 
106
  /* Remove native marker */
 
111
  content: "";
112
  }
113
 
114
+ .accordion[size="big"] .accordion__summary {
115
+ padding: 16px;
116
+ }
117
+
118
  .accordion__title {
119
  font-weight: 600;
120
  }
121
 
122
  .accordion__chevron {
123
+ flex: 0 0 auto;
 
124
  transition: transform 220ms ease;
125
  opacity: .85;
126
  }
 
163
  padding: 0;
164
  }
165
 
 
166
  /* Separator between header and content when open (edge-to-edge) */
167
  .accordion[open] .accordion__content-wrapper::before {
168
  content: "";
 
181
  outline-offset: 3px;
182
  border-radius: 8px;
183
  }
 
184
 
185
 
186
  </style>
app/src/components/Footer.astro CHANGED
@@ -215,24 +215,6 @@ const { citationText, bibtex, licence, doi } = Astro.props as Props;
215
  }
216
  }
217
 
218
- /* Avoid duplicate numbering: hide native list markers for CSL references only */
219
- .references-block #references ol,
220
- .references-block .references ol,
221
- .references-block .bibliography ol {
222
- list-style: none;
223
- padding-left: 0;
224
- margin-left: 0;
225
- }
226
-
227
- @media (min-width: 768px) {
228
- .references-block #references ol,
229
- .references-block .references ol,
230
- .references-block .bibliography ol {
231
- padding-left: 0;
232
- margin-left: 0;
233
- }
234
- }
235
-
236
  .references-block li {
237
  margin-bottom: 1em;
238
  }
 
215
  }
216
  }
217
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
218
  .references-block li {
219
  margin-bottom: 1em;
220
  }
app/src/components/RawHtml.astro ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ ---
2
+ const { html, class: className, ariaLabel } = Astro.props;
3
+ ---
4
+ <div class={className} role="img" aria-label={ariaLabel} set:html={html} />
5
+
6
+
app/src/components/TableOfContents.astro CHANGED
@@ -24,22 +24,8 @@ const { tableOfContentAutoCollapse = false } = Astro.props as Props;
24
  const headings = articleRoot.querySelectorAll('h2, h3, h4');
25
  if (!headings.length) return;
26
 
27
- // Filter out headings that should not appear in TOC
28
- const normalize = (s) => String(s || '')
29
- .toLowerCase()
30
- .replace(/[^a-z0-9]+/g, ' ')
31
- .trim();
32
- const isTocLabel = (s) => /^(table\s+of\s+contents?)$|^toc$/i.test(String(s || '').replace(/[^a-zA-Z0-9]+/g, ' ').trim());
33
- const shouldSkip = (h) => {
34
- const t = h.textContent || '';
35
- const id = String(h.id || '');
36
- const slug = normalize(t).replace(/\s+/g, '_');
37
- if (isTocLabel(t)) return true;
38
- if (isTocLabel(id.replace(/[_-]+/g, ' '))) return true;
39
- if (isTocLabel(slug.replace(/[_-]+/g, ' '))) return true;
40
- return false;
41
- };
42
- const headingsArr = Array.from(headings).filter(h => !shouldSkip(h));
43
  if (!headingsArr.length) return;
44
 
45
  // Ensure unique ids for headings (deduplicate duplicates)
@@ -265,6 +251,7 @@ const { tableOfContentAutoCollapse = false } = Astro.props as Props;
265
  .table-of-contents {
266
  position: sticky;
267
  top: 32px;
 
268
  }
269
 
270
  .table-of-contents nav {
 
24
  const headings = articleRoot.querySelectorAll('h2, h3, h4');
25
  if (!headings.length) return;
26
 
27
+ // Inclure tous les titres H2/H3/H4 sans filtrer "Table of contents"
28
+ const headingsArr = Array.from(headings);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  if (!headingsArr.length) return;
30
 
31
  // Ensure unique ids for headings (deduplicate duplicates)
 
251
  .table-of-contents {
252
  position: sticky;
253
  top: 32px;
254
+ margin-top: 12px;
255
  }
256
 
257
  .table-of-contents nav {
app/src/content/assets/images/thumb.png ADDED

Git LFS Details

  • SHA256: 2a4dc56d91f2919c80261a986b86783583bdb39e5fcdc5082d5afa96756367c7
  • Pointer size: 131 Bytes
  • Size of remote file: 493 kB
app/src/content/embeds/against-baselines-deduplicated.html CHANGED
@@ -1,6 +1,6 @@
1
- <div class="d3-line" style="width:100%;margin:10px 0;"></div>
2
  <style>
3
- .d3-line .d3-line__controls select {
4
  font-size: 12px;
5
  padding: 8px 28px 8px 10px;
6
  border: 1px solid var(--border-color);
@@ -17,21 +17,21 @@
17
  cursor: pointer;
18
  transition: border-color .15s ease, box-shadow .15s ease;
19
  }
20
- [data-theme="dark"] .d3-line .d3-line__controls select {
21
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
22
  }
23
- .d3-line .d3-line__controls select:hover {
24
  border-color: var(--primary-color);
25
  }
26
- .d3-line .d3-line__controls select:focus {
27
  border-color: var(--primary-color);
28
  box-shadow: 0 0 0 3px rgba(232,137,171,.25);
29
  outline: none;
30
  }
31
- .d3-line .d3-line__controls label { gap: 8px; }
32
 
33
  /* Range slider themed with --primary-color */
34
- .d3-line .d3-line__controls input[type="range"] {
35
  -webkit-appearance: none;
36
  appearance: none;
37
  width: 100%;
@@ -40,12 +40,12 @@
40
  background: var(--border-color);
41
  outline: none;
42
  }
43
- .d3-line .d3-line__controls input[type="range"]::-webkit-slider-runnable-track {
44
  height: 6px;
45
  background: transparent;
46
  border-radius: 999px;
47
  }
48
- .d3-line .d3-line__controls input[type="range"]::-webkit-slider-thumb {
49
  -webkit-appearance: none;
50
  appearance: none;
51
  width: 16px;
@@ -56,12 +56,12 @@
56
  margin-top: -5px;
57
  cursor: pointer;
58
  }
59
- .d3-line .d3-line__controls input[type="range"]::-moz-range-track {
60
  height: 6px;
61
  background: transparent;
62
  border-radius: 999px;
63
  }
64
- .d3-line .d3-line__controls input[type="range"]::-moz-range-thumb {
65
  width: 16px;
66
  height: 16px;
67
  border-radius: 50%;
@@ -70,7 +70,7 @@
70
  cursor: pointer;
71
  }
72
  /* Improved line color via CSS */
73
- .d3-line .lines path.improved { stroke: var(--primary-color); }
74
  </style>
75
  <script>
76
  (() => {
@@ -173,24 +173,25 @@
173
  const defs = svg.append('defs');
174
  // Clip-path to constrain drawing to chart area
175
  const clipId = `clip-${Math.random().toString(36).slice(2)}`;
176
- const clipPath = defs.append('clipPath').attr('id', clipId);
 
177
  const clipRect = clipPath.append('rect').attr('x', 0).attr('y', 0).attr('width', 0).attr('height', 0);
178
 
179
  // Academic marker shapes
180
  const markerShapes = ['circle', 'square', 'triangle', 'diamond', 'inverted-triangle'];
181
- const markerSize = 8;
182
 
183
  // Groups
184
  const gRoot = svg.append('g');
185
  const gGrid = gRoot.append('g').attr('class', 'grid');
186
  const gAxes = gRoot.append('g').attr('class', 'axes');
187
- const gAreas = gRoot.append('g').attr('class', 'areas');
188
- const gLines = gRoot.append('g').attr('class', 'lines');
189
- const gPoints = gRoot.append('g').attr('class', 'points');
190
- // Apply clipping to plotted elements (areas, lines, points)
191
- gAreas.attr('clip-path', `url(#${clipId})`);
192
- gLines.attr('clip-path', `url(#${clipId})`);
193
- gPoints.attr('clip-path', `url(#${clipId})`);
194
  const gHover = gRoot.append('g').attr('class', 'hover');
195
  const gLegend = gRoot.append('foreignObject').attr('class', 'legend');
196
 
@@ -253,8 +254,8 @@
253
  // Scales and layout
254
  let width = 800, height = 360;
255
  let margin = { top: 16, right: 28, bottom: 56, left: 64 };
256
- let xScale = d3.scaleLinear();
257
- let yScale = d3.scaleLinear();
258
 
259
  // Line generators - simple linear connections
260
  const lineGen = d3.line()
@@ -318,8 +319,23 @@
318
  const innerHeight = height - margin.top - margin.bottom;
319
  gRoot.attr('transform', `translate(${margin.left},${margin.top})`);
320
 
321
- // Update clip rect to match inner plotting area
322
- clipRect.attr('width', innerWidth).attr('height', innerHeight);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
323
 
324
  xScale.range([0, innerWidth]);
325
  yScale.range([innerHeight, 0]);
@@ -442,8 +458,13 @@
442
  if (!isFinite(minStep) || !isFinite(maxStep)) { return; }
443
  xScale.domain([minStep, maxStep]);
444
  if (isRank) {
445
- rankTickMax = Math.max(1, Math.round(maxVal));
446
- yScale.domain([rankTickMax, 1]);
 
 
 
 
 
447
  } else {
448
  yScale.domain([minVal, maxVal]).nice();
449
  }
@@ -483,15 +504,15 @@
483
  // Draw lines
484
  const paths = gLines.selectAll('path.run-line').data(series, d=>d.run);
485
  paths.enter().append('path').attr('class','run-line').attr('fill','none').attr('stroke-width',2)
486
- .attr('stroke', d=>d.color).attr('opacity',0.9)
 
487
  .attr('d', d=>lineGen(d.values))
488
  .merge(paths)
489
- .transition().duration(200)
490
  .attr('stroke', d=>d.color)
491
  .attr('d', d=>lineGen(d.values));
492
  paths.exit().remove();
493
 
494
- // Draw markers for each data point
495
  gPoints.selectAll('*').remove();
496
  series.forEach((s, seriesIndex) => {
497
  const pointGroup = gPoints.selectAll(`.points-${seriesIndex}`)
@@ -499,11 +520,10 @@
499
  .join('g')
500
  .attr('class', `points-${seriesIndex}`)
501
  .attr('transform', d => `translate(${xScale(d.step)},${yScale(d.value)})`);
502
-
503
  drawMarker(pointGroup, s.marker, markerSize)
504
  .attr('fill', s.color)
505
  .attr('stroke', s.color)
506
- .attr('stroke-width', 1.5)
507
  .style('cursor', 'crosshair');
508
  });
509
 
 
1
+ <div class="d3-line d3-embed--against-baselines-dd" style="width:100%;margin:10px 0;"></div>
2
  <style>
3
+ .d3-embed--against-baselines-dd .d3-line__controls select {
4
  font-size: 12px;
5
  padding: 8px 28px 8px 10px;
6
  border: 1px solid var(--border-color);
 
17
  cursor: pointer;
18
  transition: border-color .15s ease, box-shadow .15s ease;
19
  }
20
+ [data-theme="dark"] .d3-embed--against-baselines-dd .d3-line__controls select {
21
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
22
  }
23
+ .d3-embed--against-baselines-dd .d3-line__controls select:hover {
24
  border-color: var(--primary-color);
25
  }
26
+ .d3-embed--against-baselines-dd .d3-line__controls select:focus {
27
  border-color: var(--primary-color);
28
  box-shadow: 0 0 0 3px rgba(232,137,171,.25);
29
  outline: none;
30
  }
31
+ .d3-embed--against-baselines-dd .d3-line__controls label { gap: 8px; }
32
 
33
  /* Range slider themed with --primary-color */
34
+ .d3-embed--against-baselines-dd .d3-line__controls input[type="range"] {
35
  -webkit-appearance: none;
36
  appearance: none;
37
  width: 100%;
 
40
  background: var(--border-color);
41
  outline: none;
42
  }
43
+ .d3-embed--against-baselines-dd .d3-line__controls input[type="range"]::-webkit-slider-runnable-track {
44
  height: 6px;
45
  background: transparent;
46
  border-radius: 999px;
47
  }
48
+ .d3-embed--against-baselines-dd .d3-line__controls input[type="range"]::-webkit-slider-thumb {
49
  -webkit-appearance: none;
50
  appearance: none;
51
  width: 16px;
 
56
  margin-top: -5px;
57
  cursor: pointer;
58
  }
59
+ .d3-embed--against-baselines-dd .d3-line__controls input[type="range"]::-moz-range-track {
60
  height: 6px;
61
  background: transparent;
62
  border-radius: 999px;
63
  }
64
+ .d3-embed--against-baselines-dd .d3-line__controls input[type="range"]::-moz-range-thumb {
65
  width: 16px;
66
  height: 16px;
67
  border-radius: 50%;
 
70
  cursor: pointer;
71
  }
72
  /* Improved line color via CSS */
73
+ .d3-embed--against-baselines-dd .lines path.improved { stroke: var(--primary-color); }
74
  </style>
75
  <script>
76
  (() => {
 
173
  const defs = svg.append('defs');
174
  // Clip-path to constrain drawing to chart area
175
  const clipId = `clip-${Math.random().toString(36).slice(2)}`;
176
+ const clipPath = defs.append('clipPath').attr('id', clipId)
177
+ .attr('clipPathUnits', 'userSpaceOnUse');
178
  const clipRect = clipPath.append('rect').attr('x', 0).attr('y', 0).attr('width', 0).attr('height', 0);
179
 
180
  // Academic marker shapes
181
  const markerShapes = ['circle', 'square', 'triangle', 'diamond', 'inverted-triangle'];
182
+ const markerSize = 5;
183
 
184
  // Groups
185
  const gRoot = svg.append('g');
186
  const gGrid = gRoot.append('g').attr('class', 'grid');
187
  const gAxes = gRoot.append('g').attr('class', 'axes');
188
+ // Dedicated plotting layer that will be clipped
189
+ const gPlot = gRoot.append('g').attr('class', 'plot');
190
+ const gAreas = gPlot.append('g').attr('class', 'areas');
191
+ const gLines = gPlot.append('g').attr('class', 'lines');
192
+ const gPoints = gPlot.append('g').attr('class', 'points');
193
+ // Apply clipping to the whole plotting group
194
+ gPlot.attr('clip-path', `url(#${clipId})`);
195
  const gHover = gRoot.append('g').attr('class', 'hover');
196
  const gLegend = gRoot.append('foreignObject').attr('class', 'legend');
197
 
 
254
  // Scales and layout
255
  let width = 800, height = 360;
256
  let margin = { top: 16, right: 28, bottom: 56, left: 64 };
257
+ let xScale = d3.scaleLinear().clamp(true);
258
+ let yScale = d3.scaleLinear().clamp(true);
259
 
260
  // Line generators - simple linear connections
261
  const lineGen = d3.line()
 
319
  const innerHeight = height - margin.top - margin.bottom;
320
  gRoot.attr('transform', `translate(${margin.left},${margin.top})`);
321
 
322
+ // Responsive legend layout: wrap to two lines on narrower screens
323
+ try {
324
+ const shouldWrapLegend = width < 1300;
325
+ if (legendInline && legendInline.style) {
326
+ legendInline.style.flexWrap = shouldWrapLegend ? 'wrap' : 'nowrap';
327
+ legendInline.style.rowGap = shouldWrapLegend ? '4px' : '';
328
+ legendInline.style.maxWidth = shouldWrapLegend ? Math.max(280, Math.round(width * 0.62)) + 'px' : 'unset';
329
+ legendInline.style.alignItems = shouldWrapLegend ? 'flex-start' : 'center';
330
+ }
331
+ } catch {}
332
+
333
+ // Update clip rect to match inner plotting area (coords local to gRoot)
334
+ clipRect
335
+ .attr('x', 0)
336
+ .attr('y', 0)
337
+ .attr('width', innerWidth)
338
+ .attr('height', innerHeight);
339
 
340
  xScale.range([0, innerWidth]);
341
  yScale.range([innerHeight, 0]);
 
458
  if (!isFinite(minStep) || !isFinite(maxStep)) { return; }
459
  xScale.domain([minStep, maxStep]);
460
  if (isRank) {
461
+ // Strict rank → arrondi au supérieur entier observé
462
+ // Average rank → forcer un plancher visuel à 4 pour une petite marge en bas
463
+ const bottom = isRankStrict
464
+ ? Math.max(1, Math.round(maxVal))
465
+ : Math.max(4, Math.ceil(maxVal + 0.001));
466
+ rankTickMax = bottom;
467
+ yScale.domain([rankTickMax, 1]).nice();
468
  } else {
469
  yScale.domain([minVal, maxVal]).nice();
470
  }
 
504
  // Draw lines
505
  const paths = gLines.selectAll('path.run-line').data(series, d=>d.run);
506
  paths.enter().append('path').attr('class','run-line').attr('fill','none').attr('stroke-width',2)
507
+ .attr('stroke-linejoin','round').attr('stroke-linecap','round')
508
+ .attr('stroke', d=>d.color).attr('opacity',0.95)
509
  .attr('d', d=>lineGen(d.values))
510
  .merge(paths)
 
511
  .attr('stroke', d=>d.color)
512
  .attr('d', d=>lineGen(d.values));
513
  paths.exit().remove();
514
 
515
+ // Draw markers for each data point (clipped with gPlot)
516
  gPoints.selectAll('*').remove();
517
  series.forEach((s, seriesIndex) => {
518
  const pointGroup = gPoints.selectAll(`.points-${seriesIndex}`)
 
520
  .join('g')
521
  .attr('class', `points-${seriesIndex}`)
522
  .attr('transform', d => `translate(${xScale(d.step)},${yScale(d.value)})`);
 
523
  drawMarker(pointGroup, s.marker, markerSize)
524
  .attr('fill', s.color)
525
  .attr('stroke', s.color)
526
+ .attr('stroke-width', 1.2)
527
  .style('cursor', 'crosshair');
528
  });
529
 
app/src/content/embeds/against-baselines.html CHANGED
@@ -1,6 +1,6 @@
1
- <div class="d3-line" style="width:100%;margin:10px 0;"></div>
2
  <style>
3
- .d3-line .d3-line__controls select {
4
  font-size: 12px;
5
  padding: 8px 28px 8px 10px;
6
  border: 1px solid var(--border-color);
@@ -17,21 +17,21 @@
17
  cursor: pointer;
18
  transition: border-color .15s ease, box-shadow .15s ease;
19
  }
20
- [data-theme="dark"] .d3-line .d3-line__controls select {
21
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
22
  }
23
- .d3-line .d3-line__controls select:hover {
24
  border-color: var(--primary-color);
25
  }
26
- .d3-line .d3-line__controls select:focus {
27
  border-color: var(--primary-color);
28
  box-shadow: 0 0 0 3px rgba(232,137,171,.25);
29
  outline: none;
30
  }
31
- .d3-line .d3-line__controls label { gap: 8px; }
32
 
33
  /* Range slider themed with --primary-color */
34
- .d3-line .d3-line__controls input[type="range"] {
35
  -webkit-appearance: none;
36
  appearance: none;
37
  width: 100%;
@@ -40,12 +40,12 @@
40
  background: var(--border-color);
41
  outline: none;
42
  }
43
- .d3-line .d3-line__controls input[type="range"]::-webkit-slider-runnable-track {
44
  height: 6px;
45
  background: transparent;
46
  border-radius: 999px;
47
  }
48
- .d3-line .d3-line__controls input[type="range"]::-webkit-slider-thumb {
49
  -webkit-appearance: none;
50
  appearance: none;
51
  width: 16px;
@@ -56,12 +56,12 @@
56
  margin-top: -5px;
57
  cursor: pointer;
58
  }
59
- .d3-line .d3-line__controls input[type="range"]::-moz-range-track {
60
  height: 6px;
61
  background: transparent;
62
  border-radius: 999px;
63
  }
64
- .d3-line .d3-line__controls input[type="range"]::-moz-range-thumb {
65
  width: 16px;
66
  height: 16px;
67
  border-radius: 50%;
@@ -70,7 +70,7 @@
70
  cursor: pointer;
71
  }
72
  /* Improved line color via CSS */
73
- .d3-line .lines path.improved { stroke: var(--primary-color); }
74
  </style>
75
  <script>
76
  (() => {
@@ -166,12 +166,16 @@
166
  .attr('width', '100%')
167
  .style('display', 'block');
168
 
169
- // Add marker definitions for different shapes
170
  const defs = svg.append('defs');
 
 
 
 
171
 
172
  // Academic marker shapes
173
  const markerShapes = ['circle', 'square', 'triangle', 'diamond', 'inverted-triangle'];
174
- const markerSize = 8;
175
 
176
  // Groups
177
  const gRoot = svg.append('g');
@@ -181,6 +185,10 @@
181
  const gLines = gRoot.append('g').attr('class', 'lines');
182
  const gErrors = gRoot.append('g').attr('class', 'errors');
183
  const gPoints = gRoot.append('g').attr('class', 'points');
 
 
 
 
184
  const gHover = gRoot.append('g').attr('class', 'hover');
185
  const gLegend = gRoot.append('foreignObject').attr('class', 'legend');
186
 
@@ -312,6 +320,13 @@
312
  xScale.range([0, innerWidth]);
313
  yScale.range([innerHeight, 0]);
314
 
 
 
 
 
 
 
 
315
  // Compute Y ticks
316
  let yTicks = [];
317
  if (isRankStrictFlag) {
 
1
+ <div class="d3-line d3-embed--against-baselines" style="width:100%;margin:10px 0;"></div>
2
  <style>
3
+ .d3-embed--against-baselines .d3-line__controls select {
4
  font-size: 12px;
5
  padding: 8px 28px 8px 10px;
6
  border: 1px solid var(--border-color);
 
17
  cursor: pointer;
18
  transition: border-color .15s ease, box-shadow .15s ease;
19
  }
20
+ [data-theme="dark"] .d3-embed--against-baselines .d3-line__controls select {
21
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
22
  }
23
+ .d3-embed--against-baselines .d3-line__controls select:hover {
24
  border-color: var(--primary-color);
25
  }
26
+ .d3-embed--against-baselines .d3-line__controls select:focus {
27
  border-color: var(--primary-color);
28
  box-shadow: 0 0 0 3px rgba(232,137,171,.25);
29
  outline: none;
30
  }
31
+ .d3-embed--against-baselines .d3-line__controls label { gap: 8px; }
32
 
33
  /* Range slider themed with --primary-color */
34
+ .d3-embed--against-baselines .d3-line__controls input[type="range"] {
35
  -webkit-appearance: none;
36
  appearance: none;
37
  width: 100%;
 
40
  background: var(--border-color);
41
  outline: none;
42
  }
43
+ .d3-embed--against-baselines .d3-line__controls input[type="range"]::-webkit-slider-runnable-track {
44
  height: 6px;
45
  background: transparent;
46
  border-radius: 999px;
47
  }
48
+ .d3-embed--against-baselines .d3-line__controls input[type="range"]::-webkit-slider-thumb {
49
  -webkit-appearance: none;
50
  appearance: none;
51
  width: 16px;
 
56
  margin-top: -5px;
57
  cursor: pointer;
58
  }
59
+ .d3-embed--against-baselines .d3-line__controls input[type="range"]::-moz-range-track {
60
  height: 6px;
61
  background: transparent;
62
  border-radius: 999px;
63
  }
64
+ .d3-embed--against-baselines .d3-line__controls input[type="range"]::-moz-range-thumb {
65
  width: 16px;
66
  height: 16px;
67
  border-radius: 50%;
 
70
  cursor: pointer;
71
  }
72
  /* Improved line color via CSS */
73
+ .d3-embed--against-baselines .lines path.improved { stroke: var(--primary-color); }
74
  </style>
75
  <script>
76
  (() => {
 
166
  .attr('width', '100%')
167
  .style('display', 'block');
168
 
169
+ // Add marker definitions and clipPath for chart area
170
  const defs = svg.append('defs');
171
+ const clipId = `clip-${Math.random().toString(36).slice(2)}`;
172
+ const clipPath = defs.append('clipPath').attr('id', clipId)
173
+ .attr('clipPathUnits', 'userSpaceOnUse');
174
+ const clipRect = clipPath.append('rect').attr('x', 0).attr('y', 0).attr('width', 0).attr('height', 0);
175
 
176
  // Academic marker shapes
177
  const markerShapes = ['circle', 'square', 'triangle', 'diamond', 'inverted-triangle'];
178
+ const markerSize = 5;
179
 
180
  // Groups
181
  const gRoot = svg.append('g');
 
185
  const gLines = gRoot.append('g').attr('class', 'lines');
186
  const gErrors = gRoot.append('g').attr('class', 'errors');
187
  const gPoints = gRoot.append('g').attr('class', 'points');
188
+ // Constrain plotted layers to inner chart area
189
+ gAreas.attr('clip-path', `url(#${clipId})`);
190
+ gLines.attr('clip-path', `url(#${clipId})`);
191
+ gPoints.attr('clip-path', `url(#${clipId})`);
192
  const gHover = gRoot.append('g').attr('class', 'hover');
193
  const gLegend = gRoot.append('foreignObject').attr('class', 'legend');
194
 
 
320
  xScale.range([0, innerWidth]);
321
  yScale.range([innerHeight, 0]);
322
 
323
+ // Update clip rect to match inner plotting area (coords of gRoot after translate)
324
+ clipRect
325
+ .attr('x', 0)
326
+ .attr('y', 0)
327
+ .attr('width', innerWidth)
328
+ .attr('height', innerHeight);
329
+
330
  // Compute Y ticks
331
  let yTicks = [];
332
  if (isRankStrictFlag) {
app/src/content/embeds/all-ratings.html CHANGED
@@ -171,7 +171,7 @@
171
 
172
  // Academic marker shapes
173
  const markerShapes = ['circle', 'square', 'triangle', 'diamond', 'inverted-triangle'];
174
- const markerSize = 8;
175
 
176
  // Groups
177
  const gRoot = svg.append('g');
 
171
 
172
  // Academic marker shapes
173
  const markerShapes = ['circle', 'square', 'triangle', 'diamond', 'inverted-triangle'];
174
+ const markerSize = 5;
175
 
176
  // Groups
177
  const gRoot = svg.append('g');
app/src/content/embeds/banner.html CHANGED
@@ -1,4 +1,4 @@
1
- <div class="d3-galaxy" style="width:100%;margin:10px 0;aspect-ratio:3/1;min-height:360px;"></div>
2
  <script>
3
  (() => {
4
  const THIS_SCRIPT = document.currentScript;
@@ -43,9 +43,15 @@
43
 
44
  const csvUrl = (container.dataset && container.dataset.src) || '/data/banner_visualisation_data.csv';
45
 
 
 
 
 
 
46
  const svg = d3.select(container).append('svg')
47
  .attr('width', '100%')
48
- .style('display', 'block');
 
49
 
50
  // Tooltip (reuse style from other embeds)
51
  container.style.position = container.style.position || 'relative';
@@ -272,7 +278,7 @@
272
 
273
  tipInner.innerHTML =
274
  `<div style="display:flex; gap:10px; align-items:flex-start;">` +
275
- `<img src="${imgSrc}" alt="thumb ${d.original_id}" style="width:120px;height:120px;object-fit:contain;flex:0 0 auto;border-radius:6px;border:1px solid var(--border-color);background:var(--surface-bg);" />` +
276
  `<div style="min-width:140px; max-width:200px;">` +
277
  `<div><strong>${d.category || 'Unknown'}</strong></div>` +
278
  `<div style="word-wrap:break-word; line-height:1.3; margin:4px 0;"><strong>Q:</strong> ${userText}</div>` +
 
1
+ <div class="d3-galaxy" style="width:100%;margin:10px 0;aspect-ratio:3/1;min-height:360px; display:flex; align-items:center; justify-content:center;"></div>
2
  <script>
3
  (() => {
4
  const THIS_SCRIPT = document.currentScript;
 
43
 
44
  const csvUrl = (container.dataset && container.dataset.src) || '/data/banner_visualisation_data.csv';
45
 
46
+ // Assurer un fond transparent pour l'ensemble du composant
47
+ if (container && container.style) {
48
+ container.style.background = 'transparent';
49
+ }
50
+
51
  const svg = d3.select(container).append('svg')
52
  .attr('width', '100%')
53
+ .style('display', 'block')
54
+ .style('background', 'transparent');
55
 
56
  // Tooltip (reuse style from other embeds)
57
  container.style.position = container.style.position || 'relative';
 
278
 
279
  tipInner.innerHTML =
280
  `<div style="display:flex; gap:10px; align-items:flex-start;">` +
281
+ `<img src="${imgSrc}" alt="thumb ${d.original_id}" style="width:120px;height:120px;object-fit:cover;flex:0 0 auto;border-radius:6px;border:1px solid var(--border-color);" />` +
282
  `<div style="min-width:140px; max-width:200px;">` +
283
  `<div><strong>${d.category || 'Unknown'}</strong></div>` +
284
  `<div style="word-wrap:break-word; line-height:1.3; margin:4px 0;"><strong>Q:</strong> ${userText}</div>` +
app/src/content/embeds/d3-pie.html CHANGED
@@ -93,7 +93,7 @@
93
  const CAPTION_GAP = 24; // espace entre titre et donut
94
  const GAP_X = 20; // espace entre colonnes
95
  const GAP_Y = 12; // espace entre lignes
96
- const LEGEND_HEIGHT = 44; // hauteur de la légende
97
  const TOP_OFFSET = 4; // décalage vertical supplémentaire pour aérer le haut
98
  const updateSize = () => {
99
  width = container.clientWidth || 800;
@@ -103,15 +103,27 @@
103
  };
104
 
105
  function renderLegend(categories, colorOf, innerWidth, legendY){
106
- const legendHeight = LEGEND_HEIGHT;
107
- gLegend.attr('x', 0).attr('y', legendY).attr('width', innerWidth).attr('height', legendHeight);
108
  const root = gLegend.selectAll('div').data([0]).join('xhtml:div');
109
  root
110
- .style('height', legendHeight + 'px')
111
  .style('display', 'flex')
112
  .style('align-items', 'center')
113
  .style('justify-content', 'center');
114
  root.html(`<div class="items">${categories.map(c => `<div class="item"><span class="swatch" style="background:${colorOf(c)}"></span><span style="font-weight:500">${c}</span></div>`).join('')}</div>`);
 
 
 
 
 
 
 
 
 
 
 
 
115
  }
116
 
117
  function drawPies(rows){
@@ -174,7 +186,7 @@
174
  const legendY = TOP_OFFSET + plotsHeight + 4;
175
  // Légende centrée globalement (pleine largeur du conteneur)
176
  gLegend.attr('x', 0).attr('width', innerWidth);
177
- renderLegend(categories, colorOf, innerWidth, legendY);
178
 
179
  const captions = new Map(METRICS.map(m => [m.key, `${m.title}`]));
180
 
@@ -232,7 +244,7 @@
232
  });
233
 
234
  // Définir la hauteur totale du SVG après avoir placé les éléments
235
- const totalHeight = Math.ceil(margin.top + TOP_OFFSET + plotsHeight + 4 + LEGEND_HEIGHT + margin.bottom);
236
  svg.attr('height', totalHeight);
237
  }
238
 
 
93
  const CAPTION_GAP = 24; // espace entre titre et donut
94
  const GAP_X = 20; // espace entre colonnes
95
  const GAP_Y = 12; // espace entre lignes
96
+ const BASE_LEGEND_HEIGHT = 56; // hauteur minimale de la légende (augmentée pour éviter la troncature mobile)
97
  const TOP_OFFSET = 4; // décalage vertical supplémentaire pour aérer le haut
98
  const updateSize = () => {
99
  width = container.clientWidth || 800;
 
103
  };
104
 
105
  function renderLegend(categories, colorOf, innerWidth, legendY){
106
+ const minHeight = BASE_LEGEND_HEIGHT;
107
+ gLegend.attr('x', 0).attr('y', legendY).attr('width', innerWidth);
108
  const root = gLegend.selectAll('div').data([0]).join('xhtml:div');
109
  root
110
+ .style('min-height', minHeight + 'px')
111
  .style('display', 'flex')
112
  .style('align-items', 'center')
113
  .style('justify-content', 'center');
114
  root.html(`<div class="items">${categories.map(c => `<div class="item"><span class="swatch" style="background:${colorOf(c)}"></span><span style="font-weight:500">${c}</span></div>`).join('')}</div>`);
115
+
116
+ // Mesurer la hauteur réelle (contenu potentiellement sur plusieurs lignes en mobile)
117
+ let measured = minHeight;
118
+ try {
119
+ const n = root.node();
120
+ const h1 = n && n.scrollHeight ? Math.ceil(n.scrollHeight) : 0;
121
+ const h2 = n ? Math.ceil(n.getBoundingClientRect().height) : 0;
122
+ measured = Math.max(minHeight, h1, h2);
123
+ } catch {}
124
+ gLegend.attr('height', measured);
125
+ root.style('height', measured + 'px');
126
+ return measured;
127
  }
128
 
129
  function drawPies(rows){
 
186
  const legendY = TOP_OFFSET + plotsHeight + 4;
187
  // Légende centrée globalement (pleine largeur du conteneur)
188
  gLegend.attr('x', 0).attr('width', innerWidth);
189
+ const legendHeightUsed = renderLegend(categories, colorOf, innerWidth, legendY);
190
 
191
  const captions = new Map(METRICS.map(m => [m.key, `${m.title}`]));
192
 
 
244
  });
245
 
246
  // Définir la hauteur totale du SVG après avoir placé les éléments
247
+ const totalHeight = Math.ceil(margin.top + TOP_OFFSET + plotsHeight + 4 + (typeof legendHeightUsed === 'number' ? legendHeightUsed : BASE_LEGEND_HEIGHT) + margin.bottom);
248
  svg.attr('height', totalHeight);
249
  }
250
 
app/src/content/embeds/filters-quad.html CHANGED
@@ -15,7 +15,7 @@
15
  @media (max-width: 980px) { .filters-quad__grid { grid-template-columns: 1fr; } }
16
 
17
  .filters-quad__controls { display:flex; align-items:center; justify-content:center; gap:12px; margin: 6px 0 12px 0; flex-wrap:wrap; }
18
- .filters-quad__controls label { font-size:11px; color: var(--muted-color); opacity: .8; display:flex; align-items:center; gap:8px; }
19
  .filters-quad__controls select {
20
  font-size: 14px; padding: 8px 32px 8px 12px; border: 1px solid var(--border-color); border-radius: 10px;
21
  background-color: var(--surface-bg); color: var(--text-color);
@@ -28,7 +28,7 @@
28
  .filters-quad__controls select:hover { border-color: var(--primary-color); }
29
  .filters-quad__controls select:focus { border-color: var(--primary-color); box-shadow: 0 0 0 3px rgba(232,137,171,.25); outline: none; }
30
 
31
- .filters-quad__legend { display:flex; align-items:center; justify-content:center; gap:12px; flex-wrap:wrap; font-size:14px; color: var(--text-color); margin: 2px 0 10px 0; }
32
  .filters-quad__legend .item { display:inline-flex; align-items:center; gap:6px; white-space:nowrap; }
33
  .filters-quad__legend .swatch { width:10px; height:10px; border-radius:50%; display:inline-block; }
34
 
@@ -73,6 +73,26 @@
73
  s.addEventListener('load', onReady, { once: true }); if (window.d3) onReady();
74
  };
75
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  function initRunLine(cell){
77
  const d3 = window.d3;
78
  const csvPath = cell.getAttribute('data-csv');
@@ -107,14 +127,12 @@
107
  const xScale = d3.scaleLinear(); const yScale = d3.scaleLinear();
108
  const lineGen = d3.line().x(d => xScale(d.step)).y(d => yScale(d.value));
109
  let isRankStrictFlag = false; let rankTickMax = 1;
110
- // Optional shared Y domain override (set from outside to sync the 4 charts)
111
- let yDomainOverride = null; // { metricKey, domain:[min,max] | [max,1] for rank, isRankStrict, rankTickMax }
112
 
113
  // Colors and markers (match original embeds)
114
  const primary = getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim() || '#E889AB';
115
  const pool = [primary, '#4EA5B7', '#E38A42', '#CEC0FA', ...(d3.schemeTableau10||[])];
116
  const markerShapes = ['circle', 'square', 'triangle', 'diamond', 'inverted-triangle'];
117
- const markerSize = 9;
118
  function drawMarker(selection, shape, size) {
119
  const s = size / 2;
120
  switch (shape) {
@@ -132,6 +150,24 @@
132
  return selection.append('circle').attr('r', s);
133
  }
134
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
  // Ready signal for async load completion
136
  let readyResolve = null;
137
  const ready = new Promise((res)=> { readyResolve = res; });
@@ -165,8 +201,8 @@
165
  gAxes.selectAll('*').remove();
166
  let xAxis = d3.axisBottom(xScale).tickSizeOuter(0); xAxis = xAxis.ticks(8);
167
  const yAxis = d3.axisLeft(yScale).tickValues(yTicks).tickSizeOuter(0).tickFormat(isRankStrictFlag ? d3.format('d') : d3.format('.2f'));
168
- gAxes.append('g').attr('transform', `translate(0,${innerHeight})`).call(xAxis).call(g=>{ g.selectAll('path, line').attr('stroke',axisColor); g.selectAll('text').attr('fill',tickColor).style('font-size','12px'); });
169
- gAxes.append('g').call(yAxis).call(g=>{ g.selectAll('path, line').attr('stroke',axisColor); g.selectAll('text').attr('fill',tickColor).style('font-size','12px'); });
170
 
171
  // Legend box (top-right)
172
  // Per-cell legend hidden; global legend is used
@@ -185,14 +221,7 @@
185
  runs.forEach(r => { (map[r]||[]).forEach(pt => { const v = isRankStrict ? Math.round(pt.value) : pt.value; minStep=Math.min(minStep,pt.step); maxStep=Math.max(maxStep,pt.step); maxVal=Math.max(maxVal,v); minVal=Math.min(minVal,v); }); });
186
  if (!isFinite(minStep) || !isFinite(maxStep)) return;
187
  xScale.domain([minStep, maxStep]); if (isRank) { rankTickMax = Math.max(1, Math.round(maxVal)); yScale.domain([rankTickMax, 1]); } else { yScale.domain([minVal, maxVal]).nice(); }
188
- // Apply shared Y domain override if provided for this metric
189
- if (yDomainOverride && yDomainOverride.metricKey === metricKey && Array.isArray(yDomainOverride.domain)) {
190
- isRankStrictFlag = !!yDomainOverride.isRankStrict;
191
- rankTickMax = yDomainOverride.rankTickMax != null ? yDomainOverride.rankTickMax : rankTickMax;
192
- yScale.domain(yDomainOverride.domain);
193
- } else {
194
- isRankStrictFlag = isRankStrict;
195
- }
196
 
197
  const { innerWidth, innerHeight } = updateScales();
198
 
@@ -237,24 +266,31 @@
237
  // Hover
238
  gHover.selectAll('*').remove();
239
  const overlay = gHover.append('rect').attr('fill','transparent').style('cursor','crosshair').attr('x',0).attr('y',0).attr('width', innerWidth).attr('height', innerHeight);
240
- const axisColor = document.documentElement.getAttribute('data-theme') === 'dark' ? 'rgba(255,255,255,0.25)' : 'rgba(0,0,0,0.25)';
241
- const hoverLine = gHover.append('line').attr('stroke',axisColor).attr('stroke-width',1).attr('y1',0).attr('y2',innerHeight).style('display','none');
242
  const stepSet = new Set(); series.forEach(s=>s.values.forEach(v=>stepSet.add(v.step))); const steps = Array.from(stepSet).sort((a,b)=>a-b);
243
- function onMove(ev){ const [mx,my]=d3.pointer(ev, overlay.node()); const nearest = steps.reduce((best,s)=> Math.abs(s - xScale.invert(mx)) < Math.abs(best - xScale.invert(mx)) ? s : best, steps[0]); const xpx = xScale(nearest); hoverLine.attr('x1',xpx).attr('x2',xpx).style('display',null);
 
 
 
 
244
  let html = `<div><strong>${titleText}</strong></div><div><strong>step</strong> ${nearest}</div>`;
245
- const withPt = series.map(s=>{ const m=new Map(s.values.map(v=>[v.step,v])); const pt=m.get(nearest); return {s, pt}; }).filter(d=>d.pt && d.pt.value!=null);
246
- withPt.sort((a,b)=> isRankStrictFlag ? (b.pt.value - a.pt.value) : (a.pt.value - b.pt.value));
247
- const shapeSvg = (shape, color) => {
248
- const size=9, half=size/2, cx=9, cy=7, sw='1';
249
- if(shape==='circle') return `<svg width="18" height="14" viewBox="0 0 18 14" aria-hidden="true"><g transform="translate(${cx},${cy})"><circle r="${half}" fill="${color}" stroke="${color}" stroke-width="${sw}"/></g></svg>`;
250
- if(shape==='square') return `<svg width="18" height="14" viewBox="0 0 18 14" aria-hidden="true"><g transform="translate(${cx},${cy})"><rect x="${-half}" y="${-half}" width="${size}" height="${size}" fill="${color}" stroke="${color}" stroke-width="${sw}"/></g></svg>`;
251
- if(shape==='triangle') return `<svg width="18" height="14" viewBox="0 0 18 14" aria-hidden="true"><g transform="translate(${cx},${cy})"><path d="M0,${-half*1.2} L${half*1.1},${half*0.6} L${-half*1.1},${half*0.6} Z" fill="${color}" stroke="${color}" stroke-width="${sw}"/></g></svg>`;
252
- if(shape==='diamond') return `<svg width="18" height="14" viewBox="0 0 18 14" aria-hidden="true"><g transform="translate(${cx},${cy})"><path d="M0,${-half*1.2} L${half*1.1},0 L0,${half*1.2} L${-half*1.1},0 Z" fill="${color}" stroke="${color}" stroke-width="${sw}"/></g></svg>`;
253
- if(shape==='inverted-triangle') return `<svg width="18" height="14" viewBox="0 0 18 14" aria-hidden="true"><g transform="translate(${cx},${cy})"><path d="M0,${half*1.2} L${half*1.1},${-half*0.6} L${-half*1.1},${-half*0.6} Z" fill="${color}" stroke="${color}" stroke-width="${sw}"/></g></svg>`;
254
- return `<svg width="18" height="14" viewBox="0 0 18 14" aria-hidden="true"><g transform="translate(${cx},${cy})"><circle r="${half}" fill="${color}" stroke="${color}" stroke-width="${sw}"/></g></svg>`;
255
- };
256
- withPt.forEach(({s,pt})=>{ const fmt=(vv)=> (isRankStrictFlag? d3.format('d')(vv) : (+vv).toFixed(4)); const err=(pt.stderr!=null && isFinite(pt.stderr) && pt.stderr>0)? ` ± ${fmt(pt.stderr)}` : ''; html+=`<div style="display:flex;align-items:center;gap:6px;white-space:nowrap;">${shapeSvg(s.marker, s.color)}<strong>${s.run}</strong> ${fmt(pt.value)}${err}</div>`; });
257
- 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)`; }
 
 
 
 
258
  function onLeave(){ tip.style.opacity='0'; tip.style.transform='translate(-9999px, -9999px)'; hoverLine.style('display','none'); }
259
  overlay.on('mousemove', onMove).on('mouseleave', onLeave);
260
  }
@@ -278,7 +314,9 @@
278
  metricList = Array.from(new Set(rows.map(r=>r.metric))).sort();
279
  runList = Array.from(new Set(rows.map(r=>r.run))).sort(); runOrder = runList;
280
  metricList.forEach(m => { const map={}; runList.forEach(r=>map[r]=[]); rows.filter(r=>r.metric===m).forEach(r=>{ if(!isNaN(r.step)&&!isNaN(r.value)) map[r.run].push({ step:r.step, value:r.value, stderr:r.stderr }); }); dataByMetric.set(m, map); });
281
- const def = metricList.find(m => /average_rank/i.test(m)) || metricList[0];
 
 
282
  renderMetric(def);
283
  const ro = window.ResizeObserver ? new ResizeObserver(()=>renderMetric(def)) : null; if (ro) ro.observe(cell);
284
  if (typeof readyResolve === 'function') readyResolve();
@@ -293,21 +331,7 @@
293
  return {
294
  ready,
295
  getMetrics: () => metricList.slice(),
296
- setMetric: (m) => { if (m) renderMetric(m); },
297
- // Expose extent for a given metric so the host can compute a shared Y domain
298
- getExtent: (metricKey) => {
299
- const map = dataByMetric.get(metricKey) || {};
300
- const runs = runOrder;
301
- let maxVal = -Infinity, minVal = Infinity;
302
- const isRank = /rank/i.test(metricKey); const isAverage = /average/i.test(metricKey); const isRankStrict = isRank && !isAverage;
303
- runs.forEach(r => { (map[r]||[]).forEach(pt => { const v = isRankStrict ? Math.round(pt.value) : pt.value; if (isFinite(v)) { maxVal=Math.max(maxVal,v); minVal=Math.min(minVal,v); } }); });
304
- if (!isFinite(minVal) || !isFinite(maxVal)) return null;
305
- return { min: minVal, max: maxVal, isRankStrict };
306
- },
307
- // Allow host to set a shared Y domain override (persisted across resizes)
308
- setYDomainOverride: (metricKey, domain, isRankStrict, sharedRankTickMax) => {
309
- yDomainOverride = { metricKey, domain, isRankStrict: !!isRankStrict, rankTickMax: sharedRankTickMax };
310
- }
311
  };
312
  }
313
 
@@ -340,37 +364,19 @@
340
  const intersect = (arrs) => arrs.reduce((acc, cur) => acc.filter(x => cur.includes(x)));
341
  let metrics = lists.length ? intersect(lists) : [];
342
  if (!metrics.length) { metrics = lists[0] || []; }
343
- const def = metrics.find(m => /average_rank/i.test(m)) || metrics[0] || '';
 
344
 
345
  let ctrl = host.querySelector('.filters-quad__controls');
346
  if (!ctrl) { ctrl = document.createElement('div'); ctrl.className = 'filters-quad__controls'; host.insertBefore(ctrl, host.firstChild); }
347
  ctrl.innerHTML = '';
348
  const label = document.createElement('label'); label.textContent = 'Metric';
349
  const select = document.createElement('select');
350
- metrics.forEach(m => { const o=document.createElement('option'); o.value=m; o.textContent=m; select.appendChild(o); });
351
  if (def) select.value = def;
352
  label.appendChild(select); ctrl.appendChild(label);
353
 
354
- const applyAll = (v) => {
355
- // Compute shared Y domain across all instances for selected metric
356
- const isRank = /rank/i.test(v); const isAverage = /average/i.test(v); const isRankStrict = isRank && !isAverage;
357
- const extents = instances.map(i => (i && typeof i.getExtent === 'function') ? i.getExtent(v) : null).filter(Boolean);
358
- if (!extents.length) { instances.forEach(i => i && typeof i.setMetric === 'function' && i.setMetric(v)); return; }
359
- let globalMin = Math.min(...extents.map(e => e.min));
360
- let globalMax = Math.max(...extents.map(e => e.max));
361
- let domain, rankTickMax;
362
- if (isRankStrict) {
363
- rankTickMax = Math.max(1, Math.round(globalMax));
364
- domain = [rankTickMax, 1];
365
- } else {
366
- domain = [globalMin, globalMax];
367
- }
368
- // Apply override then render
369
- instances.forEach(i => {
370
- if (i && typeof i.setYDomainOverride === 'function') i.setYDomainOverride(v, domain, isRankStrict, rankTickMax);
371
- if (i && typeof i.setMetric === 'function') i.setMetric(v);
372
- });
373
- };
374
  if (def) applyAll(def);
375
  select.addEventListener('change', () => applyAll(select.value));
376
 
@@ -383,19 +389,18 @@
383
  if (r.ok && window.d3 && window.d3.csvParse) {
384
  const txt = await r.text();
385
  const rows = window.d3.csvParse(txt);
386
- let runList = Array.from(new Set(rows.map(row => String(row.run||'').trim()).filter(Boolean)));
387
- if (runList.includes('FineVision')) { runList = ['FineVision', ...runList.filter(r => r !== 'FineVision')]; }
388
  const primary = getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim() || '#E889AB';
389
  const pool = [primary, '#4EA5B7', '#E38A42', '#CEC0FA', ...((window.d3 && window.d3.schemeTableau10) ? window.d3.schemeTableau10 : ['#4e79a7','#f28e2b','#e15759','#76b7b2','#59a14f','#edc948','#b07aa1','#ff9da7','#9c755f','#bab0ab'])];
390
  const markerShapes = ['circle', 'square', 'triangle', 'diamond', 'inverted-triangle'];
391
  const shapeSVG = (shape, color) => {
392
- const size = 9; const s = size/2; const stroke = color;
393
- if (shape === 'circle') return `<svg width="18" height="14" viewBox="0 0 18 14" aria-hidden="true"><g transform="translate(9,7)"><circle r="${s}" fill="${color}" stroke="${stroke}" stroke-width="1" /></g></svg>`;
394
- if (shape === 'square') return `<svg width="18" height="14" viewBox="0 0 18 14" aria-hidden="true"><g transform="translate(9,7)"><rect x="${-s}" y="${-s}" width="${size}" height="${size}" fill="${color}" stroke="${stroke}" stroke-width="1" /></g></svg>`;
395
- if (shape === 'triangle') return `<svg width="18" height="14" viewBox="0 0 18 14" aria-hidden="true"><g transform="translate(9,7)"><path d="M0,${-s*1.2} L${s*1.1},${s*0.6} L${-s*1.1},${s*0.6} Z" fill="${color}" stroke="${stroke}" stroke-width="1" /></g></svg>`;
396
- if (shape === 'diamond') return `<svg width="18" height="14" viewBox="0 0 18 14" aria-hidden="true"><g transform="translate(9,7)"><path d="M0,${-s*1.2} L${s*1.1},0 L0,${s*1.2} L${-s*1.1},0 Z" fill="${color}" stroke="${stroke}" stroke-width="1" /></g></svg>`;
397
- if (shape === 'inverted-triangle') return `<svg width="18" height="14" viewBox="0 0 18 14" aria-hidden="true"><g transform="translate(9,7)"><path d="M0,${s*1.2} L${s*1.1},${-s*0.6} L${-s*1.1},${-s*0.6} Z" fill="${color}" stroke="${stroke}" stroke-width="1" /></g></svg>`;
398
- return `<svg width="18" height="14" viewBox="0 0 18 14" aria-hidden="true"><g transform="translate(9,7)"><circle r="${s}" fill="${color}" stroke="${stroke}" stroke-width="1" /></g></svg>`;
399
  };
400
  legendHost.innerHTML = runList.map((name, i)=> {
401
  const color = pool[i % pool.length];
 
15
  @media (max-width: 980px) { .filters-quad__grid { grid-template-columns: 1fr; } }
16
 
17
  .filters-quad__controls { display:flex; align-items:center; justify-content:center; gap:12px; margin: 6px 0 12px 0; flex-wrap:wrap; }
18
+ .filters-quad__controls label { font-size:14px; color: var(--text-color); font-weight:600; display:flex; align-items:center; gap:8px; }
19
  .filters-quad__controls select {
20
  font-size: 14px; padding: 8px 32px 8px 12px; border: 1px solid var(--border-color); border-radius: 10px;
21
  background-color: var(--surface-bg); color: var(--text-color);
 
28
  .filters-quad__controls select:hover { border-color: var(--primary-color); }
29
  .filters-quad__controls select:focus { border-color: var(--primary-color); box-shadow: 0 0 0 3px rgba(232,137,171,.25); outline: none; }
30
 
31
+ .filters-quad__legend { display:flex; align-items:center; justify-content:center; gap:12px; flex-wrap:wrap; font-size:12px; color: var(--text-color); margin: 2px 0 10px 0; }
32
  .filters-quad__legend .item { display:inline-flex; align-items:center; gap:6px; white-space:nowrap; }
33
  .filters-quad__legend .swatch { width:10px; height:10px; border-radius:50%; display:inline-block; }
34
 
 
73
  s.addEventListener('load', onReady, { once: true }); if (window.d3) onReady();
74
  };
75
 
76
+ // Mapping métrique -> libellé lisible (aligné avec les autres embeds)
77
+ const metricTitleMapping = {
78
+ 'docvqa_val_anls': 'DocVQA',
79
+ 'infovqa_val_anls': 'InfoVQA',
80
+ 'mme_total_score': 'MME Total',
81
+ 'mmmu_val_mmmu_acc': 'MMMU',
82
+ 'mmstar_average': 'MMStar',
83
+ 'ocrbench_ocrbench_accuracy': 'OCRBench',
84
+ 'scienceqa_exact_match': 'ScienceQA',
85
+ 'textvqa_val_exact_match': 'TextVQA',
86
+ 'average': 'Average',
87
+ 'average_rank': 'Average Rank',
88
+ 'ai2d_exact_match': 'AI2D',
89
+ 'chartqa_relaxed_overall': 'ChartQA',
90
+ 'seedbench_seed_all': 'SeedBench'
91
+ };
92
+ function getMetricDisplayName(metricKey){
93
+ return metricTitleMapping[metricKey] || metricKey;
94
+ }
95
+
96
  function initRunLine(cell){
97
  const d3 = window.d3;
98
  const csvPath = cell.getAttribute('data-csv');
 
127
  const xScale = d3.scaleLinear(); const yScale = d3.scaleLinear();
128
  const lineGen = d3.line().x(d => xScale(d.step)).y(d => yScale(d.value));
129
  let isRankStrictFlag = false; let rankTickMax = 1;
 
 
130
 
131
  // Colors and markers (match original embeds)
132
  const primary = getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim() || '#E889AB';
133
  const pool = [primary, '#4EA5B7', '#E38A42', '#CEC0FA', ...(d3.schemeTableau10||[])];
134
  const markerShapes = ['circle', 'square', 'triangle', 'diamond', 'inverted-triangle'];
135
+ const markerSize = 5;
136
  function drawMarker(selection, shape, size) {
137
  const s = size / 2;
138
  switch (shape) {
 
150
  return selection.append('circle').attr('r', s);
151
  }
152
  }
153
+ // Small SVG markup for marker shape (used in hover)
154
+ function shapeSvgMarkup(shape, color) {
155
+ const stroke = color;
156
+ switch (shape) {
157
+ case 'circle':
158
+ return `<svg width="14" height="12" viewBox="-6 -6 12 12" aria-hidden="true"><circle r="5" fill="${color}" stroke="${stroke}" stroke-width="1" /></svg>`;
159
+ case 'square':
160
+ return `<svg width="14" height="12" viewBox="-6 -6 12 12" aria-hidden="true"><rect x="-5" y="-5" width="10" height="10" fill="${color}" stroke="${stroke}" stroke-width="1" /></svg>`;
161
+ case 'triangle':
162
+ return `<svg width="14" height="12" viewBox="-6 -6 12 12" aria-hidden="true"><path d="M0,-6 L5,3 L-5,3 Z" fill="${color}" stroke="${stroke}" stroke-width="1" /></svg>`;
163
+ case 'diamond':
164
+ return `<svg width="14" height="12" viewBox="-6 -6 12 12" aria-hidden="true"><path d="M0,-6 L6,0 L0,6 L-6,0 Z" fill="${color}" stroke="${stroke}" stroke-width="1" /></svg>`;
165
+ case 'inverted-triangle':
166
+ return `<svg width="14" height="12" viewBox="-6 -6 12 12" aria-hidden="true"><path d="M0,6 L5,-3 L-5,-3 Z" fill="${color}" stroke="${stroke}" stroke-width="1" /></svg>`;
167
+ default:
168
+ return `<svg width="14" height="12" viewBox="-6 -6 12 12" aria-hidden="true"><circle r="5" fill="${color}" stroke="${stroke}" stroke-width="1" /></svg>`;
169
+ }
170
+ }
171
  // Ready signal for async load completion
172
  let readyResolve = null;
173
  const ready = new Promise((res)=> { readyResolve = res; });
 
201
  gAxes.selectAll('*').remove();
202
  let xAxis = d3.axisBottom(xScale).tickSizeOuter(0); xAxis = xAxis.ticks(8);
203
  const yAxis = d3.axisLeft(yScale).tickValues(yTicks).tickSizeOuter(0).tickFormat(isRankStrictFlag ? d3.format('d') : d3.format('.2f'));
204
+ gAxes.append('g').attr('transform', `translate(0,${innerHeight})`).call(xAxis).call(g=>{ g.selectAll('path, line').attr('stroke',axisColor); g.selectAll('text').attr('fill',tickColor).style('font-size','11px'); });
205
+ gAxes.append('g').call(yAxis).call(g=>{ g.selectAll('path, line').attr('stroke',axisColor); g.selectAll('text').attr('fill',tickColor).style('font-size','11px'); });
206
 
207
  // Legend box (top-right)
208
  // Per-cell legend hidden; global legend is used
 
221
  runs.forEach(r => { (map[r]||[]).forEach(pt => { const v = isRankStrict ? Math.round(pt.value) : pt.value; minStep=Math.min(minStep,pt.step); maxStep=Math.max(maxStep,pt.step); maxVal=Math.max(maxVal,v); minVal=Math.min(minVal,v); }); });
222
  if (!isFinite(minStep) || !isFinite(maxStep)) return;
223
  xScale.domain([minStep, maxStep]); if (isRank) { rankTickMax = Math.max(1, Math.round(maxVal)); yScale.domain([rankTickMax, 1]); } else { yScale.domain([minVal, maxVal]).nice(); }
224
+ isRankStrictFlag = isRankStrict;
 
 
 
 
 
 
 
225
 
226
  const { innerWidth, innerHeight } = updateScales();
227
 
 
266
  // Hover
267
  gHover.selectAll('*').remove();
268
  const overlay = gHover.append('rect').attr('fill','transparent').style('cursor','crosshair').attr('x',0).attr('y',0).attr('width', innerWidth).attr('height', innerHeight);
269
+ const hoverLine = gHover.append('line').attr('stroke','rgba(0,0,0,0.25)').attr('stroke-width',1).attr('y1',0).attr('y2',innerHeight).style('display','none');
 
270
  const stepSet = new Set(); series.forEach(s=>s.values.forEach(v=>stepSet.add(v.step))); const steps = Array.from(stepSet).sort((a,b)=>a-b);
271
+ function onMove(ev){
272
+ const [mx,my] = d3.pointer(ev, overlay.node());
273
+ const nearest = steps.reduce((best,s)=> Math.abs(s - xScale.invert(mx)) < Math.abs(best - xScale.invert(mx)) ? s : best, steps[0]);
274
+ const xpx = xScale(nearest);
275
+ hoverLine.attr('x1',xpx).attr('x2',xpx).style('display',null);
276
  let html = `<div><strong>${titleText}</strong></div><div><strong>step</strong> ${nearest}</div>`;
277
+ const items = [];
278
+ series.forEach(s => {
279
+ const m = new Map(s.values.map(v=>[v.step, v]));
280
+ const pt = m.get(nearest);
281
+ if (pt && pt.value != null) items.push({ s, pt });
282
+ });
283
+ const fmt = (vv)=> (isRankStrictFlag ? d3.format('d')(vv) : (+vv).toFixed(4));
284
+ items.sort((a,b)=> isRankStrictFlag ? (b.pt.value - a.pt.value) : (a.pt.value - b.pt.value));
285
+ items.forEach(({ s, pt }) => {
286
+ const err = (pt.stderr!=null && isFinite(pt.stderr) && pt.stderr>0) ? ` ± ${fmt(pt.stderr)}` : '';
287
+ html += `<div style=\"display:flex;align-items:center;gap:6px;\">${shapeSvgMarkup(s.marker, s.color)}<strong>${s.run}</strong> ${fmt(pt.value)}${err}</div>`;
288
+ });
289
+ tipInner.innerHTML = html;
290
+ const offsetX=12, offsetY=12;
291
+ tip.style.opacity='1';
292
+ tip.style.transform=`translate(${Math.round(mx+offsetX+margin.left)}px, ${Math.round(my+offsetY+margin.top)}px)`;
293
+ }
294
  function onLeave(){ tip.style.opacity='0'; tip.style.transform='translate(-9999px, -9999px)'; hoverLine.style('display','none'); }
295
  overlay.on('mousemove', onMove).on('mouseleave', onLeave);
296
  }
 
314
  metricList = Array.from(new Set(rows.map(r=>r.metric))).sort();
315
  runList = Array.from(new Set(rows.map(r=>r.run))).sort(); runOrder = runList;
316
  metricList.forEach(m => { const map={}; runList.forEach(r=>map[r]=[]); rows.filter(r=>r.metric===m).forEach(r=>{ if(!isNaN(r.step)&&!isNaN(r.value)) map[r.run].push({ step:r.step, value:r.value, stderr:r.stderr }); }); dataByMetric.set(m, map); });
317
+ // Par défaut, privilégier average_rank, sinon premier dispo
318
+ const preferred = metricList.find(m => /average_rank/i.test(m)) || metricList.find(m => m === 'ai2d_exact_match');
319
+ const def = preferred || metricList[0];
320
  renderMetric(def);
321
  const ro = window.ResizeObserver ? new ResizeObserver(()=>renderMetric(def)) : null; if (ro) ro.observe(cell);
322
  if (typeof readyResolve === 'function') readyResolve();
 
331
  return {
332
  ready,
333
  getMetrics: () => metricList.slice(),
334
+ setMetric: (m) => { if (m) renderMetric(m); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
335
  };
336
  }
337
 
 
364
  const intersect = (arrs) => arrs.reduce((acc, cur) => acc.filter(x => cur.includes(x)));
365
  let metrics = lists.length ? intersect(lists) : [];
366
  if (!metrics.length) { metrics = lists[0] || []; }
367
+ // Par défaut: average_rank si présent (pas AI2D)
368
+ const def = (metrics.find(m => /average_rank/i.test(m)) || metrics[0] || '');
369
 
370
  let ctrl = host.querySelector('.filters-quad__controls');
371
  if (!ctrl) { ctrl = document.createElement('div'); ctrl.className = 'filters-quad__controls'; host.insertBefore(ctrl, host.firstChild); }
372
  ctrl.innerHTML = '';
373
  const label = document.createElement('label'); label.textContent = 'Metric';
374
  const select = document.createElement('select');
375
+ metrics.forEach(m => { const o=document.createElement('option'); o.value=m; o.textContent=getMetricDisplayName(m); select.appendChild(o); });
376
  if (def) select.value = def;
377
  label.appendChild(select); ctrl.appendChild(label);
378
 
379
+ const applyAll = (v) => instances.forEach(i => i && typeof i.setMetric === 'function' && i.setMetric(v));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
380
  if (def) applyAll(def);
381
  select.addEventListener('change', () => applyAll(select.value));
382
 
 
389
  if (r.ok && window.d3 && window.d3.csvParse) {
390
  const txt = await r.text();
391
  const rows = window.d3.csvParse(txt);
392
+ const runList = Array.from(new Set(rows.map(row => String(row.run||'').trim()).filter(Boolean)));
 
393
  const primary = getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim() || '#E889AB';
394
  const pool = [primary, '#4EA5B7', '#E38A42', '#CEC0FA', ...((window.d3 && window.d3.schemeTableau10) ? window.d3.schemeTableau10 : ['#4e79a7','#f28e2b','#e15759','#76b7b2','#59a14f','#edc948','#b07aa1','#ff9da7','#9c755f','#bab0ab'])];
395
  const markerShapes = ['circle', 'square', 'triangle', 'diamond', 'inverted-triangle'];
396
  const shapeSVG = (shape, color) => {
397
+ const size = 12; const s = size/2; const stroke = color;
398
+ if (shape === 'circle') return `<svg width="12" height="12" viewBox="-6 -6 12 12" aria-hidden="true"><circle r="5" fill="${color}" stroke="${stroke}" stroke-width="1" /></svg>`;
399
+ if (shape === 'square') return `<svg width="12" height="12" viewBox="-6 -6 12 12" aria-hidden="true"><rect x="-5" y="-5" width="10" height="10" fill="${color}" stroke="${stroke}" stroke-width="1" /></svg>`;
400
+ if (shape === 'triangle') return `<svg width="12" height="12" viewBox="-6 -6 12 12" aria-hidden="true"><path d="M0,-6 L5,3 L-5,3 Z" fill="${color}" stroke="${stroke}" stroke-width="1" /></svg>`;
401
+ if (shape === 'diamond') return `<svg width="12" height="12" viewBox="-6 -6 12 12" aria-hidden="true"><path d="M0,-6 L6,0 L0,6 L-6,0 Z" fill="${color}" stroke="${stroke}" stroke-width="1" /></svg>`;
402
+ if (shape === 'inverted-triangle') return `<svg width="12" height="12" viewBox="-6 -6 12 12" aria-hidden="true"><path d="M0,6 L5,-3 L-5,-3 Z" fill="${color}" stroke="${stroke}" stroke-width="1" /></svg>`;
403
+ return `<svg width="12" height="12" viewBox="-6 -6 12 12" aria-hidden="true"><circle r="5" fill="${color}" stroke="${stroke}" stroke-width="1" /></svg>`;
404
  };
405
  legendHost.innerHTML = runList.map((name, i)=> {
406
  const color = pool[i % pool.length];
app/src/content/embeds/formatting-filters.html CHANGED
@@ -171,7 +171,7 @@
171
 
172
  // Academic marker shapes
173
  const markerShapes = ['circle', 'square', 'triangle', 'diamond', 'inverted-triangle'];
174
- const markerSize = 8;
175
 
176
  // Groups
177
  const gRoot = svg.append('g');
 
171
 
172
  // Academic marker shapes
173
  const markerShapes = ['circle', 'square', 'triangle', 'diamond', 'inverted-triangle'];
174
+ const markerSize = 5;
175
 
176
  // Groups
177
  const gRoot = svg.append('g');
app/src/content/embeds/image-correspondence-filters.html CHANGED
@@ -171,7 +171,7 @@
171
 
172
  // Academic marker shapes
173
  const markerShapes = ['circle', 'square', 'triangle', 'diamond', 'inverted-triangle'];
174
- const markerSize = 8;
175
 
176
  // Groups
177
  const gRoot = svg.append('g');
 
171
 
172
  // Academic marker shapes
173
  const markerShapes = ['circle', 'square', 'triangle', 'diamond', 'inverted-triangle'];
174
+ const markerSize = 5;
175
 
176
  // Groups
177
  const gRoot = svg.append('g');
app/src/content/embeds/internal-deduplication.html CHANGED
@@ -166,20 +166,26 @@
166
  .attr('width', '100%')
167
  .style('display', 'block');
168
 
169
- // Add marker definitions for different shapes
170
  const defs = svg.append('defs');
 
 
 
 
171
 
172
  // Academic marker shapes
173
  const markerShapes = ['circle', 'square', 'triangle', 'diamond', 'inverted-triangle'];
174
- const markerSize = 8;
175
 
176
  // Groups
177
  const gRoot = svg.append('g');
178
  const gGrid = gRoot.append('g').attr('class', 'grid');
179
  const gAxes = gRoot.append('g').attr('class', 'axes');
180
- const gAreas = gRoot.append('g').attr('class', 'areas');
181
- const gLines = gRoot.append('g').attr('class', 'lines');
182
- const gPoints = gRoot.append('g').attr('class', 'points');
 
 
183
  const gHover = gRoot.append('g').attr('class', 'hover');
184
  const gLegend = gRoot.append('foreignObject').attr('class', 'legend');
185
 
@@ -242,8 +248,8 @@
242
  // Scales and layout
243
  let width = 800, height = 360;
244
  let margin = { top: 16, right: 28, bottom: 56, left: 64 };
245
- let xScale = d3.scaleLinear();
246
- let yScale = d3.scaleLinear();
247
 
248
  // Line generators - simple linear connections
249
  const lineGen = d3.line()
@@ -310,6 +316,13 @@
310
  xScale.range([0, innerWidth]);
311
  yScale.range([innerHeight, 0]);
312
 
 
 
 
 
 
 
 
313
  // Compute Y ticks
314
  let yTicks = [];
315
  if (isRankStrictFlag) {
@@ -428,8 +441,8 @@
428
  if (!isFinite(minStep) || !isFinite(maxStep)) { return; }
429
  xScale.domain([minStep, maxStep]);
430
  if (isRank) {
431
- rankTickMax = Math.max(1, Math.round(maxVal));
432
- yScale.domain([rankTickMax, 1]);
433
  } else {
434
  yScale.domain([minVal, maxVal]).nice();
435
  }
 
166
  .attr('width', '100%')
167
  .style('display', 'block');
168
 
169
+ // Add marker definitions and clipPath for chart area
170
  const defs = svg.append('defs');
171
+ const clipId = `clip-${Math.random().toString(36).slice(2)}`;
172
+ const clipPath = defs.append('clipPath').attr('id', clipId)
173
+ .attr('clipPathUnits', 'userSpaceOnUse');
174
+ const clipRect = clipPath.append('rect').attr('x', 0).attr('y', 0).attr('width', 0).attr('height', 0);
175
 
176
  // Academic marker shapes
177
  const markerShapes = ['circle', 'square', 'triangle', 'diamond', 'inverted-triangle'];
178
+ const markerSize = 5;
179
 
180
  // Groups
181
  const gRoot = svg.append('g');
182
  const gGrid = gRoot.append('g').attr('class', 'grid');
183
  const gAxes = gRoot.append('g').attr('class', 'axes');
184
+ // Dedicated plotting layer that will be clipped
185
+ const gPlot = gRoot.append('g').attr('class', 'plot');
186
+ const gAreas = gPlot.append('g').attr('class', 'areas');
187
+ const gLines = gPlot.append('g').attr('class', 'lines');
188
+ const gPoints = gPlot.append('g').attr('class', 'points');
189
  const gHover = gRoot.append('g').attr('class', 'hover');
190
  const gLegend = gRoot.append('foreignObject').attr('class', 'legend');
191
 
 
248
  // Scales and layout
249
  let width = 800, height = 360;
250
  let margin = { top: 16, right: 28, bottom: 56, left: 64 };
251
+ let xScale = d3.scaleLinear().clamp(true);
252
+ let yScale = d3.scaleLinear().clamp(true);
253
 
254
  // Line generators - simple linear connections
255
  const lineGen = d3.line()
 
316
  xScale.range([0, innerWidth]);
317
  yScale.range([innerHeight, 0]);
318
 
319
+ // Update clip rect to match inner plotting area (coords local to gRoot)
320
+ clipRect
321
+ .attr('x', 0)
322
+ .attr('y', 0)
323
+ .attr('width', innerWidth)
324
+ .attr('height', innerHeight);
325
+
326
  // Compute Y ticks
327
  let yTicks = [];
328
  if (isRankStrictFlag) {
 
441
  if (!isFinite(minStep) || !isFinite(maxStep)) { return; }
442
  xScale.domain([minStep, maxStep]);
443
  if (isRank) {
444
+ rankTickMax = isRankStrict ? Math.max(1, Math.round(maxVal)) : Math.max(1, maxVal);
445
+ yScale.domain([rankTickMax, 1]).nice();
446
  } else {
447
  yScale.domain([minVal, maxVal]).nice();
448
  }
app/src/content/embeds/relevance-filters.html CHANGED
@@ -171,7 +171,7 @@
171
 
172
  // Academic marker shapes
173
  const markerShapes = ['circle', 'square', 'triangle', 'diamond', 'inverted-triangle'];
174
- const markerSize = 8;
175
 
176
  // Groups
177
  const gRoot = svg.append('g');
 
171
 
172
  // Academic marker shapes
173
  const markerShapes = ['circle', 'square', 'triangle', 'diamond', 'inverted-triangle'];
174
+ const markerSize = 5;
175
 
176
  // Groups
177
  const gRoot = svg.append('g');
app/src/content/embeds/remove-ch.html CHANGED
@@ -171,7 +171,7 @@
171
 
172
  // Academic marker shapes
173
  const markerShapes = ['circle', 'square', 'triangle', 'diamond', 'inverted-triangle'];
174
- const markerSize = 9;
175
 
176
  // Groups
177
  const gRoot = svg.append('g');
 
171
 
172
  // Academic marker shapes
173
  const markerShapes = ['circle', 'square', 'triangle', 'diamond', 'inverted-triangle'];
174
+ const markerSize = 5;
175
 
176
  // Groups
177
  const gRoot = svg.append('g');
app/src/content/embeds/s25-ratings.html CHANGED
@@ -171,7 +171,7 @@
171
 
172
  // Academic marker shapes
173
  const markerShapes = ['circle', 'square', 'triangle', 'diamond', 'inverted-triangle'];
174
- const markerSize = 8;
175
 
176
  // Groups
177
  const gRoot = svg.append('g');
 
171
 
172
  // Academic marker shapes
173
  const markerShapes = ['circle', 'square', 'triangle', 'diamond', 'inverted-triangle'];
174
+ const markerSize = 5;
175
 
176
  // Groups
177
  const gRoot = svg.append('g');
app/src/content/embeds/ss-vs-s1.html CHANGED
@@ -171,7 +171,7 @@
171
 
172
  // Academic marker shapes
173
  const markerShapes = ['circle', 'square', 'triangle', 'diamond', 'inverted-triangle'];
174
- const markerSize = 8;
175
 
176
  // Groups
177
  const gRoot = svg.append('g');
 
171
 
172
  // Academic marker shapes
173
  const markerShapes = ['circle', 'square', 'triangle', 'diamond', 'inverted-triangle'];
174
+ const markerSize = 5;
175
 
176
  // Groups
177
  const gRoot = svg.append('g');
app/src/content/embeds/visual-dependency-filters.html CHANGED
@@ -166,20 +166,26 @@
166
  .attr('width', '100%')
167
  .style('display', 'block');
168
 
169
- // Add marker definitions for different shapes
170
  const defs = svg.append('defs');
 
 
 
 
171
 
172
  // Academic marker shapes
173
  const markerShapes = ['circle', 'square', 'triangle', 'diamond', 'inverted-triangle'];
174
- const markerSize = 8;
175
 
176
  // Groups
177
  const gRoot = svg.append('g');
178
  const gGrid = gRoot.append('g').attr('class', 'grid');
179
  const gAxes = gRoot.append('g').attr('class', 'axes');
180
- const gAreas = gRoot.append('g').attr('class', 'areas');
181
- const gLines = gRoot.append('g').attr('class', 'lines');
182
- const gPoints = gRoot.append('g').attr('class', 'points');
 
 
183
  const gHover = gRoot.append('g').attr('class', 'hover');
184
  const gLegend = gRoot.append('foreignObject').attr('class', 'legend');
185
 
@@ -242,8 +248,8 @@
242
  // Scales and layout
243
  let width = 800, height = 360;
244
  let margin = { top: 16, right: 28, bottom: 56, left: 64 };
245
- let xScale = d3.scaleLinear();
246
- let yScale = d3.scaleLinear();
247
 
248
  // Line generators - simple linear connections
249
  const lineGen = d3.line()
@@ -310,6 +316,13 @@
310
  xScale.range([0, innerWidth]);
311
  yScale.range([innerHeight, 0]);
312
 
 
 
 
 
 
 
 
313
  // Compute Y ticks
314
  let yTicks = [];
315
  if (isRankStrictFlag) {
@@ -428,8 +441,8 @@
428
  if (!isFinite(minStep) || !isFinite(maxStep)) { return; }
429
  xScale.domain([minStep, maxStep]);
430
  if (isRank) {
431
- rankTickMax = Math.max(1, Math.round(maxVal));
432
- yScale.domain([rankTickMax, 1]);
433
  } else {
434
  yScale.domain([minVal, maxVal]).nice();
435
  }
 
166
  .attr('width', '100%')
167
  .style('display', 'block');
168
 
169
+ // Add marker definitions and clipPath for chart area
170
  const defs = svg.append('defs');
171
+ const clipId = `clip-${Math.random().toString(36).slice(2)}`;
172
+ const clipPath = defs.append('clipPath').attr('id', clipId)
173
+ .attr('clipPathUnits', 'userSpaceOnUse');
174
+ const clipRect = clipPath.append('rect').attr('x', 0).attr('y', 0).attr('width', 0).attr('height', 0);
175
 
176
  // Academic marker shapes
177
  const markerShapes = ['circle', 'square', 'triangle', 'diamond', 'inverted-triangle'];
178
+ const markerSize = 5;
179
 
180
  // Groups
181
  const gRoot = svg.append('g');
182
  const gGrid = gRoot.append('g').attr('class', 'grid');
183
  const gAxes = gRoot.append('g').attr('class', 'axes');
184
+ // Dedicated plotting layer that will be clipped
185
+ const gPlot = gRoot.append('g').attr('class', 'plot');
186
+ const gAreas = gPlot.append('g').attr('class', 'areas');
187
+ const gLines = gPlot.append('g').attr('class', 'lines');
188
+ const gPoints = gPlot.append('g').attr('class', 'points');
189
  const gHover = gRoot.append('g').attr('class', 'hover');
190
  const gLegend = gRoot.append('foreignObject').attr('class', 'legend');
191
 
 
248
  // Scales and layout
249
  let width = 800, height = 360;
250
  let margin = { top: 16, right: 28, bottom: 56, left: 64 };
251
+ let xScale = d3.scaleLinear().clamp(true);
252
+ let yScale = d3.scaleLinear().clamp(true);
253
 
254
  // Line generators - simple linear connections
255
  const lineGen = d3.line()
 
316
  xScale.range([0, innerWidth]);
317
  yScale.range([innerHeight, 0]);
318
 
319
+ // Update clip rect to match inner plotting area (coords local to gRoot)
320
+ clipRect
321
+ .attr('x', 0)
322
+ .attr('y', 0)
323
+ .attr('width', innerWidth)
324
+ .attr('height', innerHeight);
325
+
326
  // Compute Y ticks
327
  let yTicks = [];
328
  if (isRankStrictFlag) {
 
441
  if (!isFinite(minStep) || !isFinite(maxStep)) { return; }
442
  xScale.domain([minStep, maxStep]);
443
  if (isRank) {
444
+ rankTickMax = isRankStrict ? Math.max(1, Math.round(maxVal)) : Math.max(1, maxVal);
445
+ yScale.domain([rankTickMax, 1]).nice();
446
  } else {
447
  yScale.domain([minVal, maxVal]).nice();
448
  }
app/src/pages/index.astro CHANGED
@@ -186,7 +186,7 @@ const licence = (articleFM as any)?.licence ?? (articleFM as any)?.license ?? (a
186
  </script>
187
 
188
  <script>
189
- // Delegate copy clicks for code blocks injected by rehypeCodeCopyAndLabel
190
  document.addEventListener('click', async (e) => {
191
  const target = e.target instanceof Element ? e.target : null;
192
  const btn = target ? target.closest('.code-copy') : null;
 
186
  </script>
187
 
188
  <script>
189
+ // Delegate copy clicks for code blocks injected by rehypeCodeCopy
190
  document.addEventListener('click', async (e) => {
191
  const target = e.target instanceof Element ? e.target : null;
192
  const btn = target ? target.closest('.code-copy') : null;
app/src/styles/_layout.css CHANGED
@@ -69,9 +69,9 @@
69
  }
70
 
71
  .wide {
72
- /* Target up to ~1400px while staying within viewport minus page gutters */
73
- width: min(1400px, 100vw - 32px);
74
- margin-left: 50%;
75
  transform: translateX(-50%);
76
  }
77
 
@@ -128,4 +128,26 @@
128
  }
129
 
130
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
 
 
69
  }
70
 
71
  .wide {
72
+ /* Target up to ~1100px while staying within viewport minus page gutters */
73
+ width: min(1100px, 100vw - 32px);
74
+ margin-left: calc(50% + var(--content-padding-x) * 2);
75
  transform: translateX(-50%);
76
  }
77
 
 
128
  }
129
 
130
 
131
+ /* ------------------------------------------------------------------------- */
132
+ /* D3 neural embed responsiveness */
133
+ /* Stack canvas (left) over network (right) on small screens */
134
+ /* ------------------------------------------------------------------------- */
135
+ @media (--bp-md) {
136
+ .d3-neural .panel {
137
+ flex-direction: column;
138
+ }
139
+
140
+ .d3-neural .panel .left {
141
+ flex: 0 0 auto;
142
+ width: 100%;
143
+ }
144
+
145
+ .d3-neural .panel .right {
146
+ flex: 0 0 auto;
147
+ width: 100%;
148
+ min-width: 0;
149
+ }
150
+ }
151
+
152
+
153
 
app/src/styles/_print.css CHANGED
@@ -40,8 +40,8 @@
40
  /* Avoid page breaks inside complex visual blocks */
41
  .hero,
42
  .hero-banner,
43
- .d3-galaxy,
44
- .d3-galaxy svg,
45
  .html-embed__card,
46
  .html-embed__card,
47
  .js-plotly-plot,
 
40
  /* Avoid page breaks inside complex visual blocks */
41
  .hero,
42
  .hero-banner,
43
+ .d3-banner,
44
+ .d3-banner svg,
45
  .html-embed__card,
46
  .html-embed__card,
47
  .js-plotly-plot,
app/src/styles/components/_code.css CHANGED
@@ -12,6 +12,12 @@ code {
12
  line-height: 1.5;
13
  }
14
 
 
 
 
 
 
 
15
  /* ============================================================================ */
16
  /* Shiki code blocks */
17
  /* ============================================================================ */
@@ -142,6 +148,9 @@ section.content-grid pre code {
142
  /* ============================================================================ */
143
  /* Inside Accordions: remove padding and border on code containers */
144
  .accordion .astro-code { padding: 0; border: none; }
 
 
 
145
 
146
  /* ============================================================================ */
147
  /* Language/extension vignette (bottom-right, discreet) */
@@ -163,6 +172,24 @@ section.content-grid pre code {
163
  padding: 4px 6px;
164
  pointer-events: none;
165
  } */
166
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
167
  /* In Accordions, keep same bottom-right placement */
168
  .accordion .astro-code::after { right: 0; bottom: 0; }
 
12
  line-height: 1.5;
13
  }
14
 
15
+ p code, .note code {
16
+ white-space: nowrap;
17
+ padding: calc(var(--spacing-1)/3) calc(var(--spacing-1)/2);
18
+ }
19
+
20
+
21
  /* ============================================================================ */
22
  /* Shiki code blocks */
23
  /* ============================================================================ */
 
148
  /* ============================================================================ */
149
  /* Inside Accordions: remove padding and border on code containers */
150
  .accordion .astro-code { padding: 0; border: none; }
151
+ .accordion .astro-code { margin-bottom: 0 !important; }
152
+ .accordion pre { margin-bottom: 0 !important; }
153
+ .accordion .code-card pre { margin: 0 !important; }
154
 
155
  /* ============================================================================ */
156
  /* Language/extension vignette (bottom-right, discreet) */
 
172
  padding: 4px 6px;
173
  pointer-events: none;
174
  } */
175
+
176
+ /* Fallback if Shiki uses data-lang instead of data-language */
177
+ /* .astro-code[data-lang]::after { content: attr(data-lang); }
178
+ .astro-code[data-language="typescript"]::after { content: "ts"; }
179
+ .astro-code[data-language="tsx"]::after { content: "tsx"; }
180
+ .astro-code[data-language="javascript"]::after,
181
+ .astro-code[data-language="node"]::after,
182
+ .astro-code[data-language="jsx"]::after { content: "js"; }
183
+ .astro-code[data-language="python"]::after { content: "py"; }
184
+ .astro-code[data-language="bash"]::after,
185
+ .astro-code[data-language="shell"]::after,
186
+ .astro-code[data-language="sh"]::after { content: "sh"; }
187
+ .astro-code[data-language="markdown"]::after { content: "md"; }
188
+ .astro-code[data-language="yaml"]::after,
189
+ .astro-code[data-language="yml"]::after { content: "yml"; }
190
+ .astro-code[data-language="html"]::after { content: "html"; }
191
+ .astro-code[data-language="css"]::after { content: "css"; }
192
+ .astro-code[data-language="json"]::after { content: "json"; } */
193
+
194
  /* In Accordions, keep same bottom-right placement */
195
  .accordion .astro-code::after { right: 0; bottom: 0; }
app/src/styles/components/_table.css CHANGED
@@ -77,7 +77,10 @@
77
  border: none;
78
  border-radius: 0;
79
  margin: 0;
 
80
  }
 
 
81
  .accordion .accordion__content .table-scroll > table thead th:first-child,
82
  .accordion .accordion__content .table-scroll > table thead th:last-child,
83
  .accordion .accordion__content .table-scroll > table tbody tr:last-child td:first-child,
 
77
  border: none;
78
  border-radius: 0;
79
  margin: 0;
80
+ margin-bottom: 0 !important;
81
  }
82
+ /* Ensure no bottom margin even if table isn't wrapped (fallback) */
83
+ .accordion .accordion__content table { margin: 0 !important; }
84
  .accordion .accordion__content .table-scroll > table thead th:first-child,
85
  .accordion .accordion__content .table-scroll > table thead th:last-child,
86
  .accordion .accordion__content .table-scroll > table tbody tr:last-child td:first-child,
app/src/styles/components/_tag.css CHANGED
@@ -4,10 +4,10 @@
4
  display: inline-flex;
5
  align-items: center;
6
  gap: 6px;
7
- padding: 10px 16px;
8
  font-size: 12px;
9
  line-height: 1;
10
- border-radius: 100px;
11
  background: var(--surface-bg);
12
  border: 1px solid var(--border-color);
13
  color: var(--text-color);
 
4
  display: inline-flex;
5
  align-items: center;
6
  gap: 6px;
7
+ padding: 8px 12px;
8
  font-size: 12px;
9
  line-height: 1;
10
+ border-radius: var(--button-radius);
11
  background: var(--surface-bg);
12
  border: 1px solid var(--border-color);
13
  color: var(--text-color);