diff --git a/crates/brk_client/src/lib.rs b/crates/brk_client/src/lib.rs index 9a4772c4c..488dde531 100644 --- a/crates/brk_client/src/lib.rs +++ b/crates/brk_client/src/lib.rs @@ -6067,7 +6067,7 @@ pub struct BrkClient { impl BrkClient { /// Client version. - pub const VERSION: &'static str = "v0.1.6"; + pub const VERSION: &'static str = "v0.1.7"; /// Create a new client with the given base URL. pub fn new(base_url: impl Into) -> Self { diff --git a/website/scripts/chart/index.js b/website/scripts/chart/index.js index 469b6d365..e172e2b30 100644 --- a/website/scripts/chart/index.js +++ b/website/scripts/chart/index.js @@ -114,11 +114,37 @@ export function createChart({ parent, id: chartId, brk, fitContent }) { // Used to detect and ignore stale operations (in-flight fetches, etc.) let generation = 0; - // Shared time - fetched once per rebuild, all series register callbacks - /** @type {MetricData | null} */ - let sharedTimeData = null; - /** @type {Set<(data: MetricData) => void>} */ - let timeCallbacks = new Set(); + const time = { + /** @type {MetricData | null} */ + data: null, + /** @type {Set<(data: MetricData) => void>} */ + callbacks: new Set(), + /** @type {ReturnType | null} */ + endpoint: null, + + /** @param {ChartableIndex} idx */ + setIndex(idx) { + this.data = null; + this.callbacks = new Set(); + this.endpoint = getTimeEndpoint(idx); + }, + + fetch() { + const endpoint = this.endpoint; + if (!endpoint) return; + const currentGen = generation; + const cached = cache.get(endpoint.path); + if (cached) { + this.data = cached; + } + endpoint.slice(-10000).fetch((result) => { + if (currentGen !== generation) return; + cache.set(endpoint.path, result); + this.data = result; + this.callbacks.forEach((cb) => cb(result)); + }); + }, + }; // Memory cache for instant index switching /** @type {Map>} */ @@ -324,6 +350,10 @@ export function createChart({ parent, id: chartId, brk, fitContent }) { // Periodic refresh of active series data const refreshInterval = setInterval(() => serieses.refreshAll(), 30_000); + const onVisibilityChange = () => { + if (!document.hidden) serieses.refreshAll(); + }; + document.addEventListener("visibilitychange", onVisibilityChange); if (fitContent) { new ResizeObserver(() => ichart.timeScale().fitContent()).observe( @@ -475,6 +505,7 @@ export function createChart({ parent, id: chartId, brk, fitContent }) { all: new Set(), refreshAll() { + time.fetch(); serieses.all.forEach((s) => { if (s.active.value) s.fetch?.(); }); @@ -572,7 +603,7 @@ export function createChart({ parent, id: chartId, brk, fitContent }) { getData, update, remove() { - if (state.onTime) timeCallbacks.delete(state.onTime); + if (state.onTime) time.callbacks.delete(state.onTime); onRemove(); serieses.all.delete(series); panes.seriesByHome.get(paneIndex)?.delete(series); @@ -736,13 +767,13 @@ export function createChart({ parent, id: chartId, brk, fitContent }) { } } - // Register for shared time data (fetched once in rebuild) + // Register for shared time data state.onTime = (result) => { timeData = result; tryProcess(); }; - timeCallbacks.add(state.onTime); - if (sharedTimeData) state.onTime(sharedTimeData); + time.callbacks.add(state.onTime); + if (time.data) state.onTime(time.data); const cachedValues = cache.get(valuesEndpoint.path); if (cachedValues) { @@ -1651,21 +1682,8 @@ export function createChart({ parent, id: chartId, brk, fitContent }) { rebuild() { generation++; initialLoadComplete = false; // Reset to prevent saving stale ranges during load - const currentGen = generation; - const idx = index.get(); - sharedTimeData = null; - timeCallbacks = new Set(); - const timeEndpoint = getTimeEndpoint(idx); - const cached = cache.get(timeEndpoint.path); - if (cached) { - sharedTimeData = cached; - } - timeEndpoint.slice(-10000).fetch((result) => { - if (currentGen !== generation) return; - cache.set(timeEndpoint.path, result); - sharedTimeData = result; - timeCallbacks.forEach((cb) => cb(result)); - }); + time.setIndex(index.get()); + time.fetch(); this.rebuildPane(0); this.rebuildPane(1); }, @@ -1747,6 +1765,7 @@ export function createChart({ parent, id: chartId, brk, fitContent }) { onZoomChange.clear(); removeThemeListener(); clearInterval(refreshInterval); + document.removeEventListener("visibilitychange", onVisibilityChange); ichart.remove(); }, }; diff --git a/website/scripts/options/shared.js b/website/scripts/options/shared.js index a791161c9..28d6ff96e 100644 --- a/website/scripts/options/shared.js +++ b/website/scripts/options/shared.js @@ -265,7 +265,7 @@ export function percentileMap(ratio) { */ export function sdPatterns(ratio) { return /** @type {const} */ ([ - { nameAddon: "all", titleAddon: "", sd: ratio.ratioSd }, + { nameAddon: "All Time", titleAddon: "", sd: ratio.ratioSd }, { nameAddon: "4y", titleAddon: "4y", sd: ratio.ratio4ySd }, { nameAddon: "2y", titleAddon: "2y", sd: ratio.ratio2ySd }, { nameAddon: "1y", titleAddon: "1y", sd: ratio.ratio1ySd }, @@ -465,58 +465,82 @@ export function createZScoresFolder({ }), ], }, - ...sdPats.map(({ nameAddon, titleAddon, sd }) => ({ - name: nameAddon, - title: formatTitle(`${titleAddon ? `${titleAddon} ` : ""}Z-Score`), - top: [ - price({ metric: pricePattern, name: legend, color }), - ...sdBandsUsd(sd).map(({ name: bandName, prop, color: bandColor }) => - price({ - metric: prop, - name: bandName, - color: bandColor, - defaultActive: false, - }), - ), - ], - bottom: [ - baseline({ - metric: sd.zscore, - name: "Z-Score", - unit: Unit.sd, - }), - baseline({ - metric: ratio.ratio, - name: "Ratio", - unit: Unit.ratio, - base: 1, - }), - line({ - metric: sd.sd, - name: "Volatility", - color: colors.gray, - unit: Unit.percentage, - }), - ...sdBandsRatio(sd).map( - ({ name: bandName, prop, color: bandColor }) => - line({ - metric: prop, - name: bandName, - color: bandColor, - unit: Unit.ratio, - defaultActive: false, - }), - ), - priceLine({ - unit: Unit.sd, - }), - ...priceLines({ - unit: Unit.sd, - numbers: [1, -1, 2, -2, 3, -3], - defaultActive: false, - }), - ], - })), + ...sdPats.map(({ nameAddon, titleAddon, sd }) => { + const prefix = titleAddon ? `${titleAddon} ` : ""; + const topPrice = price({ metric: pricePattern, name: legend, color }); + return { + name: nameAddon, + tree: [ + { + name: "Score", + title: formatTitle(`${prefix}Z-Score`), + top: [ + topPrice, + ...sdBandsUsd(sd).map( + ({ name: bandName, prop, color: bandColor }) => + price({ + metric: prop, + name: bandName, + color: bandColor, + defaultActive: false, + }), + ), + ], + bottom: [ + baseline({ + metric: sd.zscore, + name: "Z-Score", + unit: Unit.sd, + }), + priceLine({ + unit: Unit.sd, + }), + ...priceLines({ + unit: Unit.sd, + numbers: [1, -1, 2, -2, 3, -3], + defaultActive: false, + }), + ], + }, + { + name: "Ratio", + title: formatTitle(`${prefix}Ratio`), + top: [topPrice], + bottom: [ + baseline({ + metric: ratio.ratio, + name: "Ratio", + unit: Unit.ratio, + base: 1, + }), + ...sdBandsRatio(sd).map( + ({ name: bandName, prop, color: bandColor }) => + line({ + metric: prop, + name: bandName, + color: bandColor, + unit: Unit.ratio, + defaultActive: false, + }), + ), + ], + }, + { + name: "Volatility", + title: formatTitle(`${prefix}Volatility`), + top: [topPrice], + bottom: [ + line({ + metric: sd.sd, + name: "Volatility", + color: colors.gray, + unit: Unit.percentage, + }), + ], + }, + ], + }; + }), ], }; }