website: rename default to bitview

This commit is contained in:
nym21
2025-08-27 11:52:22 +02:00
parent f50374f983
commit 311c4fd29d
99 changed files with 3 additions and 6062 deletions
+571
View File
@@ -0,0 +1,571 @@
// @ts-check
const keyPrefix = "chart";
const ONE_BTC_IN_SATS = 100_000_000;
const AUTO = "auto";
const LINE = "line";
const CANDLE = "candle";
/**
* @typedef {"timestamp" | "date" | "week" | "d.epoch" | "month" | "quarter" | "semester" | "year" | "decade" } SerializedChartableIndex
*/
/**
* @param {Object} args
* @param {Colors} args.colors
* @param {LightweightCharts} args.lightweightCharts
* @param {Accessor<ChartOption>} args.option
* @param {Signals} args.signals
* @param {Utilities} args.utils
* @param {WebSockets} args.webSockets
* @param {Elements} args.elements
* @param {VecsResources} args.vecsResources
* @param {VecIdToIndexes} args.vecIdToIndexes
* @param {Packages} args.packages
*/
export function init({
colors,
elements,
lightweightCharts,
option,
signals,
utils,
webSockets,
vecsResources,
vecIdToIndexes,
packages,
}) {
elements.charts.append(utils.dom.createShadow("left"));
elements.charts.append(utils.dom.createShadow("right"));
const { headerElement, headingElement } = utils.dom.createHeader();
elements.charts.append(headerElement);
const { index, fieldset } = createIndexSelector({
option,
vecIdToIndexes,
signals,
utils,
});
const TIMERANGE_LS_KEY = signals.createMemo(
() => `chart-timerange-${index()}`,
);
let firstRun = true;
const from = signals.createSignal(/** @type {number | null} */ (null), {
save: {
...utils.serde.optNumber,
keyPrefix: TIMERANGE_LS_KEY,
key: "from",
serializeParam: firstRun,
},
});
const to = signals.createSignal(/** @type {number | null} */ (null), {
save: {
...utils.serde.optNumber,
keyPrefix: TIMERANGE_LS_KEY,
key: "to",
serializeParam: firstRun,
},
});
const chart = lightweightCharts.createChartElement({
parent: elements.charts,
signals,
colors,
id: "charts",
utils,
vecsResources,
elements,
index,
timeScaleSetCallback: (unknownTimeScaleCallback) => {
// TODO: Although it mostly works in practice, need to make it more robust, there is no guarantee that this runs in order and wait for `from` and `to` to update when `index` and thus `TIMERANGE_LS_KEY` is updated
// Need to have the right values before the update
const from_ = from();
const to_ = to();
if (from_ !== null && to_ !== null) {
chart.inner.timeScale().setVisibleLogicalRange({
from: from_,
to: to_,
});
} else {
unknownTimeScaleCallback();
}
},
});
const chartBottomRightCanvas = Array.from(
chart.inner.chartElement().getElementsByTagName("tr"),
).at(-1)?.lastChild?.firstChild?.firstChild;
if (chartBottomRightCanvas) {
const charts = elements.charts;
const domain = window.document.createElement("p");
domain.innerText = `${window.location.host}`;
domain.id = "domain";
const screenshotButton = window.document.createElement("button");
screenshotButton.id = "screenshot";
const camera = "[ ◉¯]";
screenshotButton.innerHTML = camera;
screenshotButton.title = "Screenshot";
chartBottomRightCanvas.replaceWith(screenshotButton);
screenshotButton.addEventListener("click", () => {
packages.modernScreenshot().then(async ({ screenshot }) => {
elements.body.dataset.screenshot = "true";
charts.append(domain);
seriesTypeField.hidden = true;
await screenshot(charts);
charts.removeChild(domain);
seriesTypeField.hidden = false;
elements.body.dataset.screenshot = "false";
});
});
}
chart.inner.timeScale().subscribeVisibleLogicalRangeChange(
utils.debounce((t) => {
if (t) {
from.set(t.from);
to.set(t.to);
}
}),
);
elements.charts.append(fieldset);
const { field: seriesTypeField, selected: topSeriesType_ } =
utils.dom.createHorizontalChoiceField({
defaultValue: CANDLE,
keyPrefix,
key: "seriestype-0",
choices: /** @type {const} */ ([AUTO, CANDLE, LINE]),
signals,
});
const topSeriesType = signals.createMemo(() => {
const topSeriesType = topSeriesType_();
if (topSeriesType === AUTO) {
const t = to();
const f = from();
if (!t || !f) return null;
const diff = t - f;
if (diff / chart.inner.paneSize().width <= 0.5) {
return CANDLE;
} else {
return LINE;
}
} else {
return topSeriesType;
}
});
const { field: topUnitField, selected: topUnit } =
utils.dom.createHorizontalChoiceField({
defaultValue: "USD",
keyPrefix,
key: "unit-0",
choices: /** @type {const} */ ([
/** @satisfies {Unit} */ ("USD"),
/** @satisfies {Unit} */ ("Sats"),
]),
signals,
sorted: true,
});
chart.addFieldsetIfNeeded({
id: "charts-unit-0",
paneIndex: 0,
position: "nw",
createChild() {
return topUnitField;
},
});
const seriesListTop = /** @type {Series[]} */ ([]);
const seriesListBottom = /** @type {Series[]} */ ([]);
/**
* @param {Object} params
* @param {ISeries} params.iseries
* @param {Unit} params.unit
* @param {Index} params.index
*/
function printLatest({ iseries, unit, index }) {
const _latest = webSockets.kraken1dCandle.latest();
if (!_latest) return;
const latest = { ..._latest };
if (unit === "Sats") {
latest.open = Math.floor(ONE_BTC_IN_SATS / latest.open);
latest.high = Math.floor(ONE_BTC_IN_SATS / latest.high);
latest.low = Math.floor(ONE_BTC_IN_SATS / latest.low);
latest.close = Math.floor(ONE_BTC_IN_SATS / latest.close);
}
const last_ = iseries.data().at(-1);
if (!last_) return;
const last = { ...last_ };
if ("close" in last) {
last.close = latest.close;
}
if ("value" in last) {
last.value = latest.close;
}
const date = new Date(latest.time * 1000);
switch (index) {
case /** @satisfies {Height} */ (5):
case /** @satisfies {DifficultyEpoch} */ (2):
case /** @satisfies {HalvingEpoch} */ (4): {
if ("close" in last) {
last.low = Math.min(last.low, latest.close);
last.high = Math.max(last.high, latest.close);
}
iseries.update(last);
break;
}
case /** @satisfies {DateIndex} */ (0): {
iseries.update(latest);
break;
}
default: {
if (index === /** @satisfies {WeekIndex} */ (23)) {
date.setUTCDate(date.getUTCDate() - ((date.getUTCDay() + 6) % 7));
} else if (index === /** @satisfies {MonthIndex} */ (7)) {
date.setUTCDate(1);
} else if (index === /** @satisfies {QuarterIndex} */ (19)) {
const month = date.getUTCMonth();
date.setUTCMonth(month - (month % 3), 1);
} else if (index === /** @satisfies {SemesterIndex} */ (20)) {
const month = date.getUTCMonth();
date.setUTCMonth(month - (month % 6), 1);
} else if (index === /** @satisfies {YearIndex} */ (24)) {
date.setUTCMonth(0, 1);
} else if (index === /** @satisfies {DecadeIndex} */ (1)) {
date.setUTCFullYear(
Math.floor(date.getUTCFullYear() / 10) * 10,
0,
1,
);
} else {
throw Error("Unsupported");
}
const time = date.valueOf() / 1000;
if (time === last.time) {
if ("close" in last) {
last.low = Math.min(last.low, latest.low);
last.high = Math.max(last.high, latest.high);
}
iseries.update(last);
} else {
latest.time = time;
iseries.update(latest);
}
}
}
}
signals.createEffect(option, (option) => {
headingElement.innerHTML = option.title;
const bottomUnits = /** @type {readonly Unit[]} */ (
Object.keys(option.bottom)
);
const { field: bottomUnitField, selected: bottomUnit } =
utils.dom.createHorizontalChoiceField({
defaultValue: bottomUnits.at(0) || "",
keyPrefix,
key: "unit-1",
choices: bottomUnits,
signals,
sorted: true,
});
if (bottomUnits.length) {
chart.addFieldsetIfNeeded({
id: "charts-unit-1",
paneIndex: 1,
position: "nw",
createChild() {
return bottomUnitField;
},
});
}
chart.addFieldsetIfNeeded({
id: "charts-seriestype-0",
paneIndex: 0,
position: "ne",
createChild() {
return seriesTypeField;
},
});
signals.createEffect(index, (index) => {
signals.createEffect(
() => ({
topUnit: topUnit(),
topSeriesType: topSeriesType(),
}),
({ topUnit, topSeriesType }) => {
/** @type {Series | undefined} */
let series;
switch (topUnit) {
case "USD": {
switch (topSeriesType) {
case CANDLE: {
series = chart.addCandlestickSeries({
vecId: "ohlc",
name: "Price",
unit: topUnit,
setDataCallback: printLatest,
order: 0,
});
break;
}
case LINE: {
series = chart.addLineSeries({
vecId: "close",
name: "Price",
unit: topUnit,
color: colors.default,
setDataCallback: printLatest,
options: {
priceLineVisible: true,
},
order: 0,
});
}
}
break;
}
case "Sats": {
switch (topSeriesType) {
case CANDLE: {
series = chart.addCandlestickSeries({
vecId: "ohlc_in_sats",
name: "Price",
unit: topUnit,
inverse: true,
setDataCallback: printLatest,
order: 0,
});
break;
}
case LINE: {
series = chart.addLineSeries({
vecId: "close_in_sats",
name: "Price",
unit: topUnit,
color: colors.default,
setDataCallback: printLatest,
options: {
priceLineVisible: true,
},
order: 0,
});
}
}
break;
}
}
if (!series) throw Error("Unreachable");
seriesListTop[0]?.remove();
seriesListTop[0] = series;
// setDataCallback insimport("./options").tead of hasData
signals.createEffect(
() => ({
latest: webSockets.kraken1dCandle.latest(),
hasData: series.hasData(),
}),
({ latest, hasData }) => {
if (!series || !latest || !hasData) return;
printLatest({ iseries: series.inner, unit: topUnit, index });
},
);
},
);
[
{
blueprints: option.top,
paneIndex: 0,
unit: topUnit,
seriesList: seriesListTop,
orderStart: 1,
legend: chart.legendTop,
},
{
blueprints: option.bottom,
paneIndex: 1,
unit: bottomUnit,
seriesList: seriesListBottom,
orderStart: 0,
legend: chart.legendBottom,
},
].forEach(
({ blueprints, paneIndex, unit, seriesList, orderStart, legend }) => {
signals.createEffect(unit, (unit) => {
legend.removeFrom(orderStart);
seriesList.splice(orderStart).forEach((series) => {
series.remove();
});
blueprints[unit]?.forEach((blueprint, order) => {
order += orderStart;
console.log(blueprint.key);
const indexes = /** @type {readonly number[]} */ (
vecIdToIndexes[blueprint.key]
);
if (indexes.includes(index)) {
switch (blueprint.type) {
case "Baseline": {
seriesList.push(
chart.addBaselineSeries({
vecId: blueprint.key,
name: blueprint.title,
unit,
defaultActive: blueprint.defaultActive,
paneIndex,
options: {
...blueprint.options,
topLineColor:
blueprint.color?.() ?? blueprint.colors?.[0](),
bottomLineColor:
blueprint.color?.() ?? blueprint.colors?.[1](),
},
order,
}),
);
break;
}
case "Histogram": {
seriesList.push(
chart.addHistogramSeries({
vecId: blueprint.key,
name: blueprint.title,
unit,
color: blueprint.color,
defaultActive: blueprint.defaultActive,
paneIndex,
options: blueprint.options,
order,
}),
);
break;
}
case "Candlestick": {
throw Error("TODO");
}
case "Line":
case undefined:
seriesList.push(
chart.addLineSeries({
vecId: blueprint.key,
color: blueprint.color,
name: blueprint.title,
unit,
defaultActive: blueprint.defaultActive,
paneIndex,
options: blueprint.options,
order,
}),
);
}
}
});
});
},
);
firstRun = false;
});
});
}
/**
* @param {Object} args
* @param {Accessor<ChartOption>} args.option
* @param {VecIdToIndexes} args.vecIdToIndexes
* @param {Signals} args.signals
* @param {Utilities} args.utils
*/
function createIndexSelector({ option, vecIdToIndexes, signals, utils }) {
const choices_ = /** @satisfies {SerializedChartableIndex[]} */ ([
"timestamp",
"date",
"week",
"d.epoch",
"month",
"quarter",
"semester",
"year",
// "halving epoch",
"decade",
]);
/** @type {Accessor<typeof choices_>} */
const choices = signals.createMemo(() => {
const o = option();
if (!Object.keys(o.top).length && !Object.keys(o.bottom).length) {
return [...choices_];
}
const rawIndexes = new Set(
[Object.values(o.top), Object.values(o.bottom)]
.flat(2)
.filter((blueprint) => !blueprint.key.startsWith("constant_"))
.map((blueprint) => vecIdToIndexes[blueprint.key])
.flat(),
);
const serializedIndexes = [...rawIndexes].flatMap((index) => {
const c = utils.serde.chartableIndex.serialize(index);
return c ? [c] : [];
});
return /** @type {any} */ (
choices_.filter((choice) => serializedIndexes.includes(choice))
);
});
const { field, selected } = utils.dom.createHorizontalChoiceField({
defaultValue: "date",
keyPrefix,
key: "index",
choices,
id: "index",
signals,
});
const fieldset = window.document.createElement("fieldset");
fieldset.id = "interval";
const screenshotSpan = window.document.createElement("span");
screenshotSpan.innerText = "interval:";
fieldset.append(screenshotSpan);
fieldset.append(field);
fieldset.dataset.size = "sm";
const index = signals.createMemo(() =>
utils.serde.chartableIndex.deserialize(selected()),
);
return { fieldset, index };
}
+3
View File
@@ -0,0 +1,3 @@
// DO NOT CHANGE, Exact format is expected in `brk_bundler`
// @ts-ignore
import("./main.js");
+116
View File
@@ -0,0 +1,116 @@
// @ts-check
/**
* @param {Object} args
* @param {Colors} args.colors
* @param {LightweightCharts} args.lightweightCharts
* @param {Accessor<ChartOption>} args.option
* @param {Signals} args.signals
* @param {Utilities} args.utils
* @param {WebSockets} args.webSockets
* @param {Elements} args.elements
* @param {VecsResources} args.vecsResources
* @param {VecIdToIndexes} args.vecIdToIndexes
*/
export function init({
colors,
elements,
lightweightCharts,
option,
signals,
utils,
webSockets,
vecsResources,
vecIdToIndexes,
}) {
const chain = window.document.createElement("div");
chain.id = "chain";
elements.explorer.append(chain);
// vecsResources.getOrCreate(/** @satisfies {Height}*/ (5), "height");
//
const miners = [
{ name: "Foundry USA", color: "orange" },
{ name: "Via BTC", color: "teal" },
{ name: "Ant Pool", color: "emerald" },
{ name: "F2Pool", color: "indigo" },
{ name: "Spider Pool", color: "yellow" },
{ name: "Mara Pool", color: "amber" },
{ name: "SEC Pool", color: "violet" },
{ name: "Luxor", color: "orange" },
{ name: "Brains Pool", color: "cyan" },
];
for (let i = 0; i <= 10; i++) {
const { name, color } = utils.array.random(miners);
const { cubeElement, leftFaceElement, rightFaceElement, topFaceElement } =
createCube();
// cubeElement.style.setProperty("--color", `var(--${color})`);
const heightElement = window.document.createElement("p");
const height = (1_000_002 - i).toString();
const prefixLength = 7 - height.length;
const spanPrefix = window.document.createElement("span");
spanPrefix.style.opacity = "0.5";
spanPrefix.style.userSelect = "none";
heightElement.append(spanPrefix);
spanPrefix.innerHTML = "#" + "0".repeat(prefixLength);
const spanHeight = window.document.createElement("span");
heightElement.append(spanHeight);
spanHeight.innerHTML = height;
rightFaceElement.append(heightElement);
const feesElement = window.document.createElement("div");
feesElement.classList.add("fees");
leftFaceElement.append(feesElement);
const averageFeeElement = window.document.createElement("p");
feesElement.append(averageFeeElement);
averageFeeElement.innerHTML = `~1.41`;
const feeRangeElement = window.document.createElement("p");
feesElement.append(feeRangeElement);
const minFeeElement = window.document.createElement("span");
minFeeElement.innerHTML = `0.11`;
feeRangeElement.append(minFeeElement);
const dashElement = window.document.createElement("span");
dashElement.style.opacity = "0.5";
dashElement.innerHTML = `-`;
feeRangeElement.append(dashElement);
const maxFeeElement = window.document.createElement("span");
maxFeeElement.innerHTML = `12.1`;
feeRangeElement.append(maxFeeElement);
const feeUnitElement = window.document.createElement("p");
feesElement.append(feeUnitElement);
feeUnitElement.style.opacity = "0.5";
feeUnitElement.innerHTML = `sat/vB`;
const spanMiner = window.document.createElement("span");
spanMiner.innerHTML = name;
topFaceElement.append(spanMiner);
chain.prepend(cubeElement);
}
}
function createCube() {
const cubeElement = window.document.createElement("div");
cubeElement.classList.add("cube");
const rightFaceElement = window.document.createElement("div");
rightFaceElement.classList.add("face");
rightFaceElement.classList.add("right");
cubeElement.append(rightFaceElement);
const leftFaceElement = window.document.createElement("div");
leftFaceElement.classList.add("face");
leftFaceElement.classList.add("left");
cubeElement.append(leftFaceElement);
const topFaceElement = window.document.createElement("div");
topFaceElement.classList.add("face");
topFaceElement.classList.add("top");
cubeElement.append(topFaceElement);
return {
cubeElement,
leftFaceElement,
rightFaceElement,
topFaceElement,
};
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+535
View File
@@ -0,0 +1,535 @@
// @ts-check
/**
* @param {Object} args
* @param {VecIdToIndexes} args.vecIdToIndexes
* @param {Option} args.option
* @param {Utilities} args.utils
* @param {Signals} args.signals
* @param {VecsResources} args.vecsResources
*/
function createTable({
utils,
vecIdToIndexes,
signals,
option,
vecsResources,
}) {
const indexToVecIds = createIndexToVecIds(vecIdToIndexes);
const serializedIndexes = createSerializedIndexes();
/** @type {SerializedIndex} */
const defaultSerializedIndex = "height";
const serializedIndex = /** @type {Signal<SerializedIndex>} */ (
signals.createSignal(
/** @type {SerializedIndex} */ (defaultSerializedIndex),
{
save: {
...utils.serde.string,
keyPrefix: "table",
key: "index",
},
},
)
);
const index = signals.createMemo(() =>
serializedIndexToIndex(serializedIndex()),
);
const table = window.document.createElement("table");
const obj = {
element: table,
/** @type {VoidFunction | undefined} */
addRandomCol: undefined,
};
signals.createEffect(index, (index, prevIndex) => {
if (prevIndex !== undefined) {
utils.url.resetParams(option);
}
const possibleVecIds = indexToVecIds[index];
const columns = signals.createSignal(/** @type {VecId[]} */ ([]), {
equals: false,
save: {
...utils.serde.vecIds,
keyPrefix: `table-${serializedIndex()}`,
key: `columns`,
},
});
columns.set((l) => l.filter((id) => possibleVecIds.includes(id)));
signals.createEffect(columns, (columns) => {
console.log(columns);
});
table.innerHTML = "";
const thead = window.document.createElement("thead");
table.append(thead);
const trHead = window.document.createElement("tr");
thead.append(trHead);
const tbody = window.document.createElement("tbody");
table.append(tbody);
const rowElements = signals.createSignal(
/** @type {HTMLTableRowElement[]} */ ([]),
);
/**
* @param {Object} args
* @param {HTMLSelectElement} args.select
* @param {Unit} [args.unit]
* @param {(event: MouseEvent) => void} [args.onLeft]
* @param {(event: MouseEvent) => void} [args.onRight]
* @param {(event: MouseEvent) => void} [args.onRemove]
*/
function addThCol({ select, onLeft, onRight, onRemove, unit: _unit }) {
const th = window.document.createElement("th");
th.scope = "col";
trHead.append(th);
const div = window.document.createElement("div");
div.append(select);
// const top = window.document.createElement("div");
// div.append(top);
// top.append(select);
// top.append(
// utils.dom.createAnchorElement({
// href: "",
// blank: true,
// }),
// );
const bottom = window.document.createElement("div");
const unit = window.document.createElement("span");
if (_unit) {
unit.innerHTML = _unit;
}
const moveLeft = utils.dom.createButtonElement({
inside: "←",
title: "Move column to the left",
onClick: onLeft || (() => {}),
});
const moveRight = utils.dom.createButtonElement({
inside: "→",
title: "Move column to the right",
onClick: onRight || (() => {}),
});
const remove = utils.dom.createButtonElement({
inside: "×",
title: "Remove column",
onClick: onRemove || (() => {}),
});
bottom.append(unit);
bottom.append(moveLeft);
bottom.append(moveRight);
bottom.append(remove);
div.append(bottom);
th.append(div);
return {
element: th,
/**
* @param {Unit} _unit
*/
setUnit(_unit) {
unit.innerHTML = _unit;
},
};
}
addThCol({
...utils.dom.createSelect({
list: serializedIndexes,
signal: serializedIndex,
}),
unit: "Index",
});
let from = 0;
let to = 0;
vecsResources
.getOrCreate(index, serializedIndex())
.fetch()
.then((vec) => {
if (!vec) return;
from = /** @type {number} */ (vec[0]);
to = /** @type {number} */ (vec.at(-1)) + 1;
const trs = /** @type {HTMLTableRowElement[]} */ ([]);
for (let i = vec.length - 1; i >= 0; i--) {
const value = vec[i];
const tr = window.document.createElement("tr");
trs.push(tr);
tbody.append(tr);
const th = window.document.createElement("th");
th.innerHTML = serializeValue({
value,
unit: "Index",
});
th.scope = "row";
tr.append(th);
}
rowElements.set(() => trs);
});
const owner = signals.getOwner();
/**
* @param {VecId} vecId
* @param {number} [_colIndex]
*/
function addCol(vecId, _colIndex = columns().length) {
signals.runWithOwner(owner, () => {
/** @type {VoidFunction | undefined} */
let dispose;
signals.createRoot((_dispose) => {
dispose = _dispose;
const vecIdOption = signals.createSignal({
name: vecId,
value: vecId,
});
const { select } = utils.dom.createSelect({
list: possibleVecIds.map((vecId) => ({
name: vecId,
value: vecId,
})),
signal: vecIdOption,
});
signals.createEffect(vecIdOption, (vecIdOption) => {
select.style.width = `${21 + 7.25 * vecIdOption.name.length}px`;
});
if (_colIndex === columns().length) {
columns.set((l) => {
l.push(vecId);
return l;
});
}
const colIndex = signals.createSignal(_colIndex);
/**
* @param {boolean} right
* @returns {(event: MouseEvent) => void}
*/
function createMoveColumnFunction(right) {
return () => {
const oldColIndex = colIndex();
const newColIndex = oldColIndex + (right ? 1 : -1);
const currentTh = /** @type {HTMLTableCellElement} */ (
trHead.childNodes[oldColIndex + 1]
);
const oterTh = /** @type {HTMLTableCellElement} */ (
trHead.childNodes[newColIndex + 1]
);
if (right) {
oterTh.after(currentTh);
} else {
oterTh.before(currentTh);
}
columns.set((l) => {
[l[oldColIndex], l[newColIndex]] = [
l[newColIndex],
l[oldColIndex],
];
return l;
});
const rows = rowElements();
for (let i = 0; i < rows.length; i++) {
const element = rows[i].childNodes[oldColIndex + 1];
const sibling = rows[i].childNodes[newColIndex + 1];
const temp = element.textContent;
element.textContent = sibling.textContent;
sibling.textContent = temp;
}
};
}
const th = addThCol({
select,
unit: utils.vecidToUnit(vecId),
onLeft: createMoveColumnFunction(false),
onRight: createMoveColumnFunction(true),
onRemove: () => {
const ci = colIndex();
trHead.childNodes[ci + 1].remove();
columns.set((l) => {
l.splice(ci, 1);
return l;
});
const rows = rowElements();
for (let i = 0; i < rows.length; i++) {
rows[i].childNodes[ci + 1].remove();
}
dispose?.();
},
});
signals.createEffect(columns, () => {
colIndex.set(Array.from(trHead.children).indexOf(th.element) - 1);
});
console.log(colIndex());
signals.createEffect(rowElements, (rowElements) => {
if (!rowElements.length) return;
for (let i = 0; i < rowElements.length; i++) {
const td = window.document.createElement("td");
rowElements[i].append(td);
}
signals.createEffect(
() => vecIdOption().name,
(vecId, prevVecId) => {
const unit = utils.vecidToUnit(vecId);
th.setUnit(unit);
const vec = vecsResources.getOrCreate(index, vecId);
vec.fetch({ from, to });
const fetchedKey = vecsResources.genFetchedKey({ from, to });
columns.set((l) => {
const i = l.indexOf(prevVecId ?? vecId);
if (i === -1) {
l.push(vecId);
} else {
l[i] = vecId;
}
return l;
});
signals.createEffect(
() => vec.fetched().get(fetchedKey)?.vec(),
(vec) => {
if (!vec?.length) return;
const thIndex = colIndex() + 1;
for (let i = 0; i < rowElements.length; i++) {
const iRev = vec.length - 1 - i;
const value = vec[iRev];
// @ts-ignore
rowElements[i].childNodes[thIndex].innerHTML =
serializeValue({
value,
unit,
});
}
},
);
return () => vecId;
},
);
});
});
signals.onCleanup(() => {
dispose?.();
});
});
}
columns().forEach((vecId, colIndex) => addCol(vecId, colIndex));
obj.addRandomCol = function () {
addCol(utils.array.random(possibleVecIds));
};
return () => index;
});
return obj;
}
/**
* @param {Object} args
* @param {Signals} args.signals
* @param {Utilities} args.utils
* @param {Option} args.option
* @param {Elements} args.elements
* @param {VecsResources} args.vecsResources
* @param {VecIdToIndexes} args.vecIdToIndexes
*/
export function init({
elements,
signals,
option,
utils,
vecsResources,
vecIdToIndexes,
}) {
const parent = elements.table;
const { headerElement } = utils.dom.createHeader("Table");
parent.append(headerElement);
const div = window.document.createElement("div");
parent.append(div);
const table = createTable({
signals,
utils,
vecIdToIndexes,
vecsResources,
option,
});
div.append(table.element);
const span = window.document.createElement("span");
span.innerHTML = "Add column";
div.append(
utils.dom.createButtonElement({
onClick: () => {
table.addRandomCol?.();
},
inside: span,
title: "Click or tap to add a column to the table",
}),
);
}
function createSerializedIndexes() {
return /** @type {const} */ ([
/** @satisfies {VecId} */ ("dateindex"),
/** @satisfies {VecId} */ ("decadeindex"),
/** @satisfies {VecId} */ ("difficultyepoch"),
/** @satisfies {VecId} */ ("emptyoutputindex"),
/** @satisfies {VecId} */ ("halvingepoch"),
/** @satisfies {VecId} */ ("height"),
/** @satisfies {VecId} */ ("inputindex"),
/** @satisfies {VecId} */ ("monthindex"),
/** @satisfies {VecId} */ ("opreturnindex"),
/** @satisfies {VecId} */ ("semesterindex"),
/** @satisfies {VecId} */ ("outputindex"),
/** @satisfies {VecId} */ ("p2aaddressindex"),
/** @satisfies {VecId} */ ("p2msoutputindex"),
/** @satisfies {VecId} */ ("p2pk33addressindex"),
/** @satisfies {VecId} */ ("p2pk65addressindex"),
/** @satisfies {VecId} */ ("p2pkhaddressindex"),
/** @satisfies {VecId} */ ("p2shaddressindex"),
/** @satisfies {VecId} */ ("p2traddressindex"),
/** @satisfies {VecId} */ ("p2wpkhaddressindex"),
/** @satisfies {VecId} */ ("p2wshaddressindex"),
/** @satisfies {VecId} */ ("quarterindex"),
/** @satisfies {VecId} */ ("txindex"),
/** @satisfies {VecId} */ ("unknownoutputindex"),
/** @satisfies {VecId} */ ("weekindex"),
/** @satisfies {VecId} */ ("yearindex"),
]);
}
/** @typedef {ReturnType<typeof createSerializedIndexes>} SerializedIndexes */
/** @typedef {SerializedIndexes[number]} SerializedIndex */
/**
* @param {SerializedIndex} serializedIndex
* @returns {Index}
*/
function serializedIndexToIndex(serializedIndex) {
switch (serializedIndex) {
case "height":
return /** @satisfies {Height} */ (5);
case "dateindex":
return /** @satisfies {DateIndex} */ (0);
case "weekindex":
return /** @satisfies {WeekIndex} */ (23);
case "difficultyepoch":
return /** @satisfies {DifficultyEpoch} */ (2);
case "monthindex":
return /** @satisfies {MonthIndex} */ (7);
case "quarterindex":
return /** @satisfies {QuarterIndex} */ (19);
case "semesterindex":
return /** @satisfies {SemesterIndex} */ (20);
case "yearindex":
return /** @satisfies {YearIndex} */ (24);
case "decadeindex":
return /** @satisfies {DecadeIndex} */ (1);
case "halvingepoch":
return /** @satisfies {HalvingEpoch} */ (4);
case "txindex":
return /** @satisfies {TxIndex} */ (21);
case "inputindex":
return /** @satisfies {InputIndex} */ (6);
case "outputindex":
return /** @satisfies {OutputIndex} */ (9);
case "p2pk33addressindex":
return /** @satisfies {P2PK33AddressIndex} */ (12);
case "p2pk65addressindex":
return /** @satisfies {P2PK65AddressIndex} */ (13);
case "p2pkhaddressindex":
return /** @satisfies {P2PKHAddressIndex} */ (14);
case "p2shaddressindex":
return /** @satisfies {P2SHAddressIndex} */ (15);
case "p2traddressindex":
return /** @satisfies {P2TRAddressIndex} */ (16);
case "p2wpkhaddressindex":
return /** @satisfies {P2WPKHAddressIndex} */ (17);
case "p2wshaddressindex":
return /** @satisfies {P2WSHAddressIndex} */ (18);
case "p2aaddressindex":
return /** @satisfies {P2AAddressIndex} */ (10);
case "p2msoutputindex":
return /** @satisfies {P2MSOutputIndex} */ (11);
case "opreturnindex":
return /** @satisfies {OpReturnIndex} */ (8);
case "emptyoutputindex":
return /** @satisfies {EmptyOutputIndex} */ (3);
case "unknownoutputindex":
return /** @satisfies {UnknownOutputIndex} */ (22);
}
}
/**
* @param {VecIdToIndexes} vecIdToIndexes
*/
function createIndexToVecIds(vecIdToIndexes) {
const indexToVecIds = Object.entries(vecIdToIndexes).reduce(
(arr, [_id, indexes]) => {
const id = /** @type {VecId} */ (_id);
indexes.forEach((i) => {
arr[i] ??= [];
arr[i].push(id);
});
return arr;
},
/** @type {VecId[][]} */ (Array.from({ length: 24 })),
);
indexToVecIds.forEach((arr) => {
arr.sort();
});
return indexToVecIds;
}
/**
* @param {Object} args
* @param {number | OHLCTuple} args.value
* @param {Unit} args.unit
*/
function serializeValue({ value, unit }) {
if (typeof value !== "number") {
return String(value);
} else if (value !== 18446744073709552000) {
if (unit === "USD" || unit === "Difficulty" || unit === "sat/vB") {
return value.toLocaleString("en-us", {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
} else if (unit === "BTC") {
return value.toLocaleString("en-us", {
minimumFractionDigits: 8,
maximumFractionDigits: 8,
});
} else {
return value.toLocaleString("en-us");
}
} else {
return "";
}
}