Files
brk/website_next/learn/charts/highlight.js
T
2026-06-09 13:35:21 +02:00

153 lines
3.7 KiB
JavaScript

/**
* @param {HTMLElement[]} items
* @param {HTMLElement} menu
*/
export function createSeriesHighlight(items, menu) {
const seriesNodes = /** @type {SeriesNode[]} */ (items.map(() => []));
const noSeries = -1;
let selectedSeries = noSeries;
let previewedSeries = noSeries;
/** @param {number} index */
function scrollToItem(index) {
const margin = Number.parseFloat(getComputedStyle(menu).paddingLeft);
const itemRect = items[index].getBoundingClientRect();
const menuRect = menu.getBoundingClientRect();
if (itemRect.left < menuRect.left + margin) {
menu.scrollBy({
left: itemRect.left - menuRect.left - margin,
behavior: "smooth",
});
} else if (itemRect.right > menuRect.right - margin) {
menu.scrollBy({
left: itemRect.right - menuRect.right + margin,
behavior: "smooth",
});
}
}
/** @param {number} index */
function highlightSeries(index) {
for (const [itemIndex, item] of items.entries()) {
setActive(item, itemIndex === index);
}
seriesNodes.forEach((nodes, nodeIndex) => {
for (const node of nodes) {
setActive(node, nodeIndex === index);
}
});
}
function clearHighlight() {
for (const item of items) clearElementState(item);
for (const nodes of seriesNodes) {
for (const node of nodes) clearElementState(node);
}
}
function restoreSelectedHighlight() {
if (selectedSeries === noSeries) {
clearHighlight();
} else {
highlightSeries(selectedSeries);
}
}
function clearInteractionHighlight() {
clearPreview();
restoreSelectedHighlight();
}
/** @param {number} index */
function selectSeries(index) {
selectedSeries = index;
items.forEach((item, itemIndex) => {
item.setAttribute(
"aria-pressed",
(itemIndex === selectedSeries).toString(),
);
});
restoreSelectedHighlight();
}
/** @param {number} index */
function previewSeries(index) {
if (index === previewedSeries) return;
clearPreview();
scrollToItem(index);
items[index].dataset.preview = "";
previewedSeries = index;
}
function clearPreview() {
if (previewedSeries === noSeries) return;
delete items[previewedSeries].dataset.preview;
previewedSeries = noSeries;
}
items.forEach((item, index) => {
item.setAttribute("aria-pressed", "false");
item.addEventListener("pointerenter", () => highlightSeries(index));
item.addEventListener("pointerleave", clearInteractionHighlight);
item.addEventListener("focus", () => highlightSeries(index));
item.addEventListener("blur", clearInteractionHighlight);
item.addEventListener("click", () => {
selectSeries(selectedSeries === index ? noSeries : index);
});
});
/**
* @param {SVGPathElement | SVGCircleElement} node
* @param {number} index
*/
function addNode(node, index) {
if (selectedSeries !== noSeries) setActive(node, index === selectedSeries);
seriesNodes[index].push(node);
}
function clearNodes() {
clearInteractionHighlight();
for (const nodes of seriesNodes) {
nodes.length = 0;
}
}
return {
addNode,
clearPreview,
clearNodes,
preview: previewSeries,
};
}
/**
* @param {HTMLElement | SVGElement} element
* @param {boolean} active
*/
function setActive(element, active) {
if (active) {
element.dataset.active = "";
delete element.dataset.muted;
} else {
delete element.dataset.active;
element.dataset.muted = "";
}
}
/** @param {HTMLElement | SVGElement} element */
function clearElementState(element) {
delete element.dataset.active;
delete element.dataset.muted;
delete element.dataset.preview;
}
/** @typedef {(SVGPathElement | SVGCircleElement)[]} SeriesNode */