Files
brk/website_next/learn/charts/scrubber.js
T
2026-06-07 00:54:50 +02:00

172 lines
4.3 KiB
JavaScript

import { formatValue } from "./format.js";
import { createSvgElement } from "./svg.js";
import { VIEWBOX_WIDTH } from "./viewbox.js";
/** @typedef {import("./highlight.js").SeriesHighlight} SeriesHighlight */
/** @typedef {import("./legend.js").Readout} Readout */
const dateFormat = new Intl.DateTimeFormat("en-US", {
day: "2-digit",
month: "2-digit",
year: "numeric",
});
/**
* @param {number} value
* @param {number} min
* @param {number} max
*/
function clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}
/**
* @param {ScrubberSeries} series
* @param {number} ratio
*/
function getPointAtRatio(series, ratio) {
return series.points[Math.round(ratio * (series.points.length - 1))];
}
/**
* @param {HTMLTimeElement} time
* @param {Date} date
*/
function updateTime(time, date) {
time.textContent = dateFormat.format(date);
time.dateTime = date.toISOString().slice(0, 10);
}
/**
* @param {Readout} readout
* @param {ReturnType<typeof getPointAtRatio>[]} points
*/
function updateReadout(readout, points) {
updateTime(readout.time, points[0].date);
readout.rows.forEach(({ value }, index) => {
value.textContent = formatValue(points[index].value);
});
}
/**
* @param {SVGSVGElement} svg
* @param {Readout} readout
* @param {SeriesHighlight} highlight
*/
export function createScrubber(svg, readout, highlight) {
const group = createSvgElement("g");
const guide = createSvgElement("line");
/** @type {ScrubberSeries[]} */
let series = [];
/** @type {SVGCircleElement[]} */
let markers = [];
let height = 0;
let stepCount = 0;
group.dataset.scrubber = "root";
guide.dataset.scrubber = "guide";
group.append(guide);
svg.append(group);
/**
* @param {number} ratio
* @param {boolean} [scrubbing]
*/
function update(ratio, scrubbing = true) {
const nextRatio = clamp(ratio, 0, 1);
const points = series.map((item) => getPointAtRatio(item, nextRatio));
const x = points[0].x.toFixed(2);
svg.dataset.index = Math.round(nextRatio * stepCount).toString();
guide.setAttribute("x1", x);
guide.setAttribute("x2", x);
guide.setAttribute("y1", "0");
guide.setAttribute("y2", height.toString());
updateReadout(readout, points);
markers.forEach((marker, index) => {
const point = points[index];
marker.setAttribute("cx", point.x.toFixed(2));
marker.setAttribute("cy", point.y.toFixed(2));
});
if (scrubbing) {
svg.dataset.scrubbing = "true";
} else {
delete svg.dataset.scrubbing;
}
}
function hide() {
update(1, false);
}
function clear() {
series = [];
markers = [];
group.replaceChildren(guide);
delete svg.dataset.index;
delete svg.dataset.scrubbing;
}
/**
* @param {ScrubberSeries[]} nextSeries
* @param {number} nextHeight
*/
function setSeries(nextSeries, nextHeight) {
series = nextSeries;
height = nextHeight;
stepCount = Math.max(...series.map(({ points }) => points.length - 1));
markers = series.map(({ color }, index) => {
const marker = createSvgElement("circle");
marker.dataset.series = index.toString();
marker.dataset.scrubber = "marker";
marker.style.setProperty("--color", color);
marker.setAttribute("r", "3");
highlight.add(marker, index);
return marker;
});
group.replaceChildren(guide, ...markers);
update(1, false);
}
/** @param {PointerEvent} event */
function updateFromPointer(event) {
const { left, width } = svg.getBoundingClientRect();
const x = ((event.clientX - left) / width) * VIEWBOX_WIDTH;
update(x / VIEWBOX_WIDTH);
}
svg.addEventListener("pointermove", updateFromPointer);
svg.addEventListener("pointerleave", hide);
svg.addEventListener("focus", () => update(1));
svg.addEventListener("blur", hide);
svg.addEventListener("keydown", (event) => {
const current = Number(svg.dataset.index || stepCount);
if (event.key === "ArrowLeft") {
event.preventDefault();
update((current - 1) / stepCount);
}
if (event.key === "ArrowRight") {
event.preventDefault();
update((current + 1) / stepCount);
}
});
return { clear, setSeries };
}
/**
* @typedef {Object} ScrubberSeries
* @property {string} color
* @property {{ date: Date, value: number, x: number, y: number }[]} points
*/