perf scatter: log-x + alternating-position labels (kill overlap)

Two issues with the accuracy-vs-latency scatter:
1. Linear x crammed RNN/GRU/LSTM into ~25 px of axis (380/520/700 μs)
   while BERT alone took the right 80 % (3200 μs).
2. Labels placed at fixed +12 right of each point overlapped both
   neighbouring points and other labels in the recurrent cluster.

Fixes:
- X-axis switched to log10 with bounds 10μs–10ms; tick labels and
  marks added at 10μs / 100μs / 1ms / 10ms so the audience can
  read the scale.
- Y-axis bounds tightened to [0.5, 1.0] (was [0.7, 1.0]) so KNN's
  ~0.43 cross-host F1 falls within the visible plot area instead
  of off-bottom; ticks added at 0.6 / 0.8 / 1.0.
- Anti-overlap label placement: sort points by x, alternate
  above (-12) / below (+18) the circle. Adjacent labels can no
  longer share both x and y bands. repaintLabels() re-runs on
  each model_perf event so late arrivals slot into the staircase.

Y-axis title also updated: "held-out accuracy" → "held-out macro-F1"
to match the actual metric the producer reports.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Max Gorog 2026-05-08 16:31:42 -05:00
parent 9e7d9999a3
commit f429bd4223
2 changed files with 88 additions and 22 deletions

View file

