mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-05-29 21:29:27 -07:00
website: snapshot
This commit is contained in:
@@ -18,7 +18,6 @@ import { throttle, debounce } from "../utils/timing.js";
|
||||
import { serdeBool, serdeChartableIndex } from "../utils/serde.js";
|
||||
import { stringToId, numberToShortUSFormat } from "../utils/format.js";
|
||||
import { style } from "../utils/elements.js";
|
||||
import { resources } from "../resources.js";
|
||||
|
||||
/**
|
||||
* @typedef {_ISeriesApi<LCSeriesType>} ISeries
|
||||
@@ -39,7 +38,7 @@ import { resources } from "../resources.js";
|
||||
* @property {string} key
|
||||
* @property {string} id
|
||||
* @property {number} paneIndex
|
||||
* @property {Signal<boolean>} active
|
||||
* @property {PersistedValue<boolean>} active
|
||||
* @property {(value: boolean) => void} setActive
|
||||
* @property {() => void} show
|
||||
* @property {() => void} hide
|
||||
@@ -47,6 +46,7 @@ import { resources } from "../resources.js";
|
||||
* @property {() => void} highlight
|
||||
* @property {() => void} tame
|
||||
* @property {() => boolean} hasData
|
||||
* @property {() => void} [fetch]
|
||||
* @property {string | null} url
|
||||
* @property {() => readonly T[]} getData
|
||||
* @property {(data: T) => void} update
|
||||
@@ -121,7 +121,7 @@ export function createChart({
|
||||
|
||||
const range = createPersistedValue({
|
||||
defaultValue: /** @type {Range | null} */ (null),
|
||||
urlKey: "range",
|
||||
urlKey: "r",
|
||||
serialize: (v) => (v ? `${v.from.toFixed(2)}_${v.to.toFixed(2)}` : ""),
|
||||
deserialize: (s) => {
|
||||
if (!s) return null;
|
||||
@@ -143,9 +143,9 @@ export function createChart({
|
||||
div.classList.add("chart");
|
||||
parent.append(div);
|
||||
|
||||
// Registry for shared legend signals (same name = linked across panes)
|
||||
/** @type {Map<string, Signal<boolean>>} */
|
||||
const sharedActiveSignals = new Map();
|
||||
// Registry for shared active states (same name = linked across panes)
|
||||
/** @type {Map<string, PersistedValue<boolean>>} */
|
||||
const sharedActiveStates = new Map();
|
||||
|
||||
// Registry for linked series (same key = linked across panes)
|
||||
/** @type {Map<string, Set<AnySeries>>} */
|
||||
@@ -298,16 +298,14 @@ export function createChart({
|
||||
applyIndexSettings(index());
|
||||
onIndexChange.add(applyIndexSettings);
|
||||
|
||||
const activeResources = /** @type {Set<MetricResource<unknown>>} */ (
|
||||
new Set()
|
||||
);
|
||||
ichart.subscribeCrosshairMove(
|
||||
throttle(() => {
|
||||
activeResources.forEach((v) => {
|
||||
v.fetch();
|
||||
// Periodic refresh of active series data
|
||||
setInterval(() => {
|
||||
seriesByKey.forEach((set) => {
|
||||
set.forEach((s) => {
|
||||
if (s.active.value) s.fetch?.();
|
||||
});
|
||||
}, 10_000),
|
||||
);
|
||||
});
|
||||
}, 30_000);
|
||||
|
||||
if (fitContent) {
|
||||
new ResizeObserver(() => ichart.timeScale().fitContent()).observe(chartDiv);
|
||||
@@ -460,32 +458,33 @@ export function createChart({
|
||||
const key = stringToId(name);
|
||||
const id = `${key}-${paneIndex}`;
|
||||
|
||||
// Reuse existing signal if same name (links legends across panes)
|
||||
let active = sharedActiveSignals.get(key);
|
||||
if (!active) {
|
||||
active = signals.createPersistedSignal({
|
||||
// Reuse existing state if same name (links legends across panes)
|
||||
const existingActive = sharedActiveStates.get(key);
|
||||
const active =
|
||||
existingActive ??
|
||||
createPersistedValue({
|
||||
defaultValue: defaultActive ?? true,
|
||||
storageKey: id,
|
||||
urlKey: key,
|
||||
...serdeBool,
|
||||
});
|
||||
sharedActiveSignals.set(key, active);
|
||||
}
|
||||
if (!existingActive) sharedActiveStates.set(key, active);
|
||||
|
||||
setOrder(-order);
|
||||
|
||||
active() ? show() : hide();
|
||||
active.value ? show() : hide();
|
||||
|
||||
let hasData = false;
|
||||
let lastTime = -Infinity;
|
||||
|
||||
/** @type {MetricResource<unknown> | undefined} */
|
||||
let _valuesResource;
|
||||
/** @type {VoidFunction | null} */
|
||||
let _fetch = null;
|
||||
|
||||
/** @type {AnySeries} */
|
||||
const series = {
|
||||
active,
|
||||
setActive(value) {
|
||||
const wasActive = active.value;
|
||||
active.set(value);
|
||||
seriesByKey.get(key)?.forEach((s) => {
|
||||
value ? s.show() : s.hide();
|
||||
@@ -495,6 +494,7 @@ export function createChart({
|
||||
el.checked = value;
|
||||
}
|
||||
});
|
||||
if (value && !wasActive) _fetch?.();
|
||||
},
|
||||
setOrder,
|
||||
show,
|
||||
@@ -502,6 +502,7 @@ export function createChart({
|
||||
highlight,
|
||||
tame,
|
||||
hasData: () => hasData,
|
||||
fetch: () => _fetch?.(),
|
||||
key,
|
||||
id,
|
||||
paneIndex,
|
||||
@@ -512,9 +513,6 @@ export function createChart({
|
||||
dispose();
|
||||
onRemove();
|
||||
seriesByKey.get(key)?.delete(series);
|
||||
if (_valuesResource) {
|
||||
activeResources.delete(_valuesResource);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -527,206 +525,167 @@ export function createChart({
|
||||
keySet.add(series);
|
||||
|
||||
if (metric) {
|
||||
/** @type {VoidFunction | null} */
|
||||
let disposeIndexEffect = null;
|
||||
|
||||
/** @param {ChartableIndex} idx */
|
||||
function setupIndexEffect(idx) {
|
||||
if (disposeIndexEffect) {
|
||||
disposeIndexEffect();
|
||||
disposeIndexEffect = null;
|
||||
}
|
||||
// Reset data state for new index
|
||||
hasData = false;
|
||||
lastTime = -Infinity;
|
||||
signals.createRoot((_dispose) => {
|
||||
disposeIndexEffect = _dispose;
|
||||
_fetch = null;
|
||||
|
||||
// Get timestamp metric from tree based on index type
|
||||
// timestampMonotonic has height only, timestamp has date-based indexes
|
||||
/** @type {AnyMetricPattern} */
|
||||
const timeMetric =
|
||||
idx === "height"
|
||||
? brk.metrics.blocks.time.timestampMonotonic
|
||||
: brk.metrics.blocks.time.timestamp;
|
||||
const valuesMetric = /** @type {AnyMetricPattern} */ (metric);
|
||||
const timeNode = timeMetric.by[idx];
|
||||
const valuesNode = valuesMetric.by[idx];
|
||||
// Gracefully skip - series may be about to be removed by option change
|
||||
// TODO: Revisit after the signals are completely gone
|
||||
if (!timeNode || !valuesNode) return;
|
||||
// Get timestamp metric from tree based on index type
|
||||
const timeMetric =
|
||||
idx === "height"
|
||||
? brk.metrics.blocks.time.timestampMonotonic
|
||||
: brk.metrics.blocks.time.timestamp;
|
||||
const valuesMetric = /** @type {AnyMetricPattern} */ (metric);
|
||||
const _timeEndpoint = timeMetric.get(idx);
|
||||
if (!_timeEndpoint) throw "Expect time endpoint";
|
||||
const timeEndpoint = _timeEndpoint;
|
||||
const valuesEndpoint = valuesMetric.by[idx];
|
||||
// Gracefully skip - series may be about to be removed by option change
|
||||
if (!timeEndpoint || !valuesEndpoint) return;
|
||||
|
||||
const timeResource = resources.useMetricEndpoint(timeNode);
|
||||
const valuesResource = resources.useMetricEndpoint(valuesNode);
|
||||
_valuesResource = valuesResource;
|
||||
series.url = `${
|
||||
brk.baseUrl.endsWith("/") ? brk.baseUrl.slice(0, -1) : brk.baseUrl
|
||||
}${valuesEndpoint.path}`;
|
||||
|
||||
series.url = `${
|
||||
brk.baseUrl.endsWith("/") ? brk.baseUrl.slice(0, -1) : brk.baseUrl
|
||||
}${valuesResource.path}`;
|
||||
|
||||
(paneIndex ? legendBottom : legendTop).addOrReplace({
|
||||
series,
|
||||
name,
|
||||
colors,
|
||||
order,
|
||||
});
|
||||
|
||||
// Create memo outside active check (cheap, just checks data existence)
|
||||
const timeRange = timeResource.range();
|
||||
const valuesRange = valuesResource.range();
|
||||
const valuesCacheKey = signals.createMemo(() => {
|
||||
const res = valuesRange.response();
|
||||
if (!res?.data?.length) return null;
|
||||
if (!timeRange.response()?.data?.length) return null;
|
||||
return `${res.version}|${res.stamp}|${res.total}|${res.start}|${res.end}`;
|
||||
});
|
||||
|
||||
// Combined effect for active + data processing (flat, uses prev comparison)
|
||||
signals.createEffect(
|
||||
() => ({ isActive: active?.(), cacheKey: valuesCacheKey() }),
|
||||
(curr, prev) => {
|
||||
const becameActive = curr.isActive && (!prev || !prev.isActive);
|
||||
const becameInactive = !curr.isActive && prev?.isActive;
|
||||
|
||||
if (becameInactive) {
|
||||
activeResources.delete(valuesResource);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!curr.isActive) return;
|
||||
|
||||
if (becameActive) {
|
||||
timeResource.fetch();
|
||||
valuesResource.fetch();
|
||||
activeResources.add(valuesResource);
|
||||
}
|
||||
|
||||
// Process data only if cacheKey changed
|
||||
if (!curr.cacheKey || curr.cacheKey === prev?.cacheKey) return;
|
||||
|
||||
const _indexes = timeRange.response()?.data;
|
||||
const values = valuesRange.response()?.data;
|
||||
if (!_indexes?.length || !values?.length) return;
|
||||
|
||||
const indexes = /** @type {number[]} */ (_indexes);
|
||||
const length = Math.min(indexes.length, values.length);
|
||||
|
||||
// Find start index for processing
|
||||
let startIdx = 0;
|
||||
if (hasData) {
|
||||
// Binary search to find first index where time >= lastTime
|
||||
let lo = 0;
|
||||
let hi = length;
|
||||
while (lo < hi) {
|
||||
const mid = (lo + hi) >>> 1;
|
||||
if (indexes[mid] < lastTime) {
|
||||
lo = mid + 1;
|
||||
} else {
|
||||
hi = mid;
|
||||
}
|
||||
}
|
||||
startIdx = lo;
|
||||
if (startIdx >= length) return; // No new data
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} i
|
||||
* @param {(number | null | [number, number, number, number])[]} vals
|
||||
* @returns {LineData | CandlestickData}
|
||||
*/
|
||||
function buildDataPoint(i, vals) {
|
||||
const time = /** @type {Time} */ (indexes[i]);
|
||||
const v = vals[i];
|
||||
if (v === null) {
|
||||
return { time, value: NaN };
|
||||
} else if (typeof v === "number") {
|
||||
return { time, value: v };
|
||||
} else {
|
||||
if (!Array.isArray(v) || v.length !== 4)
|
||||
throw new Error(`Expected OHLC tuple, got: ${v}`);
|
||||
const [open, high, low, close] = v;
|
||||
return { time, open, high, low, close };
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasData) {
|
||||
// Initial load: build full array
|
||||
const data = /** @type {LineData[] | CandlestickData[]} */ (
|
||||
Array.from({ length })
|
||||
);
|
||||
|
||||
let prevTime = null;
|
||||
let timeOffset = 0;
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
const time = indexes[i];
|
||||
const sameTime = prevTime === time;
|
||||
if (sameTime) {
|
||||
timeOffset += 1;
|
||||
}
|
||||
const offsetedI = i - timeOffset;
|
||||
const point = buildDataPoint(i, values);
|
||||
if (sameTime && "open" in point) {
|
||||
const prev = /** @type {CandlestickData} */ (
|
||||
data[offsetedI]
|
||||
);
|
||||
point.open = prev.open;
|
||||
point.high = Math.max(prev.high, point.high);
|
||||
point.low = Math.min(prev.low, point.low);
|
||||
}
|
||||
data[offsetedI] = point;
|
||||
prevTime = time;
|
||||
}
|
||||
|
||||
data.length -= timeOffset;
|
||||
|
||||
setData(data);
|
||||
hasData = true;
|
||||
lastTime =
|
||||
/** @type {number} */ (data.at(-1)?.time) ?? -Infinity;
|
||||
|
||||
// Restore saved range or use defaults
|
||||
const savedRange = getRange();
|
||||
if (savedRange) {
|
||||
ichart.timeScale().setVisibleLogicalRange({
|
||||
from: savedRange.from,
|
||||
to: savedRange.to,
|
||||
});
|
||||
} else if (fitContent) {
|
||||
ichart.timeScale().fitContent();
|
||||
} else if (
|
||||
idx === "quarterindex" ||
|
||||
idx === "semesterindex" ||
|
||||
idx === "yearindex" ||
|
||||
idx === "decadeindex"
|
||||
) {
|
||||
ichart
|
||||
.timeScale()
|
||||
.setVisibleLogicalRange({ from: -1, to: data.length });
|
||||
}
|
||||
// Delay until chart has applied the range
|
||||
requestAnimationFrame(() => onDataLoaded?.());
|
||||
} else {
|
||||
// Incremental update: only process new data points
|
||||
for (let i = startIdx; i < length; i++) {
|
||||
const point = buildDataPoint(i, values);
|
||||
update(point);
|
||||
lastTime = /** @type {number} */ (point.time);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
(paneIndex ? legendBottom : legendTop).addOrReplace({
|
||||
series,
|
||||
name,
|
||||
colors,
|
||||
order,
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {number[]} indexes
|
||||
* @param {(number | null | [number, number, number, number])[]} values
|
||||
*/
|
||||
function processData(indexes, values) {
|
||||
const length = Math.min(indexes.length, values.length);
|
||||
|
||||
// Find start index for processing
|
||||
let startIdx = 0;
|
||||
if (hasData) {
|
||||
// Binary search to find first index where time >= lastTime
|
||||
let lo = 0;
|
||||
let hi = length;
|
||||
while (lo < hi) {
|
||||
const mid = (lo + hi) >>> 1;
|
||||
if (indexes[mid] < lastTime) {
|
||||
lo = mid + 1;
|
||||
} else {
|
||||
hi = mid;
|
||||
}
|
||||
}
|
||||
startIdx = lo;
|
||||
if (startIdx >= length) return; // No new data
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} i
|
||||
* @returns {LineData | CandlestickData}
|
||||
*/
|
||||
function buildDataPoint(i) {
|
||||
const time = /** @type {Time} */ (indexes[i]);
|
||||
const v = values[i];
|
||||
if (v === null) {
|
||||
return { time, value: NaN };
|
||||
} else if (typeof v === "number") {
|
||||
return { time, value: v };
|
||||
} else {
|
||||
if (!Array.isArray(v) || v.length !== 4)
|
||||
throw new Error(`Expected OHLC tuple, got: ${v}`);
|
||||
const [open, high, low, close] = v;
|
||||
return { time, open, high, low, close };
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasData) {
|
||||
// Initial load: build full array
|
||||
const data = /** @type {LineData[] | CandlestickData[]} */ (
|
||||
Array.from({ length })
|
||||
);
|
||||
|
||||
let prevTime = null;
|
||||
let timeOffset = 0;
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
const time = indexes[i];
|
||||
const sameTime = prevTime === time;
|
||||
if (sameTime) {
|
||||
timeOffset += 1;
|
||||
}
|
||||
const offsetedI = i - timeOffset;
|
||||
const point = buildDataPoint(i);
|
||||
if (sameTime && "open" in point) {
|
||||
const prev = /** @type {CandlestickData} */ (data[offsetedI]);
|
||||
point.open = prev.open;
|
||||
point.high = Math.max(prev.high, point.high);
|
||||
point.low = Math.min(prev.low, point.low);
|
||||
}
|
||||
data[offsetedI] = point;
|
||||
prevTime = time;
|
||||
}
|
||||
|
||||
data.length -= timeOffset;
|
||||
|
||||
setData(data);
|
||||
hasData = true;
|
||||
lastTime = /** @type {number} */ (data.at(-1)?.time) ?? -Infinity;
|
||||
|
||||
// Restore saved range or use defaults
|
||||
const savedRange = getRange();
|
||||
if (savedRange) {
|
||||
ichart.timeScale().setVisibleLogicalRange({
|
||||
from: savedRange.from,
|
||||
to: savedRange.to,
|
||||
});
|
||||
} else if (fitContent) {
|
||||
ichart.timeScale().fitContent();
|
||||
} else if (
|
||||
idx === "quarterindex" ||
|
||||
idx === "semesterindex" ||
|
||||
idx === "yearindex" ||
|
||||
idx === "decadeindex"
|
||||
) {
|
||||
ichart
|
||||
.timeScale()
|
||||
.setVisibleLogicalRange({ from: -1, to: data.length });
|
||||
}
|
||||
// Delay until chart has applied the range
|
||||
requestAnimationFrame(() => onDataLoaded?.());
|
||||
} else {
|
||||
// Incremental update: only process new data points
|
||||
for (let i = startIdx; i < length; i++) {
|
||||
const point = buildDataPoint(i);
|
||||
update(point);
|
||||
lastTime = /** @type {number} */ (point.time);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchAndProcess() {
|
||||
const [timeResult, valuesResult] = await Promise.all([
|
||||
timeEndpoint.slice(-10000).fetch(),
|
||||
valuesEndpoint?.slice(-10000).fetch(),
|
||||
]);
|
||||
if (timeResult?.data?.length && valuesResult?.data?.length) {
|
||||
processData(timeResult.data, valuesResult.data);
|
||||
}
|
||||
}
|
||||
|
||||
_fetch = fetchAndProcess;
|
||||
|
||||
// Initial fetch if active
|
||||
if (active.value) {
|
||||
fetchAndProcess();
|
||||
}
|
||||
}
|
||||
|
||||
setupIndexEffect(index());
|
||||
onIndexChange.add(setupIndexEffect);
|
||||
signals.onCleanup(() => {
|
||||
onIndexChange.delete(setupIndexEffect);
|
||||
if (disposeIndexEffect) {
|
||||
disposeIndexEffect();
|
||||
}
|
||||
});
|
||||
// Series don't subscribe to onIndexChange - panes recreates them on index change
|
||||
// onIndexChange.add(setupIndexEffect);
|
||||
// _cleanup = () => onIndexChange.delete(setupIndexEffect);
|
||||
} else {
|
||||
(paneIndex ? legendBottom : legendTop).addOrReplace({
|
||||
series,
|
||||
@@ -767,6 +726,7 @@ export function createChart({
|
||||
const chart = {
|
||||
index,
|
||||
indexName,
|
||||
onIndexChange,
|
||||
|
||||
legendTop,
|
||||
legendBottom,
|
||||
|
||||
@@ -55,7 +55,7 @@ export function createLegend() {
|
||||
inputName: stringToId(`selected-${series.id}`),
|
||||
inputValue: "value",
|
||||
title: "Click to toggle",
|
||||
inputChecked: series.active(),
|
||||
inputChecked: series.active.value,
|
||||
onClick: () => {
|
||||
series.setActive(input.checked);
|
||||
},
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
*
|
||||
* @import { Resources, MetricResource } from './resources.js'
|
||||
*
|
||||
* @import { PersistedValue } from './utils/persisted.js'
|
||||
*
|
||||
* @import { SingleValueData, CandlestickData, Series, AnySeries, ISeries, HistogramData, LineData, BaselineData, LineSeriesPartialOptions, BaselineSeriesPartialOptions, HistogramSeriesPartialOptions, CandlestickSeriesPartialOptions, Chart, Legend } from "./chart/index.js"
|
||||
*
|
||||
* @import { Color, ColorName, Colors } from "./chart/colors.js"
|
||||
|
||||
@@ -38,12 +38,16 @@ export function init({ option, brk }) {
|
||||
const fieldset = createIndexSelector(option, chart);
|
||||
chartElement.append(fieldset);
|
||||
|
||||
// Bridge chart's index changes into signals system
|
||||
const indexVersion = signals.createSignal(0);
|
||||
chart.onIndexChange.add(() => indexVersion.set(indexVersion() + 1));
|
||||
|
||||
const unitChoices = /** @type {const} */ ([Unit.usd, Unit.sats]);
|
||||
/** @type {Signal<Unit>} */
|
||||
const topUnit = signals.createPersistedSignal({
|
||||
defaultValue: /** @type {Unit} */ (Unit.usd),
|
||||
storageKey: `${keyPrefix}-price`,
|
||||
urlKey: "price",
|
||||
urlKey: "u1",
|
||||
serialize: (u) => u.id,
|
||||
deserialize: (s) =>
|
||||
/** @type {Unit} */ (unitChoices.find((u) => u.id === s) ?? Unit.usd),
|
||||
@@ -170,7 +174,7 @@ export function init({ option, brk }) {
|
||||
bottomUnit = signals.createPersistedSignal({
|
||||
defaultValue: bottomUnits[0],
|
||||
storageKey: `${keyPrefix}-unit-${unitGroupKey}`,
|
||||
urlKey: "unit",
|
||||
urlKey: "u2",
|
||||
serialize: (u) => u.id,
|
||||
deserialize: (s) =>
|
||||
bottomUnits.find((u) => u.id === s) ?? bottomUnits[0],
|
||||
@@ -313,10 +317,13 @@ export function init({ option, brk }) {
|
||||
});
|
||||
}
|
||||
|
||||
// Price series + top pane blueprints: combined effect on index + topUnit
|
||||
// Price series + top pane blueprints: react to topUnit and index changes
|
||||
signals.createScopedEffect(
|
||||
() => ({ idx: chart.index(), unit: topUnit() }),
|
||||
({ idx, unit }) => {
|
||||
() => ({ unit: topUnit(), _: indexVersion() }),
|
||||
({ unit }) => {
|
||||
// Remove old series BEFORE creating new one
|
||||
seriesListTop[0]?.remove();
|
||||
|
||||
// Create price series
|
||||
/** @type {AnySeries | undefined} */
|
||||
let series;
|
||||
@@ -343,7 +350,6 @@ export function init({ option, brk }) {
|
||||
}
|
||||
if (!series) throw Error("Unreachable");
|
||||
|
||||
seriesListTop[0]?.remove();
|
||||
seriesListTop[0] = series;
|
||||
|
||||
// Live price update effect
|
||||
@@ -354,7 +360,7 @@ export function init({ option, brk }) {
|
||||
}),
|
||||
({ latest, hasData }) => {
|
||||
if (!series || !latest || !hasData) return;
|
||||
printLatest({ series, unit, index: idx });
|
||||
printLatest({ series, unit, index: chart.index() });
|
||||
},
|
||||
);
|
||||
|
||||
@@ -363,7 +369,7 @@ export function init({ option, brk }) {
|
||||
blueprints: option.top,
|
||||
paneIndex: 0,
|
||||
unit,
|
||||
idx,
|
||||
idx: chart.index(),
|
||||
seriesList: seriesListTop,
|
||||
orderStart: 1,
|
||||
legend: chart.legendTop,
|
||||
@@ -371,16 +377,16 @@ export function init({ option, brk }) {
|
||||
},
|
||||
);
|
||||
|
||||
// Bottom pane blueprints: combined effect on index + bottomUnit
|
||||
// Bottom pane blueprints: react to bottomUnit and index changes
|
||||
if (bottomUnit) {
|
||||
signals.createScopedEffect(
|
||||
() => ({ idx: chart.index(), unit: bottomUnit() }),
|
||||
({ idx, unit }) => {
|
||||
() => ({ unit: bottomUnit(), _: indexVersion() }),
|
||||
({ unit }) => {
|
||||
createSeriesFromBlueprints({
|
||||
blueprints: option.bottom,
|
||||
paneIndex: 1,
|
||||
unit,
|
||||
idx,
|
||||
idx: chart.index(),
|
||||
seriesList: seriesListBottom,
|
||||
orderStart: 0,
|
||||
legend: chart.legendBottom,
|
||||
|
||||
@@ -73,4 +73,7 @@ export function createPersistedValue({
|
||||
};
|
||||
}
|
||||
|
||||
/** @typedef {ReturnType<typeof createPersistedValue>} PersistedValue */
|
||||
/**
|
||||
* @template T
|
||||
* @typedef {ReturnType<typeof createPersistedValue<T>>} PersistedValue
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user