joelniklaus HF Staff commited on
Commit
b5327b1
·
1 Parent(s): 145b8dc

remove old unused files

Browse files
Files changed (30) hide show
  1. app/src/content/assets/data/against_baselines copy.csv +0 -3
  2. app/src/content/assets/data/against_baselines.csv +0 -3
  3. app/src/content/assets/data/against_baselines_deduplicated.csv +0 -3
  4. app/src/content/assets/data/all_ratings_luis.csv +0 -3
  5. app/src/content/assets/data/banner_visualisation_data_enriched.csv +0 -3
  6. app/src/content/assets/data/doc-masking_evals.csv +0 -3
  7. app/src/content/assets/data/doc-masking_loss.csv +0 -3
  8. app/src/content/assets/data/internal_deduplication.csv +0 -3
  9. app/src/content/assets/data/mnist-variant-model.json +0 -3
  10. app/src/content/assets/data/no-wd_evals.csv +0 -3
  11. app/src/content/assets/data/no_wd_comparison.csv +0 -3
  12. app/src/content/assets/data/nope_evals.csv +0 -3
  13. app/src/content/assets/data/nope_loss.csv +0 -3
  14. app/src/content/assets/data/remove_ch.csv +0 -3
  15. app/src/content/assets/data/root-seq-write-heatmaps.json +0 -3
  16. app/src/content/assets/data/s25_ratings.csv +0 -3
  17. app/src/content/assets/data/ss_vs_s1.csv +0 -3
  18. app/src/content/assets/data/tied-embeddings_evals.csv +0 -3
  19. app/src/content/assets/data/zloss_comparison.csv +0 -3
  20. app/src/content/assets/data/zloss_evals.csv +0 -3
  21. app/src/content/embeds/banner-neural-network-animejs.html +0 -464
  22. app/src/content/embeds/banner-threejs-galaxy.html +0 -504
  23. app/src/content/embeds/banner-umap-lucioles.html +0 -489
  24. app/src/content/embeds/d3-bar.html +0 -459
  25. app/src/content/embeds/d3-confusion-matrix.html +0 -516
  26. app/src/content/embeds/d3-line-quad.html +0 -783
  27. app/src/content/embeds/d3-matrix.html +0 -524
  28. app/src/content/embeds/d3-scatter.html +0 -300
  29. app/src/content/embeds/rope-demo.html +0 -532
  30. app/src/content/embeds/smol-playbook/model-architecture-decision-flowchart.html +0 -490
