website: snapshot

This commit is contained in:
nym21
2026-01-22 11:11:13 +01:00
parent 758256a1a2
commit 31c5a5dde5
2 changed files with 396 additions and 387 deletions

View File

@@ -95,24 +95,26 @@ export function createChart({
? brk.metrics.blocks.time.timestampMonotonic.by[idx]
: brk.metrics.blocks.time.timestamp.by[idx];
// Chart owns its index state
/** @type {Set<(index: ChartableIndex) => void>} */
const onIndexChange = new Set();
const index = {
/** @type {Set<(index: ChartableIndex) => void>} */
onChange: new Set(),
const index = () => serdeChartableIndex.deserialize(indexName.value);
const indexName = createPersistedValue({
defaultValue: /** @type {ChartableIndexName} */ ("date"),
storageKey: "chart-index",
urlKey: "i",
serialize: (v) => v,
deserialize: (s) => /** @type {ChartableIndexName} */ (s),
onChange: () => {
// Reset URL range so getRange() falls back to per-index saved range
range.set(null);
onIndexChange.forEach((cb) => cb(index()));
get() {
return serdeChartableIndex.deserialize(index.name.value);
},
});
name: createPersistedValue({
defaultValue: /** @type {ChartableIndexName} */ ("date"),
storageKey: "chart-index",
urlKey: "i",
serialize: (v) => v,
deserialize: (s) => /** @type {ChartableIndexName} */ (s),
onChange: () => {
range.set(null);
index.onChange.forEach((cb) => cb(index.get()));
},
}),
};
// Range state: localStorage stores all ranges per-index, URL stores current range only
/** @typedef {{ from: number, to: number }} Range */
@@ -135,32 +137,36 @@ export function createChart({
});
/** @returns {Range | null} */
const getRange = () => range.value ?? ranges.value[indexName.value] ?? null;
const getRange = () => range.value ?? ranges.value[index.name.value] ?? null;
/** @param {Range} value */
const setRange = (value) => {
ranges.set({ ...ranges.value, [indexName.value]: value });
ranges.set({ ...ranges.value, [index.name.value]: value });
range.set(value);
};
// ─── DOM ───
const div = document.createElement("div");
div.classList.add("chart");
parent.append(div);
const legends = {
top: createLegend(),
bottom: createLegend(),
};
const legendTop = createLegend();
div.append(legendTop.element);
const elements = {
root: document.createElement("div"),
chart: document.createElement("div"),
const chartDiv = document.createElement("div");
chartDiv.classList.add("lightweight-chart");
div.append(chartDiv);
setup() {
elements.root.classList.add("chart");
elements.chart.classList.add("lightweight-chart");
parent.append(elements.root);
elements.root.append(legends.top.element);
elements.root.append(elements.chart);
elements.root.append(legends.bottom.element);
},
};
elements.setup();
const legendBottom = createLegend();
div.append(legendBottom.element);
// ─── Lightweight Charts ───
const ichart = /** @type {CreateLCChart} */ (untypedLcCreateChart)(
chartDiv,
elements.chart,
/** @satisfies {DeepPartial<ChartOptions>} */ ({
autoSize: true,
layout: {
@@ -291,29 +297,18 @@ export function createChart({
},
});
}
applyIndexSettings(index());
onIndexChange.add(applyIndexSettings);
applyIndexSettings(index.get());
index.onChange.add(applyIndexSettings);
// Periodic refresh of active series data
setInterval(() => {
serieses.byKey.forEach((set) => {
set.forEach((s) => {
if (s.active.value) s.fetch?.();
});
});
}, 30_000);
const refreshInterval = setInterval(() => serieses.refreshAll(), 30_000);
if (fitContent) {
new ResizeObserver(() => ichart.timeScale().fitContent()).observe(chartDiv);
new ResizeObserver(() => ichart.timeScale().fitContent()).observe(
elements.chart,
);
}
const serieses = {
/** @type {Map<string, PersistedValue<boolean>>} */
activeStates: new Map(),
/** @type {Map<string, Set<AnySeries>>} */
byKey: new Map(),
};
const fieldsets = {
/** @type {Map<number, Map<string, { id: string, position: string, createChild: (pane: IPaneApi<Time>) => HTMLElement }>>} */
configs: new Map(),
@@ -333,7 +328,9 @@ export function createChart({
if (!configs) return;
for (const { id, position, createChild } of configs.values()) {
/** @type {Element} */ (parent).querySelectorAll(`[data-position="${position}"]`).forEach((el) => el.remove());
/** @type {Element} */ (parent)
.querySelectorAll(`[data-position="${position}"]`)
.forEach((el) => el.remove());
const fieldset = document.createElement("fieldset");
fieldset.dataset.size = "xs";
@@ -399,7 +396,9 @@ export function createChart({
if (parent) {
callback();
} else if (retries > 0) {
requestAnimationFrame(() => this.whenReady(paneIndex, callback, retries - 1));
requestAnimationFrame(() =>
this.whenReady(paneIndex, callback, retries - 1),
);
}
},
@@ -449,340 +448,289 @@ export function createChart({
},
};
/**
* @param {Object} args
* @param {Unit} args.unit
* @param {LCSeriesType} args.seriesType
* @param {number} args.paneIndex
*/
function addPriceScaleSelectorIfNeeded({ unit, paneIndex, seriesType }) {
const id = `${chartId}-scale`;
const serieses = {
/** @type {Map<string, PersistedValue<boolean>>} */
activeStates: new Map(),
/** @type {Map<string, Set<AnySeries>>} */
byKey: new Map(),
/** @type {"lin" | "log"} */
const defaultValue =
unit.id === "usd" && seriesType !== "Baseline" ? "log" : "lin";
const persisted = createPersistedValue({
defaultValue,
storageKey: `${id}-scale-${paneIndex}`,
urlKey: paneIndex === 0 ? "price_scale" : "unit_scale",
serialize: (v) => v,
deserialize: (s) => /** @type {"lin" | "log"} */ (s),
});
/** @param {"lin" | "log"} value */
const applyScale = (value) => {
try {
const pane = ichart.panes().at(paneIndex);
pane?.priceScale("right").applyOptions({
mode: value === "lin" ? 0 : 1,
refreshAll() {
serieses.byKey.forEach((set) => {
set.forEach((s) => {
if (s.active.value) s.fetch?.();
});
} catch {}
};
// Apply scale immediately
applyScale(persisted.value);
fieldsets.addIfNeeded({
id,
paneIndex,
position: "sw",
createChild() {
const field = createChoiceField({
choices: /** @type {const} */ (["lin", "log"]),
id: stringToId(`${id} ${paneIndex} ${unit}`),
initialValue: persisted.value,
onChange(value) {
persisted.set(value);
applyScale(value);
},
});
return field;
},
});
}
// ─── Series Factory ───
/**
* @param {Object} args
* @param {string} args.name
* @param {Unit} args.unit
* @param {number} args.order
* @param {Color[]} args.colors
* @param {LCSeriesType} args.seriesType
* @param {AnyMetricPattern} args.metric
* @param {number} args.paneIndex
* @param {boolean} [args.defaultActive]
* @param {(order: number) => void} args.setOrder
* @param {() => void} args.show
* @param {() => void} args.hide
* @param {() => void} args.highlight
* @param {() => void} args.tame
* @param {() => readonly any[]} args.getData
* @param {(data: any[]) => void} args.setData
* @param {(data: any) => void} args.update
* @param {() => void} args.onRemove
* @param {() => void} [args.onDataLoaded]
*/
function addSeries({
metric,
name,
unit,
order,
seriesType,
paneIndex,
defaultActive,
colors,
setOrder,
show,
hide,
highlight,
tame,
getData,
setData,
update,
onRemove,
onDataLoaded,
}) {
const key = stringToId(name);
const id = `${key}-${paneIndex}`;
// Reuse existing state if same name (links legends across panes)
const existingActive = serieses.activeStates.get(key);
const active =
existingActive ??
createPersistedValue({
defaultValue: defaultActive ?? true,
storageKey: id,
urlKey: key,
...serdeBool,
});
if (!existingActive) serieses.activeStates.set(key, active);
},
setOrder(-order);
active.value ? show() : hide();
let hasData = false;
let lastTime = -Infinity;
/** @type {VoidFunction | null} */
let _fetch = null;
/** @type {AnySeries} */
const series = {
active,
setActive(value) {
const wasActive = active.value;
active.set(value);
serieses.byKey.get(key)?.forEach((s) => {
value ? s.show() : s.hide();
});
document.querySelectorAll(`[data-series="${key}"]`).forEach((el) => {
if (el instanceof HTMLInputElement && el.type === "checkbox") {
el.checked = value;
}
});
if (value && !wasActive) _fetch?.();
panes.updateVisibility();
},
/**
* @param {Object} args
* @param {string} args.name
* @param {Unit} args.unit
* @param {number} args.order
* @param {Color[]} args.colors
* @param {LCSeriesType} args.seriesType
* @param {AnyMetricPattern} args.metric
* @param {number} args.paneIndex
* @param {boolean} [args.defaultActive]
* @param {(order: number) => void} args.setOrder
* @param {() => void} args.show
* @param {() => void} args.hide
* @param {() => void} args.highlight
* @param {() => void} args.tame
* @param {() => readonly any[]} args.getData
* @param {(data: any[]) => void} args.setData
* @param {(data: any) => void} args.update
* @param {() => void} args.onRemove
* @param {() => void} [args.onDataLoaded]
*/
create({
metric,
name,
unit,
order,
seriesType,
paneIndex,
defaultActive,
colors,
setOrder,
show,
hide,
highlight,
tame,
hasData: () => hasData,
fetch: () => _fetch?.(),
key,
id,
paneIndex,
url: null,
getData,
setData,
update,
remove() {
onRemove();
serieses.byKey.get(key)?.delete(series);
panes.seriesByHome.get(paneIndex)?.delete(series);
},
};
onRemove,
onDataLoaded,
}) {
const key = stringToId(name);
const id = `${key}-${paneIndex}`;
// Register series for cross-pane linking
let keySet = serieses.byKey.get(key);
if (!keySet) {
keySet = new Set();
serieses.byKey.set(key, keySet);
}
keySet.add(series);
// Reuse existing state if same name (links legends across panes)
const existingActive = serieses.activeStates.get(key);
const active =
existingActive ??
createPersistedValue({
defaultValue: defaultActive ?? true,
storageKey: id,
urlKey: key,
...serdeBool,
});
if (!existingActive) serieses.activeStates.set(key, active);
/** @param {ChartableIndex} idx */
function setupIndexEffect(idx) {
// Reset data state for new index
hasData = false;
lastTime = -Infinity;
_fetch = null;
setOrder(-order);
const _valuesEndpoint = metric.by[idx];
// Gracefully skip - series may be about to be removed by option change
if (!_valuesEndpoint) return;
const valuesEndpoint = _valuesEndpoint;
active.value ? show() : hide();
series.url = `${baseUrl}${valuesEndpoint.path}`;
let hasData = false;
let lastTime = -Infinity;
(paneIndex ? legendBottom : legendTop).addOrReplace({
series,
name,
colors,
order,
});
/** @type {VoidFunction | null} */
let _fetch = null;
/**
* @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;
/** @type {AnySeries} */
const series = {
active,
setActive(value) {
const wasActive = active.value;
active.set(value);
serieses.byKey.get(key)?.forEach((s) => {
value ? s.show() : s.hide();
});
document.querySelectorAll(`[data-series="${key}"]`).forEach((el) => {
if (el instanceof HTMLInputElement && el.type === "checkbox") {
el.checked = value;
}
}
startIdx = lo;
if (startIdx >= length) return; // No new data
}
});
if (value && !wasActive) _fetch?.();
panes.updateVisibility();
},
setOrder,
show,
hide,
highlight,
tame,
hasData: () => hasData,
fetch: () => _fetch?.(),
key,
id,
paneIndex,
url: null,
getData,
update,
remove() {
onRemove();
serieses.byKey.get(key)?.delete(series);
panes.seriesByHome.get(paneIndex)?.delete(series);
},
};
// Register series for cross-pane linking
let keySet = serieses.byKey.get(key);
if (!keySet) {
keySet = new Set();
serieses.byKey.set(key, keySet);
}
keySet.add(series);
/** @param {ChartableIndex} idx */
function setupIndexEffect(idx) {
// Reset data state for new index
hasData = false;
lastTime = -Infinity;
_fetch = null;
const _valuesEndpoint = metric.by[idx];
// Gracefully skip - series may be about to be removed by option change
if (!_valuesEndpoint) return;
const valuesEndpoint = _valuesEndpoint;
series.url = `${baseUrl}${valuesEndpoint.path}`;
(paneIndex ? legends.bottom : legends.top).addOrReplace({
series,
name,
colors,
order,
});
/**
* @param {number} i
* @returns {LineData | CandlestickData}
* @param {number[]} indexes
* @param {(number | null | [number, number, number, number])[]} values
*/
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 };
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 (
(minBarSpacingByIndex[idx] ?? 0) >=
/** @type {number} */ (minBarSpacingByIndex.quarterindex)
) {
ichart
.timeScale()
.setVisibleLogicalRange({ from: -1, to: data.length });
}
// Delay until chart has applied the range
requestAnimationFrame(() => onDataLoaded?.());
} 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;
// 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);
}
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 (
(minBarSpacingByIndex[idx] ?? 0) >=
/** @type {number} */ (minBarSpacingByIndex.quarterindex)
) {
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([
getTimeEndpoint(idx).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();
}
}
async function fetchAndProcess() {
const [timeResult, valuesResult] = await Promise.all([
getTimeEndpoint(idx).slice(-10000).fetch(),
valuesEndpoint.slice(-10000).fetch(),
]);
if (timeResult?.data?.length && valuesResult?.data?.length) {
processData(timeResult.data, valuesResult.data);
}
}
setupIndexEffect(index.get());
// Series don't subscribe to index.onChange - panes recreates them on index change
// index.onChange.add(setupIndexEffect);
// _cleanup = () => index.onChange.delete(setupIndexEffect);
_fetch = fetchAndProcess;
addPriceScaleSelectorIfNeeded({
paneIndex,
seriesType,
unit,
});
// Initial fetch if active
if (active.value) {
fetchAndProcess();
}
}
setupIndexEffect(index());
// Series don't subscribe to onIndexChange - panes recreates them on index change
// onIndexChange.add(setupIndexEffect);
// _cleanup = () => onIndexChange.delete(setupIndexEffect);
addPriceScaleSelectorIfNeeded({
paneIndex,
seriesType,
unit,
});
return series;
}
const chart = {
index,
indexName,
onIndexChange,
legendTop,
legendBottom,
addFieldsetIfNeeded: fieldsets.addIfNeeded.bind(fieldsets),
return series;
},
/**
* @param {Object} args
@@ -796,7 +744,7 @@ export function createChart({
* @param {boolean} [args.inverse]
* @param {CandlestickSeriesPartialOptions} [args.options]
*/
addCandlestickSeries({
addCandlestick({
metric,
name,
unit,
@@ -870,7 +818,7 @@ export function createChart({
onZoomChange.add(handleZoom);
const removeSeriesThemeListener = onThemeChange(update);
const series = addSeries({
const series = serieses.create({
colors: [upColor, downColor],
name,
order,
@@ -940,7 +888,7 @@ export function createChart({
* @param {boolean} [args.defaultActive]
* @param {HistogramSeriesPartialOptions} [args.options]
*/
addHistogramSeries({
addHistogram({
metric,
name,
unit,
@@ -979,7 +927,7 @@ export function createChart({
update();
const removeSeriesThemeListener = onThemeChange(update);
const series = addSeries({
const series = serieses.create({
colors: isDualColor ? [positiveColor, negativeColor] : [positiveColor],
name,
order,
@@ -1047,7 +995,7 @@ export function createChart({
* @param {boolean} [args.defaultActive]
* @param {LineSeriesPartialOptions} [args.options]
*/
addLineSeries({
addLine({
metric,
name,
unit,
@@ -1086,7 +1034,7 @@ export function createChart({
update();
const removeSeriesThemeListener = onThemeChange(update);
const series = addSeries({
const series = serieses.create({
colors: [color],
name,
order,
@@ -1140,7 +1088,7 @@ export function createChart({
* @param {boolean} [args.defaultActive]
* @param {LineSeriesPartialOptions} [args.options]
*/
addDotsSeries({
addDots({
metric,
name,
unit,
@@ -1192,7 +1140,7 @@ export function createChart({
onZoomChange.add(handleZoom);
const removeSeriesThemeListener = onThemeChange(update);
const series = addSeries({
const series = serieses.create({
colors: [color],
name,
order,
@@ -1248,7 +1196,7 @@ export function createChart({
* @param {Color} [args.bottomColor]
* @param {BaselineSeriesPartialOptions} [args.options]
*/
addBaselineSeries({
addBaseline({
metric,
name,
unit,
@@ -1296,7 +1244,7 @@ export function createChart({
update();
const removeSeriesThemeListener = onThemeChange(update);
const series = addSeries({
const series = serieses.create({
colors: [topColor, bottomColor],
name,
order,
@@ -1339,9 +1287,70 @@ export function createChart({
return series;
},
};
/**
* @param {Object} args
* @param {Unit} args.unit
* @param {LCSeriesType} args.seriesType
* @param {number} args.paneIndex
*/
function addPriceScaleSelectorIfNeeded({ unit, paneIndex, seriesType }) {
const id = `${chartId}-scale`;
/** @type {"lin" | "log"} */
const defaultValue =
unit.id === "usd" && seriesType !== "Baseline" ? "log" : "lin";
const persisted = createPersistedValue({
defaultValue,
storageKey: `${id}-scale-${paneIndex}`,
urlKey: paneIndex === 0 ? "price_scale" : "unit_scale",
serialize: (v) => v,
deserialize: (s) => /** @type {"lin" | "log"} */ (s),
});
/** @param {"lin" | "log"} value */
const applyScale = (value) => {
try {
const pane = ichart.panes().at(paneIndex);
pane?.priceScale("right").applyOptions({
mode: value === "lin" ? 0 : 1,
});
} catch {}
};
// Apply scale immediately
applyScale(persisted.value);
fieldsets.addIfNeeded({
id,
paneIndex,
position: "sw",
createChild() {
const field = createChoiceField({
choices: /** @type {const} */ (["lin", "log"]),
id: stringToId(`${id} ${paneIndex} ${unit}`),
initialValue: persisted.value,
onChange(value) {
persisted.set(value);
applyScale(value);
},
});
return field;
},
});
}
const chart = {
index,
legends,
serieses,
addFieldsetIfNeeded: fieldsets.addIfNeeded.bind(fieldsets),
destroy() {
removeThemeListener();
clearInterval(refreshInterval);
ichart.remove();
},
};
@@ -1358,15 +1367,15 @@ export function createChart({
order,
};
if (blueprint.type === "Candlestick") {
chart.addCandlestickSeries({ ...common, colors: blueprint.colors });
serieses.addCandlestick({ ...common, colors: blueprint.colors });
} else if (blueprint.type === "Baseline") {
chart.addBaselineSeries(common);
serieses.addBaseline(common);
} else if (blueprint.type === "Histogram") {
chart.addHistogramSeries({ ...common, color: blueprint.color });
serieses.addHistogram({ ...common, color: blueprint.color });
} else if (blueprint.type === "Dots") {
chart.addDotsSeries({ ...common, color: blueprint.color });
serieses.addDots({ ...common, color: blueprint.color });
} else {
chart.addLineSeries({ ...common, color: blueprint.color });
serieses.addLine({ ...common, color: blueprint.color });
}
});
});

View File

@@ -39,7 +39,7 @@ export function init({ option, brk }) {
// Bridge chart's index changes into signals system
const indexVersion = signals.createSignal(0);
chart.onIndexChange.add(() => indexVersion.set(indexVersion() + 1));
chart.index.onChange.add(() => indexVersion.set(indexVersion() + 1));
const unitChoices = /** @type {const} */ ([Unit.usd, Unit.sats]);
/** @type {Signal<Unit>} */
@@ -200,7 +200,7 @@ export function init({ option, brk }) {
// Clean up bottom pane when new option has no bottom series
seriesListBottom.forEach((series) => series.remove());
seriesListBottom.length = 0;
chart.legendBottom.removeFrom(0);
chart.legends.bottom.removeFrom(0);
}
/**
@@ -234,7 +234,7 @@ export function init({ option, brk }) {
switch (blueprint.type) {
case "Baseline": {
seriesList.push(
chart.addBaselineSeries({
chart.serieses.addBaseline({
metric: blueprint.metric,
name: blueprint.title,
unit,
@@ -254,7 +254,7 @@ export function init({ option, brk }) {
}
case "Histogram": {
seriesList.push(
chart.addHistogramSeries({
chart.serieses.addHistogram({
metric: blueprint.metric,
name: blueprint.title,
unit,
@@ -269,7 +269,7 @@ export function init({ option, brk }) {
}
case "Candlestick": {
seriesList.push(
chart.addCandlestickSeries({
chart.serieses.addCandlestick({
metric: blueprint.metric,
name: blueprint.title,
unit,
@@ -284,7 +284,7 @@ export function init({ option, brk }) {
}
case "Dots": {
seriesList.push(
chart.addDotsSeries({
chart.serieses.addDots({
metric: blueprint.metric,
color: blueprint.color,
name: blueprint.title,
@@ -300,7 +300,7 @@ export function init({ option, brk }) {
case "Line":
case undefined:
seriesList.push(
chart.addLineSeries({
chart.serieses.addLine({
metric: blueprint.metric,
color: blueprint.color,
name: blueprint.title,
@@ -325,7 +325,7 @@ export function init({ option, brk }) {
let series;
switch (unit) {
case Unit.usd: {
series = chart.addCandlestickSeries({
series = chart.serieses.addCandlestick({
metric: brk.metrics.price.usd.ohlc,
name: "Price",
unit,
@@ -334,7 +334,7 @@ export function init({ option, brk }) {
break;
}
case Unit.sats: {
series = chart.addCandlestickSeries({
series = chart.serieses.addCandlestick({
metric: brk.metrics.price.sats.ohlc,
name: "Price",
unit,
@@ -357,7 +357,7 @@ export function init({ option, brk }) {
}),
({ latest, hasData }) => {
if (!series || !latest || !hasData) return;
printLatest({ series, unit, index: chart.index() });
printLatest({ series, unit, index: chart.index.get() });
},
);
@@ -366,10 +366,10 @@ export function init({ option, brk }) {
blueprints: option.top,
paneIndex: 0,
unit,
idx: chart.index(),
idx: chart.index.get(),
seriesList: seriesListTop,
orderStart: 1,
legend: chart.legendTop,
legend: chart.legends.top,
});
},
);
@@ -383,10 +383,10 @@ export function init({ option, brk }) {
blueprints: option.bottom,
paneIndex: 1,
unit,
idx: chart.index(),
idx: chart.index.get(),
seriesList: seriesListBottom,
orderStart: 0,
legend: chart.legendBottom,
legend: chart.legends.bottom,
});
},
);
@@ -443,7 +443,7 @@ function createIndexSelector(option, chart) {
fieldset.append(screenshotSpan);
// Track user's preferred index (only updated on explicit selection)
let preferredIndex = chart.indexName.value;
let preferredIndex = chart.index.name.value;
/** @type {HTMLElement | null} */
let field = null;
@@ -455,15 +455,15 @@ function createIndexSelector(option, chart) {
? preferredIndex
: (newChoices[0] ?? "date");
if (currentValue !== chart.indexName.value) {
chart.indexName.set(currentValue);
if (currentValue !== chart.index.name.value) {
chart.index.name.set(currentValue);
}
field = createChoiceField({
initialValue: currentValue,
onChange: (v) => {
preferredIndex = v; // User explicitly selected, update preference
chart.indexName.set(v);
chart.index.name.set(v);
},
choices: newChoices,
id: "index",