Skip to content

Commit fa59a85

Browse files
committed
feat(site): hover the growth chart to see each sync's date and delta
Hovering the History area chart now snaps a marker + guide line to the nearest sync and shows a tooltip with its date, record total, and the change (delta + per-category breakdown). Works on touch too. The static start/current captions are removed — that info now appears only on hover. Refs #1
1 parent 7ac1297 commit fa59a85

2 files changed

Lines changed: 65 additions & 16 deletions

File tree

site/src/scripts/techapi.js

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -289,17 +289,14 @@ function countUp(node, target, opts = {}) {
289289
const ys = (t) => VH - PAD - ((t - minTotal) / range) * (VH - 2 * PAD);
290290
const line = points.map((p, i) => `${i ? "L" : "M"}${xs(i).toFixed(1)} ${ys(p.total).toFixed(1)}`).join(" ");
291291
const area = `${line} L${xs(points.length - 1).toFixed(1)} ${VH} L${xs(0).toFixed(1)} ${VH} Z`;
292-
const last = points[points.length - 1];
293292
chartEl.innerHTML = `<svg class="history-svg" viewBox="0 0 ${VW} ${VH}" preserveAspectRatio="none" aria-label="Dataset growth curve">
294293
<defs><linearGradient id="histfill" x1="0" y1="0" x2="0" y2="1">
295294
<stop offset="0%" stop-color="var(--accent)" stop-opacity=".34"></stop>
296295
<stop offset="100%" stop-color="var(--accent)" stop-opacity="0"></stop>
297296
</linearGradient></defs>
298297
<path d="${area}" fill="url(#histfill)"></path>
299298
<path d="${line}" fill="none" stroke="var(--accent)" stroke-width="2" vector-effect="non-scaling-stroke" stroke-linejoin="round" stroke-linecap="round"></path>
300-
</svg>
301-
<span class="history-cap history-cap-lo">${esc(points[0].when)} · ${minTotal.toLocaleString()}</span>
302-
<span class="history-cap history-cap-hi">${esc(last.when)} · ${last.total.toLocaleString()}</span>`;
299+
</svg>`;
303300

304301
// Show every sync (newest first), growth-first. The list scrolls (CSS
305302
// max-height) so the full history stays reachable without a giant section.
@@ -318,6 +315,47 @@ function countUp(node, target, opts = {}) {
318315
<small>${esc(changes)}${tag ? ` · ${esc(tag)}` : ""}</small>
319316
</span></li>`;
320317
}).join("");
318+
319+
// Hover the curve: snap to the nearest sync and show its date + total + delta.
320+
const hover = document.createElement("div");
321+
hover.className = "history-hover";
322+
hover.hidden = true;
323+
hover.innerHTML = `<span class="hh-line"></span><span class="hh-dot"></span><div class="hh-tip"></div>`;
324+
chartEl.appendChild(hover);
325+
const hLine = hover.querySelector(".hh-line");
326+
const hDot = hover.querySelector(".hh-dot");
327+
const hTip = hover.querySelector(".hh-tip");
328+
329+
function moveHover(clientX) {
330+
const rect = chartEl.getBoundingClientRect();
331+
if (!rect.width) return;
332+
const relX = Math.min(1, Math.max(0, (clientX - rect.left) / rect.width));
333+
const i = Math.round(relX * (points.length - 1));
334+
const p = points[i];
335+
const px = (xs(i) / VW) * rect.width;
336+
const py = (ys(p.total) / VH) * rect.height;
337+
hover.hidden = false;
338+
hLine.style.left = px + "px";
339+
hDot.style.left = px + "px";
340+
hDot.style.top = py + "px";
341+
const chg = p.changes && p.changes.length
342+
? p.changes.map((row) => `${shortLabel[row.key]} ${formatDelta(row.delta)}`).join(", ")
343+
: "";
344+
hTip.innerHTML = `<b>${esc(p.when)}</b>` +
345+
`<span>${p.total.toLocaleString()} records</span>` +
346+
`<span class="hh-delta${p.delta < 0 ? " is-negative" : ""}">${p.baseline ? "baseline" : esc(formatDelta(p.delta))}</span>` +
347+
(chg ? `<span class="hh-chg">${esc(chg)}</span>` : "");
348+
const tipW = hTip.offsetWidth || 150;
349+
let tx = px + 14;
350+
if (tx + tipW > rect.width) tx = px - tipW - 14;
351+
hTip.style.left = Math.max(4, tx) + "px";
352+
hTip.style.top = Math.min(rect.height - (hTip.offsetHeight || 70) - 4, Math.max(4, py - 24)) + "px";
353+
}
354+
chartEl.onmousemove = (e) => moveHover(e.clientX);
355+
chartEl.onmouseleave = () => { hover.hidden = true; };
356+
chartEl.ontouchstart = chartEl.ontouchmove = (e) => {
357+
if (e.touches[0]) moveHover(e.touches[0].clientX);
358+
};
321359
}
322360

323361
const fmtWhen = (date) => date

site/src/styles/techapi.css

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -326,18 +326,29 @@ code, .mono { font-family: var(--mono); }
326326
var(--surface-2);
327327
}
328328
.history-svg { display: block; width: 100%; height: 100%; }
329-
.history-cap {
330-
position: absolute;
331-
bottom: 8px;
332-
font-family: var(--mono);
333-
font-size: 10.5px;
334-
color: var(--muted);
335-
background: color-mix(in srgb, var(--surface-2) 72%, transparent);
336-
padding: 1px 6px;
337-
border-radius: 3px;
338-
}
339-
.history-cap-lo { left: 9px; }
340-
.history-cap-hi { right: 9px; color: var(--accent-text); }
329+
.history-hover { position: absolute; inset: 0; pointer-events: none; z-index: 2; }
330+
.hh-line {
331+
position: absolute; top: 0; bottom: 0; width: 1px;
332+
background: color-mix(in srgb, var(--accent) 55%, transparent);
333+
transform: translateX(-0.5px);
334+
}
335+
.hh-dot {
336+
position: absolute; width: 10px; height: 10px; border-radius: 50%;
337+
background: var(--accent); border: 2px solid var(--surface-2);
338+
transform: translate(-50%, -50%);
339+
box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent) 18%, transparent);
340+
}
341+
.hh-tip {
342+
position: absolute; min-width: 130px; display: grid; gap: 2px;
343+
padding: 8px 10px; border: 1px solid var(--border-strong); border-radius: 6px;
344+
background: var(--surface); box-shadow: var(--code-shadow);
345+
font-family: var(--mono); font-size: 11.5px; white-space: nowrap; line-height: 1.35;
346+
}
347+
.hh-tip b { font-size: 12.5px; color: var(--fg); }
348+
.hh-tip span { color: var(--muted); }
349+
.hh-tip .hh-delta { color: var(--accent-text); font-weight: 600; }
350+
.hh-tip .hh-delta.is-negative { color: var(--err); }
351+
.hh-tip .hh-chg { color: var(--faint); font-size: 10.5px; }
341352
.history-empty {
342353
position: absolute;
343354
inset: 0;

0 commit comments

Comments
 (0)