Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
Commit Β·
d6bc164
1
Parent(s): 5df08f8
made various improvements to the verbosity plot
Browse files
app/src/content/embeds/verbosity.html
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
<div class="d3-verbosity" style="width:100%;margin:10px 0;aspect-ratio:
|
| 2 |
<script>
|
| 3 |
(() => {
|
| 4 |
const ensureD3 = (cb) => {
|
|
@@ -98,6 +98,9 @@
|
|
| 98 |
outEdu: d.output_edu_score,
|
| 99 |
inEdu: d.input_edu_score,
|
| 100 |
compPerDoc: d.output_token_count_mean,
|
|
|
|
|
|
|
|
|
|
| 101 |
phase: Math.random() * Math.PI * 2
|
| 102 |
};
|
| 103 |
});
|
|
@@ -106,12 +109,12 @@
|
|
| 106 |
// MODEL FAMILIES
|
| 107 |
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 108 |
const familyColors = {
|
| 109 |
-
'Gemma': '#
|
| 110 |
-
'Qwen': '#
|
| 111 |
-
'SmolLM2': '#
|
| 112 |
-
'Falcon': '#
|
| 113 |
-
'Granite': '#
|
| 114 |
-
'Llama': '#
|
| 115 |
};
|
| 116 |
const familyOrder = ['Gemma','Qwen','SmolLM2','Falcon','Granite','Llama'];
|
| 117 |
|
|
@@ -145,9 +148,11 @@
|
|
| 145 |
`<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:${familyColors[d.family]};margin-right:4px;vertical-align:middle;"></span>` +
|
| 146 |
`${d.model} \u00b7 ${d.source}</div>` +
|
| 147 |
`<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;">` +
|
| 148 |
-
`<span style="color:var(--muted-color);">
|
| 149 |
`<span style="color:var(--muted-color);">Output/doc</span><span>${Math.round(d.compPerDoc)} tokens</span>` +
|
|
|
|
| 150 |
`<span style="color:var(--muted-color);">Docs</span><span>${d.numDocs.toFixed(1)}M</span>` +
|
|
|
|
| 151 |
`</div>` +
|
| 152 |
`<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;">` +
|
| 153 |
`<span style="color:var(--muted-color);">DCLM</span>` +
|
|
@@ -168,38 +173,40 @@
|
|
| 168 |
|
| 169 |
const render = () => {
|
| 170 |
const width = container.clientWidth || 800;
|
| 171 |
-
const height = Math.max(
|
| 172 |
svg.attr('width', width).attr('height', height);
|
| 173 |
|
| 174 |
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
| 175 |
-
const textColor = isDark ? 'rgba(255,255,255,0.
|
| 176 |
-
const mutedText = isDark ? 'rgba(255,255,255,0.
|
| 177 |
-
const subtleText = isDark ? 'rgba(255,255,255,0.
|
| 178 |
const refLine = isDark ? 'rgba(255,255,255,0.10)' : 'rgba(0,0,0,0.07)';
|
| 179 |
const bandEven = isDark ? 'rgba(255,255,255,0.022)' : 'rgba(0,0,0,0.018)';
|
| 180 |
const glowColor = isDark ? 'rgba(255,255,255,0.35)' : 'rgba(0,0,0,0.25)';
|
| 181 |
-
const smolHighlight = isDark ? 'rgba(232,196,74,0.18)' : 'rgba(232,196,74,0.12)';
|
| 182 |
|
| 183 |
-
const fontSize = Math.max(
|
| 184 |
|
| 185 |
// βββ LAYOUT βββ
|
| 186 |
-
const labelW = Math.max(
|
| 187 |
-
const pl = labelW +
|
| 188 |
-
const pr = width
|
| 189 |
-
const pt =
|
| 190 |
-
const pb = height *
|
| 191 |
const plotH = pb - pt;
|
| 192 |
const rowH = plotH / sortedPrompts.length;
|
| 193 |
|
| 194 |
const rowCenter = {};
|
| 195 |
sortedPrompts.forEach((p, i) => { rowCenter[p] = pt + (i + 0.5) * rowH; });
|
| 196 |
|
| 197 |
-
// βββ X SCALE:
|
| 198 |
-
const
|
| 199 |
-
|
|
|
|
|
|
|
|
|
|
| 200 |
.range([pl, pr]);
|
| 201 |
|
| 202 |
-
const rBase = Math.max(
|
| 203 |
|
| 204 |
// βββ FORCE SIMULATION βββ
|
| 205 |
filtered.forEach(d => {
|
|
@@ -260,23 +267,13 @@
|
|
| 260 |
.attr('stroke', refLine).attr('stroke-width', 0.5);
|
| 261 |
}
|
| 262 |
|
| 263 |
-
// SmolLM2 highlight zone
|
| 264 |
-
const smolExp = filtered.filter(d => d.prompt === p && d.family === 'SmolLM2');
|
| 265 |
-
if (smolExp.length) {
|
| 266 |
-
const smolX = smolExp[0].x;
|
| 267 |
-
gRows.append('rect')
|
| 268 |
-
.attr('x', smolX - rBase * 2.5).attr('y', y - rowH * 0.42)
|
| 269 |
-
.attr('width', rBase * 5).attr('height', rowH * 0.84)
|
| 270 |
-
.attr('fill', smolHighlight).attr('rx', rBase * 2);
|
| 271 |
-
}
|
| 272 |
-
|
| 273 |
// Label
|
| 274 |
gRows.append('text')
|
| 275 |
.attr('x', pl - 10).attr('y', y)
|
| 276 |
.attr('text-anchor', 'end')
|
| 277 |
.attr('dominant-baseline', 'central')
|
| 278 |
.attr('fill', textColor)
|
| 279 |
-
.attr('font-size', fontSize + 'px')
|
| 280 |
.attr('font-weight', '700')
|
| 281 |
.attr('font-family', 'system-ui, -apple-system, sans-serif')
|
| 282 |
.attr('letter-spacing', '-0.2px')
|
|
@@ -287,19 +284,19 @@
|
|
| 287 |
const gRef = svg.selectAll('g.ref').data([0]).join('g').attr('class', 'ref');
|
| 288 |
gRef.selectAll('*').remove();
|
| 289 |
|
| 290 |
-
[
|
| 291 |
const x = xScale(v);
|
| 292 |
-
if (x > pl && x < pr) {
|
| 293 |
gRef.append('line')
|
| 294 |
.attr('x1', x).attr('x2', x)
|
| 295 |
-
.attr('y1', pb).attr('y2', pb +
|
| 296 |
.attr('stroke', mutedText).attr('stroke-width', 0.7);
|
| 297 |
gRef.append('text')
|
| 298 |
-
.attr('x', x).attr('y', pb +
|
| 299 |
.attr('text-anchor', 'middle')
|
| 300 |
.attr('dominant-baseline', 'hanging')
|
| 301 |
.attr('fill', mutedText)
|
| 302 |
-
.attr('font-size', (fontSize * 0.
|
| 303 |
.attr('font-family', 'system-ui, -apple-system, sans-serif')
|
| 304 |
.text(v);
|
| 305 |
gRef.append('line')
|
|
@@ -310,58 +307,90 @@
|
|
| 310 |
}
|
| 311 |
});
|
| 312 |
|
| 313 |
-
// Axis label
|
|
|
|
| 314 |
gRef.append('text')
|
| 315 |
-
.attr('x', pr).attr('y',
|
| 316 |
-
.attr('text-anchor', '
|
| 317 |
.attr('fill', mutedText)
|
| 318 |
-
.attr('font-size', (fontSize * 0.
|
| 319 |
.attr('font-weight', '600')
|
| 320 |
.attr('font-family', 'system-ui, -apple-system, sans-serif')
|
| 321 |
-
.text('Output tokens / document
|
| 322 |
|
| 323 |
-
// "concise" / "verbose"
|
| 324 |
gRef.append('text')
|
| 325 |
-
.attr('x',
|
| 326 |
-
.attr('text-anchor', '
|
| 327 |
.attr('fill', subtleText)
|
| 328 |
-
.attr('font-size', (fontSize * 0.
|
| 329 |
.attr('font-style', 'italic')
|
| 330 |
.attr('font-family', 'system-ui, -apple-system, sans-serif')
|
| 331 |
.text('\u2190 concise');
|
| 332 |
|
| 333 |
gRef.append('text')
|
| 334 |
-
.attr('x',
|
| 335 |
-
.attr('text-anchor', '
|
| 336 |
.attr('fill', subtleText)
|
| 337 |
-
.attr('font-size', (fontSize * 0.
|
| 338 |
.attr('font-style', 'italic')
|
| 339 |
.attr('font-family', 'system-ui, -apple-system, sans-serif')
|
| 340 |
.text('verbose \u2192');
|
| 341 |
|
|
|
|
| 342 |
// βββ MODEL FAMILY LEGEND βββ
|
| 343 |
const gLeg = svg.selectAll('g.legend').data([0]).join('g').attr('class', 'legend');
|
| 344 |
gLeg.selectAll('*').remove();
|
| 345 |
-
const legDotR = Math.max(
|
| 346 |
-
const
|
| 347 |
-
const
|
| 348 |
-
const
|
| 349 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 350 |
|
|
|
|
| 351 |
familyOrder.forEach((fam, i) => {
|
| 352 |
-
const
|
| 353 |
-
|
| 354 |
.attr('cx', lx).attr('cy', legY)
|
| 355 |
.attr('r', legDotR)
|
| 356 |
.attr('fill', familyColors[fam]).attr('fill-opacity', 0.85);
|
| 357 |
-
|
| 358 |
-
.attr('x', lx + legDotR +
|
| 359 |
.attr('dominant-baseline', 'central')
|
| 360 |
-
.attr('fill', isDark ? 'rgba(255,255,255,0.
|
| 361 |
-
.attr('font-size',
|
| 362 |
-
.attr('font-weight',
|
| 363 |
.attr('font-family', 'system-ui, -apple-system, sans-serif')
|
| 364 |
.text(fam);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 365 |
});
|
| 366 |
|
| 367 |
// βββ TOOLTIP βββ
|
|
@@ -401,10 +430,10 @@
|
|
| 401 |
const gDots = svg.selectAll('g.dots').data([0]).join('g').attr('class', 'dots');
|
| 402 |
|
| 403 |
const handleEnter = function (ev, d) {
|
| 404 |
-
gDots.selectAll('circle').attr('fill-opacity', 0.
|
| 405 |
gDots.selectAll('circle')
|
| 406 |
.filter(c => c.family === d.family)
|
| 407 |
-
.attr('fill-opacity', 0.
|
| 408 |
d3.select(this)
|
| 409 |
.raise()
|
| 410 |
.attr('fill-opacity', 1)
|
|
@@ -426,7 +455,7 @@
|
|
| 426 |
const handleLeave = function () {
|
| 427 |
gDots.selectAll('circle')
|
| 428 |
.attr('fill-opacity', 0.82)
|
| 429 |
-
.attr('stroke-opacity', 0.
|
| 430 |
d3.select(this)
|
| 431 |
.style('filter', null)
|
| 432 |
.transition().duration(90).ease(d3.easeCubicOut)
|
|
@@ -442,9 +471,9 @@
|
|
| 442 |
.attr('r', rBase)
|
| 443 |
.attr('fill', d => familyColors[d.family] || '#aaa')
|
| 444 |
.attr('fill-opacity', 0.82)
|
| 445 |
-
.attr('stroke', d =>
|
| 446 |
-
.attr('stroke-width',
|
| 447 |
-
.attr('stroke-opacity',
|
| 448 |
.on('mouseenter', handleEnter)
|
| 449 |
.on('mousemove', handleMove)
|
| 450 |
.on('mouseleave', handleLeave),
|
|
@@ -453,9 +482,9 @@
|
|
| 453 |
.attr('r', rBase)
|
| 454 |
.attr('fill', d => familyColors[d.family] || '#aaa')
|
| 455 |
.attr('fill-opacity', 0.82)
|
| 456 |
-
.attr('stroke', d =>
|
| 457 |
-
.attr('stroke-width',
|
| 458 |
-
.attr('stroke-opacity',
|
| 459 |
.on('mouseenter', handleEnter)
|
| 460 |
.on('mousemove', handleMove)
|
| 461 |
.on('mouseleave', handleLeave)
|
|
|
|
| 1 |
+
<div class="d3-verbosity" style="width:100%;margin:10px 0;aspect-ratio:2.2/1;min-height:320px;"></div>
|
| 2 |
<script>
|
| 3 |
(() => {
|
| 4 |
const ensureD3 = (cb) => {
|
|
|
|
| 98 |
outEdu: d.output_edu_score,
|
| 99 |
inEdu: d.input_edu_score,
|
| 100 |
compPerDoc: d.output_token_count_mean,
|
| 101 |
+
inputPerDoc: d.input_token_count_mean,
|
| 102 |
+
tokenReduction: d.token_reduction_mean,
|
| 103 |
+
compressionRatio: d.compression_ratio,
|
| 104 |
phase: Math.random() * Math.PI * 2
|
| 105 |
};
|
| 106 |
});
|
|
|
|
| 109 |
// MODEL FAMILIES
|
| 110 |
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 111 |
const familyColors = {
|
| 112 |
+
'Gemma': '#5b9bd5',
|
| 113 |
+
'Qwen': '#e07b54',
|
| 114 |
+
'SmolLM2': '#e06b9e',
|
| 115 |
+
'Falcon': '#c9a046',
|
| 116 |
+
'Granite': '#9a8ec2',
|
| 117 |
+
'Llama': '#8bc474',
|
| 118 |
};
|
| 119 |
const familyOrder = ['Gemma','Qwen','SmolLM2','Falcon','Granite','Llama'];
|
| 120 |
|
|
|
|
| 148 |
`<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:${familyColors[d.family]};margin-right:4px;vertical-align:middle;"></span>` +
|
| 149 |
`${d.model} \u00b7 ${d.source}</div>` +
|
| 150 |
`<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;">` +
|
| 151 |
+
`<span style="color:var(--muted-color);">Input/doc</span><span>${Math.round(d.inputPerDoc)} tokens</span>` +
|
| 152 |
`<span style="color:var(--muted-color);">Output/doc</span><span>${Math.round(d.compPerDoc)} tokens</span>` +
|
| 153 |
+
`<span style="color:var(--muted-color);">Reduction</span><span>${Math.round(d.tokenReduction)} tokens (${(d.compressionRatio * 100).toFixed(0)}% ratio)</span>` +
|
| 154 |
`<span style="color:var(--muted-color);">Docs</span><span>${d.numDocs.toFixed(1)}M</span>` +
|
| 155 |
+
`<span style="color:var(--muted-color);">Total output</span><span>${fmtB(d.compTokens)}</span>` +
|
| 156 |
`</div>` +
|
| 157 |
`<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;">` +
|
| 158 |
`<span style="color:var(--muted-color);">DCLM</span>` +
|
|
|
|
| 173 |
|
| 174 |
const render = () => {
|
| 175 |
const width = container.clientWidth || 800;
|
| 176 |
+
const height = Math.max(320, Math.round(width / 2.2));
|
| 177 |
svg.attr('width', width).attr('height', height);
|
| 178 |
|
| 179 |
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
| 180 |
+
const textColor = isDark ? 'rgba(255,255,255,0.82)' : 'rgba(0,0,0,0.72)';
|
| 181 |
+
const mutedText = isDark ? 'rgba(255,255,255,0.40)' : 'rgba(0,0,0,0.35)';
|
| 182 |
+
const subtleText = isDark ? 'rgba(255,255,255,0.20)' : 'rgba(0,0,0,0.15)';
|
| 183 |
const refLine = isDark ? 'rgba(255,255,255,0.10)' : 'rgba(0,0,0,0.07)';
|
| 184 |
const bandEven = isDark ? 'rgba(255,255,255,0.022)' : 'rgba(0,0,0,0.018)';
|
| 185 |
const glowColor = isDark ? 'rgba(255,255,255,0.35)' : 'rgba(0,0,0,0.25)';
|
|
|
|
| 186 |
|
| 187 |
+
const fontSize = Math.max(12, Math.min(15, width / 45));
|
| 188 |
|
| 189 |
// βββ LAYOUT βββ
|
| 190 |
+
const labelW = Math.max(70, width * 0.08);
|
| 191 |
+
const pl = labelW + 10;
|
| 192 |
+
const pr = width - 4;
|
| 193 |
+
const pt = fontSize * 2.8;
|
| 194 |
+
const pb = height - fontSize * 5;
|
| 195 |
const plotH = pb - pt;
|
| 196 |
const rowH = plotH / sortedPrompts.length;
|
| 197 |
|
| 198 |
const rowCenter = {};
|
| 199 |
sortedPrompts.forEach((p, i) => { rowCenter[p] = pt + (i + 0.5) * rowH; });
|
| 200 |
|
| 201 |
+
// βββ X SCALE: linear βββ
|
| 202 |
+
const allVals = filtered.map(d => d.compPerDoc);
|
| 203 |
+
const xMin = Math.min(150, d3.min(allVals) - 50);
|
| 204 |
+
const xMax = Math.max(1150, d3.max(allVals) + 50);
|
| 205 |
+
const xScale = d3.scaleLinear()
|
| 206 |
+
.domain([xMin, xMax])
|
| 207 |
.range([pl, pr]);
|
| 208 |
|
| 209 |
+
const rBase = Math.max(4.5, Math.min(10, width * 0.009));
|
| 210 |
|
| 211 |
// βββ FORCE SIMULATION βββ
|
| 212 |
filtered.forEach(d => {
|
|
|
|
| 267 |
.attr('stroke', refLine).attr('stroke-width', 0.5);
|
| 268 |
}
|
| 269 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 270 |
// Label
|
| 271 |
gRows.append('text')
|
| 272 |
.attr('x', pl - 10).attr('y', y)
|
| 273 |
.attr('text-anchor', 'end')
|
| 274 |
.attr('dominant-baseline', 'central')
|
| 275 |
.attr('fill', textColor)
|
| 276 |
+
.attr('font-size', (fontSize * 1.05) + 'px')
|
| 277 |
.attr('font-weight', '700')
|
| 278 |
.attr('font-family', 'system-ui, -apple-system, sans-serif')
|
| 279 |
.attr('letter-spacing', '-0.2px')
|
|
|
|
| 284 |
const gRef = svg.selectAll('g.ref').data([0]).join('g').attr('class', 'ref');
|
| 285 |
gRef.selectAll('*').remove();
|
| 286 |
|
| 287 |
+
[200, 500, 800, 1100].forEach(v => {
|
| 288 |
const x = xScale(v);
|
| 289 |
+
if (x > pl + 10 && x < pr - 10) {
|
| 290 |
gRef.append('line')
|
| 291 |
.attr('x1', x).attr('x2', x)
|
| 292 |
+
.attr('y1', pb).attr('y2', pb + 4)
|
| 293 |
.attr('stroke', mutedText).attr('stroke-width', 0.7);
|
| 294 |
gRef.append('text')
|
| 295 |
+
.attr('x', x).attr('y', pb + 8)
|
| 296 |
.attr('text-anchor', 'middle')
|
| 297 |
.attr('dominant-baseline', 'hanging')
|
| 298 |
.attr('fill', mutedText)
|
| 299 |
+
.attr('font-size', (fontSize * 0.9) + 'px')
|
| 300 |
.attr('font-family', 'system-ui, -apple-system, sans-serif')
|
| 301 |
.text(v);
|
| 302 |
gRef.append('line')
|
|
|
|
| 307 |
}
|
| 308 |
});
|
| 309 |
|
| 310 |
+
// Axis label (centered below tick numbers)
|
| 311 |
+
const axisLabelY = pb + 8 + fontSize * 1.6;
|
| 312 |
gRef.append('text')
|
| 313 |
+
.attr('x', (pl + pr) / 2).attr('y', axisLabelY)
|
| 314 |
+
.attr('text-anchor', 'middle')
|
| 315 |
.attr('fill', mutedText)
|
| 316 |
+
.attr('font-size', (fontSize * 0.9) + 'px')
|
| 317 |
.attr('font-weight', '600')
|
| 318 |
.attr('font-family', 'system-ui, -apple-system, sans-serif')
|
| 319 |
+
.text('Output tokens / document');
|
| 320 |
|
| 321 |
+
// "concise" / "verbose" arrows (aligned below first/last tick)
|
| 322 |
gRef.append('text')
|
| 323 |
+
.attr('x', xScale(200)).attr('y', axisLabelY)
|
| 324 |
+
.attr('text-anchor', 'middle')
|
| 325 |
.attr('fill', subtleText)
|
| 326 |
+
.attr('font-size', (fontSize * 0.82) + 'px')
|
| 327 |
.attr('font-style', 'italic')
|
| 328 |
.attr('font-family', 'system-ui, -apple-system, sans-serif')
|
| 329 |
.text('\u2190 concise');
|
| 330 |
|
| 331 |
gRef.append('text')
|
| 332 |
+
.attr('x', xScale(1100)).attr('y', axisLabelY)
|
| 333 |
+
.attr('text-anchor', 'middle')
|
| 334 |
.attr('fill', subtleText)
|
| 335 |
+
.attr('font-size', (fontSize * 0.82) + 'px')
|
| 336 |
.attr('font-style', 'italic')
|
| 337 |
.attr('font-family', 'system-ui, -apple-system, sans-serif')
|
| 338 |
.text('verbose \u2192');
|
| 339 |
|
| 340 |
+
|
| 341 |
// βββ MODEL FAMILY LEGEND βββ
|
| 342 |
const gLeg = svg.selectAll('g.legend').data([0]).join('g').attr('class', 'legend');
|
| 343 |
gLeg.selectAll('*').remove();
|
| 344 |
+
const legDotR = Math.max(4, fontSize * 0.38);
|
| 345 |
+
const legFontSize = fontSize * 0.9;
|
| 346 |
+
const legY = fontSize * 1.1;
|
| 347 |
+
const legItemSpacing = legDotR * 2 + 6;
|
| 348 |
+
|
| 349 |
+
// Measure text widths to compute equal center-to-center gaps
|
| 350 |
+
const tempTexts = familyOrder.map(fam => {
|
| 351 |
+
const t = gLeg.append('text').attr('font-size', legFontSize + 'px')
|
| 352 |
+
.attr('font-weight', '500')
|
| 353 |
+
.attr('font-family', 'system-ui, -apple-system, sans-serif')
|
| 354 |
+
.text(fam);
|
| 355 |
+
const w = t.node().getComputedTextLength();
|
| 356 |
+
t.remove();
|
| 357 |
+
return w;
|
| 358 |
+
});
|
| 359 |
+
const itemWidths = tempTexts.map(tw => legItemSpacing + tw);
|
| 360 |
+
const totalLegW = itemWidths.reduce((a, b) => a + b, 0);
|
| 361 |
+
const legPadding = Math.max(12, (width - totalLegW) / (familyOrder.length + 1));
|
| 362 |
+
const totalWithPad = totalLegW + legPadding * (familyOrder.length - 1);
|
| 363 |
+
const legStartX = (width - totalWithPad) / 2;
|
| 364 |
|
| 365 |
+
let lx = legStartX;
|
| 366 |
familyOrder.forEach((fam, i) => {
|
| 367 |
+
const ig = gLeg.append('g').style('cursor', 'pointer');
|
| 368 |
+
ig.append('circle')
|
| 369 |
.attr('cx', lx).attr('cy', legY)
|
| 370 |
.attr('r', legDotR)
|
| 371 |
.attr('fill', familyColors[fam]).attr('fill-opacity', 0.85);
|
| 372 |
+
ig.append('text')
|
| 373 |
+
.attr('x', lx + legDotR + 5).attr('y', legY)
|
| 374 |
.attr('dominant-baseline', 'central')
|
| 375 |
+
.attr('fill', isDark ? 'rgba(255,255,255,0.65)' : 'rgba(0,0,0,0.60)')
|
| 376 |
+
.attr('font-size', legFontSize + 'px')
|
| 377 |
+
.attr('font-weight', '500')
|
| 378 |
.attr('font-family', 'system-ui, -apple-system, sans-serif')
|
| 379 |
.text(fam);
|
| 380 |
+
// Hit area for easier hovering
|
| 381 |
+
ig.append('rect')
|
| 382 |
+
.attr('x', lx - legDotR - 4).attr('y', legY - legFontSize * 0.7)
|
| 383 |
+
.attr('width', itemWidths[i] + 8).attr('height', legFontSize * 1.4)
|
| 384 |
+
.attr('fill', 'transparent');
|
| 385 |
+
ig.on('mouseenter', () => {
|
| 386 |
+
gDots.selectAll('circle').transition().duration(80)
|
| 387 |
+
.attr('fill-opacity', d => d.family === fam ? 0.55 : 0.06)
|
| 388 |
+
.attr('stroke-opacity', d => d.family === fam ? 0.5 : 0.03);
|
| 389 |
+
}).on('mouseleave', () => {
|
| 390 |
+
gDots.selectAll('circle').transition().duration(160)
|
| 391 |
+
.attr('fill-opacity', 0.82).attr('stroke-opacity', 0.12);
|
| 392 |
+
});
|
| 393 |
+
lx += itemWidths[i] + legPadding;
|
| 394 |
});
|
| 395 |
|
| 396 |
// βββ TOOLTIP βββ
|
|
|
|
| 430 |
const gDots = svg.selectAll('g.dots').data([0]).join('g').attr('class', 'dots');
|
| 431 |
|
| 432 |
const handleEnter = function (ev, d) {
|
| 433 |
+
gDots.selectAll('circle').attr('fill-opacity', 0.1).attr('stroke-opacity', 0.08);
|
| 434 |
gDots.selectAll('circle')
|
| 435 |
.filter(c => c.family === d.family)
|
| 436 |
+
.attr('fill-opacity', 0.55).attr('stroke-opacity', 0.5);
|
| 437 |
d3.select(this)
|
| 438 |
.raise()
|
| 439 |
.attr('fill-opacity', 1)
|
|
|
|
| 455 |
const handleLeave = function () {
|
| 456 |
gDots.selectAll('circle')
|
| 457 |
.attr('fill-opacity', 0.82)
|
| 458 |
+
.attr('stroke-opacity', 0.12);
|
| 459 |
d3.select(this)
|
| 460 |
.style('filter', null)
|
| 461 |
.transition().duration(90).ease(d3.easeCubicOut)
|
|
|
|
| 471 |
.attr('r', rBase)
|
| 472 |
.attr('fill', d => familyColors[d.family] || '#aaa')
|
| 473 |
.attr('fill-opacity', 0.82)
|
| 474 |
+
.attr('stroke', d => familyColors[d.family] || '#aaa')
|
| 475 |
+
.attr('stroke-width', 0.7)
|
| 476 |
+
.attr('stroke-opacity', 0.12)
|
| 477 |
.on('mouseenter', handleEnter)
|
| 478 |
.on('mousemove', handleMove)
|
| 479 |
.on('mouseleave', handleLeave),
|
|
|
|
| 482 |
.attr('r', rBase)
|
| 483 |
.attr('fill', d => familyColors[d.family] || '#aaa')
|
| 484 |
.attr('fill-opacity', 0.82)
|
| 485 |
+
.attr('stroke', d => familyColors[d.family] || '#aaa')
|
| 486 |
+
.attr('stroke-width', 0.7)
|
| 487 |
+
.attr('stroke-opacity', 0.12)
|
| 488 |
.on('mouseenter', handleEnter)
|
| 489 |
.on('mousemove', handleMove)
|
| 490 |
.on('mouseleave', handleLeave)
|