app/src/content/assets/data/against_baselines copy.csv DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:a5e6173a1541b9798278da1729f1e357c0711d2e270f68aa4af8eae962f146dd
3
- size 53573
 
 
 
 
app/src/content/assets/data/against_baselines.csv DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:a5e6173a1541b9798278da1729f1e357c0711d2e270f68aa4af8eae962f146dd
3
- size 53573
 
 
 
 
app/src/content/assets/data/against_baselines_deduplicated.csv DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:56d18f581eff719023eb87c695e0e11770738d7872c8b9dac9bc23d9b0ef560b
3
- size 32738
 
 
 
 
app/src/content/assets/data/all_ratings_luis.csv DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:104433529e7d9c8a3bd297be1138e9e87677a666953d1362c517ec389c6c9172
3
- size 64966
 
 
 
 
app/src/content/assets/data/banner_visualisation_data_enriched.csv DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:98eba5e5db19f482da8a3b26498c2fa633afa458f5b75e23d2dca24e24cc7596
3
- size 844651
 
 
 
 
app/src/content/assets/data/doc-masking_evals.csv DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:6052fa3042d49a5c2d5b370d3688380220bc612f22f73cff03f1e9354e09a330
3
- size 26360
 
 
 
 
app/src/content/assets/data/doc-masking_loss.csv DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:cacd1a2f14b1c5b5c4342db37759149c5b60b83593bca9e38b7ca451e81cc086
3
- size 52272
 
 
 
 
app/src/content/assets/data/internal_deduplication.csv DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:d6b6bf0d84fe1bc67436c70f9a8d5919627e9c2bc9c3f931f4af80c01be22649
3
- size 47060
 
 
 
 
app/src/content/assets/data/mnist-variant-model.json DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:7dca86e85be46c1fca6a4e2503786e88e3f8d4609fb7284c8a1479620a5827da
3
- size 4315
 
 
 
 
app/src/content/assets/data/no-wd_evals.csv DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:b003b8eb447244e804953b26cf991460895928d2b40d64b3db01da8c3971961a
3
- size 42062
 
 
 
 
app/src/content/assets/data/no_wd_comparison.csv DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:7d10e959763dda8b6c9e5a987b2d97af004bd8a51c8680f6a9a18cf84d85da6b
3
- size 83830
 
 
 
 
app/src/content/assets/data/nope_evals.csv DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:aaceaec65082326c22a6a92757b90a37a0cfaa68bc4fa3f96d0e6b6c3d7c1cbb
3
- size 34900
 
 
 
 
app/src/content/assets/data/nope_loss.csv DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:ef7b55386054b9db1c1e2fbafc2f5b100af2ef8812523a546c40d9db08410a86
3
- size 69964
 
 
 
 
app/src/content/assets/data/remove_ch.csv DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:869fc4724af7e9c868b6024f472f9ae0f6468b74ef61db101438f80610828abb
3
- size 28837
 
 
 
 
app/src/content/assets/data/root-seq-write-heatmaps.json DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:3cd5eec18185386c9641343aa2da610a66fbd38611e4927bb016d369a8fe3972
3
- size 6651
 
 
 
 
app/src/content/assets/data/s25_ratings.csv DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:ca22654a0302da0ca335420b0a89cd770cea560b11f2a9f9f25927877d7ed231
3
- size 61626
 
 
 
 
app/src/content/assets/data/ss_vs_s1.csv DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:3f076631fcad76129ed8cab03c72a61965b465e1f3e7fa8dc68b7c7a9275616b
3
- size 28041
 
 
 
 
app/src/content/assets/data/tied-embeddings_evals.csv DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:c0b6354ce2d0dd8fbe275d6b5701025ec2435a58b30fce92f8600eb9e45e9f0b
3
- size 49810
 
 
 
 
app/src/content/assets/data/zloss_comparison.csv DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:c0ef8312bf43933601cb226df5b636261410b59f2599baca92be8ef0a612dd6a
3
- size 49739
 
 
 
 
app/src/content/assets/data/zloss_evals.csv DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:78dac31df718d071f04e55fb5e2845894cf5b68d989d14a1a568b9aec5ded5c3
3
- size 22541
 
 
 
 
app/src/content/embeds/banner-neural-network-animejs.html DELETED
@@ -1,464 +0,0 @@
1
- <div class="neural-flow"
2
- style="width:100%;margin:10px 0;aspect-ratio:3/1;min-height:260px;position:relative;overflow:hidden;"></div>
3
- <script>
4
- (() => {
5
- const ensureAnime = (cb) => {
6
- if (window.anime && typeof window.anime === 'function') return cb();
7
- let s = document.getElementById('anime-cdn-script');
8
- if (!s) {
9
- s = document.createElement('script');
10
- s.id = 'anime-cdn-script';
11
- s.src = 'https://cdn.jsdelivr.net/npm/animejs@3.2.1/lib/anime.min.js';
12
- document.head.appendChild(s);
13
- }
14
- const onReady = () => { if (window.anime && typeof window.anime === 'function') cb(); };
15
- s.addEventListener('load', onReady, { once: true });
16
- if (window.anime) onReady();
17
- };
18
-
19
- const bootstrap = () => {
20
- const mount = document.currentScript ? document.currentScript.previousElementSibling : null;
21
- const container = (mount && mount.querySelector && mount.querySelector('.neural-flow')) || document.querySelector('.neural-flow');
22
- if (!container) return;
23
- if (container.dataset) {
24
- if (container.dataset.mounted === 'true') return;
25
- container.dataset.mounted = 'true';
26
- }
27
-
28
- // Create canvas
29
- const canvas = document.createElement('canvas');
30
- canvas.style.display = 'block';
31
- canvas.style.width = '100%';
32
- canvas.style.height = '100%';
33
- container.appendChild(canvas);
34
- const ctx = canvas.getContext('2d');
35
-
36
- // Theme colors
37
- const getColors = () => {
38
- const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
39
- return {
40
- node: isDark ? 'rgba(206, 192, 250, 0.85)' : 'rgba(138, 100, 220, 0.8)',
41
- nodeActive: isDark ? 'rgba(232, 137, 171, 1)' : 'rgba(220, 80, 130, 1)',
42
- nodeGlow: isDark ? 'rgba(206, 192, 250, 0.4)' : 'rgba(138, 100, 220, 0.3)',
43
- connection: isDark ? 'rgba(78, 165, 183, 0.08)' : 'rgba(78, 165, 183, 0.15)',
44
- connectionActive: isDark ? 'rgba(232, 137, 171, 0.6)' : 'rgba(220, 80, 130, 0.5)',
45
- accent: isDark ? 'rgba(78, 165, 183, 0.9)' : 'rgba(50, 130, 160, 0.85)',
46
- particle: isDark ? 'rgba(232, 137, 171, 1)' : 'rgba(220, 80, 130, 1)',
47
- };
48
- };
49
-
50
- let colors = getColors();
51
-
52
- // Watch for theme changes
53
- const observer = new MutationObserver(() => {
54
- colors = getColors();
55
- });
56
- observer.observe(document.documentElement, {
57
- attributes: true,
58
- attributeFilter: ['data-theme']
59
- });
60
-
61
- // Neural network structure
62
- const layers = [
63
- { nodes: 6, name: 'input' },
64
- { nodes: 10, name: 'hidden1' },
65
- { nodes: 8, name: 'hidden2' },
66
- { nodes: 4, name: 'output' }
67
- ];
68
-
69
- let nodes = [];
70
- let connections = [];
71
- let particles = [];
72
- let width, height;
73
-
74
- const resize = () => {
75
- width = container.clientWidth || 800;
76
- height = Math.max(260, Math.round(width / 3));
77
- canvas.width = width;
78
- canvas.height = height;
79
- initNetwork();
80
- };
81
-
82
- const initNetwork = () => {
83
- nodes = [];
84
- connections = [];
85
- particles = [];
86
-
87
- const layerSpacing = width / (layers.length + 1);
88
- const margin = height * 0.15;
89
-
90
- // Create ALL nodes first
91
- let nodeIndex = 0;
92
- const layerStartIndices = [];
93
-
94
- layers.forEach((layer, layerIdx) => {
95
- layerStartIndices.push(nodeIndex);
96
- const x = layerSpacing * (layerIdx + 1);
97
- const availableHeight = height - 2 * margin;
98
- const nodeSpacing = availableHeight / (layer.nodes + 1);
99
-
100
- for (let i = 0; i < layer.nodes; i++) {
101
- const y = margin + nodeSpacing * (i + 1);
102
- const node = {
103
- x,
104
- y,
105
- layer: layerIdx,
106
- index: i,
107
- radius: 0,
108
- targetRadius: 3.5 + Math.random() * 1.5,
109
- pulse: Math.random() * Math.PI * 2,
110
- activation: 0,
111
- baseActivity: Math.random() * 0.1
112
- };
113
- nodes.push(node);
114
- nodeIndex++;
115
- }
116
- });
117
-
118
- // Create FULLY CONNECTED network
119
- layers.forEach((layer, layerIdx) => {
120
- if (layerIdx < layers.length - 1) {
121
- const currentLayerStart = layerStartIndices[layerIdx];
122
- const nextLayerStart = layerStartIndices[layerIdx + 1];
123
- const nextLayerNodes = layers[layerIdx + 1].nodes;
124
-
125
- for (let i = 0; i < layer.nodes; i++) {
126
- for (let j = 0; j < nextLayerNodes; j++) {
127
- connections.push({
128
- from: currentLayerStart + i,
129
- to: nextLayerStart + j,
130
- weight: Math.random(),
131
- opacity: 0,
132
- activation: 0
133
- });
134
- }
135
- }
136
- }
137
- });
138
-
139
- // Initial animation - nodes appear (plus rapide)
140
- nodes.forEach((node, i) => {
141
- anime({
142
- targets: node,
143
- radius: node.targetRadius,
144
- duration: 800,
145
- delay: i * 8,
146
- easing: 'easeOutElastic(1, .6)'
147
- });
148
- });
149
-
150
- // Connections fade in (plus rapide)
151
- connections.forEach((conn, i) => {
152
- anime({
153
- targets: conn,
154
- opacity: 1,
155
- duration: 400,
156
- delay: 300 + i * 1,
157
- easing: 'easeOutQuad'
158
- });
159
- });
160
-
161
- // Start forward propagation cycles (plus rapide)
162
- setTimeout(() => {
163
- startForwardPass();
164
- setInterval(startForwardPass, 2500 + Math.random() * 1000);
165
- }, 1000);
166
- };
167
-
168
- // Différents patterns d'activation
169
- const activationPatterns = [
170
- // Pattern 1: Tous les inputs
171
- (inputNodes) => inputNodes,
172
-
173
- // Pattern 2: Un seul input (signal focal)
174
- (inputNodes) => [inputNodes[Math.floor(Math.random() * inputNodes.length)]],
175
-
176
- // Pattern 3: Moitié haute
177
- (inputNodes) => inputNodes.slice(0, Math.ceil(inputNodes.length / 2)),
178
-
179
- // Pattern 4: Moitié basse
180
- (inputNodes) => inputNodes.slice(Math.floor(inputNodes.length / 2)),
181
-
182
- // Pattern 5: Pattern alterné
183
- (inputNodes) => inputNodes.filter((_, i) => i % 2 === 0),
184
-
185
- // Pattern 6: 2-3 inputs aléatoires
186
- (inputNodes) => {
187
- const num = 2 + Math.floor(Math.random() * 2);
188
- return [...inputNodes].sort(() => Math.random() - 0.5).slice(0, num);
189
- },
190
-
191
- // Pattern 7: Cascade (un par un avec délai)
192
- (inputNodes) => inputNodes.slice(0, 3 + Math.floor(Math.random() * 3))
193
- ];
194
-
195
- const startForwardPass = () => {
196
- const inputNodes = nodes.filter(n => n.layer === 0);
197
-
198
- // Choisir un pattern aléatoire
199
- const pattern = activationPatterns[Math.floor(Math.random() * activationPatterns.length)];
200
- const activeInputs = pattern(inputNodes);
201
-
202
- // Activer les inputs (plus rapide)
203
- activeInputs.forEach((node, idx) => {
204
- anime({
205
- targets: node,
206
- activation: 0.8 + Math.random() * 0.2,
207
- duration: 200,
208
- delay: idx * 60,
209
- easing: 'easeOutQuad',
210
- complete: () => {
211
- anime({
212
- targets: node,
213
- activation: node.baseActivity,
214
- duration: 250,
215
- delay: 400,
216
- easing: 'easeInQuad'
217
- });
218
- }
219
- });
220
- });
221
-
222
- // Propager à travers TOUTES les couches (plus rapide)
223
- for (let layerIdx = 0; layerIdx < layers.length - 1; layerIdx++) {
224
- setTimeout(() => {
225
- propagateLayer(layerIdx);
226
- }, 250 + layerIdx * 350);
227
- }
228
- };
229
-
230
- const propagateLayer = (fromLayerIdx) => {
231
- const fromNodes = nodes.filter(n => n.layer === fromLayerIdx);
232
- const toNodes = nodes.filter(n => n.layer === fromLayerIdx + 1);
233
-
234
- const layerConnections = connections.filter(c => {
235
- const fromNode = nodes[c.from];
236
- const toNode = nodes[c.to];
237
- return fromNode.layer === fromLayerIdx && toNode.layer === fromLayerIdx + 1;
238
- });
239
-
240
- // Activer connections et créer particules
241
- layerConnections.forEach((conn, idx) => {
242
- const fromNode = nodes[conn.from];
243
- const activationStrength = fromNode.activation * conn.weight;
244
-
245
- if (activationStrength > 0.2) {
246
- anime({
247
- targets: conn,
248
- activation: activationStrength,
249
- duration: 300,
250
- delay: idx * 1,
251
- easing: 'easeOutQuad',
252
- complete: () => {
253
- anime({
254
- targets: conn,
255
- activation: 0,
256
- duration: 250,
257
- easing: 'easeInQuad'
258
- });
259
- }
260
- });
261
-
262
- // Créer particule qui voyage le long de la connexion
263
- if (Math.random() < 0.3) { // Pas sur toutes les connexions
264
- createParticle(conn, activationStrength);
265
- }
266
- }
267
- });
268
-
269
- // Activer les nœuds cibles (plus rapide)
270
- setTimeout(() => {
271
- toNodes.forEach(toNode => {
272
- const toNodeIdx = nodes.indexOf(toNode);
273
- const incomingConns = layerConnections.filter(c => c.to === toNodeIdx);
274
-
275
- let sum = 0;
276
- incomingConns.forEach(conn => {
277
- const fromNode = nodes[conn.from];
278
- sum += fromNode.activation * conn.weight;
279
- });
280
-
281
- const activation = Math.min(1, sum / incomingConns.length * 1.5);
282
-
283
- if (activation > 0.25) {
284
- anime({
285
- targets: toNode,
286
- activation: activation,
287
- duration: 200,
288
- easing: 'easeOutQuad',
289
- complete: () => {
290
- anime({
291
- targets: toNode,
292
- activation: toNode.baseActivity,
293
- duration: 400,
294
- delay: 300,
295
- easing: 'easeInQuad'
296
- });
297
- }
298
- });
299
- }
300
- });
301
- }, 150);
302
- };
303
-
304
- const createParticle = (connection, strength) => {
305
- const fromNode = nodes[connection.from];
306
- const toNode = nodes[connection.to];
307
- if (!fromNode || !toNode) return;
308
-
309
- const particle = {
310
- fromX: fromNode.x,
311
- fromY: fromNode.y,
312
- toX: toNode.x,
313
- toY: toNode.y,
314
- progress: 0,
315
- strength: strength,
316
- size: 1.5 + strength * 1.5,
317
- trail: []
318
- };
319
-
320
- particles.push(particle);
321
-
322
- anime({
323
- targets: particle,
324
- progress: 1,
325
- duration: 350,
326
- easing: 'easeInOutQuad',
327
- complete: () => {
328
- // Retirer la particule
329
- const idx = particles.indexOf(particle);
330
- if (idx > -1) particles.splice(idx, 1);
331
- }
332
- });
333
- };
334
-
335
- const draw = () => {
336
- // Pas de background - transparent
337
- ctx.clearRect(0, 0, width, height);
338
-
339
- // Draw connections
340
- connections.forEach(conn => {
341
- if (conn.opacity < 0.01) return;
342
-
343
- const fromNode = nodes[conn.from];
344
- const toNode = nodes[conn.to];
345
- if (!fromNode || !toNode) return;
346
-
347
- const baseOpacity = conn.opacity * conn.weight * 0.5;
348
- const activeOpacity = conn.activation;
349
- const totalOpacity = Math.max(baseOpacity, activeOpacity);
350
-
351
- if (totalOpacity < 0.01) return;
352
-
353
- const isActive = conn.activation > 0.1;
354
- const connectionColor = isActive ? colors.connectionActive : colors.connection;
355
-
356
- ctx.beginPath();
357
- ctx.moveTo(fromNode.x, fromNode.y);
358
- ctx.lineTo(toNode.x, toNode.y);
359
-
360
- const rgb = connectionColor.match(/[\d.]+/g);
361
- ctx.strokeStyle = `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${totalOpacity})`;
362
- ctx.lineWidth = isActive ? 1.5 : 0.8;
363
- ctx.stroke();
364
- });
365
-
366
- // Draw particles
367
- particles.forEach(particle => {
368
- const x = particle.fromX + (particle.toX - particle.fromX) * particle.progress;
369
- const y = particle.fromY + (particle.toY - particle.fromY) * particle.progress;
370
-
371
- // Trail
372
- particle.trail.push({ x, y });
373
- if (particle.trail.length > 5) particle.trail.shift();
374
-
375
- particle.trail.forEach((point, i) => {
376
- const alpha = (i / particle.trail.length) * particle.strength;
377
- const size = particle.size * alpha * 0.6;
378
-
379
- ctx.beginPath();
380
- ctx.arc(point.x, point.y, size, 0, Math.PI * 2);
381
- const rgb = colors.particle.match(/[\d.]+/g);
382
- ctx.fillStyle = `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${alpha * 0.5})`;
383
- ctx.fill();
384
- });
385
-
386
- // Particle principale
387
- ctx.beginPath();
388
- ctx.arc(x, y, particle.size, 0, Math.PI * 2);
389
- ctx.fillStyle = colors.particle;
390
- ctx.shadowBlur = 8;
391
- ctx.shadowColor = colors.particle;
392
- ctx.fill();
393
- ctx.shadowBlur = 0;
394
- });
395
-
396
- // Draw nodes
397
- nodes.forEach((node, i) => {
398
- if (node.radius < 0.1) return;
399
-
400
- node.pulse += 0.015;
401
- const pulseSize = 1 + Math.sin(node.pulse) * 0.08;
402
- const activationBoost = node.activation * 1.8;
403
- const finalRadius = node.radius * pulseSize + activationBoost;
404
-
405
- // Glow for active nodes
406
- if (node.activation > 0.15) {
407
- const glowRadius = finalRadius * 4;
408
- const gradient = ctx.createRadialGradient(node.x, node.y, 0, node.x, node.y, glowRadius);
409
- const glowAlpha = node.activation * 0.5;
410
- gradient.addColorStop(0, colors.nodeGlow.replace(/[\d.]+\)$/, `${glowAlpha})`));
411
- gradient.addColorStop(1, colors.nodeGlow.replace(/[\d.]+\)$/, '0)'));
412
- ctx.beginPath();
413
- ctx.arc(node.x, node.y, glowRadius, 0, Math.PI * 2);
414
- ctx.fillStyle = gradient;
415
- ctx.fill();
416
- }
417
-
418
- // Node color based on activation
419
- const t = Math.min(1, node.activation / 0.8);
420
-
421
- const baseRgb = colors.node.match(/[\d.]+/g);
422
- const activeRgb = colors.nodeActive.match(/[\d.]+/g);
423
- const r = parseFloat(baseRgb[0]) + (parseFloat(activeRgb[0]) - parseFloat(baseRgb[0])) * t;
424
- const g = parseFloat(baseRgb[1]) + (parseFloat(activeRgb[1]) - parseFloat(baseRgb[1])) * t;
425
- const b = parseFloat(baseRgb[2]) + (parseFloat(activeRgb[2]) - parseFloat(baseRgb[2])) * t;
426
- const a = parseFloat(baseRgb[3]) + (parseFloat(activeRgb[3]) - parseFloat(baseRgb[3])) * t;
427
-
428
- // Node
429
- ctx.beginPath();
430
- ctx.arc(node.x, node.y, finalRadius, 0, Math.PI * 2);
431
- ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${a})`;
432
- ctx.fill();
433
-
434
- // Inner core for active nodes
435
- if (node.activation > 0.4) {
436
- ctx.beginPath();
437
- ctx.arc(node.x, node.y, finalRadius * 0.4, 0, Math.PI * 2);
438
- ctx.fillStyle = colors.accent.replace(/[\d.]+\)$/, `${node.activation})`);
439
- ctx.fill();
440
- }
441
- });
442
-
443
- requestAnimationFrame(draw);
444
- };
445
-
446
- // Start
447
- if (window.ResizeObserver) {
448
- const ro = new ResizeObserver(resize);
449
- ro.observe(container);
450
- } else {
451
- window.addEventListener('resize', resize);
452
- }
453
-
454
- resize();
455
- draw();
456
- };
457
-
458
- if (document.readyState === 'loading') {
459
- document.addEventListener('DOMContentLoaded', () => ensureAnime(bootstrap), { once: true });
460
- } else {
461
- ensureAnime(bootstrap);
462
- }
463
- })();
464
- </script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/src/content/embeds/banner-threejs-galaxy.html DELETED
@@ -1,504 +0,0 @@
1
- <div class="threejs-galaxy" style="width:100%;margin:10px 0;aspect-ratio:3/1;min-height:260px;"></div>
2
- <script type="importmap">
3
- {
4
- "imports": {
5
- "three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js",
6
- "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/"
7
- }
8
- }
9
- </script>
10
- <style>
11
- .threejs-galaxy {
12
- overflow: visible;
13
- background: transparent;
14
- }
15
-
16
- .threejs-galaxy canvas {
17
- display: block;
18
- width: 100%;
19
- height: 100%;
20
- }
21
-
22
- .threejs-galaxy .tp-dfwv {
23
- position: absolute !important;
24
- top: 16px !important;
25
- right: 16px !important;
26
- z-index: 100 !important;
27
- }
28
- </style>
29
- <script type="module">
30
- import * as THREE from 'three';
31
- import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
32
- import { Pane } from 'https://cdn.jsdelivr.net/npm/tweakpane@4.0.3/dist/tweakpane.js';
33
-
34
- const container = document.querySelector('.threejs-galaxy');
35
- if (!container || container.dataset.mounted === 'true') {
36
- if (container) console.log('Container already mounted');
37
- } else {
38
- container.dataset.mounted = 'true';
39
-
40
- // === Scene Setup ===
41
- const scene = new THREE.Scene();
42
-
43
- const camera = new THREE.PerspectiveCamera(
44
- 35,
45
- container.clientWidth / Math.max(260, Math.round(container.clientWidth / 3)),
46
- 0.1,
47
- 100
48
- );
49
- // Vue du dessus avec angle pour voir la profondeur - plus proche pour remplir l'espace
50
- camera.position.set(-0.03, 1.75, 5.71);
51
- camera.rotation.set(-0.43, -0.01, -0.01);
52
-
53
- const renderer = new THREE.WebGLRenderer({
54
- antialias: true,
55
- alpha: true,
56
- powerPreference: 'high-performance'
57
- });
58
- renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
59
- renderer.setClearColor(0x000000, 0);
60
- container.appendChild(renderer.domElement);
61
-
62
- // === OrbitControls ===
63
- const controls = new OrbitControls(camera, renderer.domElement);
64
- controls.enableDamping = true;
65
- controls.dampingFactor = 0.05;
66
- controls.autoRotate = true;
67
- controls.autoRotateSpeed = 0.5;
68
- controls.enableZoom = true;
69
- controls.enablePan = true;
70
- controls.panSpeed = 0.5;
71
- controls.minDistance = 3;
72
- controls.maxDistance = 12;
73
- controls.target.set(0.04, -0.75, 0.26);
74
-
75
- // Track pane visibility for logging
76
- let paneVisible = false;
77
-
78
- // Log camera position and rotation on change (only when pane is visible)
79
- controls.addEventListener('change', () => {
80
- if (paneVisible) {
81
- console.log('Camera Position:', `camera.position.set(${camera.position.x.toFixed(2)}, ${camera.position.y.toFixed(2)}, ${camera.position.z.toFixed(2)});`);
82
- console.log('Camera Rotation:', `camera.rotation.set(${camera.rotation.x.toFixed(2)}, ${camera.rotation.y.toFixed(2)}, ${camera.rotation.z.toFixed(2)});`);
83
- console.log('Target (Center):', `controls.target.set(${controls.target.x.toFixed(2)}, ${controls.target.y.toFixed(2)}, ${controls.target.z.toFixed(2)});`);
84
- console.log('---');
85
- }
86
- });
87
-
88
- // === Galaxy Parameters ===
89
- // Detect current theme
90
- const isDarkMode = () => document.documentElement.getAttribute('data-theme') === 'dark';
91
-
92
- const params = {
93
- count: 12000,
94
- size: 150,
95
- whiteSize: 25,
96
- sizeVariation: 0.8,
97
- radius: 4,
98
- branches: 2,
99
- spin: 3.0,
100
- randomness: 0.3,
101
- randomnessPower: 3,
102
- centerSizeBoost: 1.5,
103
- insideColor: isDarkMode() ? '#ff6030' : '#ff8050',
104
- outsideColor: isDarkMode() ? '#1b3984' : '#3d5fa8',
105
- fov: 35
106
- };
107
-
108
- // === Tweakpane ===
109
- const pane = new Pane({
110
- container: container,
111
- title: 'Galaxy Controls'
112
- });
113
-
114
- // Hide pane by default
115
- pane.element.style.display = 'none';
116
-
117
- let geometry = null;
118
- let material = null;
119
- let points = null;
120
- let whiteGeometry = null;
121
- let whiteMaterial = null;
122
- let whitePoints = null;
123
-
124
- // === Animation Clock ===
125
- const clock = new THREE.Clock();
126
-
127
- // === Generate Galaxy Function ===
128
- const generateGalaxy = () => {
129
- // Destroy old galaxy
130
- if (points !== null) {
131
- geometry.dispose();
132
- material.dispose();
133
- scene.remove(points);
134
- }
135
-
136
- // Destroy old white points
137
- if (whitePoints !== null) {
138
- whiteGeometry.dispose();
139
- whiteMaterial.dispose();
140
- scene.remove(whitePoints);
141
- }
142
-
143
- // New geometry
144
- geometry = new THREE.BufferGeometry();
145
-
146
- const positions = new Float32Array(params.count * 3);
147
- const colors = new Float32Array(params.count * 3);
148
- const scales = new Float32Array(params.count);
149
-
150
- const colorInside = new THREE.Color(params.insideColor);
151
- const colorOutside = new THREE.Color(params.outsideColor);
152
-
153
- for (let i = 0; i < params.count; i++) {
154
- const i3 = i * 3;
155
-
156
- // Position sur le rayon
157
- const radius = Math.random() * params.radius;
158
- const radiusRatio = radius / params.radius;
159
-
160
- // Angle de la branche
161
- const branchAngle = (i % params.branches) / params.branches * Math.PI * 2;
162
-
163
- // Angle de spin (twist)
164
- const spinAngle = radius * params.spin;
165
-
166
- // Randomness
167
- const randomX = Math.pow(Math.random(), params.randomnessPower) * (Math.random() < 0.5 ? 1 : -1) * params.randomness * radius;
168
- const randomY = Math.pow(Math.random(), params.randomnessPower) * (Math.random() < 0.5 ? 1 : -1) * params.randomness * radius * 0.3;
169
- const randomZ = Math.pow(Math.random(), params.randomnessPower) * (Math.random() < 0.5 ? 1 : -1) * params.randomness * radius;
170
-
171
- // Position finale en 3D
172
- positions[i3] = Math.cos(branchAngle + spinAngle) * radius + randomX;
173
- positions[i3 + 1] = randomY;
174
- positions[i3 + 2] = Math.sin(branchAngle + spinAngle) * radius + randomZ;
175
-
176
- // Couleur
177
- const mixedColor = colorInside.clone();
178
- mixedColor.lerp(colorOutside, radiusRatio);
179
-
180
- colors[i3] = mixedColor.r;
181
- colors[i3 + 1] = mixedColor.g;
182
- colors[i3 + 2] = mixedColor.b;
183
-
184
- // Échelle : plus gros au centre, linéairement décroissant vers l'extérieur
185
- const centerScale = (1.0 + params.centerSizeBoost) - radiusRatio * params.centerSizeBoost;
186
- // Variation aléatoire contrôlée par sizeVariation
187
- const randomScale = Math.pow(Math.random(), 2.0) * params.sizeVariation;
188
- scales[i] = randomScale + centerScale;
189
- }
190
-
191
- geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
192
- geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
193
- geometry.setAttribute('aScale', new THREE.BufferAttribute(scales, 1));
194
-
195
- // === Shader Material ===
196
- material = new THREE.ShaderMaterial({
197
- depthWrite: false,
198
- blending: THREE.AdditiveBlending,
199
- vertexColors: true,
200
- uniforms: {
201
- uTime: { value: 0 },
202
- uSize: { value: params.size * renderer.getPixelRatio() }
203
- },
204
- vertexShader: `
205
- uniform float uTime;
206
- uniform float uSize;
207
-
208
- attribute float aScale;
209
-
210
- varying vec3 vColor;
211
-
212
- void main() {
213
- vec4 modelPosition = modelMatrix * vec4(position, 1.0);
214
-
215
- vec4 viewPosition = viewMatrix * modelPosition;
216
- vec4 projectedPosition = projectionMatrix * viewPosition;
217
- gl_Position = projectedPosition;
218
-
219
- // Taille des points
220
- gl_PointSize = uSize * aScale;
221
- gl_PointSize *= (1.0 / -viewPosition.z);
222
-
223
- vColor = color;
224
- }
225
- `,
226
- fragmentShader: `
227
- varying vec3 vColor;
228
-
229
- void main() {
230
- float distanceToCenter = distance(gl_PointCoord, vec2(0.5));
231
-
232
- // Noyau brillant
233
- float core = 1.0 - smoothstep(0.0, 0.25, distanceToCenter);
234
- core = pow(core, 2.0);
235
-
236
- // Halo externe
237
- float halo = 1.0 - smoothstep(0.15, 0.5, distanceToCenter);
238
- halo = pow(halo, 3.0);
239
-
240
- // Combinaison
241
- float strength = max(core, halo * 0.3);
242
-
243
- // Couleur finale (adaptée au thème)
244
- float coreIntensity = ${isDarkMode() ? '0.8' : '0.7'};
245
- float haloIntensity = ${isDarkMode() ? '0.4' : '0.35'};
246
- vec3 coreColor = vColor * coreIntensity;
247
- vec3 haloColor = vColor * haloIntensity;
248
- vec3 finalColor = mix(haloColor, coreColor, core);
249
-
250
- // Alpha adapté au thème
251
- float alpha = strength * ${isDarkMode() ? '0.6' : '0.5'};
252
-
253
- gl_FragColor = vec4(finalColor, alpha);
254
- }
255
- `
256
- });
257
-
258
- // === Points ===
259
- points = new THREE.Points(geometry, material);
260
- scene.add(points);
261
-
262
- // === White Points (50% random subset) ===
263
- const whiteCount = Math.floor(params.count * 0.5);
264
- const whitePositions = new Float32Array(whiteCount * 3);
265
- const whiteScales = new Float32Array(whiteCount);
266
-
267
- // Sélectionner aléatoirement 50% des indices
268
- const indices = Array.from({ length: params.count }, (_, i) => i);
269
- // Mélanger les indices (Fisher-Yates shuffle)
270
- for (let i = indices.length - 1; i > 0; i--) {
271
- const j = Math.floor(Math.random() * (i + 1));
272
- [indices[i], indices[j]] = [indices[j], indices[i]];
273
- }
274
- const selectedIndices = indices.slice(0, whiteCount);
275
-
276
- // Copier les positions sélectionnées et créer des échelles plus petites
277
- for (let i = 0; i < whiteCount; i++) {
278
- const sourceIdx = selectedIndices[i];
279
- whitePositions[i * 3] = positions[sourceIdx * 3];
280
- whitePositions[i * 3 + 1] = positions[sourceIdx * 3 + 1];
281
- whitePositions[i * 3 + 2] = positions[sourceIdx * 3 + 2];
282
-
283
- // Échelles beaucoup plus petites pour les points blancs
284
- whiteScales[i] = (Math.random() * 0.2 + 0.2) / 6;
285
- }
286
-
287
- whiteGeometry = new THREE.BufferGeometry();
288
- whiteGeometry.setAttribute('position', new THREE.BufferAttribute(whitePositions, 3));
289
- whiteGeometry.setAttribute('aScale', new THREE.BufferAttribute(whiteScales, 1));
290
-
291
- // Matériau pour les points blancs
292
- whiteMaterial = new THREE.ShaderMaterial({
293
- depthWrite: false,
294
- blending: THREE.AdditiveBlending,
295
- uniforms: {
296
- uTime: { value: 0 },
297
- uSize: { value: params.whiteSize * renderer.getPixelRatio() }
298
- },
299
- vertexShader: `
300
- uniform float uTime;
301
- uniform float uSize;
302
-
303
- attribute float aScale;
304
-
305
- void main() {
306
- vec4 modelPosition = modelMatrix * vec4(position, 1.0);
307
-
308
- vec4 viewPosition = viewMatrix * modelPosition;
309
- vec4 projectedPosition = projectionMatrix * viewPosition;
310
- gl_Position = projectedPosition;
311
-
312
- gl_PointSize = uSize * aScale;
313
- gl_PointSize *= (1.0 / -viewPosition.z);
314
- }
315
- `,
316
- fragmentShader: `
317
- void main() {
318
- float distanceToCenter = distance(gl_PointCoord, vec2(0.5));
319
-
320
- // Créer une boule bien définie
321
- float strength = 1.0 - smoothstep(0.3, 0.5, distanceToCenter);
322
- strength = pow(strength, 2.0);
323
-
324
- // Couleur blanche (adaptée au thème)
325
- vec3 whiteColor = vec3(${isDarkMode() ? '1.0, 1.0, 1.0' : '0.95, 0.95, 0.98'});
326
-
327
- gl_FragColor = vec4(whiteColor, strength * ${isDarkMode() ? '0.8' : '0.9'});
328
- }
329
- `
330
- });
331
-
332
- whitePoints = new THREE.Points(whiteGeometry, whiteMaterial);
333
- scene.add(whitePoints);
334
-
335
- };
336
-
337
- // Generate initial galaxy
338
- generateGalaxy();
339
-
340
- // === Tweakpane Controls ===
341
- pane.addBinding(params, 'count', {
342
- label: 'Particles',
343
- min: 1000,
344
- max: 50000,
345
- step: 1000
346
- }).on('change', () => {
347
- generateGalaxy();
348
- });
349
-
350
- pane.addBinding(params, 'spin', {
351
- label: 'Twist',
352
- min: 0,
353
- max: 5,
354
- step: 0.1
355
- }).on('change', () => {
356
- generateGalaxy();
357
- });
358
-
359
- pane.addBinding(params, 'size', {
360
- label: 'Point Size',
361
- min: 10,
362
- max: 200,
363
- step: 1
364
- }).on('change', () => {
365
- if (material) {
366
- material.uniforms.uSize.value = params.size * renderer.getPixelRatio();
367
- }
368
- });
369
-
370
- pane.addBinding(params, 'sizeVariation', {
371
- label: 'Size Variation',
372
- min: 0,
373
- max: 2,
374
- step: 0.05
375
- }).on('change', () => {
376
- generateGalaxy();
377
- });
378
-
379
- pane.addBinding(params, 'whiteSize', {
380
- label: 'White Size',
381
- min: 5,
382
- max: 100,
383
- step: 1
384
- }).on('change', () => {
385
- if (whiteMaterial) {
386
- whiteMaterial.uniforms.uSize.value = params.whiteSize * renderer.getPixelRatio();
387
- }
388
- });
389
-
390
- pane.addBinding(params, 'centerSizeBoost', {
391
- label: 'Center Boost',
392
- min: 0,
393
- max: 3,
394
- step: 0.1
395
- }).on('change', () => {
396
- generateGalaxy();
397
- });
398
-
399
- pane.addBinding(params, 'branches', {
400
- label: 'Branches',
401
- min: 2,
402
- max: 6,
403
- step: 1
404
- }).on('change', () => {
405
- generateGalaxy();
406
- });
407
-
408
- pane.addBinding(params, 'fov', {
409
- label: 'FOV (Zoom)',
410
- min: 20,
411
- max: 75,
412
- step: 1
413
- }).on('change', () => {
414
- camera.fov = params.fov;
415
- camera.updateProjectionMatrix();
416
- });
417
-
418
- // === Animation ===
419
- const animate = () => {
420
- requestAnimationFrame(animate);
421
-
422
- const elapsedTime = clock.getElapsedTime();
423
-
424
- if (material) {
425
- material.uniforms.uTime.value = elapsedTime;
426
- }
427
-
428
- // Mise à jour des contrôles OrbitControls (gère la rotation automatique)
429
- controls.update();
430
-
431
- renderer.render(scene, camera);
432
- };
433
-
434
- // === Resize ===
435
- const onResize = () => {
436
- const width = container.clientWidth;
437
- const height = Math.max(260, Math.round(width / 3));
438
-
439
- camera.aspect = width / height;
440
- camera.updateProjectionMatrix();
441
-
442
- renderer.setSize(width, height);
443
- renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
444
-
445
- if (material) {
446
- material.uniforms.uSize.value = params.size * renderer.getPixelRatio();
447
- }
448
- if (whiteMaterial) {
449
- whiteMaterial.uniforms.uSize.value = params.whiteSize * renderer.getPixelRatio();
450
- }
451
- };
452
-
453
- onResize();
454
-
455
- if (window.ResizeObserver) {
456
- new ResizeObserver(onResize).observe(container);
457
- } else {
458
- window.addEventListener('resize', onResize);
459
- }
460
-
461
- // === Theme Support ===
462
- const updateTheme = () => {
463
- renderer.setClearColor(0x000000, 0);
464
- // Update colors based on theme
465
- params.insideColor = isDarkMode() ? '#ff6030' : '#ff8050';
466
- params.outsideColor = isDarkMode() ? '#1b3984' : '#3d5fa8';
467
- // Regenerate galaxy with new colors
468
- generateGalaxy();
469
- };
470
-
471
- const observer = new MutationObserver(updateTheme);
472
- observer.observe(document.documentElement, {
473
- attributes: true,
474
- attributeFilter: ['data-theme']
475
- });
476
-
477
- // === Keyboard Controls ===
478
- window.addEventListener('keydown', (event) => {
479
- if (event.key === 'd' || event.key === 'D') {
480
- paneVisible = !paneVisible;
481
- if (paneVisible) {
482
- pane.element.style.display = '';
483
- } else {
484
- pane.element.style.display = 'none';
485
- }
486
- }
487
- });
488
-
489
- // === Start ===
490
- animate();
491
-
492
- // === Cleanup ===
493
- container._cleanup = () => {
494
- observer.disconnect();
495
- if (geometry) geometry.dispose();
496
- if (material) material.dispose();
497
- if (whiteGeometry) whiteGeometry.dispose();
498
- if (whiteMaterial) whiteMaterial.dispose();
499
- controls.dispose();
500
- renderer.dispose();
501
- pane.dispose();
502
- };
503
- }
504
- </script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/src/content/embeds/banner-umap-lucioles.html DELETED
@@ -1,489 +0,0 @@
1
- <div class="d3-latent-space"></div>
2
- <style>
3
- .d3-latent-space {
4
- width: 100%;
5
- margin: 10px 0;
6
- aspect-ratio: 3/1;
7
- min-height: 260px;
8
- overflow: hidden;
9
- background: transparent;
10
- border-radius: 12px;
11
- border: 1px solid var(--border-color);
12
- }
13
-
14
- .d3-latent-space canvas {
15
- top: 0;
16
- left: 0;
17
- width: 100%;
18
- height: 100%;
19
- }
20
-
21
- .d3-latent-space .tp-dfwv {
22
- top: 16px;
23
- right: 16px;
24
- z-index: 10;
25
- }
26
-
27
- .d3-latent-space .d3-tooltip {
28
- position: absolute;
29
- background: color-mix(in srgb, var(--surface-bg) 95%, transparent);
30
- backdrop-filter: blur(16px) saturate(1.2);
31
- border: 1px solid var(--border-color);
32
- border-radius: 12px;
33
- padding: 14px 18px;
34
- box-shadow: 0 12px 48px rgba(0, 0, 0, 0.18), 0 4px 12px rgba(0, 0, 0, 0.12);
35
- pointer-events: none;
36
- opacity: 0;
37
- transform: translate(-50%, -120%);
38
- transition: opacity 0.15s ease;
39
- z-index: 10;
40
- max-width: 400px;
41
- }
42
-
43
- .d3-latent-space .tooltip-category {
44
- font-size: 10px;
45
- font-weight: 800;
46
- text-transform: uppercase;
47
- letter-spacing: 1px;
48
- margin-bottom: 8px;
49
- display: flex;
50
- align-items: center;
51
- gap: 6px;
52
- }
53
-
54
- .d3-latent-space .tooltip-badge {
55
- display: inline-block;
56
- width: 8px;
57
- height: 8px;
58
- border-radius: 50%;
59
- box-shadow: 0 0 8px currentColor;
60
- }
61
-
62
- .d3-latent-space .tooltip-question {
63
- font-size: 12px;
64
- font-weight: 600;
65
- color: var(--text-color);
66
- margin-bottom: 6px;
67
- line-height: 1.4;
68
- }
69
-
70
- .d3-latent-space .tooltip-answer {
71
- font-size: 11px;
72
- color: var(--muted-color);
73
- line-height: 1.4;
74
- border-top: 1px solid var(--border-color);
75
- padding-top: 8px;
76
- margin-top: 6px;
77
- }
78
- </style>
79
- <script>
80
- (() => {
81
- const ensureD3 = (cb) => {
82
- if (window.d3 && typeof window.d3.csvParse === 'function') return cb();
83
- let s = document.getElementById('d3-cdn-script');
84
- if (!s) {
85
- s = document.createElement('script');
86
- s.id = 'd3-cdn-script';
87
- s.src = 'https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js';
88
- document.head.appendChild(s);
89
- }
90
- const onReady = () => { if (window.d3 && typeof window.d3.csvParse === 'function') cb(); };
91
- s.addEventListener('load', onReady, { once: true });
92
- if (window.d3) onReady();
93
- };
94
-
95
- const ensureAnime = (cb) => {
96
- if (window.anime) return cb();
97
- let s = document.getElementById('anime-cdn-script');
98
- if (!s) {
99
- s = document.createElement('script');
100
- s.id = 'anime-cdn-script';
101
- s.src = 'https://cdn.jsdelivr.net/npm/animejs@3.2.1/lib/anime.min.js';
102
- document.head.appendChild(s);
103
- }
104
- const onReady = () => { if (window.anime) cb(); };
105
- s.addEventListener('load', onReady, { once: true });
106
- if (window.anime) onReady();
107
- };
108
-
109
- const ensureTweakpane = (cb) => {
110
- if (window.Tweakpane) return cb();
111
- let s = document.getElementById('tweakpane-cdn-script');
112
- if (!s) {
113
- s = document.createElement('script');
114
- s.id = 'tweakpane-cdn-script';
115
- s.src = 'https://cdn.jsdelivr.net/npm/tweakpane@3.1.10/dist/tweakpane.min.js';
116
- document.head.appendChild(s);
117
- }
118
- const onReady = () => { if (window.Tweakpane) cb(); };
119
- s.addEventListener('load', onReady, { once: true });
120
- if (window.Tweakpane) onReady();
121
- };
122
-
123
- const bootstrap = () => {
124
- const scriptEl = document.currentScript;
125
- let container = scriptEl ? scriptEl.previousElementSibling : null;
126
- if (!(container && container.classList && container.classList.contains('d3-latent-space'))) {
127
- const candidates = Array.from(document.querySelectorAll('.d3-latent-space'))
128
- .filter((el) => !(el.dataset && el.dataset.mounted === 'true'));
129
- container = candidates[candidates.length - 1] || null;
130
- }
131
- if (!container) return;
132
- if (container.dataset) {
133
- if (container.dataset.mounted === 'true') return;
134
- container.dataset.mounted = 'true';
135
- }
136
-
137
- // Setup canvas
138
- const canvas = document.createElement('canvas');
139
- container.appendChild(canvas);
140
- const ctx = canvas.getContext('2d');
141
-
142
- // Tooltip
143
- const tooltip = document.createElement('div');
144
- tooltip.className = 'd3-tooltip';
145
- container.appendChild(tooltip);
146
-
147
- let width = container.clientWidth || 800;
148
- let height = Math.max(260, Math.round(width / 3));
149
- let points = [];
150
- let categories = new Map();
151
- let selectedCategory = null;
152
- let animationFrame;
153
- let time = 0;
154
-
155
- // Tweakpane params
156
- const params = {
157
- baseSize: 2.5
158
- };
159
-
160
- const resizeCanvas = () => {
161
- width = container.clientWidth || 800;
162
- height = Math.max(260, Math.round(width / 3));
163
- canvas.width = width * window.devicePixelRatio || 1;
164
- canvas.height = height * window.devicePixelRatio || 1;
165
- canvas.style.width = width + 'px';
166
- canvas.style.height = height + 'px';
167
- ctx.scale(window.devicePixelRatio || 1, window.devicePixelRatio || 1);
168
- };
169
-
170
- const getColors = () => {
171
- const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
172
- const colors = window.ColorPalettes
173
- ? window.ColorPalettes.getColors('categorical', 10)
174
- : ['#FF6B6B', '#4ECDC4', '#45B7D1', '#FFA07A', '#98D8C8', '#F7DC6F', '#B983FF', '#FF85A2', '#5DADE2', '#52BE80'];
175
- return { isDark, colors };
176
- };
177
-
178
- class Point {
179
- constructor(data, color, index) {
180
- this.originalX = parseFloat(data.x);
181
- this.originalY = parseFloat(data.y);
182
- this.category = data.primary_category;
183
- this.title = data.title;
184
- this.authors = data.authors;
185
- this.year = data.year;
186
- this.abstract = data.abstract;
187
- this.color = color;
188
- this.index = index;
189
-
190
- // Animation properties
191
- this.x = this.originalX;
192
- this.y = this.originalY;
193
- this.displayX = 0;
194
- this.displayY = 0;
195
- this.opacity = 0;
196
- this.sizeVariation = 0.5 + Math.random() * 1;
197
- this.size = params.baseSize * this.sizeVariation;
198
- this.baseSize = this.size;
199
- this.glowIntensity = 0;
200
- this.phase = Math.random() * Math.PI * 2;
201
- this.speed = 0.3 + Math.random() * 0.4;
202
- }
203
-
204
- updateSize() {
205
- this.baseSize = params.baseSize * this.sizeVariation;
206
- if (!this.isHighlighted) {
207
- this.size = this.baseSize;
208
- }
209
- }
210
-
211
- update(time, selectedCat) {
212
- // Position statique - pas de mouvement
213
- this.x = this.originalX;
214
- this.y = this.originalY;
215
-
216
- // Highlight if selected or dim if other selected
217
- if (selectedCat === null) {
218
- this.glowIntensity = 0.8;
219
- this.isHighlighted = false;
220
- this.size = this.baseSize;
221
- } else if (this.category === selectedCat) {
222
- this.glowIntensity = 1;
223
- this.isHighlighted = true;
224
- this.size = this.baseSize * 1.4;
225
- } else {
226
- this.glowIntensity = 0.1;
227
- this.isHighlighted = true;
228
- this.size = this.baseSize * 0.6;
229
- }
230
- }
231
-
232
- draw(ctx, scaleX, scaleY, offsetX, offsetY) {
233
- this.displayX = this.x * scaleX + offsetX;
234
- this.displayY = this.y * scaleY + offsetY;
235
-
236
- const alpha = this.opacity * this.glowIntensity;
237
-
238
- // Outer glow
239
- if (this.glowIntensity > 0.3) {
240
- const gradient = ctx.createRadialGradient(
241
- this.displayX, this.displayY, 0,
242
- this.displayX, this.displayY, this.size * 4
243
- );
244
- gradient.addColorStop(0, this.color + Math.floor(alpha * 60).toString(16).padStart(2, '0'));
245
- gradient.addColorStop(0.5, this.color + Math.floor(alpha * 30).toString(16).padStart(2, '0'));
246
- gradient.addColorStop(1, this.color + '00');
247
-
248
- ctx.fillStyle = gradient;
249
- ctx.beginPath();
250
- ctx.arc(this.displayX, this.displayY, this.size * 4, 0, Math.PI * 2);
251
- ctx.fill();
252
- }
253
-
254
- // Core point
255
- ctx.fillStyle = this.color + Math.floor(alpha * 255).toString(16).padStart(2, '0');
256
- ctx.beginPath();
257
- ctx.arc(this.displayX, this.displayY, this.size, 0, Math.PI * 2);
258
- ctx.fill();
259
- }
260
-
261
- isNear(mx, my, threshold = 20) {
262
- const dx = this.displayX - mx;
263
- const dy = this.displayY - my;
264
- return Math.sqrt(dx * dx + dy * dy) < threshold;
265
- }
266
- }
267
-
268
- const loadData = async () => {
269
- try {
270
- console.log('Loading research papers data...');
271
- const response = await fetch('/data/data.json', { cache: 'no-cache' });
272
- if (!response.ok) {
273
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
274
- }
275
-
276
- console.log('Parsing JSON...');
277
- const rawData = await response.json();
278
- console.log(`Loaded ${rawData.length} papers`);
279
-
280
- // Sample data for performance (max 3000 points)
281
- const sampledData = rawData.length > 3000
282
- ? rawData.filter((_, i) => i % Math.ceil(rawData.length / 3000) === 0)
283
- : rawData;
284
-
285
- // Group by primary category
286
- sampledData.forEach(paper => {
287
- const cat = paper.primary_category || 'Unknown';
288
- if (!categories.has(cat)) {
289
- categories.set(cat, []);
290
- }
291
- categories.get(cat).push(paper);
292
- });
293
-
294
- // Create points with colors
295
- const { colors } = getColors();
296
- const categoryList = Array.from(categories.keys());
297
-
298
- categoryList.forEach((cat, i) => {
299
- const color = colors[i % colors.length];
300
- categories.get(cat).forEach((data, j) => {
301
- points.push(new Point(data, color, i * 100 + j));
302
- });
303
- });
304
-
305
- // Animate points in
306
- points.forEach((point, i) => {
307
- anime({
308
- targets: point,
309
- opacity: [0, 1],
310
- duration: 1500,
311
- delay: i * 2,
312
- easing: 'easeOutQuad'
313
- });
314
- });
315
-
316
- // Setup Tweakpane
317
- let pane;
318
- try {
319
- if (window.Tweakpane && window.Tweakpane.Pane) {
320
- pane = new window.Tweakpane.Pane({
321
- container: container,
322
- title: 'Controls'
323
- });
324
- } else if (window.Tweakpane) {
325
- pane = new window.Tweakpane({
326
- container: container,
327
- title: 'Controls'
328
- });
329
- }
330
-
331
- if (pane) {
332
- const input = pane.addInput ? pane.addInput(params, 'baseSize', {
333
- label: 'Point Size',
334
- min: 0.5,
335
- max: 8,
336
- step: 0.1
337
- }) : pane.addBinding ? pane.addBinding(params, 'baseSize', {
338
- label: 'Point Size',
339
- min: 0.5,
340
- max: 8,
341
- step: 0.1
342
- }) : null;
343
-
344
- if (input) {
345
- input.on('change', () => {
346
- points.forEach(p => p.updateSize());
347
- });
348
- }
349
- }
350
- } catch (err) {
351
- console.warn('Tweakpane initialization failed:', err);
352
- // Fallback to HTML slider
353
- const controls = document.createElement('div');
354
- controls.style.cssText = 'position:absolute;top:16px;right:16px;background:var(--surface-bg);border:1px solid var(--border-color);border-radius:8px;padding:12px;z-index:10;';
355
- controls.innerHTML = `
356
- <label style="font-size:11px;font-weight:700;color:var(--text-color);display:block;margin-bottom:6px;">Point Size</label>
357
- <input type="range" min="0.5" max="8" step="0.1" value="2.5" style="width:120px;">
358
- <span style="font-size:11px;color:var(--muted-color);margin-left:8px;">2.5</span>
359
- `;
360
- const slider = controls.querySelector('input');
361
- const label = controls.querySelector('span');
362
- slider.addEventListener('input', (e) => {
363
- params.baseSize = parseFloat(e.target.value);
364
- label.textContent = params.baseSize.toFixed(1);
365
- points.forEach(p => p.updateSize());
366
- });
367
- container.appendChild(controls);
368
- }
369
-
370
- console.log(`Created ${points.length} points from ${categoryList.length} categories`);
371
- render();
372
-
373
- } catch (error) {
374
- console.error('Error loading data:', error);
375
- const errorMsg = error.message || error.toString();
376
- container.innerHTML = `<pre style="color:red;padding:20px;margin:0;font-size:12px;">Error: ${errorMsg}<br><br>Trying to load: /data/data.json<br>Check console for details.</pre>`;
377
- }
378
- };
379
-
380
- const render = () => {
381
- time += 16;
382
- ctx.clearRect(0, 0, width, height);
383
-
384
- if (points.length === 0) {
385
- animationFrame = requestAnimationFrame(render);
386
- return;
387
- }
388
-
389
- // Calculate bounds
390
- const xValues = points.map(p => p.x);
391
- const yValues = points.map(p => p.y);
392
- const minX = Math.min(...xValues);
393
- const maxX = Math.max(...xValues);
394
- const minY = Math.min(...yValues);
395
- const maxY = Math.max(...yValues);
396
-
397
- const padding = 40;
398
- const scaleX = (width - padding * 2) / (maxX - minX);
399
- const scaleY = (height - padding * 2) / (maxY - minY);
400
-
401
- const offsetX = padding - minX * scaleX;
402
- const offsetY = padding - minY * scaleY;
403
-
404
- // Update and draw points
405
- points.forEach(point => {
406
- point.update(time, selectedCategory);
407
- point.draw(ctx, scaleX, scaleY, offsetX, offsetY);
408
- });
409
-
410
- animationFrame = requestAnimationFrame(render);
411
- };
412
-
413
- // Mouse interaction
414
- let hoveredPoint = null;
415
- canvas.addEventListener('mousemove', (e) => {
416
- const rect = canvas.getBoundingClientRect();
417
- const mx = e.clientX - rect.left;
418
- const my = e.clientY - rect.top;
419
-
420
- const closest = points.find(p => p.isNear(mx, my));
421
-
422
- if (closest && closest !== hoveredPoint) {
423
- hoveredPoint = closest;
424
- const authorsStr = Array.isArray(closest.authors)
425
- ? (closest.authors.length > 3
426
- ? `${closest.authors.slice(0, 3).join(', ')} et al.`
427
- : closest.authors.join(', '))
428
- : closest.authors || 'Unknown';
429
-
430
- tooltip.innerHTML = `
431
- <div class="tooltip-category">
432
- <span class="tooltip-badge" style="background: ${closest.color}; color: ${closest.color}"></span>
433
- ${closest.category} · ${closest.year}
434
- </div>
435
- <div class="tooltip-question">${closest.title.substring(0, 120)}${closest.title.length > 120 ? '...' : ''}</div>
436
- <div class="tooltip-answer">${authorsStr}<br>${closest.abstract.substring(0, 180)}${closest.abstract.length > 180 ? '...' : ''}</div>
437
- `;
438
- tooltip.style.left = mx + 'px';
439
- tooltip.style.top = my + 'px';
440
- tooltip.style.opacity = '1';
441
- canvas.style.cursor = 'pointer';
442
- } else if (!closest) {
443
- hoveredPoint = null;
444
- tooltip.style.opacity = '0';
445
- canvas.style.cursor = 'crosshair';
446
- }
447
- });
448
-
449
- canvas.addEventListener('mouseleave', () => {
450
- hoveredPoint = null;
451
- tooltip.style.opacity = '0';
452
- canvas.style.cursor = 'crosshair';
453
- });
454
-
455
- // Resize handling
456
- resizeCanvas();
457
- if (window.ResizeObserver) {
458
- const ro = new ResizeObserver(() => resizeCanvas());
459
- ro.observe(container);
460
- } else {
461
- window.addEventListener('resize', resizeCanvas);
462
- }
463
-
464
- // Theme observer
465
- const observer = new MutationObserver(() => {
466
- const { colors } = getColors();
467
- const categoryList = Array.from(categories.keys());
468
- points.forEach(point => {
469
- const catIndex = categoryList.indexOf(point.category);
470
- if (catIndex >= 0) {
471
- point.color = colors[catIndex % colors.length];
472
- }
473
- });
474
- });
475
- observer.observe(document.documentElement, {
476
- attributes: true,
477
- attributeFilter: ['data-theme']
478
- });
479
-
480
- loadData();
481
- };
482
-
483
- if (document.readyState === 'loading') {
484
- document.addEventListener('DOMContentLoaded', () => ensureD3(() => ensureAnime(() => ensureTweakpane(bootstrap))), { once: true });
485
- } else {
486
- ensureD3(() => ensureAnime(() => ensureTweakpane(bootstrap)));
487
- }
488
- })();
489
- </script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/src/content/embeds/d3-bar.html DELETED
@@ -1,459 +0,0 @@
1
- <div class="d3-bar"></div>
2
- <style>
3
- .d3-bar .controls {
4
- margin-top: 0;
5
- display: flex;
6
- gap: 16px;
7
- align-items: center;
8
- justify-content: flex-end;
9
- flex-wrap: wrap;
10
- }
11
-
12
- .d3-bar .controls .control-group {
13
- display: flex;
14
- flex-direction: column;
15
- align-items: flex-start;
16
- gap: 6px;
17
- }
18
-
19
- .d3-bar .controls label {
20
- font-size: 12px;
21
- color: var(--text-color);
22
- font-weight: 700;
23
- }
24
-
25
- .d3-bar .controls select {
26
- font-size: 12px;
27
- padding: 8px 28px 8px 10px;
28
- border: 1px solid var(--border-color);
29
- border-radius: 8px;
30
- background-color: var(--surface-bg);
31
- color: var(--text-color);
32
- 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='%230f1115' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
33
- background-repeat: no-repeat;
34
- background-position: right 8px center;
35
- background-size: 12px;
36
- -webkit-appearance: none;
37
- -moz-appearance: none;
38
- appearance: none;
39
- cursor: pointer;
40
- transition: border-color .15s ease, box-shadow .15s ease;
41
- }
42
-
43
- [data-theme="dark"] .d3-bar .controls select {
44
- 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");
45
- }
46
-
47
- .d3-bar .controls select:hover {
48
- border-color: var(--primary-color);
49
- }
50
-
51
- .d3-bar .controls select:focus {
52
- border-color: var(--primary-color);
53
- box-shadow: 0 0 0 3px rgba(232, 137, 171, .25);
54
- outline: none;
55
- }
56
-
57
- /* Header (legend + controls) placed after chart */
58
- .d3-bar .chart-header {
59
- display: flex;
60
- align-items: flex-start;
61
- justify-content: flex-start;
62
- gap: 12px;
63
- margin: 8px 0 0 0;
64
- flex-wrap: wrap;
65
- }
66
-
67
- .d3-bar .legend-bottom {
68
- display: flex;
69
- flex-direction: column;
70
- align-items: flex-start;
71
- gap: 6px;
72
- font-size: 12px;
73
- color: var(--text-color);
74
- }
75
-
76
- .d3-bar .legend-bottom .legend-title {
77
- font-size: 12px;
78
- font-weight: 700;
79
- color: var(--text-color);
80
- }
81
-
82
- .d3-bar .legend-bottom .items {
83
- display: flex;
84
- flex-wrap: wrap;
85
- gap: 8px 14px;
86
- }
87
-
88
- .d3-bar .legend-bottom .item {
89
- display: inline-flex;
90
- align-items: center;
91
- gap: 6px;
92
- white-space: nowrap;
93
- }
94
-
95
- .d3-bar .legend-bottom .swatch {
96
- width: 14px;
97
- height: 14px;
98
- border-radius: 3px;
99
- border: 1px solid var(--border-color);
100
- display: inline-block;
101
- }
102
-
103
- .d3-bar.hovering .legend-bottom .item.ghost {
104
- opacity: .35;
105
- }
106
-
107
- .d3-bar.hovering .bars path.ghost {
108
- opacity: .35;
109
- }
110
-
111
- .d3-bar .axis-label {
112
- fill: var(--text-color);
113
- font-size: 12px;
114
- font-weight: 700;
115
- }
116
-
117
- /* Apply axis/tick/grid purely via CSS */
118
- .d3-bar .axes path,
119
- .d3-bar .axes line {
120
- stroke: var(--axis-color);
121
- }
122
-
123
- .d3-bar .axes text {
124
- fill: var(--tick-color);
125
- }
126
-
127
- .d3-bar .grid line {
128
- stroke: var(--grid-color);
129
- }
130
-
131
- /* Tooltip improvements */
132
- .d3-bar .d3-tooltip {
133
- z-index: var(--z-tooltip);
134
- backdrop-filter: saturate(1.12) blur(8px);
135
- }
136
-
137
- /* Hover/transition styling for bars and legend */
138
- .d3-bar .bars path.bar {
139
- transition: opacity .12s ease, stroke .12s ease, stroke-width .12s ease;
140
- }
141
-
142
- .d3-bar .bars path.bar.highlight {
143
- stroke: none;
144
- stroke-width: 0;
145
- }
146
-
147
- .d3-bar.hovering .bars path.ghost {
148
- opacity: .25;
149
- }
150
-
151
- .d3-bar .legend-bottom .item.hovered {
152
- color: inherit;
153
- }
154
-
155
- .d3-bar .legend-bottom .item.hovered .swatch {
156
- border-color: var(--border-color);
157
- }
158
-
159
- .d3-bar .d3-tooltip .swatch {
160
- width: 12px;
161
- height: 12px;
162
- border-radius: 3px;
163
- border: 1px solid var(--border-color);
164
- display: inline-block;
165
- margin-right: 6px;
166
- vertical-align: -2px;
167
- }
168
-
169
- /* Chart card wrapper */
170
- .d3-bar .chart-card {
171
- background: var(--surface-bg);
172
- border: 1px solid var(--border-color);
173
- border-radius: 10px;
174
- padding: 8px;
175
- }
176
-
177
- /* Layout adjustments to give controls more space */
178
- .d3-bar .chart-header {
179
- padding-left: 8px;
180
- padding-right: 8px;
181
- gap: 20px;
182
- }
183
-
184
- .d3-bar .controls {
185
- justify-content: flex-start;
186
- min-width: 320px;
187
- }
188
-
189
- .d3-bar .controls .control-group {
190
- min-width: 150px;
191
- }
192
-
193
- .d3-bar .controls select {
194
- font-size: 13px;
195
- min-width: 160px;
196
- }
197
- </style>
198
- <script>
199
- (() => {
200
- const ensureD3 = (cb) => {
201
- if (window.d3 && typeof window.d3.select === 'function') return cb();
202
- let s = document.getElementById('d3-cdn-script');
203
- if (!s) { s = document.createElement('script'); s.id = 'd3-cdn-script'; s.src = 'https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js'; document.head.appendChild(s); }
204
- const onReady = () => { if (window.d3 && typeof window.d3.select === 'function') cb(); };
205
- s.addEventListener('load', onReady, { once: true });
206
- if (window.d3) onReady();
207
- };
208
-
209
- const bootstrap = () => {
210
- const mount = document.currentScript ? document.currentScript.previousElementSibling : null;
211
- const container = (mount && mount.querySelector && mount.querySelector('.d3-bar')) || document.querySelector('.d3-bar');
212
- if (!container) return;
213
- if (container.dataset) { if (container.dataset.mounted === 'true') return; container.dataset.mounted = 'true'; }
214
-
215
- // Data, matching bar.py
216
- const seqLabels = ["1024", "2048", "4096", "8192"];
217
- const seqScale = [1, 2, 4, 8];
218
- const componentKeys = ['parameters', 'gradients', 'optimizer', 'activations'];
219
- const modelSizes = ["1B", "3B", "8B", "70B", "405B"];
220
- const paramsMem = { "1B": 4.0, "3B": 13.3, "8B": 26.0, "70B": 244.0, "405B": 1520.0 };
221
- const actCoeff = { "1B": 3.6, "3B": 9.3, "8B": 46.2, "70B": 145.7, "405B": 1519.9 };
222
- const recomputeModes = ["none", "selective", "full"];
223
-
224
- const activationsCurve = (sizeKey, mode) => {
225
- const coeff = actCoeff[sizeKey];
226
- let arr = seqScale.map((v) => coeff * (v * v));
227
- if (mode === 'selective') arr = arr.map((v) => v * 0.25);
228
- else if (mode === 'full') arr = arr.map((v) => v * (1 / 16));
229
- return arr;
230
- };
231
- const stackFor = (sizeKey, mode) => {
232
- const p = seqScale.map(() => paramsMem[sizeKey]);
233
- const g = seqScale.map(() => paramsMem[sizeKey]);
234
- const o = seqScale.map(() => 2 * paramsMem[sizeKey]);
235
- const a = activationsCurve(sizeKey, mode);
236
- return { parameters: p, gradients: g, optimizer: o, activations: a };
237
- };
238
-
239
- const Y = {}; // Y[mode][size][component] => array
240
- recomputeModes.forEach((m) => {
241
- Y[m] = {}; modelSizes.forEach((s) => { Y[m][s] = stackFor(s, m); });
242
- });
243
-
244
- // Controls
245
- const controls = document.createElement('div');
246
- controls.className = 'controls';
247
- const groupSize = document.createElement('div'); groupSize.className = 'control-group';
248
- const labelSize = document.createElement('label'); labelSize.textContent = 'Model Size';
249
- const selSize = document.createElement('select'); modelSizes.forEach((s) => { const o = document.createElement('option'); o.value = s; o.textContent = s; selSize.appendChild(o); });
250
- groupSize.appendChild(labelSize); groupSize.appendChild(selSize);
251
- const groupRecomp = document.createElement('div'); groupRecomp.className = 'control-group';
252
- const labelRecomp = document.createElement('label'); labelRecomp.textContent = 'Recomputation';
253
- const selRecomp = document.createElement('select'); recomputeModes.forEach((m) => { const o = document.createElement('option'); o.value = m; o.textContent = m; selRecomp.appendChild(o); });
254
- groupRecomp.appendChild(labelRecomp); groupRecomp.appendChild(selRecomp);
255
-
256
- // Header (legend + controls) to be placed after chart
257
- const header = document.createElement('div'); header.className = 'chart-header';
258
- const legendBottom = document.createElement('div'); legendBottom.className = 'legend-bottom';
259
- const legendTitle = document.createElement('div'); legendTitle.className = 'legend-title'; legendTitle.textContent = 'Legend';
260
- const legendItems = document.createElement('div'); legendItems.className = 'items';
261
- legendBottom.appendChild(legendTitle); legendBottom.appendChild(legendItems);
262
- header.appendChild(legendBottom);
263
- header.appendChild(controls);
264
- // SVG scaffolding inside a card wrapper
265
- const card = document.createElement('div'); card.className = 'chart-card'; container.appendChild(card);
266
- // Place header after the chart card
267
- container.appendChild(header);
268
- const svg = d3.select(card).append('svg').attr('width', '100%').style('display', 'block');
269
- const gRoot = svg.append('g');
270
- const gGrid = gRoot.append('g').attr('class', 'grid');
271
- const gAxes = gRoot.append('g').attr('class', 'axes');
272
- const gBars = gRoot.append('g').attr('class', 'bars');
273
-
274
- // Tooltip
275
- container.style.position = container.style.position || 'relative';
276
- let tip = container.querySelector('.d3-tooltip'); let tipInner;
277
- if (!tip) { tip = document.createElement('div'); tip.className = 'd3-tooltip'; Object.assign(tip.style, { position: 'absolute', top: '0px', left: '0px', transform: 'translate(-9999px, -9999px)', pointerEvents: 'none', padding: '8px 10px', borderRadius: '8px', fontSize: '12px', lineHeight: '1.35', border: '1px solid var(--border-color)', background: 'var(--surface-bg)', color: 'var(--text-color)', boxShadow: '0 4px 24px rgba(0,0,0,.18)', opacity: '0', transition: 'opacity .12s ease' }); tipInner = document.createElement('div'); tipInner.className = 'd3-tooltip__inner'; tipInner.style.textAlign = 'left'; tip.appendChild(tipInner); container.appendChild(tip); } else { tipInner = tip.querySelector('.d3-tooltip__inner') || tip; }
278
-
279
- // State
280
- let currentSize = modelSizes[0];
281
- let currentMode = 'selective';
282
- selRecomp.value = currentMode;
283
-
284
- // Layout & scales
285
- let width = 800, height = 360; const margin = { top: 16, right: 28, bottom: 56, left: 64 };
286
- const x0 = d3.scaleBand().paddingInner(0.25).paddingOuter(0.1); // groups (seq)
287
- const y = d3.scaleLinear();
288
- function getCategoricalColors(count) {
289
- try {
290
- if (window.ColorPalettes && typeof window.ColorPalettes.getColors === 'function') {
291
- return window.ColorPalettes.getColors('categorical', count);
292
- }
293
- } catch (_) { }
294
- const primary = getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim();
295
- const tableau = (window.d3 && window.d3.schemeTableau10) ? window.d3.schemeTableau10 : ['#4e79a7', '#f28e2b', '#e15759', '#76b7b2', '#59a14f', '#edc948', '#b07aa1', '#ff9da7', '#9c755f', '#bab0ab'];
296
- const pool = [primary, ...tableau];
297
- const arr = []; for (let i = 0; i < count; i++) { arr.push(pool[i % pool.length]); }
298
- return arr;
299
- }
300
- const palette = getCategoricalColors(componentKeys.length);
301
- const colorMap = new Map(componentKeys.map((k, i) => [k, palette[i]]));
302
- const colorOf = (key) => colorMap.get(key) || 'var(--primary-color)';
303
-
304
- function yMax(sizeKey, mode) {
305
- const s = Y[mode][sizeKey];
306
- let max = 0; for (let i = 0; i < seqLabels.length; i++) { const sum = s.parameters[i] + s.gradients[i] + s.optimizer[i] + s.activations[i]; if (sum > max) max = sum; }
307
- return max * 1.05;
308
- }
309
-
310
- function renderLegend() {
311
- legendItems.innerHTML = componentKeys.map((key, i) => {
312
- const color = palette[i];
313
- return `<span class="item" data-key="${key}"><span class=\"swatch\" style=\"background:${color}\"></span><span>${key}</span></span>`;
314
- }).join('');
315
- legendItems.querySelectorAll('.item').forEach((el) => {
316
- el.addEventListener('mouseenter', () => {
317
- const k = el.getAttribute('data-key'); if (!k) return;
318
- container.classList.add('hovering');
319
- gBars.selectAll('path.bar').classed('ghost', d => d && d.key !== k);
320
- legendItems.querySelectorAll('.item').forEach(it => it.classList.toggle('ghost', it.getAttribute('data-key') !== k));
321
- });
322
- el.addEventListener('mouseleave', () => {
323
- container.classList.remove('hovering');
324
- gBars.selectAll('path.bar').classed('ghost', false);
325
- legendItems.querySelectorAll('.item').forEach(it => it.classList.remove('ghost'));
326
- });
327
- });
328
- }
329
-
330
- function updateScales() {
331
- width = container.clientWidth || 800; height = Math.max(260, Math.round(width / 3)); svg.attr('width', width).attr('height', height);
332
- const innerWidth = width - margin.left - margin.right; const innerHeight = height - margin.top - margin.bottom; gRoot.attr('transform', `translate(${margin.left},${margin.top})`);
333
-
334
- x0.domain(seqLabels).range([0, innerWidth]);
335
- y.domain([0, yMax(currentSize, currentMode)]).range([innerHeight, 0]).nice();
336
-
337
- // Grid
338
- gGrid.selectAll('*').remove();
339
- gGrid.selectAll('line').data(y.ticks(6)).join('line')
340
- .attr('x1', 0).attr('x2', innerWidth).attr('y1', (d) => y(d)).attr('y2', (d) => y(d))
341
- .attr('stroke', 'var(--grid-color)').attr('stroke-width', 1).attr('shape-rendering', 'crispEdges');
342
-
343
- // Axes
344
- gAxes.selectAll('*').remove();
345
- gAxes.append('g').attr('transform', `translate(0,${innerHeight})`).call(d3.axisBottom(x0)).call((g) => { g.selectAll('path, line').attr('stroke', 'var(--axis-color)'); g.selectAll('text').attr('fill', 'var(--tick-color)').style('font-size', '12px'); });
346
- gAxes.append('g').call(d3.axisLeft(y).ticks(6).tickFormat(d3.format('~f'))).call((g) => { g.selectAll('path, line').attr('stroke', 'var(--axis-color)'); g.selectAll('text').attr('fill', 'var(--tick-color)').style('font-size', '12px'); });
347
-
348
- // Axis labels
349
- gAxes.append('text').attr('class', 'axis-label axis-label--x').attr('x', innerWidth / 2).attr('y', innerHeight + 44).attr('text-anchor', 'middle').text('Sequence Length');
350
- gAxes.append('text').attr('class', 'axis-label axis-label--y').attr('text-anchor', 'middle').attr('transform', `translate(${-52},${innerHeight / 2}) rotate(-90)`).text('Memory (GB)');
351
-
352
- renderLegend();
353
-
354
- return { innerWidth, innerHeight };
355
- }
356
-
357
- function drawBars() {
358
- const stacks = Y[currentMode][currentSize];
359
- const series = componentKeys.map((key, i) => ({ key, color: palette[i], values: stacks[key] }));
360
- // Stack values
361
- const stacked = seqLabels.map((label, i) => {
362
- let acc = 0; const items = [];
363
- series.forEach((s, idx) => {
364
- const y0 = acc; const y1 = acc + s.values[i];
365
- items.push({ key: s.key, color: s.color, i, y0, y1, xLabel: label, value: s.values[i], isBottom: idx === 0, isTop: idx === series.length - 1 });
366
- acc = y1;
367
- });
368
- const total = acc;
369
- items.forEach(it => { it.total = total; });
370
- return { label, items };
371
- });
372
-
373
- const { innerWidth, innerHeight } = updateScales();
374
-
375
- const bandWidth = x0.bandwidth();
376
- const groups = gBars.selectAll('g.bar-group').data(stacked, d => d.label);
377
- const groupsEnter = groups.enter().append('g').attr('class', 'bar-group');
378
- groupsEnter.merge(groups).attr('transform', (d) => `translate(${x0(d.label)},0)`);
379
- groups.exit().remove();
380
-
381
- // Helper to draw per-corner rounded rectangle path
382
- const rCorner = 4;
383
- const roundedPath = (x, yTop, w, h, isTop, isBottom) => {
384
- const r = Math.min(rCorner, Math.max(0, Math.min(w, h) / 2));
385
- const rTL = isTop ? r : 0, rTR = isTop ? r : 0, rBR = isBottom ? r : 0, rBL = isBottom ? r : 0;
386
- const x0 = x, y0 = yTop, x1 = x + w, y1 = yTop + h;
387
- return `M${x0 + rTL},${y0}`
388
- + `H${x1 - rTR}`
389
- + (rTR ? `Q${x1},${y0} ${x1},${y0 + rTR}` : `V${y0}`)
390
- + `V${y1 - rBR}`
391
- + (rBR ? `Q${x1},${y1} ${x1 - rBR},${y1}` : `H${x1}`)
392
- + `H${x0 + rBL}`
393
- + (rBL ? `Q${x0},${y1} ${x0},${y1 - rBL}` : `V${y1}`)
394
- + `V${y0 + rTL}`
395
- + (rTL ? `Q${x0},${y0} ${x0 + rTL},${y0}` : `H${x0}`)
396
- + 'Z';
397
- };
398
-
399
- const bars = groupsEnter.merge(groups).selectAll('path.bar').data(d => d.items, d => d.key);
400
- bars.enter().append('path').attr('class', 'bar')
401
- .attr('d', (d) => roundedPath(0, y(d.y1), bandWidth, Math.max(0.5, y(d.y0) - y(d.y1)), d.isTop, d.isBottom))
402
- .attr('fill', (d) => d.color)
403
- .on('mouseenter', function (ev, d) {
404
- container.classList.add('hovering');
405
- gBars.selectAll('path.bar').classed('ghost', (dd) => !(dd && dd.key === d.key));
406
- const pct = d.total > 0 ? (d.value / d.total * 100) : 0;
407
- tipInner.innerHTML = `
408
- <div style="display:flex;align-items:center;gap:6px;margin-bottom:4px;">
409
- <span class="swatch" style="background:${d.color}"></span>
410
- <strong>${d.key}</strong>
411
- </div>
412
- <div><strong>Seq</strong> ${d.xLabel}</div>
413
- <div><strong>Mem</strong> ${d.value.toFixed(1)} GB <span style="opacity:.7">(${pct.toFixed(0)}%)</span></div>
414
- <div style="opacity:.7"><strong>Total</strong> ${d.total.toFixed(1)} GB</div>
415
- `;
416
- tip.style.opacity = '1';
417
- const li = legendItems.querySelector(`.item[data-key="${d.key}"]`);
418
- if (li) li.classList.add('hovered');
419
- legendItems.querySelectorAll('.item').forEach(it => it.classList.toggle('ghost', it.getAttribute('data-key') !== d.key));
420
- })
421
- .on('mousemove', function (ev, d) {
422
- const [mx, my] = d3.pointer(ev, container);
423
- const offsetX = 12, offsetY = 12;
424
- const maxX = (container.clientWidth || 0) - (tip.offsetWidth + 6);
425
- const maxY = (container.clientHeight || 0) - (tip.offsetHeight + 6);
426
- const tx = Math.max(0, Math.min(mx + offsetX, maxX));
427
- const ty = Math.max(0, Math.min(my + offsetY, maxY));
428
- tip.style.transform = `translate(${Math.round(tx)}px, ${Math.round(ty)}px)`;
429
- })
430
- .on('mouseleave', function () {
431
- tip.style.opacity = '0';
432
- tip.style.transform = 'translate(-9999px, -9999px)';
433
- container.classList.remove('hovering');
434
- gBars.selectAll('path.bar').classed('ghost', false).classed('highlight', false);
435
- legendItems.querySelectorAll('.item').forEach(it => { it.classList.remove('hovered'); it.classList.remove('ghost'); });
436
- })
437
- .merge(bars)
438
- .transition().duration(200)
439
- .attr('d', (d) => roundedPath(0, y(d.y1), bandWidth, Math.max(0.5, y(d.y0) - y(d.y1)), d.isTop, d.isBottom))
440
- .attr('fill', (d) => d.color);
441
- bars.exit().remove();
442
- }
443
-
444
- function update() { drawBars(); }
445
-
446
- // Boot
447
- update();
448
- // controls already appended to footer; populate control groups
449
- controls.appendChild(groupSize); controls.appendChild(groupRecomp);
450
- selSize.addEventListener('change', (e) => { currentSize = e.target.value; update(); });
451
- selRecomp.addEventListener('change', (e) => { currentMode = e.target.value; update(); });
452
-
453
- const rerender = () => { update(); };
454
- if (window.ResizeObserver) { const ro = new ResizeObserver(() => rerender()); ro.observe(container); } else { window.addEventListener('resize', rerender); }
455
- };
456
-
457
- if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true }); } else { ensureD3(bootstrap); }
458
- })();
459
- </script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/src/content/embeds/d3-confusion-matrix.html DELETED
@@ -1,516 +0,0 @@
1
- <div class="d3-confusion-matrix" ></div>
2
- <style>
3
- .d3-confusion-matrix {
4
- position: relative;
5
- }
6
- .d3-confusion-matrix .panels {
7
- display: flex;
8
- flex-wrap: wrap;
9
- gap: 16px;
10
- margin-bottom: 4px;
11
- }
12
- .d3-confusion-matrix .panel {
13
- flex: 1 1 320px;
14
- min-width: 280px;
15
- }
16
- .d3-confusion-matrix .panel__title {
17
- color: var(--text-color);
18
- font-size: 12px;
19
- line-height: 1.35;
20
- margin: 0 0 6px 0;
21
- font-weight: 600;
22
- }
23
- .d3-confusion-matrix .axis-label {
24
- fill: var(--text-color);
25
- font-size: 11px;
26
- font-weight: 700;
27
- }
28
- .d3-confusion-matrix .cell-border {
29
- stroke: var(--border-color);
30
- stroke-width: 1px;
31
- fill: none;
32
- }
33
- .d3-confusion-matrix .cell-text {
34
- fill: var(--muted-color);
35
- font-size: 11px;
36
- pointer-events: none;
37
- }
38
- .d3-confusion-matrix .chart-card { background: var(--surface-bg); border: 1px solid var(--border-color); border-radius: 10px; padding: 8px; }
39
- </style>
40
- <script>
41
- (() => {
42
- // Load D3 from CDN once
43
- const ensureD3 = (cb) => {
44
- if (window.d3 && typeof window.d3.select === 'function') return cb();
45
- let s = document.getElementById('d3-cdn-script');
46
- if (!s) {
47
- s = document.createElement('script');
48
- s.id = 'd3-cdn-script';
49
- s.src = 'https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js';
50
- document.head.appendChild(s);
51
- }
52
- const onReady = () => { if (window.d3 && typeof window.d3.select === 'function') cb(); };
53
- s.addEventListener('load', onReady, { once: true });
54
- if (window.d3) onReady();
55
- };
56
-
57
- const bootstrap = () => {
58
- const scriptEl = document.currentScript;
59
- let container = scriptEl ? scriptEl.previousElementSibling : null;
60
- if (!(container && container.classList && container.classList.contains('d3-confusion-matrix'))){
61
- const cs = Array.from(document.querySelectorAll('.d3-confusion-matrix')).filter(el => !(el.dataset && el.dataset.mounted === 'true'));
62
- container = cs[cs.length - 1] || null;
63
- }
64
- if (!container) return;
65
- if (container.dataset) {
66
- if (container.dataset.mounted === 'true') return;
67
- container.dataset.mounted = 'true';
68
- }
69
-
70
- // Tooltip (HTML, single instance inside container)
71
- container.style.position = container.style.position || 'relative';
72
- let tip = container.querySelector('.d3-tooltip');
73
- let tipInner;
74
- if (!tip) {
75
- tip = document.createElement('div');
76
- tip.className = 'd3-tooltip';
77
- Object.assign(tip.style, {
78
- position: 'absolute',
79
- top: '0px',
80
- left: '0px',
81
- transform: 'translate(-9999px, -9999px)',
82
- pointerEvents: 'none',
83
- padding: '8px 10px',
84
- borderRadius: '8px',
85
- fontSize: '12px',
86
- lineHeight: '1.35',
87
- border: '1px solid var(--border-color)',
88
- background: 'var(--surface-bg)',
89
- color: 'var(--text-color)',
90
- boxShadow: '0 4px 24px rgba(0,0,0,.18)',
91
- opacity: '0',
92
- transition: 'opacity .12s ease'
93
- });
94
- tipInner = document.createElement('div');
95
- tipInner.className = 'd3-tooltip__inner';
96
- tipInner.style.textAlign = 'left';
97
- tip.appendChild(tipInner);
98
- container.appendChild(tip);
99
- } else {
100
- tipInner = tip.querySelector('.d3-tooltip__inner') || tip;
101
- }
102
-
103
- // Panels container (two side-by-side matrices)
104
- const panels = document.createElement('div');
105
- panels.className = 'panels';
106
- const panelA = document.createElement('div');
107
- panelA.className = 'panel';
108
- const titleA = document.createElement('div'); titleA.className = 'panel__title'; titleA.textContent = 'Baseline (row-normalized %)';
109
- panelA.appendChild(titleA);
110
- const mountA = document.createElement('div'); panelA.appendChild(mountA);
111
- const panelB = document.createElement('div');
112
- panelB.className = 'panel';
113
- const titleB = document.createElement('div'); titleB.className = 'panel__title'; titleB.textContent = 'Delta (Improved − Baseline, pp)';
114
- panelB.appendChild(titleB);
115
- const mountB = document.createElement('div'); panelB.appendChild(mountB);
116
- panels.appendChild(panelA);
117
- panels.appendChild(panelB);
118
- container.appendChild(panels);
119
-
120
- // SVG scaffolding
121
- const cardA = document.createElement('div'); cardA.className = 'chart-card'; mountA.appendChild(cardA);
122
- const svgA = d3.select(cardA).append('svg').attr('width', '100%').style('display', 'block');
123
- const gRootA = svgA.append('g');
124
- const gCellsA = gRootA.append('g');
125
- const gAxesA = gRootA.append('g');
126
- const cardB = document.createElement('div'); cardB.className = 'chart-card'; mountB.appendChild(cardB);
127
- const svgB = d3.select(cardB).append('svg').attr('width', '100%').style('display', 'block');
128
- const gRootB = svgB.append('g');
129
- const gCellsB = gRootB.append('g');
130
- const gAxesB = gRootB.append('g');
131
-
132
- // Demo data (two distinct 10x10 confusion matrices: Baseline vs Improved)
133
- // Rows: actual, Columns: predicted
134
- const classes = ['0','1','2','3','4','5','6','7','8','9'];
135
- const matrixA = [
136
- [90, 2, 1, 0, 0, 0, 1, 0, 5, 1],
137
- [ 3, 85, 5, 1, 0, 1, 2, 1, 1, 1],
138
- [ 1, 6, 70, 10, 4, 4, 1, 1, 1, 2],
139
- [ 0, 1, 8, 65, 10, 10, 2, 1, 1, 2],
140
- [ 0, 0, 2, 6, 83, 3, 1, 1, 3, 1],
141
- [ 0, 1, 2, 12, 4, 70, 5, 2, 2, 2],
142
- [ 1, 2, 1, 0, 1, 2, 88, 1, 3, 1],
143
- [ 0, 1, 1, 1, 1, 1, 2, 90, 1, 2],
144
- [ 6, 2, 2, 4, 6, 3, 3, 2, 70, 2],
145
- [ 1, 1, 1, 1, 2, 1, 1, 2, 1, 89]
146
- ];
147
- const matrixB = [
148
- [94, 1, 0, 0, 0, 0, 1, 0, 3, 1],
149
- [ 2, 90, 3, 1, 0, 0, 1, 1, 1, 1],
150
- [ 1, 4, 78, 7, 3, 3, 1, 1, 1, 1],
151
- [ 0, 1, 5, 74, 7, 8, 1, 1, 1, 2],
152
- [ 0, 0, 1, 4, 88, 2, 1, 1, 2, 1],
153
- [ 0, 1, 1, 9, 3, 78, 3, 1, 2, 2],
154
- [ 1, 1, 1, 0, 1, 1, 91, 1, 2, 1],
155
- [ 0, 1, 1, 1, 1, 1, 1, 92, 1, 1],
156
- [ 4, 1, 1, 3, 4, 2, 2, 2, 79, 2],
157
- [ 1, 1, 1, 1, 2, 1, 1, 1, 1, 90]
158
- ];
159
-
160
- // Colors: sequential palette via window.ColorPalettes with graceful fallback
161
- const getSequentialColors = (count) => {
162
- try {
163
- if (window.ColorPalettes && typeof window.ColorPalettes.getColors === 'function') {
164
- return window.ColorPalettes.getColors('sequential', count);
165
- }
166
- } catch (_) {}
167
- // Fallback: generate a monochrome scale using the primary color with varying opacity
168
- const arr = [];
169
- for (let i = 0; i < count; i++) arr.push('var(--primary-color)');
170
- return arr;
171
- };
172
-
173
- const palette = getSequentialColors(13);
174
- const getDivergingColors = (count) => {
175
- try {
176
- if (window.ColorPalettes && typeof window.ColorPalettes.getColors === 'function') {
177
- return window.ColorPalettes.getColors('diverging', count);
178
- }
179
- } catch (_) {}
180
- const steps = Math.max(3, count|0);
181
- const arr = [];
182
- for (let i = 0; i < steps; i++) {
183
- const t = i / (steps - 1);
184
- const pct = Math.round(t * 100);
185
- arr.push(`color-mix(in srgb, #D64545 ${100-pct}%, #3A7BD5 ${pct}%)`);
186
- }
187
- return arr;
188
- };
189
-
190
- let width = 800;
191
- let height = 480;
192
- const margin = { top: 36, right: 24, bottom: 26, left: 56 };
193
-
194
- function updateSize() {
195
- const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
196
- width = container.clientWidth || 800;
197
- const gap = 16; // matches CSS .panels gap
198
- const minPanel = 320;
199
- const nCols = (width >= (minPanel * 2 + gap)) ? 2 : 1;
200
- const panelWidth = nCols === 2 ? Math.max(minPanel, Math.floor((width - gap) / 2)) : Math.max(minPanel, width);
201
- const base = Math.max(minPanel, Math.round(panelWidth * 0.92));
202
- height = base;
203
- // Responsive SVG: width 100%, height auto, preserve aspect via viewBox
204
- svgA
205
- .attr('viewBox', `0 0 ${panelWidth} ${height}`)
206
- .attr('preserveAspectRatio', 'xMidYMid meet')
207
- .style('width', '100%')
208
- .style('height', 'auto');
209
- svgB
210
- .attr('viewBox', `0 0 ${panelWidth} ${height}`)
211
- .attr('preserveAspectRatio', 'xMidYMid meet')
212
- .style('width', '100%')
213
- .style('height', 'auto');
214
- gRootA.attr('transform', `translate(${margin.left},${margin.top})`);
215
- gRootB.attr('transform', `translate(${margin.left},${margin.top})`);
216
- const innerWidth = panelWidth - margin.left - margin.right;
217
- const innerHeight = height - margin.top - margin.bottom;
218
- return { innerWidth, innerHeight, isDark };
219
- }
220
-
221
- function computeValues(normalization, matrix) {
222
- const n = classes.length;
223
- const totalsByRow = matrix.map(row => row.reduce((a, b) => a + b, 0));
224
- const flat = [];
225
- let minV = Infinity, maxV = -Infinity;
226
- for (let r = 0; r < n; r++) {
227
- for (let c = 0; c < n; c++) {
228
- const count = matrix[r][c];
229
- const value = normalization === 'row' ? (totalsByRow[r] ? count / totalsByRow[r] : 0) : count;
230
- if (value < minV) minV = value;
231
- if (value > maxV) maxV = value;
232
- flat.push({ r, c, count, value });
233
- }
234
- }
235
- return { data: flat, minV, maxV };
236
- }
237
-
238
- function getColorScale(values, minV, maxV) {
239
- // If ColorPalettes is available, use quantiles to enhance visual variation across the distribution
240
- const hasPalette = !(palette.length === 0);
241
- if (hasPalette && (window.ColorPalettes && typeof window.ColorPalettes.getColors === 'function')) {
242
- const scale = d3.scaleQuantile().domain(values).range(palette);
243
- return (v) => scale(v);
244
- }
245
- // Fallback: primary color with opacity mapped to normalized value
246
- const norm = d3.scaleLinear().domain([minV, maxV]).range([0.08, 0.9]).clamp(true);
247
- return (v) => `color-mix(in oklab, var(--primary-color) ${Math.round(norm(v) * 100)}%, var(--surface-bg))`;
248
- }
249
-
250
- // (no local contrast function — use ColorPalettes.getTextStyleForBackground)
251
-
252
- // Compute a fixed readable text color from a CSS rgb()/rgba() string
253
- function chooseFixedReadableTextOnBg(bgCss){
254
- try {
255
- const m = String(bgCss||'').match(/rgba?\(([^)]+)\)/);
256
- if (!m) return '#0e1116';
257
- const parts = m[1].split(',').map(s => parseFloat(s.trim()));
258
- const [r, g, b] = parts;
259
- // sRGB → relative luminance
260
- const srgb = [r, g, b].map(v => Math.max(0, Math.min(255, v)) / 255);
261
- const linear = srgb.map(c => (c <= 0.03928 ? c/12.92 : Math.pow((c + 0.055)/1.055, 2.4)));
262
- const L = 0.2126*linear[0] + 0.7152*linear[1] + 0.0722*linear[2];
263
- // Threshold ~ 0.5 for readability; darker BG → white text, else near-black
264
- return L < 0.5 ? '#ffffff' : '#0e1116';
265
- } catch(_) { return '#0e1116'; }
266
- }
267
-
268
- function render() {
269
- const { innerWidth, innerHeight } = updateSize();
270
- const n = classes.length;
271
- const gridSize = Math.min(innerWidth, innerHeight);
272
- const cellSize = gridSize / n;
273
-
274
- const x = d3.scaleBand().domain(d3.range(n)).range([0, gridSize]).paddingInner(0.06);
275
- const y = d3.scaleBand().domain(d3.range(n)).range([0, gridSize]).paddingInner(0.06);
276
-
277
- // Panel A: Baseline (row-normalized)
278
- const dataA = computeValues('row', matrixA);
279
- const colorA = getColorScale(dataA.data.map(d => d.value), dataA.minV, dataA.maxV);
280
-
281
- gCellsA.selectAll('rect.cell-bg')
282
- .data([0])
283
- .join('rect')
284
- .attr('class', 'cell-bg')
285
- .attr('x', 0)
286
- .attr('y', 0)
287
- .attr('width', gridSize)
288
- .attr('height', gridSize)
289
- .attr('fill', 'none')
290
- .attr('stroke', 'var(--border-color)')
291
- .attr('stroke-width', 1);
292
-
293
- const cellsA = gCellsA.selectAll('g.cell')
294
- .data(dataA.data, d => `${d.r}-${d.c}-A`);
295
-
296
- const cellsEnterA = cellsA.enter()
297
- .append('g')
298
- .attr('class', 'cell');
299
-
300
- cellsEnterA.append('rect')
301
- .attr('rx', 2)
302
- .attr('ry', 2)
303
- .on('mousemove', (event, d) => {
304
- const [px, py] = d3.pointer(event, container);
305
- tipInner.innerHTML = `<strong>${classes[d.r]}</strong> → <strong>${classes[d.c]}</strong><br/>${(d.value * 100).toFixed(1)}% (${d.count})`;
306
- tip.style.transform = `translate(${px + 10}px, ${py + 10}px)`;
307
- tip.style.opacity = '1';
308
- })
309
- .on('mouseleave', () => {
310
- tip.style.opacity = '0';
311
- });
312
-
313
- cellsEnterA.append('text')
314
- .attr('class', 'cell-text')
315
- .attr('text-anchor', 'middle')
316
- .attr('dominant-baseline', 'middle');
317
-
318
- const cellsMergedA = cellsEnterA.merge(cellsA);
319
-
320
- cellsMergedA.select('rect')
321
- .attr('x', d => x(d.c))
322
- .attr('y', d => y(d.r))
323
- .attr('width', Math.max(1, x.bandwidth()))
324
- .attr('height', Math.max(1, y.bandwidth()))
325
- .attr('fill', d => colorA(d.value));
326
-
327
- cellsMergedA.select('text')
328
- .attr('x', d => x(d.c) + x.bandwidth() / 2)
329
- .attr('y', d => y(d.r) + y.bandwidth() / 2)
330
- .text(d => `${Math.round(d.value * 100)}`)
331
- .style('fill', function(d){
332
- try {
333
- const rect = this && this.parentNode ? this.parentNode.querySelector('rect') : null;
334
- const bg = rect ? getComputedStyle(rect).fill : colorA(d.value);
335
- return chooseFixedReadableTextOnBg(bg);
336
- } catch (_) {
337
- return '#0e1116';
338
- }
339
- });
340
-
341
- cellsA.exit().remove();
342
-
343
- gAxesA.selectAll('*').remove();
344
-
345
- gAxesA.append('g')
346
- .selectAll('text')
347
- .data(classes)
348
- .join('text')
349
- .attr('class', 'axis-label')
350
- .attr('text-anchor', 'middle')
351
- .attr('x', (_, i) => x(i) + x.bandwidth() / 2)
352
- .attr('y', -8)
353
- .text(d => d);
354
-
355
- gAxesA.append('g')
356
- .selectAll('text')
357
- .data(classes)
358
- .join('text')
359
- .attr('class', 'axis-label')
360
- .attr('text-anchor', 'end')
361
- .attr('x', -8)
362
- .attr('y', (_, i) => y(i) + y.bandwidth() / 2)
363
- .attr('dominant-baseline', 'middle')
364
- .text(d => d);
365
-
366
- gAxesA.append('text')
367
- .attr('class', 'axis-label')
368
- .attr('text-anchor', 'middle')
369
- .attr('x', gridSize / 2)
370
- .attr('y', innerHeight + 20)
371
- .text('Predicted');
372
-
373
- gAxesA.append('text')
374
- .attr('class', 'axis-label')
375
- .attr('text-anchor', 'middle')
376
- .attr('transform', `translate(${-40}, ${gridSize / 2}) rotate(-90)`)
377
- .text('Actual');
378
-
379
- // Panel B: Delta (Improved − Baseline), row-normalized differences in percentage points
380
- const dataB = computeValues('row', matrixB);
381
- const diverging = getDivergingColors(13);
382
- // Build delta values aligned to A's ordering
383
- const mapA = new Map(dataA.data.map(d => [d.r + '-' + d.c, d.value]));
384
- const delta = dataB.data.map(d => ({ r: d.r, c: d.c, count: d.count, value: (d.value - (mapA.get(d.r + '-' + d.c) || 0)) }));
385
- // Symmetric domain around 0 (in proportions), express later as pp in labels
386
- const maxAbsDelta = Math.max(0.01, d3.max(delta, d => Math.abs(d.value)) || 0.01);
387
- const colorB = d3.scaleQuantize().domain([-maxAbsDelta, maxAbsDelta]).range(diverging);
388
-
389
- gCellsB.selectAll('rect.cell-bg')
390
- .data([0])
391
- .join('rect')
392
- .attr('class', 'cell-bg')
393
- .attr('x', 0)
394
- .attr('y', 0)
395
- .attr('width', gridSize)
396
- .attr('height', gridSize)
397
- .attr('fill', 'none')
398
- .attr('stroke', 'var(--border-color)')
399
- .attr('stroke-width', 1);
400
-
401
- const cellsB = gCellsB.selectAll('g.cell')
402
- .data(dataB.data, d => `${d.r}-${d.c}-B`);
403
-
404
- const cellsEnterB = cellsB.enter()
405
- .append('g')
406
- .attr('class', 'cell');
407
-
408
- cellsEnterB.append('rect')
409
- .attr('rx', 2)
410
- .attr('ry', 2)
411
- .on('mousemove', (event, d) => {
412
- const [px, py] = d3.pointer(event, container);
413
- const a = dataA.data.find(x => x.r===d.r && x.c===d.c);
414
- const b = dataB.data.find(x => x.r===d.r && x.c===d.c);
415
- const dv = ((b ? b.value : 0) - (a ? a.value : 0)) * 100;
416
- tipInner.innerHTML = `<strong>${classes[d.r]}</strong> → <strong>${classes[d.c]}</strong>` +
417
- `<br/>baseline ${(a ? a.value*100 : 0).toFixed(1)}%` +
418
- `<br/>improved ${(b ? b.value*100 : 0).toFixed(1)}%` +
419
- `<br/>delta ${dv.toFixed(1)} pp`;
420
- tip.style.transform = `translate(${px + 10}px, ${py + 10}px)`;
421
- tip.style.opacity = '1';
422
- })
423
- .on('mouseleave', () => {
424
- tip.style.opacity = '0';
425
- });
426
-
427
- cellsEnterB.append('text')
428
- .attr('class', 'cell-text')
429
- .attr('text-anchor', 'middle')
430
- .attr('dominant-baseline', 'middle');
431
-
432
- const cellsMergedB = cellsEnterB.merge(cellsB);
433
-
434
- cellsMergedB.select('rect')
435
- .attr('x', d => x(d.c))
436
- .attr('y', d => y(d.r))
437
- .attr('width', Math.max(1, x.bandwidth()))
438
- .attr('height', Math.max(1, y.bandwidth()))
439
- .attr('fill', d => colorB(delta.find(x => x.r===d.r && x.c===d.c).value));
440
-
441
- cellsMergedB.select('text')
442
- .attr('x', d => x(d.c) + x.bandwidth() / 2)
443
- .attr('y', d => y(d.r) + y.bandwidth() / 2)
444
- .text(d => {
445
- const dv = delta.find(x => x.r===d.r && x.c===d.c).value; return `${Math.round(dv * 100)}`;
446
- })
447
- .style('fill', function(d){
448
- try {
449
- const rect = this && this.parentNode ? this.parentNode.querySelector('rect') : null;
450
- const dv = delta.find(x => x.r===d.r && x.c===d.c).value;
451
- const bg = rect ? getComputedStyle(rect).fill : colorB(dv);
452
- return chooseFixedReadableTextOnBg(bg);
453
- } catch (_) {
454
- return '#0e1116';
455
- }
456
- });
457
-
458
- cellsB.exit().remove();
459
-
460
- gAxesB.selectAll('*').remove();
461
-
462
- gAxesB.append('g')
463
- .selectAll('text')
464
- .data(classes)
465
- .join('text')
466
- .attr('class', 'axis-label')
467
- .attr('text-anchor', 'middle')
468
- .attr('x', (_, i) => x(i) + x.bandwidth() / 2)
469
- .attr('y', -8)
470
- .text(d => d);
471
-
472
- gAxesB.append('g')
473
- .selectAll('text')
474
- .data(classes)
475
- .join('text')
476
- .attr('class', 'axis-label')
477
- .attr('text-anchor', 'end')
478
- .attr('x', -8)
479
- .attr('y', (_, i) => y(i) + y.bandwidth() / 2)
480
- .attr('dominant-baseline', 'middle')
481
- .text(d => d);
482
-
483
- gAxesB.append('text')
484
- .attr('class', 'axis-label')
485
- .attr('text-anchor', 'middle')
486
- .attr('x', gridSize / 2)
487
- .attr('y', innerHeight + 20)
488
- .text('Predicted');
489
-
490
- gAxesB.append('text')
491
- .attr('class', 'axis-label')
492
- .attr('text-anchor', 'middle')
493
- .attr('transform', `translate(${-40}, ${gridSize / 2}) rotate(-90)`)
494
- .text('Actual');
495
- }
496
-
497
- // Initial render + resize handling
498
- render();
499
- const rerender = () => render();
500
- if (window.ResizeObserver) {
501
- const ro = new ResizeObserver(() => rerender());
502
- ro.observe(container);
503
- } else {
504
- window.addEventListener('resize', rerender);
505
- }
506
- };
507
-
508
- if (document.readyState === 'loading') {
509
- document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true });
510
- } else {
511
- ensureD3(bootstrap);
512
- }
513
- })();
514
- </script>
515
-
516
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/src/content/embeds/d3-line-quad.html DELETED
@@ -1,783 +0,0 @@
1
- <div class="line-quad">
2
-
3
- <div class="line-quad__grid">
4
- <div class="quad-cell" data-title="Formatting Filter" data-csv="/data/formatting_filters.csv"></div>
5
- <div class="quad-cell" data-title="Relevance Filter" data-csv="/data/relevance_filters.csv"></div>
6
- <div class="quad-cell" data-title="Visual Dependency Filter" data-csv="/data/visual_dependency_filters.csv"></div>
7
- <div class="quad-cell" data-title="Image Correspondence Filter" data-csv="/data/image_correspondence_filters.csv">
8
- </div>
9
- </div>
10
- <noscript>JavaScript is required to render these charts.</noscript>
11
-
12
- </div>
13
- <style>
14
- .line-quad {
15
- position: relative;
16
- }
17
-
18
- /* Axis/tick/grid use global variables from _variables.css */
19
- /* Apply axis/tick/grid purely via CSS */
20
- .line-quad .axes path,
21
- .line-quad .axes line {
22
- stroke: var(--axis-color);
23
- }
24
-
25
- .line-quad .axes text {
26
- fill: var(--tick-color);
27
- }
28
-
29
- .line-quad .grid line {
30
- stroke: var(--grid-color);
31
- }
32
-
33
- .line-quad__grid {
34
- display: grid;
35
- grid-template-columns: repeat(2, minmax(0, 1fr));
36
- gap: 12px;
37
- }
38
-
39
- @media (max-width: 980px) {
40
- .line-quad__grid {
41
- grid-template-columns: 1fr;
42
- }
43
- }
44
-
45
-
46
-
47
- .quad-cell {
48
- border: 1px solid var(--border-color);
49
- border-radius: 10px;
50
- background: var(--surface-bg);
51
- display: flex;
52
- flex-direction: column;
53
- position: relative;
54
- }
55
-
56
- /* Stacking order to ensure hover/tooltip overlays are not hidden by neighbors */
57
- .line-quad__grid .quad-cell:nth-child(1) {
58
- z-index: 4;
59
- }
60
-
61
- /* top-left */
62
- .line-quad__grid .quad-cell:nth-child(3) {
63
- z-index: 3;
64
- }
65
-
66
- /* bottom-left */
67
- .line-quad__grid .quad-cell:nth-child(2) {
68
- z-index: 2;
69
- }
70
-
71
- /* top-right */
72
- .line-quad__grid .quad-cell:nth-child(4) {
73
- z-index: 1;
74
- }
75
-
76
- /* bottom-right */
77
- .quad-cell .cell-header {
78
- padding: 8px 10px;
79
- border-bottom: 1px solid var(--border-color);
80
- display: flex;
81
- align-items: center;
82
- justify-content: space-between;
83
- gap: 8px;
84
- }
85
-
86
- .quad-cell .cell-title {
87
- font-size: 13px;
88
- font-weight: 700;
89
- color: var(--text-color);
90
- }
91
-
92
- .quad-cell .cell-controls {
93
- display: flex;
94
- align-items: center;
95
- gap: 12px;
96
- }
97
-
98
- .quad-cell .cell-controls label {
99
- font-size: 12px;
100
- color: var(--muted-color);
101
- display: flex;
102
- align-items: center;
103
- gap: 6px;
104
- white-space: nowrap;
105
- }
106
-
107
- .quad-cell select {
108
- font-size: 12px;
109
- padding: 6px 28px 6px 10px;
110
- border: 1px solid var(--border-color);
111
- border-radius: 8px;
112
- background-color: var(--surface-bg);
113
- color: var(--text-color);
114
- 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='%230f1115' stroke-width='1' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
115
- background-repeat: no-repeat;
116
- background-position: right 8px center;
117
- background-size: 12px;
118
- -webkit-appearance: none;
119
- appearance: none;
120
- cursor: pointer;
121
- transition: border-color .15s ease, box-shadow .15s ease;
122
- }
123
-
124
- [data-theme="dark"] .quad-cell select {
125
- 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='1' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
126
- }
127
-
128
- .quad-cell select:hover {
129
- border-color: var(--primary-color);
130
- }
131
-
132
- .quad-cell select:focus {
133
- border-color: var(--primary-color);
134
- box-shadow: 0 0 0 3px rgba(232, 137, 171, .25);
135
- outline: none;
136
- }
137
-
138
- .quad-cell .cell-body {
139
- position: relative;
140
- }
141
-
142
- .quad-cell .cell-body {
143
- width: 100%;
144
- overflow: hidden;
145
- }
146
-
147
- .quad-cell .cell-body svg {
148
- max-width: 100%;
149
- height: auto;
150
- }
151
-
152
- .line-quad.hovering .lines path.ghost {
153
- opacity: .25;
154
- }
155
-
156
- .line-quad.hovering .points circle.ghost {
157
- opacity: .25;
158
- }
159
-
160
- .line-quad.hovering .areas path.ghost {
161
- opacity: .08;
162
- }
163
-
164
- .line-quad.hovering .legend-bottom .item.ghost {
165
- opacity: .35;
166
- }
167
-
168
- /* Tooltip refined styling */
169
- .line-quad .d3-tooltip {
170
- z-index: 20;
171
- backdrop-filter: saturate(1.12) blur(8px);
172
- }
173
-
174
- .line-quad .d3-tooltip__inner {
175
- display: flex;
176
- flex-direction: column;
177
- gap: 6px;
178
- min-width: 220px;
179
- }
180
-
181
- .line-quad .d3-tooltip__inner>div:first-child {
182
- font-weight: 800;
183
- letter-spacing: 0.1px;
184
- margin-bottom: 0;
185
- }
186
-
187
- .line-quad .d3-tooltip__inner>div:nth-child(2) {
188
- font-size: 11px;
189
- color: var(--muted-color);
190
- display: block;
191
- margin-top: -4px;
192
- margin-bottom: 2px;
193
- letter-spacing: 0.1px;
194
- }
195
-
196
- .line-quad .d3-tooltip__inner>div:nth-child(n+3) {
197
- padding-top: 6px;
198
- border-top: 1px solid var(--border-color);
199
- }
200
-
201
- .line-quad .d3-tooltip__inner svg {
202
- display: inline-block;
203
- vertical-align: middle;
204
- margin-right: 2px;
205
- }
206
-
207
- .line-quad .d3-tooltip__inner strong {
208
- margin-right: 6px;
209
- }
210
-
211
- .line-quad .d3-tooltip__color-dot {
212
- display: inline-block;
213
- width: 12px;
214
- height: 12px;
215
- border-radius: 3px;
216
- border: 1px solid var(--border-color);
217
- }
218
-
219
- /* Header layout (like d3-line-simple) */
220
- .line-quad__header {
221
- display: flex;
222
- align-items: flex-start;
223
- justify-content: flex-start;
224
- gap: 12px;
225
- margin: 8px 0 0 0;
226
- flex-wrap: wrap;
227
- }
228
-
229
- .line-quad__header .legend-bottom {
230
- display: flex;
231
- flex-direction: column;
232
- align-items: flex-start;
233
- gap: 6px;
234
- font-size: 12px;
235
- color: var(--text-color);
236
- }
237
-
238
- .line-quad__header .legend-bottom .legend-title {
239
- font-size: 12px;
240
- font-weight: 700;
241
- color: var(--text-color);
242
- }
243
-
244
- .line-quad__header .legend-bottom .items {
245
- display: flex;
246
- flex-wrap: wrap;
247
- gap: 8px 14px;
248
- }
249
-
250
- .line-quad__header .legend-bottom .item {
251
- display: inline-flex;
252
- align-items: center;
253
- gap: 6px;
254
- white-space: nowrap;
255
- }
256
-
257
- .line-quad__header .legend-bottom .swatch {
258
- width: 14px;
259
- height: 14px;
260
- border-radius: 3px;
261
- border: 1px solid var(--border-color);
262
- display: inline-block;
263
- }
264
-
265
- .line-quad .controls {
266
- margin-top: 0;
267
- display: flex;
268
- gap: 16px;
269
- align-items: center;
270
- justify-content: flex-end;
271
- width: auto;
272
- flex-wrap: wrap;
273
- }
274
-
275
- .line-quad .controls .control-group {
276
- display: flex;
277
- flex-direction: column;
278
- align-items: flex-start;
279
- gap: 6px;
280
- }
281
-
282
- .line-quad .controls label {
283
- font-size: 12px;
284
- color: var(--text-color);
285
- display: flex;
286
- align-items: center;
287
- gap: 6px;
288
- white-space: nowrap;
289
- font-weight: 700;
290
- }
291
-
292
- .line-quad .controls select {
293
- font-size: 12px;
294
- padding: 8px 28px 8px 10px;
295
- border: 1px solid var(--border-color);
296
- border-radius: 8px;
297
- background-color: var(--surface-bg);
298
- color: var(--text-color);
299
- 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='%230f1115' stroke-width='1' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
300
- background-repeat: no-repeat;
301
- background-position: right 8px center;
302
- background-size: 12px;
303
- -webkit-appearance: none;
304
- appearance: none;
305
- cursor: pointer;
306
- transition: border-color .15s ease, box-shadow .15s ease;
307
- }
308
-
309
- [data-theme="dark"] .line-quad .controls select {
310
- 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='1' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
311
- }
312
-
313
- .line-quad .controls select:hover {
314
- border-color: var(--primary-color);
315
- }
316
-
317
- .line-quad .controls select:focus {
318
- border-color: var(--primary-color);
319
- box-shadow: 0 0 0 3px rgba(232, 137, 171, .25);
320
- outline: none;
321
- }
322
- </style>
323
- <script>
324
- (() => {
325
- const THIS_SCRIPT = document.currentScript;
326
- // Shared run->color mapping to keep legend and series perfectly in sync
327
- let SHARED_RUN_COLOR = null;
328
- // Pretty label mapping for metric keys
329
- const prettyMetricLabel = (key) => {
330
- if (!key) return '';
331
- const table = {
332
- 'ai2d_exact_match': 'AI2D Exact Match',
333
- 'average_rank': 'Average Rank',
334
- };
335
- if (table[key]) return table[key];
336
- const cleaned = String(key).replace(/[_-]+/g, ' ').trim();
337
- return cleaned.split(/\s+/).map(w => {
338
- if (/^(ai2d|umap|id|auc|f1)$/i.test(w)) return w.toUpperCase();
339
- return w.charAt(0).toUpperCase() + w.slice(1);
340
- }).join(' ');
341
- };
342
- const ensureD3 = (cb) => {
343
- if (window.d3 && typeof window.d3.select === 'function') return cb();
344
- let s = document.getElementById('d3-cdn-script');
345
- if (!s) { s = document.createElement('script'); s.id = 'd3-cdn-script'; s.src = 'https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js'; document.head.appendChild(s); }
346
- const onReady = () => { if (window.d3 && typeof window.d3.select === 'function') cb(); };
347
- s.addEventListener('load', onReady, { once: true }); if (window.d3) onReady();
348
- };
349
-
350
- function initRunLine(cell) {
351
- const d3 = window.d3;
352
- const csvPath = cell.getAttribute('data-csv');
353
- const titleText = cell.getAttribute('data-title') || '';
354
-
355
- // Header
356
- const header = document.createElement('div'); header.className = 'cell-header';
357
- const title = document.createElement('div'); title.className = 'cell-title'; title.textContent = titleText; header.appendChild(title);
358
- // Per-cell controls supprimés (contrôle global utilisé)
359
- cell.appendChild(header);
360
-
361
- // Body & SVG
362
- const body = document.createElement('div'); body.className = 'cell-body'; cell.appendChild(body);
363
- const svg = d3.select(body).append('svg').attr('width', '100%').style('display', 'block');
364
- const gRoot = svg.append('g');
365
- const gGrid = gRoot.append('g').attr('class', 'grid');
366
- const gAxes = gRoot.append('g').attr('class', 'axes');
367
- const gAreas = gRoot.append('g').attr('class', 'areas');
368
- const gLines = gRoot.append('g').attr('class', 'lines');
369
- const gPoints = gRoot.append('g').attr('class', 'points');
370
- const gHover = gRoot.append('g').attr('class', 'hover');
371
- // Removed per-cell legend; using global footer legend
372
-
373
- // Tooltip
374
- cell.style.position = cell.style.position || 'relative';
375
- let tip = cell.querySelector('.d3-tooltip'); let tipInner; let hideTipTimer = null;
376
- if (!tip) {
377
- tip = document.createElement('div');
378
- tip.className = 'd3-tooltip';
379
- Object.assign(tip.style, {
380
- position: 'absolute',
381
- top: '0',
382
- left: '0',
383
- transform: 'translate(-9999px,-9999px)',
384
- pointerEvents: 'none',
385
- padding: '10px 12px',
386
- borderRadius: '12px',
387
- fontSize: '12px',
388
- lineHeight: '1.35',
389
- border: '1px solid var(--border-color)',
390
- background: 'var(--surface-bg)',
391
- color: 'var(--text-color)',
392
- boxShadow: '0 8px 32px rgba(0,0,0,.28), 0 2px 8px rgba(0,0,0,.12)',
393
- opacity: '0',
394
- transition: 'opacity .12s ease',
395
- backdropFilter: 'saturate(1.12) blur(8px)'
396
- });
397
- tipInner = document.createElement('div');
398
- tipInner.className = 'd3-tooltip__inner';
399
- tipInner.style.textAlign = 'left';
400
- tip.appendChild(tipInner);
401
- cell.appendChild(tip);
402
- } else { tipInner = tip.querySelector('.d3-tooltip__inner') || tip; }
403
-
404
- // State
405
- let metricList = []; let runList = []; let runOrder = []; const dataByMetric = new Map();
406
- let width = 800, height = 340; const margin = { top: 16, right: 20, bottom: 46, left: 56 };
407
- const xScale = d3.scaleLinear(); const yScale = d3.scaleLinear();
408
- const lineGen = d3.line().x(d => xScale(d.step)).y(d => yScale(d.value));
409
- let isRankStrictFlag = false; let isRankMetricFlag = false; let rankTickMax = 1;
410
- let sharedYConfig = null; // { type: 'rank_strict', maxRank } | { type: 'value', min, max }
411
- let axisLabelY = 'Value';
412
-
413
- // Colors and markers (match original embeds)
414
- const getRunColors = (n) => {
415
- try { if (window.ColorPalettes && typeof window.ColorPalettes.getColors === 'function') return window.ColorPalettes.getColors('categorical', n); } catch (_) { }
416
- const primary = getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim();
417
- return [primary, '#4EA5B7', '#E38A42', '#CEC0FA', ...(d3.schemeTableau10 || [])].slice(0, n);
418
- };
419
- const pool = getRunColors(12);
420
- // Shapes removed: we only use color
421
- // Ready signal for async load completion
422
- let readyResolve = null;
423
- const ready = new Promise((res) => { readyResolve = res; });
424
-
425
- // Shared formatter for thousands: 5000 -> 5k, 1500 -> 1.5k (trim .0)
426
- const formatK = (v) => {
427
- const abs = Math.abs(v);
428
- if (abs >= 1000) {
429
- const n = v / 1000;
430
- const s = d3.format('.1f')(n);
431
- return (s.endsWith('.0') ? s.slice(0, -2) : s) + 'k';
432
- }
433
- return d3.format('d')(v);
434
- };
435
-
436
- function updateScales() {
437
- const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
438
- const axisColor = 'var(--axis-color)';
439
- const tickColor = 'var(--tick-color)';
440
- const gridColor = 'var(--grid-color)';
441
-
442
- const rect = cell.getBoundingClientRect();
443
- width = Math.max(1, Math.round(rect && rect.width ? rect.width : (cell.clientWidth || 800)));
444
- height = Math.max(280, Math.round(width / 2.3));
445
- svg.attr('width', width).attr('height', height).attr('viewBox', `0 0 ${width} ${height}`).attr('preserveAspectRatio', 'xMidYMid meet');
446
- const innerWidth = width - margin.left - margin.right; const innerHeight = height - margin.top - margin.bottom;
447
- gRoot.attr('transform', `translate(${margin.left},${margin.top})`);
448
- xScale.range([0, innerWidth]); yScale.range([innerHeight, 0]);
449
-
450
- // Y ticks
451
- let yTicks = [];
452
- if (isRankStrictFlag) { const maxR = Math.max(1, Math.round(rankTickMax)); for (let v = 1; v <= maxR; v += 1) yTicks.push(v); }
453
- else { yTicks = yScale.ticks(6); }
454
-
455
- // Grid
456
- gGrid.selectAll('*').remove();
457
- gGrid.selectAll('line').data(yTicks).join('line')
458
- .attr('x1', 0).attr('x2', innerWidth).attr('y1', d => yScale(d)).attr('y2', d => yScale(d))
459
- .attr('stroke', gridColor).attr('stroke-width', 1).attr('shape-rendering', 'crispEdges');
460
-
461
- // Axes
462
- gAxes.selectAll('*').remove();
463
- let xAxis = d3.axisBottom(xScale).tickSizeOuter(0); xAxis = xAxis.ticks(8);
464
- xAxis = xAxis.tickFormat(formatK);
465
- const yAxis = d3.axisLeft(yScale).tickValues(yTicks).tickSizeOuter(0).tickFormat(isRankStrictFlag ? d3.format('d') : d3.format('.2f'));
466
- 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'); });
467
- gAxes.append('g').call(yAxis).call(g => { g.selectAll('path, line').attr('stroke', axisColor); g.selectAll('text').attr('fill', tickColor).style('font-size', '11px'); });
468
-
469
- // Axis labels
470
- gAxes.append('text')
471
- .attr('class', 'x-axis-label')
472
- .attr('x', innerWidth / 2)
473
- .attr('y', innerHeight + Math.max(20, Math.min(36, margin.bottom - 10)))
474
- .attr('fill', 'var(--text-color)')
475
- .attr('text-anchor', 'middle')
476
- .style('font-size', '12px')
477
- .style('font-weight', '700')
478
- .text('Steps');
479
-
480
- gAxes.append('text')
481
- .attr('class', 'y-axis-label')
482
- .attr('transform', 'rotate(-90)')
483
- .attr('x', -innerHeight / 2)
484
- .attr('y', -Math.max(16, Math.min(28, margin.left - 8) + 10))
485
- .attr('fill', 'var(--text-color)')
486
- .attr('text-anchor', 'middle')
487
- .style('font-size', '12px')
488
- .style('font-weight', '700')
489
- .text(axisLabelY);
490
-
491
- return { innerWidth, innerHeight, tickColor };
492
- }
493
-
494
- function renderMetric(metricKey) {
495
- const map = dataByMetric.get(metricKey) || {};
496
- const runs = runOrder;
497
- let minStep = Infinity, maxStep = -Infinity, maxVal = 0, minVal = Infinity;
498
- const isRank = /rank/i.test(metricKey); const isAverage = /average/i.test(metricKey); const isRankStrict = isRank && !isAverage;
499
- 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); }); });
500
- if (!isFinite(minStep) || !isFinite(maxStep)) return;
501
- xScale.domain([minStep, maxStep]);
502
- if (sharedYConfig && sharedYConfig.type === 'rank_strict') {
503
- rankTickMax = Math.max(1, Math.round(sharedYConfig.maxRank || 1));
504
- yScale.domain([rankTickMax, 1]);
505
- isRankStrictFlag = true;
506
- isRankMetricFlag = true;
507
- } else if (sharedYConfig && sharedYConfig.type === 'value') {
508
- yScale.domain([sharedYConfig.min, sharedYConfig.max]);
509
- isRankStrictFlag = isRankStrict;
510
- isRankMetricFlag = isRank;
511
- } else {
512
- if (isRank) { rankTickMax = Math.max(1, Math.round(maxVal)); yScale.domain([rankTickMax, 1]); }
513
- else { yScale.domain([minVal, maxVal]).nice(); }
514
- isRankStrictFlag = isRankStrict;
515
- isRankMetricFlag = isRank;
516
- }
517
-
518
- axisLabelY = isRankStrict ? 'Rank' : prettyMetricLabel(metricKey);
519
- const tChange = (window.d3 && d3.transition) ? d3.transition().duration(260).ease(d3.easeCubicOut) : null;
520
- const { innerWidth, innerHeight } = updateScales();
521
-
522
- const colorForRun = (run, idx) => {
523
- if (SHARED_RUN_COLOR && Object.prototype.hasOwnProperty.call(SHARED_RUN_COLOR, run)) return SHARED_RUN_COLOR[run];
524
- const j = (typeof idx === 'number' ? idx : runs.indexOf(run));
525
- return pool[(j >= 0 ? j : 0) % pool.length];
526
- };
527
- const series = runs.map((r, i) => ({ run: r, color: colorForRun(r, i), values: (map[r] || []).slice().sort((a, b) => a.step - b.step).map(pt => isRankStrict ? { step: pt.step, value: Math.round(pt.value), stderr: pt.stderr } : pt) }));
528
-
529
- // zones ± stderr (métriques non rank)
530
- gAreas.selectAll('*').remove();
531
- if (!isRank) {
532
- series.forEach((s) => {
533
- const withErr = s.values.filter(v => v && v.stderr != null && isFinite(v.stderr) && v.stderr > 0 && isFinite(v.value));
534
- if (!withErr.length) return;
535
- const upper = withErr.map(d => [xScale(d.step), yScale(d.value + d.stderr)]);
536
- const lower = withErr.slice().reverse().map(d => [xScale(d.step), yScale(d.value - d.stderr)]);
537
- const coords = upper.concat(lower);
538
- const pathData = d3.line().x(d => d[0]).y(d => d[1]).curve(d3.curveLinearClosed)(coords);
539
- gAreas.append('path')
540
- .attr('class', 'area')
541
- .attr('data-run', s.run)
542
- .attr('d', pathData)
543
- .attr('fill', s.color)
544
- .attr('opacity', 0)
545
- .attr('stroke', 'none')
546
- .transition().duration(450).ease(d3.easeCubicOut)
547
- .attr('opacity', 0.15);
548
- });
549
- }
550
-
551
- const paths = gLines.selectAll('path.run-line').data(series, d => d.run);
552
- paths.enter()
553
- .append('path')
554
- .attr('class', 'run-line')
555
- .attr('data-run', d => d.run)
556
- .attr('fill', 'none')
557
- .attr('stroke-width', 1)
558
- .attr('opacity', 0)
559
- .attr('stroke', d => d.color)
560
- .attr('d', d => lineGen(d.values))
561
- .transition(tChange || undefined)
562
- .attr('opacity', 0.9);
563
- paths
564
- .transition(tChange || undefined)
565
- .attr('stroke', d => d.color)
566
- .attr('opacity', 0.9)
567
- .attr('d', d => lineGen(d.values));
568
- paths.exit().remove();
569
-
570
- // Draw light point markers at each data sample (subtle)
571
- const allPoints = series.flatMap(s => s.values.map(v => ({ run: s.run, color: s.color, step: v.step, value: v.value })));
572
- const ptsSel = gPoints.selectAll('circle.pt').data(allPoints, d => `${d.run}-${d.step}`);
573
- ptsSel.enter().append('circle').attr('class', 'pt')
574
- .attr('data-run', d => d.run)
575
- .attr('r', 1.5)
576
- .attr('fill', d => d.color)
577
- .attr('fill-opacity', 0.6)
578
- .attr('stroke', 'none')
579
- .attr('cx', d => xScale(d.step))
580
- .attr('cy', d => yScale(d.value))
581
- .merge(ptsSel)
582
- .attr('fill', d => d.color)
583
- .transition(tChange || undefined)
584
- .attr('r', 2)
585
- .attr('cx', d => xScale(d.step))
586
- .attr('cy', d => yScale(d.value));
587
- ptsSel.exit().remove();
588
-
589
- // No per-cell legend content (handled globally)
590
-
591
- // Hover
592
- gHover.selectAll('*').remove();
593
- const overlay = gHover.append('rect').attr('fill', 'transparent').style('cursor', 'crosshair').attr('x', 0).attr('y', 0).attr('width', innerWidth).attr('height', innerHeight);
594
- const hoverLine = gHover.append('line').style('stroke', 'var(--text-color)').attr('stroke-opacity', 0.25).attr('stroke-width', 1).attr('y1', 0).attr('y2', innerHeight).style('display', 'none');
595
- 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);
596
- function onMove(ev) {
597
- if (hideTipTimer) { clearTimeout(hideTipTimer); hideTipTimer = null; } const [mx, my] = d3.pointer(ev, overlay.node()); const nearest = steps.reduce((best, s) => Math.abs(s - xScale.invert(mx)) < Math.abs(best - xScale.invert(mx)) ? s : best, steps[0]); const xpx = xScale(nearest); hoverLine.attr('x1', xpx).attr('x2', xpx).style('display', null);
598
- let html = `<div><strong>${titleText}</strong></div><div><strong>step</strong> ${formatK(nearest)}</div>`;
599
- const entries = series.map(s => { const map = new Map(s.values.map(v => [v.step, v])); const pt = map.get(nearest); return { run: s.run, color: s.color, pt }; }).filter(e => e.pt && e.pt.value != null);
600
- entries.sort((a, b) => (a.pt.value - b.pt.value));
601
- const fmt = (vv) => (isRankStrictFlag ? d3.format('d')(vv) : (+vv).toFixed(4));
602
- entries.forEach(e => {
603
- const err = (e.pt.stderr != null && isFinite(e.pt.stderr) && e.pt.stderr > 0) ? ` ± ${fmt(e.pt.stderr)}` : '';
604
- html += `<div style="display:flex;align-items:center;gap:8px;white-space:nowrap;"><span class=\"d3-tooltip__color-dot\" style=\"background:${e.color}\"></span><strong>${e.run}</strong><span style=\"margin-left:auto;text-align:right;\">${fmt(e.pt.value)}${err}</span></div>`;
605
- });
606
- 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)`;
607
- }
608
- function onLeave() { hideTipTimer = setTimeout(() => { tip.style.opacity = '0'; tip.style.transform = 'translate(-9999px, -9999px)'; hoverLine.style('display', 'none'); }, 100); }
609
- overlay.on('mousemove', onMove).on('mouseleave', onLeave);
610
- }
611
-
612
- async function load() {
613
- try {
614
- const file = (csvPath || '').split('/').pop();
615
- const CANDIDATES = [
616
- csvPath,
617
- `/data/${file}`,
618
- `./assets/data/${file}`,
619
- `../assets/data/${file}`,
620
- `../../assets/data/${file}`
621
- ].filter(Boolean);
622
- let text = null;
623
- for (const p of CANDIDATES) {
624
- try { const r = await fetch(p, { cache: 'no-cache' }); if (r.ok) { text = await r.text(); break; } } catch (e) { }
625
- }
626
- if (text == null) throw new Error(`CSV not found: ${file}`);
627
- const rows = d3.csvParse(text, d => ({ run: (d.run || '').trim(), step: +d.step, metric: (d.metric || '').trim(), value: +d.value, stderr: (d.stderr != null && d.stderr !== '') ? +d.stderr : null }));
628
- metricList = Array.from(new Set(rows.map(r => r.metric))).sort();
629
- runList = Array.from(new Set(rows.map(r => r.run))).sort(); runOrder = runList;
630
- 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); });
631
- const preferred = metricList.find(m => m === 'ai2d_exact_match') || metricList.find(m => /average_rank/i.test(m));
632
- const def = preferred || metricList[0];
633
- renderMetric(def);
634
- const ro = window.ResizeObserver ? new ResizeObserver(() => renderMetric(def)) : null; if (ro) ro.observe(cell);
635
- if (typeof readyResolve === 'function') readyResolve();
636
- } catch (e) {
637
- const pre = document.createElement('pre'); pre.textContent = 'CSV load error: ' + (e && e.message ? e.message : e);
638
- pre.style.color = 'var(--danger, #b00020)'; pre.style.fontSize = '12px'; pre.style.whiteSpace = 'pre-wrap'; cell.appendChild(pre);
639
- if (typeof readyResolve === 'function') readyResolve();
640
- }
641
- }
642
- load();
643
-
644
- return {
645
- ready,
646
- getMetrics: () => metricList.slice(),
647
- setMetric: (m) => { if (m) renderMetric(m); },
648
- getYInfo: (m) => {
649
- const key = m; const map = dataByMetric.get(key) || {}; const runs = runOrder;
650
- let maxVal = 0, minVal = Infinity; let minStep = Infinity, maxStep = -Infinity;
651
- const isRank = /rank/i.test(key); const isAverage = /average/i.test(key); const isRankStrict = isRank && !isAverage;
652
- 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); }); });
653
- const rankMax = isRank ? Math.max(1, Math.round(maxVal)) : null;
654
- return { isRank, isRankStrict, min: maxVal === 0 && minVal === Infinity ? null : minVal, max: maxVal, rankMax };
655
- },
656
- setSharedY: (cfg) => { sharedYConfig = cfg || null; if (metricList && metricList.length) { /* re-render last metric if possible */ const current = cfg && cfg.key ? cfg.key : null; const m = current || metricList[0]; renderMetric(m); } }
657
- };
658
- }
659
-
660
- const bootstrap = () => {
661
- const scriptEl = THIS_SCRIPT;
662
- let host = null;
663
- // Build header (legend + controls) and append after grid
664
- const header = document.createElement('div'); header.className = 'line-quad__header';
665
- const legend = document.createElement('div'); legend.className = 'legend-bottom'; legend.innerHTML = '<div class="legend-title">Legend</div><div class="items"></div>';
666
- const controls = document.createElement('div'); controls.className = 'controls'; controls.innerHTML = '<div class="control-group"><label>Metric</label><select></select></div>';
667
- header.appendChild(legend);
668
- header.appendChild(controls);
669
- // Try finding within parent (fragment mount is inside parent)
670
- if (scriptEl && scriptEl.parentElement && scriptEl.parentElement.querySelector) {
671
- host = scriptEl.parentElement.querySelector('.line-quad');
672
- }
673
- // Fallback: scan previous siblings
674
- if (!host) {
675
- let sib = scriptEl && scriptEl.previousElementSibling;
676
- while (sib && !(sib.classList && sib.classList.contains('line-quad'))) {
677
- sib = sib.previousElementSibling;
678
- }
679
- host = sib || null;
680
- }
681
- // Last resort: global query
682
- if (!host) { host = document.querySelector('.line-quad'); }
683
- if (!host) return;
684
- if (host.dataset && host.dataset.mounted === 'true') return; if (host.dataset) host.dataset.mounted = 'true';
685
- const cells = host.querySelectorAll('.quad-cell'); if (!cells.length) return;
686
- host.appendChild(header);
687
- const instances = Array.from(cells).map(cell => initRunLine(cell));
688
-
689
- (async () => {
690
- // Wait for all charts to finish loading their CSVs
691
- await Promise.all(instances.map(i => i.ready));
692
- const lists = instances.map(i => i.getMetrics()).filter(a => Array.isArray(a) && a.length);
693
- const intersect = (arrs) => arrs.reduce((acc, cur) => acc.filter(x => cur.includes(x)));
694
- let metrics = lists.length ? intersect(lists) : [];
695
- if (!metrics.length) { metrics = lists[0] || []; }
696
- const def = (metrics.includes('ai2d_exact_match') ? 'ai2d_exact_match' : (metrics.find(m => /average_rank/i.test(m)) || metrics[0] || ''));
697
-
698
- // Wire header controls (select under "Metric" label)
699
- const headerEl = host.querySelector('.line-quad__header');
700
- if (headerEl && !headerEl.isConnected) host.appendChild(header);
701
- const select = (headerEl || header).querySelector('.controls select');
702
- if (select) {
703
- select.innerHTML = '';
704
- metrics.forEach(m => { const o = document.createElement('option'); o.value = m; o.textContent = prettyMetricLabel(m); select.appendChild(o); });
705
- if (def) select.value = def;
706
- }
707
-
708
- const computeAndApplySharedY = (metric) => {
709
- try {
710
- const infos = instances.map(i => i && typeof i.getYInfo === 'function' ? i.getYInfo(metric) : null).filter(Boolean);
711
- if (!infos.length) return;
712
- const anyRank = infos.some(info => info.isRank);
713
- if (anyRank) {
714
- const maxRank = Math.max(1, ...infos.map(info => Math.round(info.rankMax || 1)));
715
- instances.forEach(i => i && typeof i.setSharedY === 'function' && i.setSharedY({ type: 'rank_strict', maxRank, key: metric }));
716
- } else {
717
- const min = Math.min(...infos.map(info => info.min));
718
- const max = Math.max(...infos.map(info => info.max));
719
- instances.forEach(i => i && typeof i.setSharedY === 'function' && i.setSharedY({ type: 'value', min, max, key: metric }));
720
- }
721
- } catch (_) { }
722
- };
723
-
724
- const applyAll = (v) => { computeAndApplySharedY(v); instances.forEach(i => i && typeof i.setMetric === 'function' && i.setMetric(v)); };
725
- if (def) applyAll(def);
726
- if (select) select.addEventListener('change', () => applyAll(select.value));
727
-
728
- // Global legend (in header, colors only)
729
- const legendItemsHost = (headerEl || header).querySelector('.legend-bottom .items');
730
- if (legendItemsHost) {
731
- try {
732
- const f = '/data/formatting_filters.csv';
733
- const r = await fetch(f, { cache: 'no-cache' });
734
- if (r.ok && window.d3 && window.d3.csvParse) {
735
- const txt = await r.text();
736
- const rows = window.d3.csvParse(txt);
737
- const runList = Array.from(new Set(rows.map(row => String(row.run || '').trim()).filter(Boolean))).sort();
738
- const poolLegend = (window.ColorPalettes && typeof window.ColorPalettes.getColors === 'function')
739
- ? window.ColorPalettes.getColors('categorical', runList.length)
740
- : (() => { const primary = getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim(); return [primary, '#4EA5B7', '#E38A42', '#CEC0FA', ...((window.d3 && window.d3.schemeTableau10) ? window.d3.schemeTableau10 : ['#4e79a7', '#f28e2b', '#e15759', '#76b7b2', '#59a14f', '#edc948', '#b07aa1', '#ff9da7', '#9c755f', '#bab0ab'])]; })();
741
- // Build shared run->color map once
742
- SHARED_RUN_COLOR = {};
743
- runList.forEach((name, i) => { SHARED_RUN_COLOR[name] = poolLegend[i % poolLegend.length]; });
744
- legendItemsHost.innerHTML = runList.map((name) => {
745
- const color = SHARED_RUN_COLOR[name];
746
- return `<span class="item" data-run="${name}"><span class=\"swatch\" style=\"background:${color}\"></span><span>${name}</span></span>`;
747
- }).join('');
748
- // Re-render all cells with the shared mapping to ensure perfect sync
749
- try {
750
- const currentMetric = (select && select.value) || def;
751
- if (currentMetric) applyAll(currentMetric);
752
- } catch { }
753
- // Legend hover ghosting across all cells
754
- legendItemsHost.querySelectorAll('.item').forEach(el => {
755
- el.addEventListener('mouseenter', () => {
756
- const run = el.getAttribute('data-run'); if (!run) return;
757
- host.classList.add('hovering');
758
- host.querySelectorAll('.quad-cell').forEach(cell => {
759
- cell.querySelectorAll('.lines path.run-line').forEach(p => p.classList.toggle('ghost', p.getAttribute('data-run') !== run));
760
- cell.querySelectorAll('.points circle.pt').forEach(c => c.classList.toggle('ghost', c.getAttribute('data-run') !== run));
761
- cell.querySelectorAll('.areas path.area').forEach(a => a.classList.toggle('ghost', a.getAttribute('data-run') !== run));
762
- });
763
- legendItemsHost.querySelectorAll('.item').forEach(it => it.classList.toggle('ghost', it.getAttribute('data-run') !== run));
764
- });
765
- el.addEventListener('mouseleave', () => {
766
- host.classList.remove('hovering');
767
- host.querySelectorAll('.quad-cell').forEach(cell => {
768
- cell.querySelectorAll('.lines path.run-line').forEach(p => p.classList.remove('ghost'));
769
- cell.querySelectorAll('.points circle.pt').forEach(c => c.classList.remove('ghost'));
770
- cell.querySelectorAll('.areas path.area').forEach(a => a.classList.remove('ghost'));
771
- });
772
- legendItemsHost.querySelectorAll('.item').forEach(it => it.classList.remove('ghost'));
773
- });
774
- });
775
- }
776
- } catch { }
777
- }
778
- })();
779
- };
780
-
781
- if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true }); } else { ensureD3(bootstrap); }
782
- })();
783
- </script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/src/content/embeds/d3-matrix.html DELETED
@@ -1,524 +0,0 @@
1
- <div class="d3-matrix"></div>
2
- <style>
3
- .d3-matrix {
4
- position: relative;
5
- }
6
-
7
- .d3-matrix .panels {
8
- display: flex;
9
- flex-wrap: wrap;
10
- gap: 16px;
11
- margin-bottom: 4px;
12
- }
13
-
14
- .d3-matrix .panel {
15
- flex: 1 1 320px;
16
- min-width: 280px;
17
- }
18
-
19
- .d3-matrix .panel__title {
20
- color: var(--text-color);
21
- font-size: 12px;
22
- line-height: 1.35;
23
- margin: 0 0 6px 0;
24
- font-weight: 600;
25
- }
26
-
27
- .d3-matrix .axis-label {
28
- fill: var(--text-color);
29
- font-size: 11px;
30
- font-weight: 700;
31
- }
32
-
33
- .d3-matrix .cell-border {
34
- stroke: var(--border-color);
35
- stroke-width: 1px;
36
- fill: none;
37
- }
38
-
39
- .d3-matrix .cell-text {
40
- fill: var(--muted-color);
41
- font-size: 11px;
42
- pointer-events: none;
43
- }
44
-
45
- .d3-matrix .chart-card {
46
- background: var(--surface-bg);
47
- border: 1px solid var(--border-color);
48
- border-radius: 10px;
49
- padding: 8px;
50
- }
51
- </style>
52
- <script>
53
- (() => {
54
- // Load D3 from CDN once
55
- const ensureD3 = (cb) => {
56
- if (window.d3 && typeof window.d3.select === 'function') return cb();
57
- let s = document.getElementById('d3-cdn-script');
58
- if (!s) {
59
- s = document.createElement('script');
60
- s.id = 'd3-cdn-script';
61
- s.src = 'https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js';
62
- document.head.appendChild(s);
63
- }
64
- const onReady = () => { if (window.d3 && typeof window.d3.select === 'function') cb(); };
65
- s.addEventListener('load', onReady, { once: true });
66
- if (window.d3) onReady();
67
- };
68
-
69
- const bootstrap = () => {
70
- const scriptEl = document.currentScript;
71
- let container = scriptEl ? scriptEl.previousElementSibling : null;
72
- if (!(container && container.classList && container.classList.contains('d3-matrix'))) {
73
- const cs = Array.from(document.querySelectorAll('.d3-matrix')).filter(el => !(el.dataset && el.dataset.mounted === 'true'));
74
- container = cs[cs.length - 1] || null;
75
- }
76
- if (!container) return;
77
- if (container.dataset) {
78
- if (container.dataset.mounted === 'true') return;
79
- container.dataset.mounted = 'true';
80
- }
81
-
82
- // Tooltip (HTML, single instance inside container)
83
- container.style.position = container.style.position || 'relative';
84
- let tip = container.querySelector('.d3-tooltip');
85
- let tipInner;
86
- if (!tip) {
87
- tip = document.createElement('div');
88
- tip.className = 'd3-tooltip';
89
- Object.assign(tip.style, {
90
- position: 'absolute',
91
- top: '0px',
92
- left: '0px',
93
- transform: 'translate(-9999px, -9999px)',
94
- pointerEvents: 'none',
95
- padding: '8px 10px',
96
- borderRadius: '8px',
97
- fontSize: '12px',
98
- lineHeight: '1.35',
99
- border: '1px solid var(--border-color)',
100
- background: 'var(--surface-bg)',
101
- color: 'var(--text-color)',
102
- boxShadow: '0 4px 24px rgba(0,0,0,.18)',
103
- opacity: '0',
104
- transition: 'opacity .12s ease'
105
- });
106
- tipInner = document.createElement('div');
107
- tipInner.className = 'd3-tooltip__inner';
108
- tipInner.style.textAlign = 'left';
109
- tip.appendChild(tipInner);
110
- container.appendChild(tip);
111
- } else {
112
- tipInner = tip.querySelector('.d3-tooltip__inner') || tip;
113
- }
114
-
115
- // Panels container (two side-by-side matrices)
116
- const panels = document.createElement('div');
117
- panels.className = 'panels';
118
- const panelA = document.createElement('div');
119
- panelA.className = 'panel';
120
- const titleA = document.createElement('div'); titleA.className = 'panel__title'; titleA.textContent = 'Baseline (row-normalized %)';
121
- panelA.appendChild(titleA);
122
- const mountA = document.createElement('div'); panelA.appendChild(mountA);
123
- const panelB = document.createElement('div');
124
- panelB.className = 'panel';
125
- const titleB = document.createElement('div'); titleB.className = 'panel__title'; titleB.textContent = 'Delta (Improved − Baseline, pp)';
126
- panelB.appendChild(titleB);
127
- const mountB = document.createElement('div'); panelB.appendChild(mountB);
128
- panels.appendChild(panelA);
129
- panels.appendChild(panelB);
130
- container.appendChild(panels);
131
-
132
- // SVG scaffolding
133
- const cardA = document.createElement('div'); cardA.className = 'chart-card'; mountA.appendChild(cardA);
134
- const svgA = d3.select(cardA).append('svg').attr('width', '100%').style('display', 'block');
135
- const gRootA = svgA.append('g');
136
- const gCellsA = gRootA.append('g');
137
- const gAxesA = gRootA.append('g');
138
- const cardB = document.createElement('div'); cardB.className = 'chart-card'; mountB.appendChild(cardB);
139
- const svgB = d3.select(cardB).append('svg').attr('width', '100%').style('display', 'block');
140
- const gRootB = svgB.append('g');
141
- const gCellsB = gRootB.append('g');
142
- const gAxesB = gRootB.append('g');
143
-
144
- // Demo data (two distinct 10x10 matrices: Baseline vs Improved)
145
- // Rows / Columns are generic class labels
146
- const classes = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
147
- const matrixA = [
148
- [90, 2, 1, 0, 0, 0, 1, 0, 5, 1],
149
- [3, 85, 5, 1, 0, 1, 2, 1, 1, 1],
150
- [1, 6, 70, 10, 4, 4, 1, 1, 1, 2],
151
- [0, 1, 8, 65, 10, 10, 2, 1, 1, 2],
152
- [0, 0, 2, 6, 83, 3, 1, 1, 3, 1],
153
- [0, 1, 2, 12, 4, 70, 5, 2, 2, 2],
154
- [1, 2, 1, 0, 1, 2, 88, 1, 3, 1],
155
- [0, 1, 1, 1, 1, 1, 2, 90, 1, 2],
156
- [6, 2, 2, 4, 6, 3, 3, 2, 70, 2],
157
- [1, 1, 1, 1, 2, 1, 1, 2, 1, 89]
158
- ];
159
- const matrixB = [
160
- [94, 1, 0, 0, 0, 0, 1, 0, 3, 1],
161
- [2, 90, 3, 1, 0, 0, 1, 1, 1, 1],
162
- [1, 4, 78, 7, 3, 3, 1, 1, 1, 1],
163
- [0, 1, 5, 74, 7, 8, 1, 1, 1, 2],
164
- [0, 0, 1, 4, 88, 2, 1, 1, 2, 1],
165
- [0, 1, 1, 9, 3, 78, 3, 1, 2, 2],
166
- [1, 1, 1, 0, 1, 1, 91, 1, 2, 1],
167
- [0, 1, 1, 1, 1, 1, 1, 92, 1, 1],
168
- [4, 1, 1, 3, 4, 2, 2, 2, 79, 2],
169
- [1, 1, 1, 1, 2, 1, 1, 1, 1, 90]
170
- ];
171
-
172
- // Colors: sequential palette via window.ColorPalettes with graceful fallback
173
- const getSequentialColors = (count) => {
174
- try {
175
- if (window.ColorPalettes && typeof window.ColorPalettes.getColors === 'function') {
176
- return window.ColorPalettes.getColors('sequential', count);
177
- }
178
- } catch (_) { }
179
- // Fallback: generate a monochrome scale using the primary color with varying opacity
180
- const arr = [];
181
- for (let i = 0; i < count; i++) arr.push('var(--primary-color)');
182
- return arr;
183
- };
184
-
185
- const palette = getSequentialColors(13);
186
- const getDivergingColors = (count) => {
187
- try {
188
- if (window.ColorPalettes && typeof window.ColorPalettes.getColors === 'function') {
189
- return window.ColorPalettes.getColors('diverging', count);
190
- }
191
- } catch (_) { }
192
- const steps = Math.max(3, count | 0);
193
- const arr = [];
194
- for (let i = 0; i < steps; i++) {
195
- const t = i / (steps - 1);
196
- const pct = Math.round(t * 100);
197
- arr.push(`color-mix(in srgb, #D64545 ${100 - pct}%, #3A7BD5 ${pct}%)`);
198
- }
199
- return arr;
200
- };
201
-
202
- let width = 800;
203
- let height = 480;
204
- const margin = { top: 36, right: 24, bottom: 26, left: 56 };
205
-
206
- function updateSize() {
207
- const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
208
- width = container.clientWidth || 800;
209
- const gap = 16; // matches CSS .panels gap
210
- const minPanel = 320;
211
- const nCols = (width >= (minPanel * 2 + gap)) ? 2 : 1;
212
- const panelWidth = nCols === 2 ? Math.max(minPanel, Math.floor((width - gap) / 2)) : Math.max(minPanel, width);
213
- const base = Math.max(minPanel, Math.round(panelWidth * 0.92));
214
- height = base;
215
- // Responsive SVG: width 100%, height auto, preserve aspect via viewBox
216
- svgA
217
- .attr('viewBox', `0 0 ${panelWidth} ${height}`)
218
- .attr('preserveAspectRatio', 'xMidYMid meet')
219
- .style('width', '100%')
220
- .style('height', 'auto');
221
- svgB
222
- .attr('viewBox', `0 0 ${panelWidth} ${height}`)
223
- .attr('preserveAspectRatio', 'xMidYMid meet')
224
- .style('width', '100%')
225
- .style('height', 'auto');
226
- gRootA.attr('transform', `translate(${margin.left},${margin.top})`);
227
- gRootB.attr('transform', `translate(${margin.left},${margin.top})`);
228
- const innerWidth = panelWidth - margin.left - margin.right;
229
- const innerHeight = height - margin.top - margin.bottom;
230
- return { innerWidth, innerHeight, isDark };
231
- }
232
-
233
- function computeValues(normalization, matrix) {
234
- const n = classes.length;
235
- const totalsByRow = matrix.map(row => row.reduce((a, b) => a + b, 0));
236
- const flat = [];
237
- let minV = Infinity, maxV = -Infinity;
238
- for (let r = 0; r < n; r++) {
239
- for (let c = 0; c < n; c++) {
240
- const count = matrix[r][c];
241
- const value = normalization === 'row' ? (totalsByRow[r] ? count / totalsByRow[r] : 0) : count;
242
- if (value < minV) minV = value;
243
- if (value > maxV) maxV = value;
244
- flat.push({ r, c, count, value });
245
- }
246
- }
247
- return { data: flat, minV, maxV };
248
- }
249
-
250
- function getColorScale(values, minV, maxV) {
251
- // If ColorPalettes is available, use quantiles to enhance visual variation across the distribution
252
- const hasPalette = !(palette.length === 0);
253
- if (hasPalette && (window.ColorPalettes && typeof window.ColorPalettes.getColors === 'function')) {
254
- const scale = d3.scaleQuantile().domain(values).range(palette);
255
- return (v) => scale(v);
256
- }
257
- // Fallback: primary color with opacity mapped to normalized value
258
- const norm = d3.scaleLinear().domain([minV, maxV]).range([0.08, 0.9]).clamp(true);
259
- return (v) => `color-mix(in oklab, var(--primary-color) ${Math.round(norm(v) * 100)}%, var(--surface-bg))`;
260
- }
261
-
262
- // Compute a fixed readable text color from a CSS rgb()/rgba() string
263
- function chooseFixedReadableTextOnBg(bgCss) {
264
- try {
265
- const m = String(bgCss || '').match(/rgba?\(([^)]+)\)/);
266
- if (!m) return '#0e1116';
267
- const parts = m[1].split(',').map(s => parseFloat(s.trim()));
268
- const [r, g, b] = parts;
269
- // sRGB → relative luminance
270
- const srgb = [r, g, b].map(v => Math.max(0, Math.min(255, v)) / 255);
271
- const linear = srgb.map(c => (c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4)));
272
- const L = 0.2126 * linear[0] + 0.7152 * linear[1] + 0.0722 * linear[2];
273
- // Threshold ~ 0.5 for readability; darker BG → white text, else near-black
274
- return L < 0.5 ? '#ffffff' : '#0e1116';
275
- } catch (_) { return '#0e1116'; }
276
- }
277
-
278
- function render() {
279
- const { innerWidth, innerHeight } = updateSize();
280
- const n = classes.length;
281
- const gridSize = Math.min(innerWidth, innerHeight);
282
- const cellSize = gridSize / n;
283
-
284
- const x = d3.scaleBand().domain(d3.range(n)).range([0, gridSize]).paddingInner(0.06);
285
- const y = d3.scaleBand().domain(d3.range(n)).range([0, gridSize]).paddingInner(0.06);
286
-
287
- // Panel A: Baseline (row-normalized)
288
- const dataA = computeValues('row', matrixA);
289
- const colorA = getColorScale(dataA.data.map(d => d.value), dataA.minV, dataA.maxV);
290
-
291
- gCellsA.selectAll('rect.cell-bg')
292
- .data([0])
293
- .join('rect')
294
- .attr('class', 'cell-bg')
295
- .attr('x', 0)
296
- .attr('y', 0)
297
- .attr('width', gridSize)
298
- .attr('height', gridSize)
299
- .attr('fill', 'none')
300
- .attr('stroke', 'var(--border-color)')
301
- .attr('stroke-width', 1);
302
-
303
- const cellsA = gCellsA.selectAll('g.cell')
304
- .data(dataA.data, d => `${d.r}-${d.c}-A`);
305
-
306
- const cellsEnterA = cellsA.enter()
307
- .append('g')
308
- .attr('class', 'cell');
309
-
310
- cellsEnterA.append('rect')
311
- .attr('rx', 2)
312
- .attr('ry', 2)
313
- .on('mousemove', (event, d) => {
314
- const [px, py] = d3.pointer(event, container);
315
- tipInner.innerHTML = `<strong>${classes[d.r]}</strong> → <strong>${classes[d.c]}</strong><br/>${(d.value * 100).toFixed(1)}% (${d.count})`;
316
- tip.style.transform = `translate(${px + 10}px, ${py + 10}px)`;
317
- tip.style.opacity = '1';
318
- })
319
- .on('mouseleave', () => {
320
- tip.style.opacity = '0';
321
- });
322
-
323
- cellsEnterA.append('text')
324
- .attr('class', 'cell-text')
325
- .attr('text-anchor', 'middle')
326
- .attr('dominant-baseline', 'middle');
327
-
328
- const cellsMergedA = cellsEnterA.merge(cellsA);
329
-
330
- cellsMergedA.select('text')
331
- .attr('x', d => x(d.c) + x.bandwidth() / 2)
332
- .attr('y', d => y(d.r) + y.bandwidth() / 2)
333
- .text(d => `${Math.round(d.value * 100)}`)
334
- .style('fill', function (d) {
335
- try {
336
- const rect = this && this.parentNode ? this.parentNode.querySelector('rect') : null;
337
- const bg = rect ? getComputedStyle(rect).fill : colorA(d.value);
338
- return chooseFixedReadableTextOnBg(bg);
339
- } catch (_) {
340
- return '#0e1116';
341
- }
342
- });
343
-
344
- cellsMergedA.select('rect')
345
- .attr('x', d => x(d.c))
346
- .attr('y', d => y(d.r))
347
- .attr('width', Math.max(1, x.bandwidth()))
348
- .attr('height', Math.max(1, y.bandwidth()))
349
- .attr('fill', d => colorA(d.value));
350
-
351
- cellsA.exit().remove();
352
-
353
- gAxesA.selectAll('*').remove();
354
-
355
- gAxesA.append('g')
356
- .selectAll('text')
357
- .data(classes)
358
- .join('text')
359
- .attr('class', 'axis-label')
360
- .attr('text-anchor', 'middle')
361
- .attr('x', (_, i) => x(i) + x.bandwidth() / 2)
362
- .attr('y', -8)
363
- .text(d => d);
364
-
365
- gAxesA.append('g')
366
- .selectAll('text')
367
- .data(classes)
368
- .join('text')
369
- .attr('class', 'axis-label')
370
- .attr('text-anchor', 'end')
371
- .attr('x', -8)
372
- .attr('y', (_, i) => y(i) + y.bandwidth() / 2)
373
- .attr('dominant-baseline', 'middle')
374
- .text(d => d);
375
-
376
- gAxesA.append('text')
377
- .attr('class', 'axis-label')
378
- .attr('text-anchor', 'middle')
379
- .attr('x', gridSize / 2)
380
- .attr('y', innerHeight + 20)
381
- .text('Columns');
382
-
383
- gAxesA.append('text')
384
- .attr('class', 'axis-label')
385
- .attr('text-anchor', 'middle')
386
- .attr('transform', `translate(${-40}, ${gridSize / 2}) rotate(-90)`)
387
- .text('Rows');
388
-
389
- // Panel B: Delta (Improved − Baseline), row-normalized differences in percentage points
390
- const dataB = computeValues('row', matrixB);
391
- const diverging = getDivergingColors(13);
392
- // Build delta values aligned to A's ordering
393
- const mapA = new Map(dataA.data.map(d => [d.r + '-' + d.c, d.value]));
394
- const delta = dataB.data.map(d => ({ r: d.r, c: d.c, count: d.count, value: (d.value - (mapA.get(d.r + '-' + d.c) || 0)) }));
395
- // Symmetric domain around 0 (in proportions), express later as pp in labels
396
- const maxAbsDelta = Math.max(0.01, d3.max(delta, d => Math.abs(d.value)) || 0.01);
397
- const colorB = d3.scaleQuantize().domain([-maxAbsDelta / 2, maxAbsDelta]).range(diverging);
398
-
399
- gCellsB.selectAll('rect.cell-bg')
400
- .data([0])
401
- .join('rect')
402
- .attr('class', 'cell-bg')
403
- .attr('x', 0)
404
- .attr('y', 0)
405
- .attr('width', gridSize)
406
- .attr('height', gridSize)
407
- .attr('fill', 'none')
408
- .attr('stroke', 'var(--border-color)')
409
- .attr('stroke-width', 1);
410
-
411
- const cellsB = gCellsB.selectAll('g.cell')
412
- .data(dataB.data, d => `${d.r}-${d.c}-B`);
413
-
414
- const cellsEnterB = cellsB.enter()
415
- .append('g')
416
- .attr('class', 'cell');
417
-
418
- cellsEnterB.append('rect')
419
- .attr('rx', 2)
420
- .attr('ry', 2)
421
- .on('mousemove', (event, d) => {
422
- const [px, py] = d3.pointer(event, container);
423
- const a = dataA.data.find(x => x.r === d.r && x.c === d.c);
424
- const b = dataB.data.find(x => x.r === d.r && x.c === d.c);
425
- const dv = ((b ? b.value : 0) - (a ? a.value : 0)) * 100;
426
- tipInner.innerHTML = `<strong>${classes[d.r]}</strong> → <strong>${classes[d.c]}</strong>` +
427
- `<br/>baseline ${(a ? a.value * 100 : 0).toFixed(1)}%` +
428
- `<br/>improved ${(b ? b.value * 100 : 0).toFixed(1)}%` +
429
- `<br/>delta ${dv.toFixed(1)} pp`;
430
- tip.style.transform = `translate(${px + 10}px, ${py + 10}px)`;
431
- tip.style.opacity = '1';
432
- })
433
- .on('mouseleave', () => {
434
- tip.style.opacity = '0';
435
- });
436
-
437
- cellsEnterB.append('text')
438
- .attr('class', 'cell-text')
439
- .attr('text-anchor', 'middle')
440
- .attr('dominant-baseline', 'middle');
441
-
442
- const cellsMergedB = cellsEnterB.merge(cellsB);
443
-
444
- cellsMergedB.select('rect')
445
- .attr('x', d => x(d.c))
446
- .attr('y', d => y(d.r))
447
- .attr('width', Math.max(1, x.bandwidth()))
448
- .attr('height', Math.max(1, y.bandwidth()))
449
- .attr('fill', d => colorB(delta.find(x => x.r === d.r && x.c === d.c).value));
450
-
451
- cellsMergedB.select('text')
452
- .attr('x', d => x(d.c) + x.bandwidth() / 2)
453
- .attr('y', d => y(d.r) + y.bandwidth() / 2)
454
- .text(d => {
455
- const dv = delta.find(x => x.r === d.r && x.c === d.c).value; return `${Math.round(dv * 100)}`;
456
- })
457
- .style('fill', function (d) {
458
- try {
459
- const rect = this && this.parentNode ? this.parentNode.querySelector('rect') : null;
460
- const dv = delta.find(x => x.r === d.r && x.c === d.c).value;
461
- const bg = rect ? getComputedStyle(rect).fill : colorB(dv);
462
- return chooseFixedReadableTextOnBg(bg);
463
- } catch (_) {
464
- return '#0e1116';
465
- }
466
- });
467
-
468
- cellsB.exit().remove();
469
-
470
- gAxesB.selectAll('*').remove();
471
-
472
- gAxesB.append('g')
473
- .selectAll('text')
474
- .data(classes)
475
- .join('text')
476
- .attr('class', 'axis-label')
477
- .attr('text-anchor', 'middle')
478
- .attr('x', (_, i) => x(i) + x.bandwidth() / 2)
479
- .attr('y', -8)
480
- .text(d => d);
481
-
482
- gAxesB.append('g')
483
- .selectAll('text')
484
- .data(classes)
485
- .join('text')
486
- .attr('class', 'axis-label')
487
- .attr('text-anchor', 'end')
488
- .attr('x', -8)
489
- .attr('y', (_, i) => y(i) + y.bandwidth() / 2)
490
- .attr('dominant-baseline', 'middle')
491
- .text(d => d);
492
-
493
- gAxesB.append('text')
494
- .attr('class', 'axis-label')
495
- .attr('text-anchor', 'middle')
496
- .attr('x', gridSize / 2)
497
- .attr('y', innerHeight + 20)
498
- .text('Columns');
499
-
500
- gAxesB.append('text')
501
- .attr('class', 'axis-label')
502
- .attr('text-anchor', 'middle')
503
- .attr('transform', `translate(${-40}, ${gridSize / 2}) rotate(-90)`)
504
- .text('Rows');
505
- }
506
-
507
- // Initial render + resize handling
508
- render();
509
- const rerender = () => render();
510
- if (window.ResizeObserver) {
511
- const ro = new ResizeObserver(() => rerender());
512
- ro.observe(container);
513
- } else {
514
- window.addEventListener('resize', rerender);
515
- }
516
- };
517
-
518
- if (document.readyState === 'loading') {
519
- document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true });
520
- } else {
521
- ensureD3(bootstrap);
522
- }
523
- })();
524
- </script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/src/content/embeds/d3-scatter.html DELETED
@@ -1,300 +0,0 @@
1
- <div class="d3-scatter" ></div>
2
- <style>
3
- /* Frameless: no controls, no axes, only dots */
4
- .d3-scatter svg { display: block; }
5
- /* Tooltip refined styling (align with filters-quad) */
6
- .d3-scatter .d3-tooltip {
7
- z-index: 20;
8
- backdrop-filter: saturate(1.12) blur(8px);
9
- }
10
- .d3-scatter .d3-tooltip__inner {
11
- display: flex;
12
- flex-direction: column;
13
- gap: 6px;
14
- min-width: 200px;
15
- }
16
- .d3-scatter .d3-tooltip__inner > div:first-child {
17
- font-weight: 800;
18
- letter-spacing: 0.1px;
19
- margin-bottom: 0;
20
- }
21
- .d3-scatter .d3-tooltip__inner > div:nth-child(2) {
22
- font-size: 11px;
23
- color: var(--muted-color);
24
- display: block;
25
- margin-top: -4px;
26
- margin-bottom: 2px;
27
- letter-spacing: 0.1px;
28
- }
29
- .d3-scatter .d3-tooltip__inner > div:nth-child(n+3) {
30
- padding-top: 6px;
31
- border-top: 1px solid var(--border-color);
32
- }
33
- </style>
34
- <script>
35
- (() => {
36
- const ensureD3 = (cb) => {
37
- if (window.d3 && typeof window.d3.select === 'function') return cb();
38
- let s = document.getElementById('d3-cdn-script');
39
- if (!s) { s = document.createElement('script'); s.id = 'd3-cdn-script'; s.src = 'https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js'; document.head.appendChild(s); }
40
- const onReady = () => { if (window.d3 && typeof window.d3.select === 'function') cb(); };
41
- s.addEventListener('load', onReady, { once: true });
42
- if (window.d3) onReady();
43
- };
44
-
45
- const bootstrap = () => {
46
- const scriptEl = document.currentScript;
47
- let container = scriptEl ? scriptEl.previousElementSibling : null;
48
- if (!(container && container.classList && container.classList.contains('d3-scatter'))){
49
- const cs = Array.from(document.querySelectorAll('.d3-scatter')).filter(el => !(el.dataset && el.dataset.mounted==='true'));
50
- container = cs[cs.length-1] || null;
51
- }
52
- if (!container) return;
53
- if (container.dataset){ if (container.dataset.mounted==='true') return; container.dataset.mounted='true'; }
54
-
55
- // Tooltip
56
- container.style.position = container.style.position || 'relative';
57
- let tip = container.querySelector('.d3-tooltip'); let tipInner;
58
- if (!tip) {
59
- tip = document.createElement('div'); tip.className = 'd3-tooltip';
60
- Object.assign(tip.style, { position:'absolute', top:'0px', left:'0px', transform:'translate(-9999px, -9999px)', pointerEvents:'none', padding:'10px 12px', borderRadius:'12px', fontSize:'12px', lineHeight:'1.35', border:'1px solid var(--border-color)', background:'var(--surface-bg)', color:'var(--text-color)', boxShadow:'0 8px 32px rgba(0,0,0,.28), 0 2px 8px rgba(0,0,0,.12)', opacity:'0', transition:'opacity .12s ease', backdropFilter:'saturate(1.12) blur(8px)' });
61
- tipInner = document.createElement('div'); tipInner.className = 'd3-tooltip__inner'; tipInner.style.textAlign='left'; tip.appendChild(tipInner); container.appendChild(tip);
62
- } else { tipInner = tip.querySelector('.d3-tooltip__inner') || tip; }
63
-
64
- // SVG
65
- const svg = d3.select(container).append('svg').attr('width','100%').style('display','block');
66
- const gRoot = svg.append('g');
67
- const gGrid = gRoot.append('g').attr('class','grid');
68
- const gAxes = gRoot.append('g').attr('class','axes');
69
- const gDots = gRoot.append('g').attr('class','dots');
70
- const gCentroids = gRoot.append('g').attr('class','centroids');
71
- const gLegend = gRoot.append('foreignObject').attr('class','legend');
72
-
73
- // State & scales
74
- let width=800, height=360; const margin = { top: 8, right: 12, bottom: 8, left: 12 };
75
- const x = d3.scaleLinear();
76
- const y = d3.scaleLinear();
77
- const color = d3.scaleOrdinal();
78
- const radius = () => 4;
79
- let isDarkMode = false;
80
- function getDotStrokeColor(fillColor = null){
81
- if (!fillColor) return 'var(--muted-color)';
82
-
83
- // Resolve CSS variables to actual colors
84
- let resolvedColor = fillColor;
85
- if (fillColor.startsWith('var(')) {
86
- const tempEl = document.createElement('div');
87
- tempEl.style.color = fillColor;
88
- document.body.appendChild(tempEl);
89
- resolvedColor = getComputedStyle(tempEl).color;
90
- document.body.removeChild(tempEl);
91
- }
92
-
93
- try {
94
- const colorObj = d3.color(resolvedColor);
95
- if (!colorObj) return 'var(--muted-color)';
96
-
97
- // En mode light: bordure plus claire, en mode dark: bordure plus sombre
98
- return isDarkMode ?
99
- colorObj.darker(0.3).toString() :
100
- colorObj.brighter(0.8).toString();
101
- } catch {
102
- return 'var(--muted-color)';
103
- }
104
- }
105
-
106
- // Data loading (real): banner visualization positions by category
107
- async function fetchFirstAvailable(paths){
108
- for (const p of paths){
109
- try {
110
- const res = await fetch(p, { cache: 'no-cache' });
111
- if (res.ok){ return await res.text(); }
112
- } catch (e) {}
113
- }
114
- throw new Error('Failed to load data from provided paths');
115
- }
116
-
117
- let data = [];
118
- let categories = [];
119
- let colorMode = 'group';
120
-
121
- function renderLegend(innerWidth){ gLegend.remove(); }
122
-
123
- function updateScales(data){
124
- const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
125
- isDarkMode = !!isDark;
126
- const axisColor = "var(--page-bg)";
127
- const tickColor = "var(--page-bg)";
128
- const gridColor = "var(--page-bg)";
129
-
130
- width = container.clientWidth || 800; height = Math.max(260, Math.round(width/3)); svg.attr('width', width).attr('height', height);
131
- const innerWidth = width - margin.left - margin.right; const innerHeight = height - margin.top - margin.bottom; gRoot.attr('transform', `translate(${margin.left},${margin.top})`);
132
-
133
- const xExtent = d3.extent(data, d=>d.x);
134
- const yExtent = d3.extent(data, d=>d.y);
135
- x.domain([xExtent[0], xExtent[1]]).range([0, innerWidth]).nice();
136
- y.domain([yExtent[0], yExtent[1]]).range([innerHeight, 0]).nice();
137
-
138
- // Frameless: no grid, no axes
139
- gGrid.selectAll('*').remove();
140
- gAxes.selectAll('*').remove();
141
-
142
- renderLegend(innerWidth);
143
-
144
- return { innerWidth, innerHeight };
145
- }
146
-
147
- function refreshPalette(){
148
- try {
149
- const cats = categories && categories.length ? categories.length : 6;
150
- if (window.ColorPalettes && typeof window.ColorPalettes.getColors === 'function') {
151
- const arr = window.ColorPalettes.getColors('categorical', cats) || [];
152
- if (arr && arr.length) { color.range(arr); return; }
153
- }
154
- // fallback
155
- color.range((d3.schemeTableau10 ? d3.schemeTableau10 : ['#4e79a7','#f28e2b','#e15759','#76b7b2','#59a14f','#edc948','#b07aa1','#ff9da7','#9c755f','#bab0ab']).slice(0, cats));
156
- } catch {
157
- const cats = categories && categories.length ? categories.length : 6;
158
- color.range((d3.schemeTableau10 ? d3.schemeTableau10 : ['#4e79a7','#f28e2b','#e15759','#76b7b2','#59a14f','#edc948','#b07aa1','#ff9da7','#9c755f','#bab0ab']).slice(0, cats));
159
- }
160
- // Recolor existing marks/labels after palette changes
161
- try { if (data && data.length) draw(); } catch {}
162
- }
163
-
164
- function draw(){
165
- if (!data || !data.length) return;
166
- const { innerWidth, innerHeight } = updateScales(data);
167
- const fillFor = d => colorMode === 'group' ? color(d.group) : 'var(--primary-color)';
168
-
169
- const dots = gDots.selectAll('circle.dot').data(data, (d,i)=>d.id || i);
170
- dots.enter().append('circle').attr('class','dot')
171
- .attr('cx', d=>x(d.x)).attr('cy', d=>y(d.y)).attr('r', radius())
172
- .attr('fill', fillFor).attr('fill-opacity', 0.85)
173
- .attr('stroke', d => getDotStrokeColor(fillFor(d))).attr('stroke-width', '0.75px')
174
- .on('mouseenter', function(ev, d){
175
- d3.select(this).style('stroke','var(--text-color)').style('stroke-width','1.5px').attr('fill-opacity', 1);
176
- const swatch = `<svg width="10" height="10" viewBox="0 0 10 10" aria-hidden="true"><circle cx="5" cy="5" r="5" fill="${fillFor(d)}" /></svg>`;
177
- tipInner.innerHTML = `
178
- <div><strong>${d.label || 'Item'}</strong></div>
179
- <div style="display:flex;align-items:center;gap:6px;">${swatch}<span>${d.group}</span></div>
180
- <div style="display:flex;align-items:center;gap:6px;white-space:nowrap;"><strong>x</strong><span style="margin-left:auto;text-align:right;">${d.x.toFixed(2)}</span></div>
181
- <div style="display:flex;align-items:center;gap:6px;white-space:nowrap;"><strong>y</strong><span style="margin-left:auto;text-align:right;">${d.y.toFixed(2)}</span></div>`;
182
- tip.style.opacity = '1';
183
- })
184
- .on('mousemove', function(ev){ const [mx, my] = d3.pointer(ev, container); const ox=12, oy=12; tip.style.transform = `translate(${Math.round(mx+ox)}px, ${Math.round(my+oy)}px)`; })
185
- .on('mouseleave', function(ev, d){ tip.style.opacity='0'; tip.style.transform='translate(-9999px, -9999px)'; d3.select(this).style('stroke', getDotStrokeColor(fillFor(d))).style('stroke-width','0.75px').attr('fill-opacity', 0.85); })
186
- .merge(dots)
187
- .transition().duration(180)
188
- .attr('cx', d=>x(d.x)).attr('cy', d=>y(d.y)).attr('r', radius())
189
- .attr('fill', fillFor).attr('fill-opacity', 0.85)
190
- .attr('stroke', d => getDotStrokeColor(fillFor(d))).attr('stroke-width','0.75px');
191
- dots.exit().remove();
192
-
193
- // Compute centroids per category
194
- const centroids = Array.from(
195
- d3.rollup(
196
- data,
197
- (v) => ({
198
- category: v[0] ? v[0].group : 'Unknown',
199
- x: d3.mean(v, (d) => d.x),
200
- y: d3.mean(v, (d) => d.y),
201
- count: v.length
202
- }),
203
- (d) => d.group
204
- ).values()
205
- );
206
-
207
- // Map to pixel space nodes for collision-avoiding label placement
208
- const nodes = centroids.map((c) => ({
209
- category: c.category,
210
- count: c.count,
211
- targetX: x(c.x),
212
- targetY: y(c.y),
213
- x: x(c.x),
214
- y: y(c.y),
215
- width: Math.max(18, (String(c.category || '').length || 6) * 11),
216
- height: 16
217
- }));
218
-
219
- if (nodes.length > 1) {
220
- const sim = d3.forceSimulation(nodes)
221
- .force('x', d3.forceX((d) => d.targetX).strength(0.9))
222
- .force('y', d3.forceY((d) => d.targetY).strength(0.9))
223
- .force('collide', d3.forceCollide((d) => Math.hypot(d.width/2, d.height/2) + 15))
224
- .stop();
225
- for (let i = 0; i < 650; i++) sim.tick();
226
- const maxOffset = 45;
227
- nodes.forEach((n) => {
228
- const dx = n.x - n.targetX, dy = n.y - n.targetY; const dist = Math.hypot(dx, dy);
229
- if (dist > maxOffset && dist > 0) { const s = maxOffset / dist; n.x = n.targetX + dx * s; n.y = n.targetY + dy * s; }
230
- });
231
- }
232
-
233
- const labels = gCentroids.selectAll('g.centroid').data(nodes, d => d.category || 'Unknown');
234
- const enter = labels.enter().append('g').attr('class','centroid').attr('pointer-events','none');
235
- enter.append('text').attr('class','label-bg').attr('text-anchor','middle').attr('dominant-baseline','middle');
236
- enter.append('text').attr('class','label-fg').attr('text-anchor','middle').attr('dominant-baseline','middle');
237
- const merged = enter.merge(labels);
238
- merged
239
- .attr('transform', d => `translate(${Math.round(d.x)}, ${Math.round(d.y)})`)
240
- .each(function(d){
241
- const base = color(d.category || 'Unknown') || 'var(--text-color)';
242
- const bg = getComputedStyle(document.documentElement).getPropertyValue('--page-bg').trim() || '#fff';
243
- const bgNode = this.querySelector('text.label-bg');
244
- const fgNode = this.querySelector('text.label-fg');
245
- if (bgNode) {
246
- bgNode.textContent = d.category;
247
- bgNode.style.setProperty('fill', "var(--page-bg)", 'important');
248
- bgNode.style.setProperty('stroke', "var(--page-bg)");
249
- bgNode.style.setProperty('stroke-width', '8px');
250
- bgNode.style.setProperty('paint-order', 'stroke fill');
251
- bgNode.style.setProperty('font-weight','800');
252
- bgNode.style.setProperty('font-size','16px');
253
- }
254
- if (fgNode) {
255
- fgNode.textContent = d.category;
256
- fgNode.style.setProperty('fill', base, 'important');
257
- fgNode.style.setProperty('font-weight','800');
258
- fgNode.style.setProperty('font-size','16px');
259
- }
260
- });
261
- labels.exit().remove();
262
- }
263
-
264
- // Initial load
265
- refreshPalette();
266
- document.addEventListener('palettes:updated', refreshPalette);
267
-
268
- (async () => {
269
- try {
270
- const csvText = await fetchFirstAvailable([
271
- '/data/banner_visualisation_data.csv',
272
- './assets/data/banner_visualisation_data.csv',
273
- '../assets/data/banner_visualisation_data.csv',
274
- '/data/banner_visualisation_data.csv'
275
- ]);
276
- const rows = d3.csvParse(csvText);
277
- data = rows.map((r, i) => ({
278
- id: +r.original_id ?? i,
279
- x: +r.x_position,
280
- y: +r.y_position,
281
- group: r.category || 'Unknown',
282
- label: r.subset || r.category || `Item ${i+1}`
283
- })).filter(d => Number.isFinite(d.x) && Number.isFinite(d.y));
284
- categories = Array.from(new Set(data.map(d=>d.group)));
285
- color.domain(categories);
286
- draw();
287
- } catch (e) {
288
- const pre = document.createElement('pre'); pre.style.color = 'crimson'; pre.textContent = 'Failed to load scatter data.'; container.appendChild(pre);
289
- }
290
- })();
291
-
292
- const rerender = () => { draw(); };
293
- if (window.ResizeObserver) { const ro = new ResizeObserver(()=>rerender()); ro.observe(container); } else { window.addEventListener('resize', rerender); }
294
- };
295
-
296
- if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true }); } else { ensureD3(bootstrap); }
297
- })();
298
- </script>
299
-
300
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/src/content/embeds/rope-demo.html DELETED
@@ -1,532 +0,0 @@
1
- <div class="d3-rope-demo"></div>
2
-
3
- <style>
4
- .d3-rope-demo {
5
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
6
- line-height: 1.5;
7
- color: var(--text-color);
8
- padding: 20px 0;
9
- display: flex;
10
- flex-direction: column;
11
- align-items: center;
12
- }
13
-
14
- .d3-rope-demo .subtitle {
15
- color: var(--text-color);
16
- font-size: 18px;
17
- font-weight: 600;
18
- margin-bottom: 20px;
19
- text-align: center;
20
- max-width: 600px;
21
- line-height: 1.5;
22
- }
23
-
24
- .d3-rope-demo .sentence {
25
- display: flex;
26
- gap: 0;
27
- margin: 25px 0;
28
- flex-wrap: wrap;
29
- justify-content: center;
30
- font-size: 18px;
31
- }
32
-
33
- .d3-rope-demo .slider-container {
34
- display: flex;
35
- align-items: center;
36
- justify-content: center;
37
- margin: 15px 0;
38
- }
39
-
40
- .d3-rope-demo .slider-label {
41
- font-size: 14px;
42
- color: var(--muted-color);
43
- font-weight: 500;
44
- min-width: 80px;
45
- }
46
-
47
- .d3-rope-demo .slider {
48
- width: 200px;
49
- height: 6px;
50
- border-radius: 3px;
51
- background: var(--border-color);
52
- outline: none;
53
- cursor: pointer;
54
- }
55
-
56
- .d3-rope-demo .slider::-webkit-slider-thumb {
57
- appearance: none;
58
- width: 18px;
59
- height: 18px;
60
- border-radius: 50%;
61
- background: var(--primary-color);
62
- cursor: pointer;
63
- border: 2px solid var(--page-bg);
64
- box-shadow: 0 2px 4px var(--border-color);
65
- }
66
-
67
- .d3-rope-demo .slider::-moz-range-thumb {
68
- width: 18px;
69
- height: 18px;
70
- border-radius: 50%;
71
- background: var(--primary-color);
72
- cursor: pointer;
73
- border: 2px solid var(--page-bg);
74
- box-shadow: 0 2px 4px var(--border-color);
75
- }
76
-
77
- .d3-rope-demo .slider-value {
78
- font-size: 14px;
79
- color: var(--text-color);
80
- font-weight: 600;
81
- min-width: 40px;
82
- text-align: center;
83
- }
84
-
85
- .d3-rope-demo .rotation-info {
86
- text-align: center;
87
- margin: 20px auto;
88
- font-size: 16px;
89
- font-weight: 500;
90
- color: var(--text-color);
91
- padding: 20px;
92
- background: var(--page-bg);
93
- border-radius: 8px;
94
- border: 1px solid var(--border-color) !important;
95
- box-shadow: 0 2px 4px rgba(0,0,0,0.05);
96
- max-width: 500px;
97
- }
98
-
99
- .d3-rope-demo .equation-gap {
100
- height: 15px;
101
- }
102
-
103
- .d3-rope-demo .word-highlight {
104
- color: var(--primary-color);
105
- font-weight: 700;
106
- background: var(--page-bg);
107
- padding: 2px 6px;
108
- border-radius: 4px;
109
- border: 1px solid var(--border-color);
110
- display: inline-block;
111
- min-width: 60px;
112
- text-align: center;
113
- }
114
-
115
- .d3-rope-demo .position-highlight {
116
- color: var(--primary-color);
117
- font-weight: 700;
118
- background: var(--page-bg);
119
- padding: 2px 6px;
120
- border-radius: 4px;
121
- border: 1px solid var(--border-color);
122
- }
123
-
124
- .d3-rope-demo .angle-highlight {
125
- color: var(--primary-color);
126
- font-weight: 600;
127
- font-family: 'Courier New', monospace;
128
- font-size: 20px;
129
- padding: 12px 16px;
130
- border-radius: 6px;
131
- background: var(--page-bg);
132
- border: 1px solid var(--border-color);
133
- display: inline-block;
134
- width: 100%;
135
- text-align: center;
136
- }
137
-
138
- .d3-rope-demo .word {
139
- cursor: pointer;
140
- font-weight: 700;
141
- font-size: 18px;
142
- user-select: none;
143
- padding: 8px 12px;
144
- border-radius: 0;
145
- transition: all 0.2s ease;
146
- border: 1px solid var(--border-color);
147
- border-right: none;
148
- }
149
-
150
- .d3-rope-demo .word:first-child {
151
- border-radius: 6px 0 0 6px;
152
- }
153
-
154
- .d3-rope-demo .word:last-child {
155
- border-radius: 0 6px 6px 0;
156
- border-right: 1px solid var(--border-color);
157
- }
158
-
159
- .d3-rope-demo .word:only-child {
160
- border-radius: 6px;
161
- border-right: 1px solid var(--border-color);
162
- }
163
-
164
-
165
- .button {
166
- background: var(--primary-color)!important;
167
- color: var(--page-bg)!important;
168
- border: 1px solid var(--primary-color)!important;
169
- }
170
-
171
- .button--ghost {
172
- background: var(--page-bg)!important;
173
- color: var(--primary-color)!important;
174
- border: 1px solid var(--primary-color)!important;
175
- }
176
-
177
- .d3-rope-demo .svg-container {
178
- margin: 0;
179
- display: inline-block;
180
- }
181
-
182
- .d3-rope-demo svg {
183
- display: block;
184
- }
185
-
186
- .d3-rope-demo .explanation {
187
- max-width: 700px;
188
- text-align: center;
189
- margin-top: 20px;
190
- color: var(--text-color);
191
- font-size: 15px;
192
- line-height: 1.6;
193
- }
194
-
195
- /* Responsive design */
196
- @media (max-width: 768px) {
197
- .d3-rope-demo {
198
- padding: 16px 0;
199
- }
200
-
201
- .d3-rope-demo .sentence {
202
- gap: 10px;
203
- }
204
-
205
- .d3-rope-demo .word {
206
- font-size: 16px;
207
- padding: 6px 10px;
208
- }
209
-
210
- .d3-rope-demo .svg-container {
211
- width: 100%;
212
- max-width: 400px;
213
- }
214
-
215
- .d3-rope-demo svg {
216
- width: 100%;
217
- height: auto;
218
- }
219
-
220
- .d3-rope-demo .explanation {
221
- font-size: 14px;
222
- }
223
- }
224
- </style>
225
-
226
- <script>
227
- (() => {
228
- const bootstrap = () => {
229
- const scriptEl = document.currentScript;
230
- let container = scriptEl ? scriptEl.previousElementSibling : null;
231
- if (!(container && container.classList && container.classList.contains('d3-rope-demo'))) {
232
- const candidates = Array.from(document.querySelectorAll('.d3-rope-demo'))
233
- .filter((el) => !(el.dataset && el.dataset.mounted === 'true'));
234
- container = candidates[candidates.length - 1] || null;
235
- }
236
- if (!container) return;
237
- if (container.dataset) {
238
- if (container.dataset.mounted === 'true') return;
239
- container.dataset.mounted = 'true';
240
- }
241
-
242
- const sentence = ["The", "quick", "brown", "fox", "jumps", "..."];
243
-
244
- // Create the HTML structure
245
- container.innerHTML = `
246
- <div class="subtitle">RoPE rotation of the first (x₁, x₂) pair in Q/K vectors<br/> based on token position</div>
247
- <div class="sentence" id="sentence"></div>
248
- <div class="slider-container">
249
- <input type="range" class="slider" id="positionSlider" min="0" max="5" step="1" value="0">
250
- </div>
251
- <div class="svg-container">
252
- <svg id="ropeSvg" width="500" height="400" viewBox="0 0 500 400"></svg>
253
- </div>
254
-
255
- <div class="rotation-info" id="rotationInfo">
256
- <span class="word-highlight">The</span> at position <span class="position-highlight">0</span> gets rotated by
257
- <div class="equation-gap"></div>
258
- <span class="angle-highlight">θ = 0 rad (0°)</span>
259
- </div>
260
- <div class="explanation">
261
- <strong>RoPE Formula:</strong> θ (theta) = position × 1 / base<sup>2 × pair_index/h_dim</sup> (pair_index=0 here)
262
- <br><br>
263
- <strong>Key insight:</strong> The first dimension pair gets the largest rotations, and the relative angle between words depends only on their distance apart.
264
- </div>
265
- `;
266
-
267
- const svg = container.querySelector('#ropeSvg');
268
- const sentenceEl = container.querySelector('#sentence');
269
- const slider = container.querySelector('#positionSlider');
270
- const rotationInfo = container.querySelector('#rotationInfo');
271
-
272
- const R = 140;
273
- const R_LABELS = 180; // Cercle plus grand pour les labels
274
- const cx = 250;
275
- const cy = 200;
276
- const ANGLE_OFFSET = 5; // Offset en degrés pour mieux aligner les 6 mots
277
-
278
- // RoPE parameters
279
- const base = 10000;
280
- const d = 2048;
281
- const m = 0;
282
-
283
- function getRopeAngle(pos) {
284
- return pos * (1 / Math.pow(base, (2 * m) / d));
285
- }
286
-
287
- let activeIndex = 0;
288
- let animating = true;
289
- let animationTimeout = null;
290
-
291
- function renderSentence() {
292
- sentenceEl.innerHTML = "";
293
- sentence.forEach((word, i) => {
294
- const span = document.createElement("span");
295
- span.textContent = word;
296
- span.className = "word button" + (i === activeIndex ? "" : " button--ghost");
297
- span.addEventListener("click", () => {
298
- stopAnimation();
299
- activeIndex = i;
300
- slider.value = i;
301
- updateRotationInfo();
302
- draw();
303
- renderSentence();
304
- });
305
- sentenceEl.appendChild(span);
306
- });
307
- }
308
-
309
- function draw() {
310
- // Clear SVG
311
- svg.innerHTML = '';
312
-
313
- // Create arrays to store elements for proper layering
314
- const backgroundElements = [];
315
- const foregroundElements = [];
316
- const textElements = [];
317
-
318
- // Draw circle (background)
319
- const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
320
- circle.setAttribute('cx', cx);
321
- circle.setAttribute('cy', cy);
322
- circle.setAttribute('r', R);
323
- circle.setAttribute('fill', 'none');
324
- circle.setAttribute('stroke', 'var(--border-color)');
325
- circle.setAttribute('stroke-width', '1.5');
326
- circle.setAttribute('opacity', '0.6');
327
- backgroundElements.push(circle);
328
-
329
- // Draw all word positions
330
- sentence.forEach((word, i) => {
331
- const theta = getRopeAngle(i) + (ANGLE_OFFSET * Math.PI / 180); // Ajouter l'offset en radians
332
- const x = cx + R * Math.cos(theta);
333
- const y = cy + R * Math.sin(theta);
334
-
335
- const isActive = (i === activeIndex);
336
- const isGhost = i > activeIndex; // Éléments après la position active sont en ghost
337
-
338
- // Draw point (background)
339
- const point = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
340
- point.setAttribute('cx', x);
341
- point.setAttribute('cy', y);
342
- point.setAttribute('r', isActive ? 10 : 5);
343
- point.setAttribute('fill', isActive ? 'var(--primary-color)' : (isGhost ? 'var(--muted-color)' : 'var(--primary-color)'));
344
- point.setAttribute('stroke', isActive ? 'var(--page-bg)' : (isGhost ? 'var(--surface-bg)' : 'var(--page-bg)'));
345
- point.setAttribute('stroke-width', isActive ? '3' : '2');
346
- point.setAttribute('opacity', isActive ? '1' : (isGhost ? '0.3' : '0.7'));
347
- backgroundElements.push(point);
348
-
349
- // Draw arrow for active word (foreground)
350
- if (isActive) {
351
- const arrow = document.createElementNS('http://www.w3.org/2000/svg', 'line');
352
- arrow.setAttribute('x1', cx);
353
- arrow.setAttribute('y1', cy);
354
- arrow.setAttribute('x2', x);
355
- arrow.setAttribute('y2', y);
356
- arrow.setAttribute('stroke', 'var(--primary-color)');
357
- arrow.setAttribute('stroke-width', '3');
358
- arrow.setAttribute('stroke-linecap', 'round');
359
- arrow.setAttribute('opacity', '0.8');
360
- foregroundElements.push(arrow);
361
- }
362
- });
363
-
364
- // Draw center point (foreground)
365
- const centerPoint = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
366
- centerPoint.setAttribute('cx', cx);
367
- centerPoint.setAttribute('cy', cy);
368
- centerPoint.setAttribute('r', 5);
369
- centerPoint.setAttribute('fill', 'var(--text-color)');
370
- centerPoint.setAttribute('stroke', 'var(--page-bg)');
371
- centerPoint.setAttribute('stroke-width', '2');
372
- centerPoint.setAttribute('opacity', '0.8');
373
- foregroundElements.push(centerPoint);
374
-
375
- // Draw angle arc for active word (foreground)
376
- if (activeIndex !== null && activeIndex > 0) {
377
- const theta = getRopeAngle(activeIndex) + (ANGLE_OFFSET * Math.PI / 180);
378
- const startAngle = ANGLE_OFFSET * Math.PI / 180;
379
- const endAngle = theta;
380
-
381
- // Create arc path
382
- const radius = R * 0.7;
383
- const startX = cx + radius * Math.cos(startAngle);
384
- const startY = cy + radius * Math.sin(startAngle);
385
- const endX = cx + radius * Math.cos(endAngle);
386
- const endY = cy + radius * Math.sin(endAngle);
387
-
388
- const largeArcFlag = theta > Math.PI ? 1 : 0;
389
- const pathData = `M ${startX} ${startY} A ${radius} ${radius} 0 ${largeArcFlag} 1 ${endX} ${endY}`;
390
-
391
- const arc = document.createElementNS('http://www.w3.org/2000/svg', 'path');
392
- arc.setAttribute('d', pathData);
393
- arc.setAttribute('fill', 'none');
394
- arc.setAttribute('stroke', 'var(--primary-color)');
395
- arc.setAttribute('stroke-width', '2.5');
396
- arc.setAttribute('stroke-dasharray', '6,4');
397
- arc.setAttribute('opacity', '0.8');
398
- foregroundElements.push(arc);
399
- }
400
-
401
- // Draw all text elements (top layer)
402
- sentence.forEach((word, i) => {
403
- const theta = getRopeAngle(i);
404
- const x = cx + R * Math.cos(theta);
405
- const y = cy + R * Math.sin(theta);
406
-
407
- const isActive = (i === activeIndex);
408
- const isGhost = i > activeIndex; // Éléments après la position active sont en ghost
409
-
410
- // Draw word label on larger circle
411
- const labelX = cx + R_LABELS * Math.cos(theta);
412
- const labelY = cy + R_LABELS * Math.sin(theta);
413
- const wordLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text');
414
- wordLabel.setAttribute('x', labelX);
415
- wordLabel.setAttribute('y', labelY);
416
- wordLabel.setAttribute('text-anchor', 'middle');
417
- wordLabel.setAttribute('dominant-baseline', 'middle');
418
- wordLabel.setAttribute('fill', isActive ? 'var(--text-color)' : (isGhost ? 'var(--muted-color)' : 'var(--text-color)'));
419
- wordLabel.setAttribute('font-family', '-apple-system, BlinkMacSystemFont, sans-serif');
420
- wordLabel.setAttribute('font-size', isActive ? '18' : '15');
421
- wordLabel.setAttribute('font-weight', isActive ? '700' : '500');
422
- wordLabel.setAttribute('opacity', isActive ? '1' : (isGhost ? '0.3' : '0.8'));
423
- wordLabel.textContent = word;
424
- textElements.push(wordLabel);
425
- });
426
-
427
- // Add angle label (top layer)
428
- if (activeIndex !== null && activeIndex > 0) {
429
- const theta = getRopeAngle(activeIndex) + (ANGLE_OFFSET * Math.PI / 180);
430
- const radius = R * 0.7;
431
- const angleLabelX = cx + radius * 0.5 * Math.cos(theta / 2);
432
- const angleLabelY = cy + radius * 0.5 * Math.sin(theta / 2);
433
-
434
- // Create tspan elements for different styling
435
- const angleLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text');
436
- angleLabel.setAttribute('x', angleLabelX);
437
- angleLabel.setAttribute('y', angleLabelY);
438
- angleLabel.setAttribute('text-anchor', 'middle');
439
- angleLabel.setAttribute('font-family', '-apple-system, BlinkMacSystemFont, sans-serif');
440
- angleLabel.setAttribute('font-size', '13');
441
- angleLabel.setAttribute('font-weight', '600');
442
-
443
- // θ in primary color
444
- const thetaSpan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
445
- thetaSpan.setAttribute('fill', 'var(--primary-color)');
446
- thetaSpan.textContent = 'θ';
447
- angleLabel.appendChild(thetaSpan);
448
-
449
- // = with reduced opacity
450
- const equalsSpan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
451
- equalsSpan.setAttribute('fill', 'var(--primary-color)');
452
- equalsSpan.setAttribute('opacity', '0.5');
453
- equalsSpan.textContent = ' = ';
454
- angleLabel.appendChild(equalsSpan);
455
-
456
- // Number in primary color
457
- const numberSpan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
458
- numberSpan.setAttribute('fill', 'var(--primary-color)');
459
- numberSpan.textContent = activeIndex.toString();
460
- angleLabel.appendChild(numberSpan);
461
-
462
- textElements.push(angleLabel);
463
- }
464
-
465
- // Append elements in correct order: background -> foreground -> text
466
- backgroundElements.forEach(el => svg.appendChild(el));
467
- foregroundElements.forEach(el => svg.appendChild(el));
468
- textElements.forEach(el => svg.appendChild(el));
469
- }
470
-
471
- function updateRotationInfo() {
472
- const theta = getRopeAngle(activeIndex);
473
- const degrees = Math.round(theta * 180 / Math.PI);
474
- rotationInfo.innerHTML = `
475
- <span class="word-highlight">${sentence[activeIndex]}</span> at position <span class="position-highlight">${activeIndex}</span> gets rotated by
476
- <div class="equation-gap"></div>
477
- <div class="angle-highlight">
478
- <span style="color: var(--muted-color); opacity: 0.6;">θ</span>
479
- <span style="color: var(--muted-color); opacity: 0.4; margin: 0 8px;">=</span>
480
- <span style="opacity: 1;">${activeIndex}</span>
481
- <span style="color: var(--muted-color); opacity: 0.6;">rad</span>
482
- <span style="color: var(--muted-color); opacity: 0.4;">(</span>
483
- <span style="opacity: 1;">${degrees}°</span>
484
- <span style="color: var(--muted-color); opacity: 0.4;">)</span>
485
- </div>
486
- `;
487
- }
488
-
489
- function stopAnimation() {
490
- animating = false;
491
- if (animationTimeout) {
492
- clearTimeout(animationTimeout);
493
- animationTimeout = null;
494
- }
495
- }
496
-
497
- function animate() {
498
- if (!animating) return;
499
-
500
- animationTimeout = setTimeout(() => {
501
- activeIndex = (activeIndex + 1) % sentence.length;
502
- slider.value = activeIndex;
503
- updateRotationInfo();
504
- renderSentence();
505
- draw();
506
- animate();
507
- }, 1500);
508
- }
509
-
510
- // Slider event listener
511
- slider.addEventListener('input', (e) => {
512
- stopAnimation();
513
- activeIndex = parseInt(e.target.value);
514
- updateRotationInfo();
515
- renderSentence();
516
- draw();
517
- });
518
-
519
- // Initialize and start
520
- renderSentence();
521
- updateRotationInfo();
522
- draw();
523
- animate();
524
- };
525
-
526
- if (document.readyState === 'loading') {
527
- document.addEventListener('DOMContentLoaded', bootstrap, { once: true });
528
- } else {
529
- bootstrap();
530
- }
531
- })();
532
- </script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/src/content/embeds/smol-playbook/model-architecture-decision-flowchart.html DELETED
@@ -1,490 +0,0 @@
1
- <!--
2
- Model Architecture Decision Flowchart
3
-
4
- Usage:
5
- <HtmlEmbed src="/embeds/model-architecture-decision-flowchart.html" />
6
- -->
7
- <div class="model-architecture-decision-flowchart"></div>
8
- <style>
9
- .model-architecture-decision-flowchart {
10
- width: 100%;
11
- min-height: 300px;
12
- position: relative;
13
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif;
14
- }
15
-
16
- .model-architecture-decision-flowchart svg {
17
- display: block;
18
- width: 100%;
19
- }
20
-
21
- .model-architecture-decision-flowchart .node-rect {
22
- stroke-width: 2.5px;
23
- rx: 14px;
24
- ry: 14px;
25
- }
26
-
27
- .model-architecture-decision-flowchart .node-text {
28
- font-size: 18px;
29
- font-weight: 600;
30
- text-anchor: middle;
31
- pointer-events: none;
32
- fill: var(--text-color, #333);
33
- }
34
-
35
- .model-architecture-decision-flowchart .node-question {
36
- fill: oklch(from var(--primary-color) calc(l + 0.4) c h / 0.26);
37
- stroke: oklch(from var(--primary-color) calc(l + 0.15) c h / 0.5) !important;
38
- }
39
-
40
- .model-architecture-decision-flowchart .node-success {
41
- fill: oklch(from var(--success-color) calc(l + 0.4) c h / 0.26);
42
- stroke: oklch(from var(--success-color) calc(l + 0.15) c h / 0.5) !important;
43
- }
44
-
45
- .model-architecture-decision-flowchart .node-category {
46
- fill: oklch(from var(--danger-color) calc(l + 0.4) c h / 0.26);
47
- stroke: oklch(from var(--danger-color) calc(l + 0.15) c h / 0.5) !important;
48
- }
49
-
50
- .model-architecture-decision-flowchart .node-decision {
51
- stroke: var(--border-color, #ddd) !important;
52
- }
53
-
54
- /* Dark mode adjustments */
55
- [data-theme="dark"] .model-architecture-decision-flowchart .node-question {
56
- fill: oklch(from var(--primary-color) calc(l + 0.3) c h / 0.2);
57
- stroke: oklch(from var(--primary-color) calc(l + 0.1) c h / 0.6) !important;
58
- }
59
-
60
- [data-theme="dark"] .model-architecture-decision-flowchart .node-success {
61
- fill: oklch(from var(--success-color) calc(l + 0.3) c h / 0.2);
62
- stroke: oklch(from var(--success-color) calc(l + 0.1) c h / 0.6) !important;
63
- }
64
-
65
- [data-theme="dark"] .model-architecture-decision-flowchart .node-category {
66
- fill: oklch(from var(--danger-color) calc(l + 0.3) c h / 0.2);
67
- stroke: oklch(from var(--danger-color) calc(l + 0.1) c h / 0.6) !important;
68
- }
69
-
70
- .model-architecture-decision-flowchart .link-path {
71
- fill: none;
72
- stroke: var(--muted-color, #666);
73
- stroke-width: 2.5px;
74
- marker-end: url(#arrowhead);
75
- }
76
-
77
- .model-architecture-decision-flowchart .link-label {
78
- font-size: 14px;
79
- font-weight: 700;
80
- fill: var(--text-color, #333);
81
- text-anchor: middle;
82
- pointer-events: none;
83
- }
84
-
85
- .model-architecture-decision-flowchart .link-label-bg {
86
- fill: var(--page-bg, #ffffff);
87
- stroke: none;
88
- }
89
- </style>
90
- <script>
91
- (() => {
92
- const ensureD3 = (cb) => {
93
- if (window.d3 && typeof window.d3.select === 'function') return cb();
94
- let s = document.getElementById('d3-cdn-script');
95
- if (!s) {
96
- s = document.createElement('script');
97
- s.id = 'd3-cdn-script';
98
- s.src = 'https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js';
99
- document.head.appendChild(s);
100
- }
101
- const onReady = () => {
102
- if (window.d3 && typeof window.d3.select === 'function') cb();
103
- };
104
- s.addEventListener('load', onReady, { once: true });
105
- if (window.d3) onReady();
106
- };
107
-
108
- const bootstrap = () => {
109
- const scriptEl = document.currentScript;
110
- let container = scriptEl ? scriptEl.previousElementSibling : null;
111
- if (!(container && container.classList && container.classList.contains('model-architecture-decision-flowchart'))) {
112
- const candidates = Array.from(document.querySelectorAll('.model-architecture-decision-flowchart'))
113
- .filter((el) => !(el.dataset && el.dataset.mounted === 'true'));
114
- container = candidates[candidates.length - 1] || null;
115
- }
116
- if (!container) return;
117
- if (container.dataset) {
118
- if (container.dataset.mounted === 'true') return;
119
- container.dataset.mounted = 'true';
120
- }
121
-
122
- // Define color scheme
123
- const getColors = () => {
124
- const getCSSVar = (varName, fallback) => {
125
- if (typeof getComputedStyle !== 'undefined') {
126
- const value = getComputedStyle(document.documentElement)
127
- .getPropertyValue(varName);
128
- if (value && value.trim()) {
129
- return value.trim();
130
- }
131
- }
132
- return fallback;
133
- };
134
-
135
- return {
136
- question: getCSSVar('--primary-color', '#0084ff'),
137
- decision: getCSSVar('--surface-bg', '#f9f9f9'),
138
- success: getCSSVar('--success-color', '#42d9b3'),
139
- category: getCSSVar('--danger-color', '#e85c42'),
140
- link: getCSSVar('--muted-color', '#666')
141
- };
142
- };
143
-
144
- // Define the flowchart structure - Model Architecture Decision
145
- const nodes = [
146
- { id: 'B', label: 'Edge/Phones\nMemory-constrained environments', type: 'decision', x: 180, y: 100 },
147
- { id: 'C', label: 'Other\nMore memory available', type: 'decision', x: 620, y: 100 },
148
- { id: 'D', label: 'Dense (most cases)\nHybrid or other (for experienced teams)', type: 'success', x: 180, y: 320 },
149
- { id: 'E', label: 'What\'s your team\'s expertise?', type: 'question', x: 620, y: 320 },
150
- { id: 'F', label: 'First LLM training', type: 'decision', x: 380, y: 540 },
151
- { id: 'G', label: 'Experienced\nComfortable with dense', type: 'decision', x: 620, y: 540 },
152
- { id: 'H', label: 'Very experienced', type: 'decision', x: 860, y: 540 },
153
- { id: 'I', label: 'Dense\n(Focus on basics)', type: 'success', x: 380, y: 760 },
154
- { id: 'J', label: 'What\'s your timeline?', type: 'question', x: 620, y: 760 },
155
- { id: 'K', label: 'Tight\nProven path required', type: 'decision', x: 480, y: 980 },
156
- { id: 'L', label: 'Flexible\nOpen to exploration', type: 'decision', x: 760, y: 980 },
157
- { id: 'M', label: 'Dense', type: 'success', x: 480, y: 1200 },
158
- { id: 'N', label: 'MoE or MoE + Hybrid:\nbetter perf/compute', type: 'category', x: 760, y: 1200 },
159
- { id: 'O', label: 'MoE or MoE + Hybrid:\nbetter perf/compute', type: 'category', x: 860, y: 760 }
160
- ];
161
-
162
- const links = [
163
- { source: 'B', target: 'D', label: '' },
164
- { source: 'C', target: 'E', label: '' },
165
- { source: 'E', target: 'F', label: '' },
166
- { source: 'E', target: 'G', label: '' },
167
- { source: 'E', target: 'H', label: '' },
168
- { source: 'F', target: 'I', label: '' },
169
- { source: 'G', target: 'J', label: '' },
170
- { source: 'J', target: 'K', label: '' },
171
- { source: 'J', target: 'L', label: '' },
172
- { source: 'K', target: 'M', label: '' },
173
- { source: 'L', target: 'N', label: '' },
174
- { source: 'H', target: 'O', label: '' }
175
- ];
176
-
177
- // Create SVG
178
- const svg = d3.select(container).append('svg').attr('width', '100%').style('display', 'block');
179
- const gRoot = svg.append('g');
180
-
181
- // Define arrowhead marker (solid triangle arrowhead)
182
- const defs = svg.append('defs');
183
- const marker = defs.append('marker')
184
- .attr('id', 'arrowhead')
185
- .attr('viewBox', '0 0 10 10')
186
- .attr('refX', 2.5)
187
- .attr('refY', 5)
188
- .attr('markerWidth', 4)
189
- .attr('markerHeight', 4)
190
- .attr('orient', 'auto');
191
-
192
- // Create solid arrowhead pointing right (smaller)
193
- marker.append('path')
194
- .attr('d', 'M 0 0 L 8 5 L 0 10 Z')
195
- .attr('fill', () => getColors().link);
196
-
197
- let width = 1000, height = 800;
198
-
199
- function render() {
200
- width = container.clientWidth || 1000;
201
- height = Math.max(800, Math.round(width * 1.3));
202
- svg.attr('width', width).attr('height', height);
203
-
204
- const colors = getColors();
205
-
206
- // Calculate scale to fit content (no padding, allow to touch edges)
207
- const nodeExtent = {
208
- minX: d3.min(nodes, d => d.x) - 160,
209
- maxX: d3.max(nodes, d => d.x) + 160,
210
- minY: d3.min(nodes, d => d.y) - 40,
211
- maxY: d3.max(nodes, d => d.y) + 80
212
- };
213
-
214
- const contentWidth = nodeExtent.maxX - nodeExtent.minX;
215
- const contentHeight = nodeExtent.maxY - nodeExtent.minY;
216
-
217
- const scale = Math.min(width / contentWidth, height / contentHeight);
218
- const offsetX = (width - contentWidth * scale) / 2 - nodeExtent.minX * scale;
219
- const offsetY = (height - contentHeight * scale) / 2 - nodeExtent.minY * scale;
220
-
221
- gRoot.attr('transform', `translate(${offsetX}, ${offsetY}) scale(${scale})`);
222
-
223
- // Create a temporary text element for measuring text width
224
- const tempText = gRoot.append('text')
225
- .style('visibility', 'hidden')
226
- .style('font-size', '18px')
227
- .style('font-weight', '500');
228
-
229
- // Word wrap function - intelligently breaks text into lines
230
- const wordWrap = (text, maxWidth, fontSize = '18px') => {
231
- const explicitLines = text.split('\n');
232
- const wrappedLines = [];
233
-
234
- explicitLines.forEach(line => {
235
- if (!line.trim()) {
236
- wrappedLines.push(line);
237
- return;
238
- }
239
-
240
- tempText.attr('font-size', fontSize).text(line);
241
- const textWidth = tempText.node().getComputedTextLength();
242
-
243
- // If line fits, keep it as is
244
- if (textWidth <= maxWidth) {
245
- wrappedLines.push(line);
246
- return;
247
- }
248
-
249
- // Otherwise, break into words and wrap
250
- const words = line.split(/\s+/);
251
- let currentLine = '';
252
-
253
- words.forEach(word => {
254
- const testLine = currentLine ? `${currentLine} ${word}` : word;
255
- tempText.text(testLine);
256
- const testWidth = tempText.node().getComputedTextLength();
257
-
258
- if (testWidth <= maxWidth && currentLine) {
259
- currentLine = testLine;
260
- } else {
261
- if (currentLine) {
262
- wrappedLines.push(currentLine);
263
- }
264
- currentLine = word;
265
- }
266
- });
267
-
268
- if (currentLine) {
269
- wrappedLines.push(currentLine);
270
- }
271
- });
272
-
273
- return wrappedLines.filter(line => line.trim().length > 0);
274
- };
275
-
276
- // Calculate node dimensions with word wrapping
277
- const getNodeDimensions = (node) => {
278
- const maxWidths = {
279
- question: 240,
280
- decision: 250,
281
- success: 240,
282
- category: 260
283
- };
284
-
285
- const maxWidth = maxWidths[node.type] || 180;
286
- const fontSize = node.type === 'category' ? '19px' : '18px';
287
-
288
- const wrappedLines = wordWrap(node.label, maxWidth, fontSize);
289
- node.wrappedLines = wrappedLines;
290
-
291
- tempText.attr('font-size', fontSize);
292
- const lineWidths = wrappedLines.map(line => {
293
- tempText.text(line);
294
- return tempText.node().getComputedTextLength();
295
- });
296
- const maxLineWidth = Math.max(...lineWidths, 0);
297
-
298
- const padding = 36;
299
- const lineHeight = node.type === 'category' ? 28 : 26;
300
-
301
- const width = Math.max(120, maxLineWidth + padding);
302
- const height = Math.max(30, wrappedLines.length * lineHeight + padding);
303
-
304
- return { width, height, wrappedLines };
305
- };
306
-
307
- // Pre-calculate all node dimensions with wrapping
308
- nodes.forEach(node => {
309
- const dims = getNodeDimensions(node);
310
- node.width = dims.width;
311
- node.height = dims.height;
312
- });
313
-
314
- // Draw links first (so labels can be on top)
315
- const linkGroup = gRoot.selectAll('.link-group').data(links);
316
- const linkEnter = linkGroup.enter().append('g').attr('class', 'link-group');
317
-
318
- linkEnter.append('path').attr('class', 'link-path');
319
- linkEnter.append('rect').attr('class', 'link-label-bg').style('opacity', 0);
320
- linkEnter.append('text').attr('class', 'link-label').attr('dy', -5);
321
-
322
- const linkMerge = linkEnter.merge(linkGroup);
323
-
324
- linkMerge.select('.link-path')
325
- .attr('d', d => {
326
- const sourceNode = nodes.find(n => n.id === d.source);
327
- const targetNode = nodes.find(n => n.id === d.target);
328
-
329
- const gap = 12;
330
-
331
- if (Math.abs(sourceNode.x - targetNode.x) < 50) {
332
- const sx = sourceNode.x;
333
- const sy = sourceNode.y + sourceNode.height / 2 + gap;
334
- const tx = targetNode.x;
335
- const ty = targetNode.y - targetNode.height / 2 - gap;
336
- return `M ${sx} ${sy} L ${tx} ${ty}`;
337
- }
338
-
339
- let sx, sy, tx, ty;
340
-
341
- if (Math.abs(sourceNode.y - targetNode.y) < 50) {
342
- const sourceIsLeft = sourceNode.x < targetNode.x;
343
- sx = sourceNode.x + (sourceIsLeft ? sourceNode.width / 2 + gap : -(sourceNode.width / 2 + gap));
344
- sy = sourceNode.y;
345
- tx = targetNode.x + (sourceIsLeft ? -(targetNode.width / 2 + gap) : targetNode.width / 2 + gap);
346
- ty = targetNode.y;
347
- } else {
348
- sx = sourceNode.x;
349
- sy = sourceNode.y + (sourceNode.y < targetNode.y ? sourceNode.height / 2 + gap : -(sourceNode.height / 2 + gap));
350
- tx = targetNode.x;
351
- ty = targetNode.y + (targetNode.y > sourceNode.y ? -(targetNode.height / 2 + gap) : targetNode.height / 2 + gap);
352
- }
353
-
354
- const midX = (sx + tx) / 2;
355
- const midY = (sy + ty) / 2;
356
- return `M ${sx} ${sy} C ${sx} ${midY}, ${tx} ${midY}, ${tx} ${ty}`;
357
- })
358
- .attr('stroke', colors.link);
359
-
360
- // Draw label backgrounds and text (only for non-empty labels)
361
- linkMerge.filter(d => d.label && d.label.trim())
362
- .each(function (d) {
363
- const sourceNode = nodes.find(n => n.id === d.source);
364
- const targetNode = nodes.find(n => n.id === d.target);
365
- const x = (sourceNode.x + targetNode.x) / 2;
366
- const y = (sourceNode.y + targetNode.y) / 2;
367
-
368
- const labelEl = d3.select(this);
369
- const textEl = labelEl.select('.link-label');
370
-
371
- tempText.style('font-size', '14px').style('font-weight', '700').text(d.label);
372
- const textWidth = tempText.node().getComputedTextLength();
373
- const textHeight = 20;
374
- const padding = 10;
375
-
376
- labelEl.select('.link-label-bg')
377
- .attr('x', x - textWidth / 2 - padding)
378
- .attr('y', y - textHeight / 2 - padding / 2)
379
- .attr('width', textWidth + padding * 2)
380
- .attr('height', textHeight + padding)
381
- .style('opacity', 1);
382
-
383
- textEl
384
- .attr('x', x)
385
- .attr('y', y)
386
- .text(d.label);
387
- });
388
-
389
- linkMerge.filter(d => !d.label || !d.label.trim())
390
- .select('.link-label')
391
- .attr('x', d => {
392
- const sourceNode = nodes.find(n => n.id === d.source);
393
- const targetNode = nodes.find(n => n.id === d.target);
394
- return (sourceNode.x + targetNode.x) / 2;
395
- })
396
- .attr('y', d => {
397
- const sourceNode = nodes.find(n => n.id === d.source);
398
- const targetNode = nodes.find(n => n.id === d.target);
399
- return (sourceNode.y + targetNode.y) / 2;
400
- })
401
- .text('');
402
-
403
- tempText.remove();
404
-
405
- // Draw nodes
406
- const nodeGroup = gRoot.selectAll('.node-group').data(nodes);
407
- const nodeEnter = nodeGroup.enter().append('g').attr('class', 'node-group');
408
-
409
- nodeEnter.append('rect').attr('class', d => `node-rect node-${d.type}`);
410
- nodeEnter.append('text').attr('class', 'node-text');
411
-
412
- const nodeMerge = nodeEnter.merge(nodeGroup);
413
-
414
- nodeMerge.select('.node-rect')
415
- .attr('x', d => d.x - d.width / 2)
416
- .attr('y', d => d.y - d.height / 2)
417
- .attr('width', d => d.width)
418
- .attr('height', d => d.height)
419
- .attr('fill', d => {
420
- switch (d.type) {
421
- case 'question': return 'currentColor';
422
- case 'decision': return colors.decision;
423
- case 'success': return 'currentColor';
424
- case 'category': return 'currentColor';
425
- default: return colors.decision;
426
- }
427
- });
428
-
429
- nodeMerge.select('.node-text')
430
- .attr('x', d => d.x)
431
- .each(function (d) {
432
- const lines = d.wrappedLines || d.label.split('\n');
433
- const textEl = d3.select(this);
434
- textEl.selectAll('tspan').remove();
435
-
436
- const fontSize = d.type === 'category' ? '19px' : '18px';
437
- const lineHeight = d.type === 'category' ? 28 : 26;
438
-
439
- textEl.attr('y', d.y);
440
-
441
- const numLines = lines.length;
442
- const totalTextHeight = (numLines - 1) * lineHeight;
443
-
444
- lines.forEach((line, i) => {
445
- const offsetFromCenter = (i - (numLines - 1) / 2) * lineHeight;
446
-
447
- textEl.append('tspan')
448
- .attr('x', d.x)
449
- .attr('dy', i === 0 ? `${offsetFromCenter}px` : `${lineHeight}px`)
450
- .attr('font-size', fontSize)
451
- .attr('text-anchor', 'middle')
452
- .attr('dominant-baseline', 'central')
453
- .text(line);
454
- });
455
- });
456
-
457
- // Update arrowhead color
458
- marker.select('path').attr('fill', colors.link);
459
- }
460
-
461
- // Initial render + resize handling
462
- render();
463
- const rerender = () => render();
464
- if (window.ResizeObserver) {
465
- const ro = new ResizeObserver(() => rerender());
466
- ro.observe(container);
467
- } else {
468
- window.addEventListener('resize', rerender);
469
- }
470
-
471
- // Listen for theme changes
472
- const observer = new MutationObserver(() => {
473
- render();
474
- });
475
-
476
- if (document.documentElement) {
477
- observer.observe(document.documentElement, {
478
- attributes: true,
479
- attributeFilter: ['data-theme', 'class']
480
- });
481
- }
482
- };
483
-
484
- if (document.readyState === 'loading') {
485
- document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true });
486
- } else {
487
- ensureD3(bootstrap);
488
- }
489
- })();
490
- </script>