joelniklaus HF Staff commited on
Commit
663cec5
Β·
1 Parent(s): d00ac2c

add verbosity analysis plot from Thibaud

Browse files
app/src/content/chapters/analyses.mdx CHANGED
@@ -1,3 +1,6 @@
 
 
 
1
  ## Analyses
2
 
3
  Our final experiment explores an even more counterintuitive finding.
@@ -64,3 +67,13 @@ SmolLM2's quality distribution was actually reasonable:
64
  | Poor | {'<'}30 tokens | 8% |
65
 
66
  For pretraining data, diversity beats consistency. Models that don't follow instructions perfectly can produce better training data than those that do.
 
 
 
 
 
 
 
 
 
 
 
1
+ import HtmlEmbed from "../../components/HtmlEmbed.astro";
2
+ import FigRef from "../../components/FigRef.astro";
3
+
4
  ## Analyses
5
 
6
  Our final experiment explores an even more counterintuitive finding.
 
67
  | Poor | {'<'}30 tokens | 8% |
68
 
69
  For pretraining data, diversity beats consistency. Models that don't follow instructions perfectly can produce better training data than those that do.
70
+
71
+ ### Verbosity Analysis
72
+
73
+ Different prompt formats produce wildly different output lengths. <FigRef target="verbosity" /> shows the output tokens per document across four prompt types, broken down by model family. Table and Math prompts tend to be concise, while FAQ and Tutorial prompts generate significantly more tokens per document. Notably, the spread within each prompt type varies across model families: some models are consistently verbose regardless of the prompt, while others adapt their output length to the task.
74
+
75
+ <HtmlEmbed
76
+ id="verbosity"
77
+ src="verbosity.html"
78
+ desc="Output tokens per document across prompt types and model families. Hover over dots to see detailed statistics for each experiment."
79
+ />
app/src/content/embeds/verbosity.html ADDED
@@ -0,0 +1,506 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div class="d3-synth-banner" style="width:100%;margin:10px 0;aspect-ratio:3/1;min-height:260px;"></div>
2
+ <script>
3
+ (() => {
4
+ const ensureD3 = (cb) => {
5
+ if (window.d3 && typeof window.d3.select === 'function') return cb();
6
+ let s = document.getElementById('d3-cdn-script');
7
+ if (!s) {
8
+ s = document.createElement('script');
9
+ s.id = 'd3-cdn-script';
10
+ s.src = 'https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js';
11
+ document.head.appendChild(s);
12
+ }
13
+ const onReady = () => { if (window.d3 && typeof window.d3.select === 'function') cb(); };
14
+ s.addEventListener('load', onReady, { once: true });
15
+ if (window.d3) onReady();
16
+ };
17
+
18
+ const bootstrap = () => {
19
+ const mount = document.currentScript ? document.currentScript.previousElementSibling : null;
20
+ const container = (mount && mount.querySelector && mount.querySelector('.d3-synth-banner')) ||
21
+ Array.from(document.querySelectorAll('.d3-synth-banner')).find(el => el.dataset.mounted !== 'true');
22
+ if (!container) return;
23
+ if (container.dataset) {
24
+ if (container.dataset.mounted === 'true') return;
25
+ container.dataset.mounted = 'true';
26
+ }
27
+
28
+ // ═══════════════════════════════════════════════════════════════
29
+ // DATA β€” 65 rephrasing experiments
30
+ // [cat, prompt, model, source, compTokensB, promptTokensB,
31
+ // numDocsM, dclmDiff, outDclm, inDclm, eduDiff, outEdu, inEdu]
32
+ // ═══════════════════════════════════════════════════════════════
33
+ const raw = [
34
+ ['Format','Article','Gemma-3 1B','FW-Edu HQ',4.00,5.66,19.7,-0.018,0.080,0.098,-0.432,1.484,1.679],
35
+ ['Format','Commentary','Qwen3 1.7B','FW-Edu HQ',1.64,5.94,20.1,0.630,0.847,0.217,-0.229,3.533,3.762],
36
+ ['Format','Commentary','Gemma-3 1B','DCLM',1.46,5.52,29.5,0.202,0.344,0.142,0.359,1.943,1.584],
37
+ ['Format','Commentary','Gemma-3 1B','FW-Edu HQ',0.64,2.42,20.1,0.055,0.109,0.054,-0.517,0.793,0.921],
38
+ ['Format','Discussion','Qwen3 1.7B','FW-Edu HQ',2.90,7.20,20.1,0.715,0.932,0.217,-0.122,3.640,3.762],
39
+ ['Format','Discussion','Gemma-3 1B','FW-Edu HQ',3.96,6.25,20.1,0.036,0.161,0.125,-0.574,1.862,2.197],
40
+ ['Format','FAQ','Gemma-3 12B','FW-Edu HQ',3.22,4.06,20.1,0.712,0.930,0.217,-0.295,3.467,3.762],
41
+ ['Format','FAQ','Gemma-3 12B','FW-Edu LQ',1.91,1.90,32.9,0.799,0.828,0.029,0.357,1.263,0.906],
42
+ ['Format','FAQ','Gemma-3 1B','Cosmopedia',1.33,1.31,31.1,0.329,0.906,0.578,-0.264,2.226,2.490],
43
+ ['Format','FAQ','Gemma-3 1B','DCLM',1.52,2.08,29.5,0.709,0.851,0.142,0.346,1.930,1.585],
44
+ ['Format','FAQ','Gemma-3 1B','FW-Edu HQ',17.67,22.21,20.1,0.711,0.929,0.218,-0.346,3.416,3.762],
45
+ ['Format','FAQ','Gemma-3 1B','FW-Edu LQ',1.84,1.75,32.9,0.690,0.719,0.029,0.369,1.275,0.905],
46
+ ['Format','FAQ','Falcon3 1B','FW-Edu HQ',3.41,4.84,20.1,0.691,0.909,0.217,-0.249,3.512,3.762],
47
+ ['Format','FAQ','Granite3 1B','FW-Edu HQ',2.55,4.17,20.1,0.680,0.898,0.218,-0.166,3.596,3.762],
48
+ ['Format','FAQ','Llama-3.2 1B','FW-Edu HQ',6.29,11.62,20.1,0.735,0.952,0.217,-0.375,3.387,3.762],
49
+ ['Format','FAQ','Qwen3 1.7B','FW-Edu HQ',2.88,4.77,20.1,0.632,0.850,0.218,-0.219,3.543,3.762],
50
+ ['Format','FAQ','SmolLM2 1.7B','FW-Edu HQ',0.89,2.00,20.1,0.648,0.867,0.219,-0.406,3.355,3.762],
51
+ ['Format','Math','Gemma-3 12B','FW-Edu HQ',2.51,5.93,20.1,0.755,0.972,0.217,-0.370,3.392,3.762],
52
+ ['Format','Math','Gemma-3 1B','FW-Edu HQ',2.46,5.08,20.1,0.620,0.838,0.218,-0.174,3.587,3.762],
53
+ ['Format','Math','Gemma-3 270M','FW-Edu HQ',1.21,1.33,20.1,0.600,0.818,0.218,-0.406,3.357,3.762],
54
+ ['Format','Math','Gemma-3 27B','FW-Edu HQ',1.05,2.62,20.1,0.754,0.971,0.217,-0.429,3.333,3.762],
55
+ ['Format','Math','Gemma-3 4B','FW-Edu HQ',0.68,1.75,20.1,0.743,0.960,0.217,-0.335,3.427,3.762],
56
+ ['Format','Math','Falcon3 1B','FW-Edu HQ',1.42,2.17,20.1,0.704,0.921,0.217,-0.156,3.606,3.762],
57
+ ['Format','Math','Granite3 1B','FW-Edu HQ',1.57,3.35,20.1,0.698,0.916,0.218,-0.324,3.438,3.762],
58
+ ['Format','Math','Llama-3.2 1B','FW-Edu HQ',0.86,2.17,20.1,0.737,0.955,0.218,-0.384,3.378,3.762],
59
+ ['Format','Math','Qwen3 1.7B','FW-Edu HQ',0.56,2.00,20.1,0.742,0.959,0.218,-0.310,3.452,3.762],
60
+ ['Format','Math','SmolLM2 1.7B','FW-Edu HQ',0.64,2.28,20.1,0.657,0.875,0.218,-0.669,3.093,3.762],
61
+ ['Format','Table','Gemma-3 12B','FW-Edu HQ',0.76,2.49,20.1,0.719,0.937,0.217,-0.774,2.988,3.762],
62
+ ['Format','Table','Gemma-3 1B','FW-Edu HQ',1.68,5.62,20.1,0.715,0.932,0.217,-0.776,2.985,3.762],
63
+ ['Format','Table','Falcon3 1B','FW-Edu HQ',0.65,2.00,20.1,0.722,0.940,0.218,-0.748,3.015,3.763],
64
+ ['Format','Table','Granite3 1B','FW-Edu HQ',1.44,3.90,20.1,0.726,0.944,0.218,-0.597,3.165,3.762],
65
+ ['Format','Table','Llama-3.2 1B','FW-Edu HQ',0.88,2.73,20.1,0.772,0.989,0.217,-0.815,2.947,3.762],
66
+ ['Format','Table','Qwen3 1.7B','FW-Edu HQ',0.80,2.69,20.1,0.751,0.969,0.218,-0.638,3.123,3.762],
67
+ ['Format','Table','SmolLM2 1.7B','FW-Edu HQ',0.41,1.52,20.1,0.668,0.885,0.218,-0.943,2.818,3.762],
68
+ ['Format','Tutorial','Gemma-3 12B','FW-Edu HQ',5.33,6.57,20.1,0.398,0.553,0.155,-0.077,2.612,2.667],
69
+ ['Format','Tutorial','Gemma-3 12B','FW-Edu LQ',2.35,2.57,32.9,0.189,0.210,0.020,0.379,0.869,0.613],
70
+ ['Format','Tutorial','Gemma-3 1B','Cosmopedia',1.02,1.47,31.1,-0.049,0.529,0.578,-0.080,2.415,2.495],
71
+ ['Format','Tutorial','Gemma-3 1B','DCLM',1.67,3.30,29.5,0.371,0.514,0.142,0.528,2.113,1.585],
72
+ ['Format','Tutorial','Gemma-3 1B','FW-Edu HQ',1.51,2.70,19.4,0.074,0.112,0.038,-0.077,0.632,0.646],
73
+ ['Format','Tutorial','Gemma-3 1B','FW-Edu LQ',1.90,2.51,32.9,0.172,0.193,0.021,0.413,0.917,0.630],
74
+ ['Format','Tutorial','Gemma-3 270M','FW-Edu HQ',1.64,1.71,20.1,0.514,0.731,0.217,-0.337,3.425,3.762],
75
+ ['Format','Tutorial','Gemma-3 27B','FW-Edu HQ',2.55,3.29,20.1,0.485,0.702,0.218,-0.057,3.705,3.762],
76
+ ['Format','Tutorial','Gemma-3 4B','FW-Edu HQ',1.50,2.02,20.1,0.404,0.621,0.217,-0.049,3.713,3.762],
77
+ ['Format','Tutorial','Falcon3 1B','FW-Edu HQ',4.97,7.58,20.1,0.447,0.665,0.218,-0.145,3.617,3.762],
78
+ ['Format','Tutorial','Granite3 1B','FW-Edu HQ',3.51,6.42,20.1,0.449,0.666,0.218,-0.072,3.690,3.762],
79
+ ['Format','Tutorial','Llama-3.2 1B','FW-Edu HQ',2.36,4.88,20.1,0.557,0.774,0.217,-0.031,3.731,3.762],
80
+ ['Format','Tutorial','Qwen1.5 1.8B','FW-Edu HQ',2.22,2.40,20.1,0.647,0.864,0.218,0.072,3.834,3.762],
81
+ ['Format','Tutorial','Qwen2 1.5B','FW-Edu HQ',3.09,8.00,20.1,0.527,0.746,0.218,-0.010,3.751,3.762],
82
+ ['Format','Tutorial','Qwen2.5 1.5B','FW-Edu HQ',2.27,4.87,20.1,0.576,0.794,0.218,-0.083,3.678,3.762],
83
+ ['Format','Tutorial','Qwen3 1.7B','FW-Edu HQ',3.56,5.69,20.1,0.281,0.497,0.217,0.056,3.818,3.762],
84
+ ['Format','Tutorial','SmolLM2 1.7B','FW-Edu HQ',1.52,3.38,20.1,0.439,0.658,0.219,-0.244,3.518,3.762],
85
+ ['Nemotron','Distill','Gemma-3 1B','FW-Edu HQ',2.12,5.37,20.1,0.331,0.549,0.217,-0.132,3.629,3.762],
86
+ ['Nemotron','Diverse QA','Gemma-3 1B','FW-Edu HQ',1.97,7.01,20.1,0.779,0.996,0.217,-0.208,3.554,3.762],
87
+ ['Nemotron','Extract Knowledge','Gemma-3 1B','FW-Edu HQ',2.41,6.59,20.1,0.254,0.472,0.218,0.025,3.787,3.762],
88
+ ['Nemotron','Knowledge List','Gemma-3 1B','FW-Edu HQ',2.90,9.15,20.1,0.589,0.806,0.217,-0.193,3.569,3.762],
89
+ ['Nemotron','Wikipedia Style','Gemma-3 1B','FW-Edu HQ',3.43,8.37,20.1,0.297,0.515,0.217,-0.193,3.568,3.762],
90
+ ['REWIRE','Guided Rewrite+','Gemma-3 12B','FW-Edu HQ',1.45,2.14,20.1,0.222,0.439,0.217,-0.102,3.660,3.762],
91
+ ['REWIRE','Guided Rewrite+','Gemma-3 1B','FW-Edu HQ',1.63,3.42,20.1,0.097,0.315,0.217,-0.428,3.334,3.762],
92
+ ['REWIRE','Guided Rewrite','Gemma-3 12B','FW-Edu HQ',1.78,1.72,20.1,0.607,0.824,0.217,-1.214,2.547,3.762],
93
+ ['REWIRE','Guided Rewrite','Gemma-3 1B','FW-Edu HQ',8.69,10.30,20.1,0.278,0.495,0.217,-1.012,2.750,3.762],
94
+ ['REWIRE','Guided Rewrite','Gemma-3 270M','FW-Edu HQ',0.78,1.71,20.1,0.755,0.972,0.217,-2.407,1.355,3.762],
95
+ ['REWIRE','Guided Rewrite','Gemma-3 4B','FW-Edu HQ',0.91,1.10,20.1,0.436,0.654,0.218,-0.919,2.843,3.762],
96
+ ['REWIRE','Guided Rewrite','Gemma-3 27B','FW-Edu HQ',8.43,8.64,19.95,0.546,0.763,0.217,-1.228,2.533,3.762],
97
+ ['Format','Tutorial','SmolLM2 135M','FW-Edu HQ',4.51,6.99,20.1,0.423,0.641,0.218,-0.572,3.190,3.762],
98
+ ['Format','Tutorial','SmolLM2 360M','FW-Edu HQ',3.14,6.52,20.1,0.441,0.659,0.218,-0.565,3.197,3.762]
99
+ ];
100
+
101
+ const experiments = raw.map((r, i) => ({
102
+ idx: i, cat: r[0], prompt: r[1], model: r[2], source: r[3],
103
+ compTokens: r[4], promptTokens: r[5], numDocs: r[6],
104
+ dclmDiff: r[7], outDclm: r[8], inDclm: r[9],
105
+ eduDiff: r[10], outEdu: r[11], inEdu: r[12],
106
+ compPerDoc: r[4] * 1000 / r[6],
107
+ phase: Math.random() * Math.PI * 2
108
+ }));
109
+
110
+ // ══════════════════════════════════════════��════════════════
111
+ // MODEL FAMILIES
112
+ // ═══════════════════════════════════════════════════════════
113
+ const getFamily = (m) => {
114
+ if (m.includes('SmolLM')) return 'SmolLM2';
115
+ if (m.includes('Gemma')) return 'Gemma';
116
+ if (m.includes('Qwen')) return 'Qwen';
117
+ if (m.includes('Falcon')) return 'Falcon';
118
+ if (m.includes('Granite')) return 'Granite';
119
+ if (m.includes('Llama')) return 'Llama';
120
+ return 'Other';
121
+ };
122
+ const familyColors = {
123
+ 'Gemma': '#4EA5B7',
124
+ 'Qwen': '#8B7BE8',
125
+ 'SmolLM2': '#E8C44A',
126
+ 'Falcon': '#E889AB',
127
+ 'Granite': '#5BC0A4',
128
+ 'Llama': '#D09090',
129
+ };
130
+ const familyOrder = ['Gemma','Qwen','SmolLM2','Falcon','Granite','Llama'];
131
+ experiments.forEach(d => { d.family = getFamily(d.model); });
132
+
133
+ // ═══════════════════════════════════════════════════════════
134
+ // FILTER: Only prompts tested across 6 model families
135
+ // (keep multi-source Gemma variants for Tutorial/FAQ)
136
+ // ═══════════════════════════════════════════════════════════
137
+ const focusPrompts = ['Math', 'Table', 'FAQ', 'Tutorial'];
138
+ const filtered = experiments.filter(d => focusPrompts.includes(d.prompt));
139
+
140
+ // Sort rows by median compPerDoc (most verbose on top)
141
+ const promptStats = {};
142
+ focusPrompts.forEach(p => {
143
+ const vals = filtered.filter(d => d.prompt === p).map(d => d.compPerDoc).sort((a,b) => a - b);
144
+ const mid = Math.floor(vals.length / 2);
145
+ promptStats[p] = {
146
+ median: vals.length % 2 ? vals[mid] : (vals[mid-1] + vals[mid]) / 2,
147
+ count: vals.length
148
+ };
149
+ });
150
+ const sortedPrompts = [...focusPrompts].sort((a, b) => promptStats[b].median - promptStats[a].median);
151
+
152
+ // ═══════════════════════════════════════════════════════════
153
+ // TOOLTIP
154
+ // ═══════════════════════════════════════════════════════════
155
+ const fmtB = (v) => v >= 10 ? v.toFixed(0) + 'B' : v.toFixed(1) + 'B';
156
+ const fmtSign = (v, p) => (v >= 0 ? '+' : '') + v.toFixed(p || 3);
157
+ const dCol = (v) => v >= 0 ? '#5BC0A4' : '#E889AB';
158
+ const buildTip = (d) =>
159
+ `<div style="font-weight:800;font-size:13px;">${d.prompt}</div>` +
160
+ `<div style="font-size:11px;color:var(--muted-color);margin-top:-2px;margin-bottom:4px;">` +
161
+ `<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:${familyColors[d.family]};margin-right:4px;vertical-align:middle;"></span>` +
162
+ `${d.model} \u00b7 ${d.source}</div>` +
163
+ `<div style="padding-top:5px;border-top:1px solid var(--border-color);display:grid;grid-template-columns:auto 1fr;gap:2px 8px;font-size:11.5px;">` +
164
+ `<span style="color:var(--muted-color);">Total output</span><span>${fmtB(d.compTokens)}</span>` +
165
+ `<span style="color:var(--muted-color);">Output/doc</span><span>${Math.round(d.compPerDoc)} tokens</span>` +
166
+ `<span style="color:var(--muted-color);">Docs</span><span>${d.numDocs.toFixed(1)}M</span>` +
167
+ `</div>` +
168
+ `<div style="margin-top:5px;padding-top:5px;border-top:1px solid var(--border-color);display:grid;grid-template-columns:auto 1fr;gap:2px 8px;font-size:11.5px;">` +
169
+ `<span style="color:var(--muted-color);">DCLM</span>` +
170
+ `<span>${d.inDclm.toFixed(3)} \u2192 ${d.outDclm.toFixed(3)} <b style="color:${dCol(d.dclmDiff)};">${fmtSign(d.dclmDiff)}</b></span>` +
171
+ `<span style="color:var(--muted-color);">Edu</span>` +
172
+ `<span>${d.inEdu.toFixed(2)} \u2192 ${d.outEdu.toFixed(2)} <b style="color:${dCol(d.eduDiff)};">${fmtSign(d.eduDiff, 2)}</b></span>` +
173
+ `</div>`;
174
+
175
+ // ═══════════════════════════════════════════════════════════
176
+ // SVG
177
+ // ═══════════════════════════════════════════════════════════
178
+ const svg = d3.select(container).append('svg')
179
+ .attr('width', '100%')
180
+ .style('display', 'block')
181
+ .style('cursor', 'crosshair');
182
+
183
+ let animFrame = null;
184
+
185
+ const render = () => {
186
+ const width = container.clientWidth || 800;
187
+ const height = Math.max(260, Math.round(width / 3));
188
+ svg.attr('width', width).attr('height', height);
189
+
190
+ const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
191
+ const textColor = isDark ? 'rgba(255,255,255,0.72)' : 'rgba(0,0,0,0.62)';
192
+ const mutedText = isDark ? 'rgba(255,255,255,0.22)' : 'rgba(0,0,0,0.18)';
193
+ const subtleText = isDark ? 'rgba(255,255,255,0.12)' : 'rgba(0,0,0,0.08)';
194
+ const refLine = isDark ? 'rgba(255,255,255,0.10)' : 'rgba(0,0,0,0.07)';
195
+ const bandEven = isDark ? 'rgba(255,255,255,0.022)' : 'rgba(0,0,0,0.018)';
196
+ const glowColor = isDark ? 'rgba(255,255,255,0.35)' : 'rgba(0,0,0,0.25)';
197
+ const smolHighlight = isDark ? 'rgba(232,196,74,0.18)' : 'rgba(232,196,74,0.12)';
198
+
199
+ const fontSize = Math.max(8, Math.min(12, width / 80));
200
+
201
+ // ─── LAYOUT ───
202
+ const labelW = Math.max(68, width * 0.085);
203
+ const pl = labelW + 12;
204
+ const pr = width * 0.965;
205
+ const pt = height * 0.20;
206
+ const pb = height * 0.86;
207
+ const plotH = pb - pt;
208
+ const rowH = plotH / sortedPrompts.length;
209
+
210
+ const rowCenter = {};
211
+ sortedPrompts.forEach((p, i) => { rowCenter[p] = pt + (i + 0.5) * rowH; });
212
+
213
+ // ─── X SCALE: completion tokens per document (log) ───
214
+ const xScale = d3.scaleLog().base(10)
215
+ .domain([15, 1000])
216
+ .range([pl, pr]);
217
+
218
+ const rBase = Math.max(3.5, Math.min(8, width * 0.007));
219
+
220
+ // ─── FORCE SIMULATION ───
221
+ filtered.forEach(d => {
222
+ d.tx = xScale(Math.max(15, d.compPerDoc));
223
+ d.ty = rowCenter[d.prompt];
224
+ d.x = d.tx + (Math.random() - 0.5) * 6;
225
+ d.y = d.ty + (Math.random() - 0.5) * rowH * 0.3;
226
+ });
227
+
228
+ const sim = d3.forceSimulation(filtered)
229
+ .force('x', d3.forceX(d => d.tx).strength(0.72))
230
+ .force('y', d3.forceY(d => d.ty).strength(0.55))
231
+ .force('collide', d3.forceCollide(rBase + 1.5).iterations(5))
232
+ .stop();
233
+ for (let i = 0; i < 250; i++) sim.tick();
234
+
235
+ filtered.forEach(d => {
236
+ d.x = Math.max(pl + rBase, Math.min(pr - rBase, d.x));
237
+ const yMin = rowCenter[d.prompt] - rowH * 0.46;
238
+ const yMax = rowCenter[d.prompt] + rowH * 0.46;
239
+ d.y = Math.max(yMin, Math.min(yMax, d.y));
240
+ });
241
+
242
+ // ─── BG STARS ───
243
+ const bgCol = (() => {
244
+ const i01 = d3.interpolateRgb(d3.rgb(78,165,183), d3.rgb(206,192,250));
245
+ const i12 = d3.interpolateRgb(d3.rgb(206,192,250), d3.rgb(232,137,171));
246
+ return (v) => { const t = Math.max(0, Math.min(1, v)); return t <= 0.5 ? i01(t / 0.5) : i12((t - 0.5) / 0.5); };
247
+ })();
248
+ const gBg = svg.selectAll('g.bg').data([0]).join('g').attr('class', 'bg');
249
+ if (!gBg.selectAll('circle').size()) {
250
+ const stars = d3.range(50).map(() => ({
251
+ x: Math.random() * width, y: Math.random() * height,
252
+ z: Math.random(), r: 0.3 + Math.random() * 0.6
253
+ }));
254
+ gBg.selectAll('circle').data(stars).join('circle')
255
+ .attr('cx', d => d.x).attr('cy', d => d.y).attr('r', d => d.r)
256
+ .attr('fill', d => bgCol(d.z)).attr('fill-opacity', d => 0.03 + d.z * 0.04);
257
+ }
258
+
259
+ // ─── ROW BANDS & LABELS ───
260
+ const gRows = svg.selectAll('g.rows').data([0]).join('g').attr('class', 'rows');
261
+ gRows.selectAll('*').remove();
262
+
263
+ sortedPrompts.forEach((p, i) => {
264
+ const y = rowCenter[p];
265
+
266
+ if (i % 2 === 0) {
267
+ gRows.append('rect')
268
+ .attr('x', pl - 6).attr('y', y - rowH / 2)
269
+ .attr('width', pr - pl + 12).attr('height', rowH)
270
+ .attr('fill', bandEven).attr('rx', 4);
271
+ }
272
+ if (i > 0) {
273
+ gRows.append('line')
274
+ .attr('x1', pl - 2).attr('x2', pr + 2)
275
+ .attr('y1', y - rowH / 2).attr('y2', y - rowH / 2)
276
+ .attr('stroke', refLine).attr('stroke-width', 0.5);
277
+ }
278
+
279
+ // SmolLM2 highlight zone β€” show where SmolLM2 dots land
280
+ const smolExp = filtered.filter(d => d.prompt === p && d.family === 'SmolLM2');
281
+ if (smolExp.length) {
282
+ const smolX = smolExp[0].x;
283
+ gRows.append('rect')
284
+ .attr('x', smolX - rBase * 2.5).attr('y', y - rowH * 0.42)
285
+ .attr('width', rBase * 5).attr('height', rowH * 0.84)
286
+ .attr('fill', smolHighlight).attr('rx', rBase * 2);
287
+ }
288
+
289
+ // Label
290
+ gRows.append('text')
291
+ .attr('x', pl - 10).attr('y', y)
292
+ .attr('text-anchor', 'end')
293
+ .attr('dominant-baseline', 'central')
294
+ .attr('fill', textColor)
295
+ .attr('font-size', fontSize + 'px')
296
+ .attr('font-weight', '700')
297
+ .attr('font-family', 'system-ui, -apple-system, sans-serif')
298
+ .attr('letter-spacing', '-0.2px')
299
+ .text(p);
300
+ });
301
+
302
+ // ─── X AXIS TICKS ───
303
+ const gRef = svg.selectAll('g.ref').data([0]).join('g').attr('class', 'ref');
304
+ gRef.selectAll('*').remove();
305
+
306
+ [20, 50, 100, 200, 500].forEach(v => {
307
+ const x = xScale(v);
308
+ if (x > pl && x < pr) {
309
+ gRef.append('line')
310
+ .attr('x1', x).attr('x2', x)
311
+ .attr('y1', pb).attr('y2', pb + 3)
312
+ .attr('stroke', mutedText).attr('stroke-width', 0.7);
313
+ gRef.append('text')
314
+ .attr('x', x).attr('y', pb + 5)
315
+ .attr('text-anchor', 'middle')
316
+ .attr('dominant-baseline', 'hanging')
317
+ .attr('fill', mutedText)
318
+ .attr('font-size', (fontSize * 0.65) + 'px')
319
+ .attr('font-family', 'system-ui, -apple-system, sans-serif')
320
+ .text(v);
321
+ // Subtle grid line
322
+ gRef.append('line')
323
+ .attr('x1', x).attr('x2', x)
324
+ .attr('y1', pt).attr('y2', pb)
325
+ .attr('stroke', subtleText).attr('stroke-width', 0.4)
326
+ .attr('stroke-dasharray', '2,6');
327
+ }
328
+ });
329
+
330
+ // Axis label
331
+ gRef.append('text')
332
+ .attr('x', pr).attr('y', pb + 16)
333
+ .attr('text-anchor', 'end')
334
+ .attr('fill', mutedText)
335
+ .attr('font-size', (fontSize * 0.72) + 'px')
336
+ .attr('font-weight', '600')
337
+ .attr('font-family', 'system-ui, -apple-system, sans-serif')
338
+ .text('Output tokens / document \u2192');
339
+
340
+ // "Less verbose" / "More verbose" labels
341
+ gRef.append('text')
342
+ .attr('x', pl + 6).attr('y', pt - 5)
343
+ .attr('text-anchor', 'start')
344
+ .attr('fill', subtleText)
345
+ .attr('font-size', (fontSize * 0.62) + 'px')
346
+ .attr('font-style', 'italic')
347
+ .attr('font-family', 'system-ui, -apple-system, sans-serif')
348
+ .text('\u2190 concise');
349
+
350
+ gRef.append('text')
351
+ .attr('x', pr - 6).attr('y', pt - 5)
352
+ .attr('text-anchor', 'end')
353
+ .attr('fill', subtleText)
354
+ .attr('font-size', (fontSize * 0.62) + 'px')
355
+ .attr('font-style', 'italic')
356
+ .attr('font-family', 'system-ui, -apple-system, sans-serif')
357
+ .text('verbose \u2192');
358
+
359
+ // ─── MODEL FAMILY LEGEND ───
360
+ const gLeg = svg.selectAll('g.legend').data([0]).join('g').attr('class', 'legend');
361
+ gLeg.selectAll('*').remove();
362
+ const legDotR = Math.max(3, fontSize * 0.35);
363
+ const legGap = Math.max(68, width * 0.082);
364
+ const totalLegW = familyOrder.length * legGap;
365
+ const legStartX = Math.max(pl, (width - totalLegW) / 2);
366
+ const legY = height * 0.055;
367
+
368
+ familyOrder.forEach((fam, i) => {
369
+ const lx = legStartX + i * legGap;
370
+ gLeg.append('circle')
371
+ .attr('cx', lx).attr('cy', legY)
372
+ .attr('r', legDotR)
373
+ .attr('fill', familyColors[fam]).attr('fill-opacity', 0.85);
374
+ gLeg.append('text')
375
+ .attr('x', lx + legDotR + 4).attr('y', legY)
376
+ .attr('dominant-baseline', 'central')
377
+ .attr('fill', isDark ? 'rgba(255,255,255,0.48)' : 'rgba(0,0,0,0.42)')
378
+ .attr('font-size', (fontSize * 0.72) + 'px')
379
+ .attr('font-weight', fam === 'SmolLM2' ? '700' : '500')
380
+ .attr('font-family', 'system-ui, -apple-system, sans-serif')
381
+ .text(fam);
382
+ });
383
+
384
+ // ─── TOOLTIP ───
385
+ container.style.position = container.style.position || 'relative';
386
+ let tip = container.querySelector('.d3-tooltip');
387
+ let tipInner;
388
+ if (!tip) {
389
+ tip = document.createElement('div');
390
+ tip.className = 'd3-tooltip';
391
+ Object.assign(tip.style, {
392
+ position: 'absolute', top: '0px', left: '0px',
393
+ transform: 'translate(-9999px, -9999px)',
394
+ pointerEvents: 'none',
395
+ padding: '10px 14px', borderRadius: '12px',
396
+ fontSize: '12px', lineHeight: '1.4',
397
+ border: '1px solid var(--border-color)',
398
+ background: 'var(--surface-bg)',
399
+ color: 'var(--text-color)',
400
+ boxShadow: '0 8px 32px rgba(0,0,0,.28), 0 2px 8px rgba(0,0,0,.12)',
401
+ opacity: '0', transition: 'opacity .12s ease',
402
+ backdropFilter: 'saturate(1.12) blur(8px)',
403
+ zIndex: '20', maxWidth: '320px'
404
+ });
405
+ tipInner = document.createElement('div');
406
+ tipInner.className = 'd3-tooltip__inner';
407
+ Object.assign(tipInner.style, {
408
+ textAlign: 'left', display: 'flex', flexDirection: 'column',
409
+ gap: '4px', minWidth: '220px'
410
+ });
411
+ tip.appendChild(tipInner);
412
+ container.appendChild(tip);
413
+ } else {
414
+ tipInner = tip.querySelector('.d3-tooltip__inner') || tip;
415
+ }
416
+
417
+ // ─── DOTS ───
418
+ const gDots = svg.selectAll('g.dots').data([0]).join('g').attr('class', 'dots');
419
+
420
+ const handleEnter = function (ev, d) {
421
+ gDots.selectAll('circle').attr('fill-opacity', 0.12).attr('stroke-opacity', 0.05);
422
+ // Highlight same family across all prompts
423
+ gDots.selectAll('circle')
424
+ .filter(c => c.family === d.family)
425
+ .attr('fill-opacity', 0.60).attr('stroke-opacity', 0.4);
426
+ d3.select(this)
427
+ .raise()
428
+ .attr('fill-opacity', 1)
429
+ .attr('stroke-opacity', 1)
430
+ .style('filter', `drop-shadow(0 0 8px ${glowColor})`)
431
+ .transition().duration(90).ease(d3.easeCubicOut)
432
+ .attr('r', rBase * 1.5);
433
+ tipInner.innerHTML = buildTip(d);
434
+ tip.style.opacity = '1';
435
+ };
436
+ const handleMove = (ev) => {
437
+ const [mx, my] = d3.pointer(ev, container);
438
+ const bw = tip.offsetWidth || 260;
439
+ const bh = tip.offsetHeight || 180;
440
+ const ox = (mx + bw + 20 > width) ? -(bw + 12) : 12;
441
+ const oy = (my + bh + 20 > height) ? -(bh + 12) : 14;
442
+ tip.style.transform = `translate(${Math.round(mx + ox)}px, ${Math.round(my + oy)}px)`;
443
+ };
444
+ const handleLeave = function (ev, d) {
445
+ gDots.selectAll('circle')
446
+ .attr('fill-opacity', 0.82)
447
+ .attr('stroke-opacity', 0.30);
448
+ d3.select(this)
449
+ .style('filter', null)
450
+ .transition().duration(90).ease(d3.easeCubicOut)
451
+ .attr('r', rBase);
452
+ tip.style.opacity = '0';
453
+ tip.style.transform = 'translate(-9999px, -9999px)';
454
+ };
455
+
456
+ gDots.selectAll('circle').data(filtered, d => d.idx)
457
+ .join(
458
+ enter => enter.append('circle')
459
+ .attr('cx', d => d.x).attr('cy', d => d.y)
460
+ .attr('r', rBase)
461
+ .attr('fill', d => familyColors[d.family] || '#aaa')
462
+ .attr('fill-opacity', 0.82)
463
+ .attr('stroke', d => d.family === 'SmolLM2' ? '#fff' : (familyColors[d.family] || '#aaa'))
464
+ .attr('stroke-width', d => d.family === 'SmolLM2' ? 1.5 : 0.9)
465
+ .attr('stroke-opacity', d => d.family === 'SmolLM2' ? 0.5 : 0.30)
466
+ .on('mouseenter', handleEnter)
467
+ .on('mousemove', handleMove)
468
+ .on('mouseleave', handleLeave),
469
+ update => update
470
+ .attr('cx', d => d.x).attr('cy', d => d.y)
471
+ .attr('r', rBase)
472
+ .attr('fill', d => familyColors[d.family] || '#aaa')
473
+ .attr('fill-opacity', 0.82)
474
+ .attr('stroke', d => d.family === 'SmolLM2' ? '#fff' : (familyColors[d.family] || '#aaa'))
475
+ .attr('stroke-width', d => d.family === 'SmolLM2' ? 1.5 : 0.9)
476
+ .attr('stroke-opacity', d => d.family === 'SmolLM2' ? 0.5 : 0.30)
477
+ .on('mouseenter', handleEnter)
478
+ .on('mousemove', handleMove)
479
+ .on('mouseleave', handleLeave)
480
+ );
481
+
482
+ // ─── BREATHING ───
483
+ if (animFrame) cancelAnimationFrame(animFrame);
484
+ const breathe = () => {
485
+ const now = Date.now() * 0.001;
486
+ gDots.selectAll('circle').each(function (d) {
487
+ d3.select(this).attr('r', rBase + Math.sin(now * 0.5 + d.phase) * rBase * 0.03);
488
+ });
489
+ animFrame = requestAnimationFrame(breathe);
490
+ };
491
+ breathe();
492
+ };
493
+
494
+ if (window.ResizeObserver) {
495
+ new ResizeObserver(() => render()).observe(container);
496
+ } else {
497
+ window.addEventListener('resize', render);
498
+ }
499
+ render();
500
+ };
501
+
502
+ if (document.readyState === 'loading') {
503
+ document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true });
504
+ } else { ensureD3(bootstrap); }
505
+ })();
506
+ </script>