Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 42 additions & 4 deletions site/src/scripts/techapi.js
Original file line number Diff line number Diff line change
Expand Up @@ -289,17 +289,14 @@ function countUp(node, target, opts = {}) {
const ys = (t) => VH - PAD - ((t - minTotal) / range) * (VH - 2 * PAD);
const line = points.map((p, i) => `${i ? "L" : "M"}${xs(i).toFixed(1)} ${ys(p.total).toFixed(1)}`).join(" ");
const area = `${line} L${xs(points.length - 1).toFixed(1)} ${VH} L${xs(0).toFixed(1)} ${VH} Z`;
const last = points[points.length - 1];
chartEl.innerHTML = `<svg class="history-svg" viewBox="0 0 ${VW} ${VH}" preserveAspectRatio="none" aria-label="Dataset growth curve">
<defs><linearGradient id="histfill" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="var(--accent)" stop-opacity=".34"></stop>
<stop offset="100%" stop-color="var(--accent)" stop-opacity="0"></stop>
</linearGradient></defs>
<path d="${area}" fill="url(#histfill)"></path>
<path d="${line}" fill="none" stroke="var(--accent)" stroke-width="2" vector-effect="non-scaling-stroke" stroke-linejoin="round" stroke-linecap="round"></path>
</svg>
<span class="history-cap history-cap-lo">${esc(points[0].when)} · ${minTotal.toLocaleString()}</span>
<span class="history-cap history-cap-hi">${esc(last.when)} · ${last.total.toLocaleString()}</span>`;
</svg>`;

// Show every sync (newest first), growth-first. The list scrolls (CSS
// max-height) so the full history stays reachable without a giant section.
Expand All @@ -318,6 +315,47 @@ function countUp(node, target, opts = {}) {
<small>${esc(changes)}${tag ? ` · ${esc(tag)}` : ""}</small>
</span></li>`;
}).join("");

// Hover the curve: snap to the nearest sync and show its date + total + delta.
const hover = document.createElement("div");
hover.className = "history-hover";
hover.hidden = true;
hover.innerHTML = `<span class="hh-line"></span><span class="hh-dot"></span><div class="hh-tip"></div>`;
chartEl.appendChild(hover);
const hLine = hover.querySelector(".hh-line");
const hDot = hover.querySelector(".hh-dot");
const hTip = hover.querySelector(".hh-tip");

function moveHover(clientX) {
const rect = chartEl.getBoundingClientRect();
if (!rect.width) return;
const relX = Math.min(1, Math.max(0, (clientX - rect.left) / rect.width));
const i = Math.round(relX * (points.length - 1));
const p = points[i];
const px = (xs(i) / VW) * rect.width;
const py = (ys(p.total) / VH) * rect.height;
hover.hidden = false;
hLine.style.left = px + "px";
hDot.style.left = px + "px";
hDot.style.top = py + "px";
const chg = p.changes && p.changes.length
? p.changes.map((row) => `${shortLabel[row.key]} ${formatDelta(row.delta)}`).join(", ")
: "";
hTip.innerHTML = `<b>${esc(p.when)}</b>` +
`<span>${p.total.toLocaleString()} records</span>` +
`<span class="hh-delta${p.delta < 0 ? " is-negative" : ""}">${p.baseline ? "baseline" : esc(formatDelta(p.delta))}</span>` +
(chg ? `<span class="hh-chg">${esc(chg)}</span>` : "");
const tipW = hTip.offsetWidth || 150;
let tx = px + 14;
if (tx + tipW > rect.width) tx = px - tipW - 14;
hTip.style.left = Math.max(4, tx) + "px";
hTip.style.top = Math.min(rect.height - (hTip.offsetHeight || 70) - 4, Math.max(4, py - 24)) + "px";
}
chartEl.onmousemove = (e) => moveHover(e.clientX);
chartEl.onmouseleave = () => { hover.hidden = true; };
chartEl.ontouchstart = chartEl.ontouchmove = (e) => {
if (e.touches[0]) moveHover(e.touches[0].clientX);
};
}

const fmtWhen = (date) => date
Expand Down
35 changes: 23 additions & 12 deletions site/src/styles/techapi.css
Original file line number Diff line number Diff line change
Expand Up @@ -326,18 +326,29 @@ code, .mono { font-family: var(--mono); }
var(--surface-2);
}
.history-svg { display: block; width: 100%; height: 100%; }
.history-cap {
position: absolute;
bottom: 8px;
font-family: var(--mono);
font-size: 10.5px;
color: var(--muted);
background: color-mix(in srgb, var(--surface-2) 72%, transparent);
padding: 1px 6px;
border-radius: 3px;
}
.history-cap-lo { left: 9px; }
.history-cap-hi { right: 9px; color: var(--accent-text); }
.history-hover { position: absolute; inset: 0; pointer-events: none; z-index: 2; }
.hh-line {
position: absolute; top: 0; bottom: 0; width: 1px;
background: color-mix(in srgb, var(--accent) 55%, transparent);
transform: translateX(-0.5px);
}
.hh-dot {
position: absolute; width: 10px; height: 10px; border-radius: 50%;
background: var(--accent); border: 2px solid var(--surface-2);
transform: translate(-50%, -50%);
box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent) 18%, transparent);
}
.hh-tip {
position: absolute; min-width: 130px; display: grid; gap: 2px;
padding: 8px 10px; border: 1px solid var(--border-strong); border-radius: 6px;
background: var(--surface); box-shadow: var(--code-shadow);
font-family: var(--mono); font-size: 11.5px; white-space: nowrap; line-height: 1.35;
}
.hh-tip b { font-size: 12.5px; color: var(--fg); }
.hh-tip span { color: var(--muted); }
.hh-tip .hh-delta { color: var(--accent-text); font-weight: 600; }
.hh-tip .hh-delta.is-negative { color: var(--err); }
.hh-tip .hh-chg { color: var(--faint); font-size: 10.5px; }
.history-empty {
position: absolute;
inset: 0;
Expand Down
Loading