Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
Commit ·
b5327b1
1
Parent(s): 145b8dc
remove old unused files
Browse files- app/src/content/assets/data/against_baselines copy.csv +0 -3
- app/src/content/assets/data/against_baselines.csv +0 -3
- app/src/content/assets/data/against_baselines_deduplicated.csv +0 -3
- app/src/content/assets/data/all_ratings_luis.csv +0 -3
- app/src/content/assets/data/banner_visualisation_data_enriched.csv +0 -3
- app/src/content/assets/data/doc-masking_evals.csv +0 -3
- app/src/content/assets/data/doc-masking_loss.csv +0 -3
- app/src/content/assets/data/internal_deduplication.csv +0 -3
- app/src/content/assets/data/mnist-variant-model.json +0 -3
- app/src/content/assets/data/no-wd_evals.csv +0 -3
- app/src/content/assets/data/no_wd_comparison.csv +0 -3
- app/src/content/assets/data/nope_evals.csv +0 -3
- app/src/content/assets/data/nope_loss.csv +0 -3
- app/src/content/assets/data/remove_ch.csv +0 -3
- app/src/content/assets/data/root-seq-write-heatmaps.json +0 -3
- app/src/content/assets/data/s25_ratings.csv +0 -3
- app/src/content/assets/data/ss_vs_s1.csv +0 -3
- app/src/content/assets/data/tied-embeddings_evals.csv +0 -3
- app/src/content/assets/data/zloss_comparison.csv +0 -3
- app/src/content/assets/data/zloss_evals.csv +0 -3
- app/src/content/embeds/banner-neural-network-animejs.html +0 -464
- app/src/content/embeds/banner-threejs-galaxy.html +0 -504
- app/src/content/embeds/banner-umap-lucioles.html +0 -489
- app/src/content/embeds/d3-bar.html +0 -459
- app/src/content/embeds/d3-confusion-matrix.html +0 -516
- app/src/content/embeds/d3-line-quad.html +0 -783
- app/src/content/embeds/d3-matrix.html +0 -524
- app/src/content/embeds/d3-scatter.html +0 -300
- app/src/content/embeds/rope-demo.html +0 -532
- 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>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|