website: fetch on focus + split zscore charts

This commit is contained in:
nym21
2026-02-10 11:47:51 +01:00
parent 474c430ad1
commit 1d63b8901d
3 changed files with 121 additions and 78 deletions

View File

@@ -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<String>) -> Self {

View File

@@ -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<number> | null} */
let sharedTimeData = null;
/** @type {Set<(data: MetricData<number>) => void>} */
let timeCallbacks = new Set();
const time = {
/** @type {MetricData<number> | null} */
data: null,
/** @type {Set<(data: MetricData<number>) => void>} */
callbacks: new Set(),
/** @type {ReturnType<typeof getTimeEndpoint> | 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<string, MetricData<any>>} */
@@ -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();
},
};

View File

@@ -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,
}),
],
},
],
};
}),
],
};
}