diff --git a/website/src/heatmap/index.js b/website/src/heatmap/index.js index affe6d987..98a31766a 100644 --- a/website/src/heatmap/index.js +++ b/website/src/heatmap/index.js @@ -116,6 +116,7 @@ function loadRange() { } let cursor = 0; + let needsRebuild = false; const workers = Array.from({ length: Math.min(MAX_PARALLEL_FETCHES, missing.length), }).map(async () => { @@ -123,10 +124,17 @@ function loadRange() { while (index !== undefined) { const entry = missing[index]; try { - const points = await option.points.fetch(entry.date, controller.signal); + const points = await option.points.fetch( + entry.date, + controller.signal, + (points) => { + if (isCurrentLoad(option, controller, generation)) { + setPoints(entry, points); + } + }, + ); if (isCurrentLoad(option, controller, generation)) { - pointsByDate.set(entry.date, points); - addDateToGrid(entry.dateIndex, points); + setPoints(entry, points); } } catch (error) { if (controller.signal.aborted) return; @@ -147,7 +155,11 @@ function loadRange() { void Promise.all(workers).then(() => { if (isCurrentLoad(option, controller, generation)) { updateStatus(completed, currentDates.length, failed); - paint(); + if (needsRebuild) { + rebuildGrid(); + } else { + paint(); + } } }); @@ -157,6 +169,21 @@ function loadRange() { cursor += 1; return index; } + + /** + * @param {{ date: string, dateIndex: number }} entry + * @param {HeatmapPoints} points + */ + function setPoints(entry, points) { + const previous = pointsByDate.get(entry.date); + if (previous && samePoints(previous, points)) return; + pointsByDate.set(entry.date, points); + if (previous) { + needsRebuild = true; + } else { + addDateToGrid(entry.dateIndex, points); + } + } } /** @@ -209,6 +236,20 @@ function addDateToGrid(dateIndex, points) { if (dirtyCol !== undefined) schedulePaint(dirtyCol); } +/** + * @param {HeatmapPoints} a + * @param {HeatmapPoints} b + */ +function samePoints(a, b) { + if (a === b) return true; + if (a.kind !== b.kind || a.values !== b.values) return false; + if (a.kind === "implicit" && b.kind === "implicit") { + return a.yStart === b.yStart && a.yStep === b.yStep; + } + if (a.kind === "explicit" && b.kind === "explicit") return a.y === b.y; + return false; +} + /** @param {number} col */ function schedulePaint(col) { dirtyCols.add(col); diff --git a/website/src/heatmap/oracle.js b/website/src/heatmap/oracle.js index b8391a7cb..6aca8a52d 100644 --- a/website/src/heatmap/oracle.js +++ b/website/src/heatmap/oracle.js @@ -9,6 +9,7 @@ import { defaultTooltip } from "./tooltip.js"; const BINS = 2400; const MIN_LOG = -8; const BINS_PER_DECADE = 200; +const MAX_LOG = MIN_LOG + (BINS - 1) / BINS_PER_DECADE; export const oracleRawHeatmapOption = createOracleHeatmapOption("raw", "Raw"); export const oracleEmaHeatmapOption = createOracleHeatmapOption("ema", "EMA"); @@ -24,7 +25,8 @@ function createOracleHeatmapOption(mode, name) { name, title: `Oracle ${name} Histogram`, points: { - fetch: (date, signal) => fetchOraclePoints(mode, date, signal), + fetch: (date, signal, onPoints) => + fetchOraclePoints(mode, date, signal, onPoints), }, grid: createAverageGrid({ yStart: MIN_LOG, @@ -40,40 +42,44 @@ function createOracleHeatmapOption(mode, name) { * @param {"raw" | "ema"} mode * @param {string} date * @param {AbortSignal} signal + * @param {(points: HeatmapPoints) => void} [onPoints] * @returns {Promise} */ -async function fetchOraclePoints(mode, date, signal) { - const values = await firstAvailable((onValue) => - mode === "raw" - ? brk.getOracleHistogramRaw(date, { signal, onValue }) - : brk.getOracleHistogramEma(date, { signal, onValue }), +async function fetchOraclePoints(mode, date, signal, onPoints) { + const values = await fetchOracleValues( + mode, + date, + signal, + onPoints ? (values) => onPoints(toOraclePoints(values)) : undefined, ); - return { - kind: "implicit", - yStart: MIN_LOG, - yStep: 1 / BINS_PER_DECADE, - values, - }; + return toOraclePoints(values); } /** - * @param {(onValue: (value: number[]) => void) => Promise} fetch + * @param {"raw" | "ema"} mode + * @param {string} date + * @param {AbortSignal} signal + * @param {(values: number[]) => void} [onValue] * @returns {Promise} */ -function firstAvailable(fetch) { - return new Promise((resolve, reject) => { - let settled = false; - - /** @param {number[]} value */ - const resolveOnce = (value) => { - if (settled) return; - settled = true; - resolve(value); - }; - - fetch(resolveOnce).then(resolveOnce, (error) => { - if (!settled) reject(error); - }); - }); +function fetchOracleValues(mode, date, signal, onValue) { + return ( + mode === "raw" + ? brk.getOracleHistogramRaw(date, { signal, onValue }) + : brk.getOracleHistogramEma(date, { signal, onValue }) + ); +} + +/** + * @param {number[]} values + * @returns {HeatmapPoints} + */ +function toOraclePoints(values) { + return { + kind: "implicit", + yStart: MAX_LOG, + yStep: -1 / BINS_PER_DECADE, + values, + }; } diff --git a/website/src/heatmap/types.js b/website/src/heatmap/types.js index 5a84cf0ee..4cc6f0254 100644 --- a/website/src/heatmap/types.js +++ b/website/src/heatmap/types.js @@ -13,7 +13,7 @@ * @typedef {HeatmapImplicitPoints | HeatmapExplicitPoints} HeatmapPoints * * @typedef {Object} HeatmapPointSource - * @property {(date: string, signal: AbortSignal) => Promise} fetch + * @property {(date: string, signal: AbortSignal, onPoints?: (points: HeatmapPoints) => void) => Promise} fetch * * @typedef {Object} HeatmapRange * @property {number} start