Files
brk/website_next/learn/charts/renderer.js
T
2026-06-07 16:46:53 +02:00

143 lines
3.5 KiB
JavaScript

import { createSeriesHighlight } from "./highlight.js";
import { createSeriesLoader } from "./loader.js";
import { renderPlot } from "./plot.js";
import { createScrubber } from "./scrubber/index.js";
import { createSvgElement } from "./svg.js";
import { getViewBoxHeight, VIEWBOX_WIDTH } from "./viewbox.js";
/**
* @param {Object} args
* @param {SVGSVGElement} args.svg
* @param {Readout} args.readout
* @param {HTMLElement} args.menu
* @param {HTMLElement[]} args.items
* @param {HTMLElement} args.status
* @param {Chart} args.chart
* @param {() => ChartView} args.getView
* @param {() => ChartScale} args.getScale
* @param {() => ChartOrder} args.getOrder
* @param {() => TimeframeValue} args.getTimeframe
*/
export function createChartRenderer({
svg,
readout,
menu,
items,
status,
chart,
getView,
getScale,
getOrder,
getTimeframe,
}) {
const group = createSvgElement("g");
const highlight = createSeriesHighlight(items, menu);
const loadSeries = createSeriesLoader(chart);
/** @type {LoadedSeries[]} */
let loadedSeries = [];
/** @type {ReturnType<typeof createScrubber> | undefined} */
let scrubber;
const resizeObserver = new ResizeObserver(renderCurrent);
let active = false;
let loadId = 0;
svg.append(group);
function clearStatus() {
status.textContent = "";
svg.removeAttribute("aria-busy");
}
/** @param {string} message */
function setStatus(message) {
status.textContent = message;
}
function renderCurrent() {
if (!active || !loadedSeries.length) return;
const height = getViewBoxHeight(svg);
svg.setAttribute("viewBox", `0 0 ${VIEWBOX_WIDTH} ${height}`);
group.replaceChildren();
highlight.clearNodes();
scrubber ??= createScrubber(svg, readout, highlight);
scrubber.setSeries(
renderPlot(
getView(),
group,
loadedSeries,
height,
highlight,
getScale(),
getOrder(),
),
height,
);
}
async function loadCurrent() {
const id = (loadId += 1);
const loadingTimer = setTimeout(() => {
if (id === loadId && active) setStatus("Loading");
}, 250);
setStatus("");
svg.setAttribute("aria-busy", "true");
try {
const nextSeries = await loadSeries(getTimeframe());
if (id !== loadId || !active) return;
loadedSeries = nextSeries;
renderCurrent();
clearStatus();
} catch (error) {
if (id !== loadId) return;
console.error(error);
setStatus("Chart unavailable");
} finally {
clearTimeout(loadingTimer);
if (id === loadId) svg.removeAttribute("aria-busy");
}
}
function resume() {
if (active) return;
active = true;
resizeObserver.observe(svg);
void loadCurrent();
}
function suspend() {
if (!active) return;
active = false;
loadedSeries = [];
loadId += 1;
resizeObserver.disconnect();
group.replaceChildren();
highlight.clearNodes();
scrubber?.clear();
clearStatus();
}
return {
loadCurrent,
renderCurrent,
resume,
suspend,
};
}
/** @typedef {import("./index.js").Chart} Chart */
/** @typedef {import("./index.js").LoadedSeries} LoadedSeries */
/** @typedef {import("./legend/index.js").Readout} Readout */
/** @typedef {import("./order.js").ChartOrder} ChartOrder */
/** @typedef {import("./scale.js").ChartScale} ChartScale */
/** @typedef {import("./timeframes.js").TimeframeValue} TimeframeValue */
/** @typedef {import("./views.js").ChartView} ChartView */
/** @typedef {import("./highlight.js").SeriesHighlight} SeriesHighlight */