diff --git a/training/dashboard/static/dashboard.js b/training/dashboard/static/dashboard.js
index 45b9793..0d5b233 100644
--- a/training/dashboard/static/dashboard.js
+++ b/training/dashboard/static/dashboard.js
@@ -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', () => {
[
diff --git a/training/dashboard/static/index.html b/training/dashboard/static/index.html
index dc09048..1b96fdc 100644
--- a/training/dashboard/static/index.html
+++ b/training/dashboard/static/index.html
@@ -1314,6 +1314,6 @@
-
+