@ -2387,32 +2387,80 @@ def train_nn(*, model, X_train, y_train, X_val, y_val,
window.dashboard.scene('references', { onEnter: loadFirstIfReady });
})();
// ── Performance scatter — DEMO ONLY ───────────────────────────
// ── Performance scatter (log-x, anti-overlap labels) ──────────
// X-axis is log10(latency_us) so the 35× range KNN→BERT spreads
// evenly. Labels are sorted by x and placed alternately above /
// below their points so adjacent labels can't horizontally
// overlap even when points cluster (e.g. RNN/GRU/LSTM near each
// other in latency).
(function () {
const svg = document.getElementById('perf-scatter');
const ns = 'http://www.w3.org/2000/svg';
const W = 600, H = 360;
const xmin = 0, xmax = 4000, ymin = 0.7, ymax = 1.0;
const points = new Map();
// Log-scale x bounds: 10 μs (1e1) to 10 ms (1e4).
const xLogMin = 1, xLogMax = 4;
const ymin = 0.5, ymax = 1.0;
const points = new Map(); // model → { g, latency, accuracy }
function setupAxes() {
svg.innerHTML = '';
const ax = document.createElementNS(ns, 'line');
ax.setAttribute('x1', 50); ax.setAttribute('y1', H - 30);
ax.setAttribute('x2', W - 10); ax.setAttribute('y2', H - 30);
ax.setAttribute('x1', 50); ax.setAttribute('y1', H - 40);
ax.setAttribute('x2', W - 10); ax.setAttribute('y2', H - 40);
ax.setAttribute('class', 'axis'); svg.appendChild(ax);
const ay = document.createElementNS(ns, 'line');
ay.setAttribute('x1', 50); ay.setAttribute('y1', 10);
ay.setAttribute('x2', 50); ay.setAttribute('y2', H - 30);
ay.setAttribute('x2', 50); ay.setAttribute('y2', H - 40);
ay.setAttribute('class', 'axis'); svg.appendChild(ay);
// x-axis log ticks at 10μs, 100μs, 1ms, 10ms.
[
{ v: 10, lbl: '10μs' },
{ v: 100, lbl: '100μs' },
{ v: 1000, lbl: '1ms' },
{ v: 10000, lbl: '10ms' },
].forEach(({ v, lbl }) => {
const lx = Math.log10(v);
const x = 50 + ((lx - xLogMin) / (xLogMax - xLogMin)) * (W - 60);
const tick = document.createElementNS(ns, 'line');
tick.setAttribute('x1', x.toFixed(1)); tick.setAttribute('x2', x.toFixed(1));
tick.setAttribute('y1', H - 40); tick.setAttribute('y2', H - 35);
tick.setAttribute('class', 'axis');
svg.appendChild(tick);
const tlbl = document.createElementNS(ns, 'text');
tlbl.setAttribute('x', x.toFixed(1));
tlbl.setAttribute('y', H - 22);
tlbl.setAttribute('class', 'axis-label');
tlbl.setAttribute('text-anchor', 'middle');
tlbl.style.fontSize = '11px';
tlbl.textContent = lbl;
svg.appendChild(tlbl);
});
// y-axis ticks at 0.6, 0.8, 1.0
[0.6, 0.8, 1.0].forEach(v => {
const y = (H - 40) - ((v - ymin) / (ymax - ymin)) * (H - 50);
const tick = document.createElementNS(ns, 'line');
tick.setAttribute('x1', 45); tick.setAttribute('x2', 50);
tick.setAttribute('y1', y.toFixed(1)); tick.setAttribute('y2', y.toFixed(1));
tick.setAttribute('class', 'axis');
svg.appendChild(tick);
const tlbl = document.createElementNS(ns, 'text');
tlbl.setAttribute('x', 42);
tlbl.setAttribute('y', (y + 4).toFixed(1));
tlbl.setAttribute('class', 'axis-label');
tlbl.setAttribute('text-anchor', 'end');
tlbl.style.fontSize = '11px';
tlbl.textContent = v.toFixed(1);
svg.appendChild(tlbl);
});
const xl = document.createElementNS(ns, 'text');
xl.setAttribute('x', W / 2); xl.setAttribute('y', H - 8);
xl.setAttribute('x', W / 2); xl.setAttribute('y', H - 6);
xl.setAttribute('class', 'axis-label'); xl.setAttribute('text-anchor', 'middle');
xl.textContent = 'inference latency (μs / window) →'; svg.appendChild(xl);
xl.textContent = 'inference latency (log scale, μs / window) →';
svg.appendChild(xl);
const yl = document.createElementNS(ns, 'text');
yl.setAttribute('transform', `translate(14,${H/2}) rotate(-90)`);
yl.setAttribute('class', 'axis-label'); yl.setAttribute('text-anchor', 'middle');
yl.textContent = '↑ held-out accuracy'; svg.appendChild(yl);
yl.textContent = '↑ held-out macro-F1'; svg.appendChild(yl);
}
function emptyState() {
points.clear(); svg.innerHTML = '';
@ -2423,15 +2471,33 @@ def train_nn(*, model, X_train, y_train, X_val, y_val,
svg.appendChild(t);
}
function project(latency, accuracy) {
const x = 50 + Math.min(1, (latency - xmin) / (xmax - xmin)) * (W - 60);
const y = (H - 30) - Math.max(0, Math.min(1, (accuracy - ymin) / (ymax - ymin))) * (H - 40);
const lat = Math.max(1, latency);
const lx = Math.log10(lat);
const x = 50 + Math.min(1, Math.max(0, (lx - xLogMin) / (xLogMax - xLogMin))) * (W - 60);
const y = (H - 40) - Math.max(0, Math.min(1, (accuracy - ymin) / (ymax - ymin))) * (H - 50);
return [x, y];
}
// Anti-overlap: sort points by x, place labels alternately
// above (even index) and below (odd index) so adjacent labels
// never share both x and y bands. Re-runs every render so a
// late-arriving model_perf event slots into the staircase.
function repaintLabels() {
const entries = Array.from(points.entries()).map(([model, rec]) => ({
model, rec, x: rec.cx, y: rec.cy,
})).sort((a, b) => a.x - b.x);
entries.forEach((e, i) => {
const t = e.rec.g.querySelector('text');
const above = (i % 2 === 0);
t.setAttribute('x', e.x.toFixed(1));
t.setAttribute('y', (above ? e.y - 12 : e.y + 18).toFixed(1));
t.setAttribute('text-anchor', 'middle');
});
}
function render(model, latency, accuracy) {
let g = points.get(model);
if (!g) {
let rec = points.get(model);
if (!rec) {
if (!points.size) setupAxes();
g = document.createElementNS(ns, 'g');
const g = document.createElementNS(ns, 'g');
const c = document.createElementNS(ns, 'circle');
c.setAttribute('r', 7); c.setAttribute('class', 'perf-point');
g.appendChild(c);
@ -2439,15 +2505,15 @@ def train_nn(*, model, X_train, y_train, X_val, y_val,
t.setAttribute('class', 'perf-label');
g.appendChild(t);
svg.appendChild(g);
points.set(model, g);
rec = { g, cx: 0, cy: 0 };
points.set(model, rec);
}
const [px, py] = project(latency, accuracy);
g.querySelector('circle').setAttribute('cx', px.toFixed(1));
g.querySelector('circle').setAttribute('cy', py.toFixed(1));
const t = g.querySelector('text');
t.setAttribute('x', (px + 12).toFixed(1));
t.setAttribute('y', (py + 4).toFixed(1));
t.textContent = model;
rec.cx = px; rec.cy = py;
rec.g.querySelector('circle').setAttribute('cx', px.toFixed(1));
rec.g.querySelector('circle').setAttribute('cy', py.toFixed(1));
rec.g.querySelector('text').textContent = model;
repaintLabels();
}
on('demo_start', () => {
[

View file

@ -1314,6 +1314,6 @@
</article>
</div>
<script src="/static/dashboard.js?v=eb69e920"></script>
<script src="/static/dashboard.js?v=7f398906"></script>
</body>
</html>