mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-04-24 22:59:58 -07:00
global: snapshot
This commit is contained in:
525
website/scripts/panes/chart/index.js
Normal file
525
website/scripts/panes/chart/index.js
Normal file
@@ -0,0 +1,525 @@
|
||||
import {
|
||||
createShadow,
|
||||
createChoiceField,
|
||||
createHeader,
|
||||
} from "../../utils/dom.js";
|
||||
import { chartElement } from "../../utils/elements.js";
|
||||
import { ios, canShare } from "../../utils/env.js";
|
||||
import { serdeChartableIndex, serdeOptNumber } from "../../utils/serde.js";
|
||||
import { throttle } from "../../utils/timing.js";
|
||||
import { Unit } from "../../utils/units.js";
|
||||
import signals from "../../signals.js";
|
||||
import { createChartElement } from "../../chart/index.js";
|
||||
import { webSockets } from "../../utils/ws.js";
|
||||
|
||||
const keyPrefix = "chart";
|
||||
const ONE_BTC_IN_SATS = 100_000_000;
|
||||
|
||||
/**
|
||||
* @typedef {"timestamp" | "date" | "week" | "month" | "quarter" | "semester" | "year" | "decade" } ChartableIndexName
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {Object} args
|
||||
* @param {Colors} args.colors
|
||||
* @param {Accessor<ChartOption>} args.option
|
||||
* @param {BrkClient} args.brk
|
||||
*/
|
||||
export function init({ colors, option, brk }) {
|
||||
chartElement.append(createShadow("left"));
|
||||
chartElement.append(createShadow("right"));
|
||||
|
||||
const { headerElement, headingElement } = createHeader();
|
||||
chartElement.append(headerElement);
|
||||
|
||||
const { index, fieldset } = createIndexSelector(option);
|
||||
|
||||
const TIMERANGE_LS_KEY = signals.createMemo(
|
||||
() => `chart-timerange-${index()}`,
|
||||
);
|
||||
|
||||
let firstRun = true;
|
||||
|
||||
const from = signals.createSignal(/** @type {number | null} */ (null), {
|
||||
save: {
|
||||
...serdeOptNumber,
|
||||
keyPrefix: TIMERANGE_LS_KEY,
|
||||
key: "from",
|
||||
serializeParam: firstRun,
|
||||
},
|
||||
});
|
||||
const to = signals.createSignal(/** @type {number | null} */ (null), {
|
||||
save: {
|
||||
...serdeOptNumber,
|
||||
keyPrefix: TIMERANGE_LS_KEY,
|
||||
key: "to",
|
||||
serializeParam: firstRun,
|
||||
},
|
||||
});
|
||||
|
||||
const chart = createChartElement({
|
||||
parent: chartElement,
|
||||
signals,
|
||||
colors,
|
||||
id: "charts",
|
||||
brk,
|
||||
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();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if (!(ios && !canShare)) {
|
||||
const chartBottomRightCanvas = Array.from(
|
||||
chart.inner.chartElement().getElementsByTagName("tr"),
|
||||
).at(-1)?.lastChild?.firstChild?.firstChild;
|
||||
if (chartBottomRightCanvas) {
|
||||
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", () => {
|
||||
import("./screenshot").then(async ({ screenshot }) => {
|
||||
chartElement.dataset.screenshot = "true";
|
||||
chartElement.append(domain);
|
||||
try {
|
||||
await screenshot({
|
||||
element: chartElement,
|
||||
name: option().path.join("-"),
|
||||
title: option().title,
|
||||
});
|
||||
} catch {}
|
||||
chartElement.removeChild(domain);
|
||||
chartElement.dataset.screenshot = "false";
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
chart.inner.timeScale().subscribeVisibleLogicalRangeChange(
|
||||
throttle((t) => {
|
||||
if (!t) return;
|
||||
from.set(t.from);
|
||||
to.set(t.to);
|
||||
}, 250),
|
||||
);
|
||||
|
||||
chartElement.append(fieldset);
|
||||
|
||||
const { field: topUnitField, selected: topUnit } = createChoiceField({
|
||||
defaultValue: Unit.usd,
|
||||
keyPrefix,
|
||||
key: "unit-0",
|
||||
choices: [Unit.usd, Unit.sats],
|
||||
toKey: (u) => u.id,
|
||||
toLabel: (u) => u.name,
|
||||
signals,
|
||||
sorted: true,
|
||||
type: "select",
|
||||
});
|
||||
|
||||
chart.addFieldsetIfNeeded({
|
||||
id: "charts-unit-0",
|
||||
paneIndex: 0,
|
||||
position: "nw",
|
||||
createChild() {
|
||||
return topUnitField;
|
||||
},
|
||||
});
|
||||
|
||||
const seriesListTop = /** @type {AnySeries[]} */ ([]);
|
||||
const seriesListBottom = /** @type {AnySeries[]} */ ([]);
|
||||
|
||||
/**
|
||||
* @param {Object} params
|
||||
* @param {AnySeries} params.series
|
||||
* @param {Unit} params.unit
|
||||
* @param {IndexName} params.index
|
||||
*/
|
||||
function printLatest({ series, unit, index }) {
|
||||
const _latest = webSockets.kraken1dCandle.latest();
|
||||
|
||||
if (!_latest) return;
|
||||
|
||||
const latest = { ..._latest };
|
||||
|
||||
if (unit === 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_ = series.getData().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(/** @type {number} */ (latest.time) * 1000);
|
||||
|
||||
switch (index) {
|
||||
case "height":
|
||||
case "difficultyepoch":
|
||||
case "halvingepoch": {
|
||||
if ("close" in last) {
|
||||
last.low = Math.min(last.low, latest.close);
|
||||
last.high = Math.max(last.high, latest.close);
|
||||
}
|
||||
series.update(last);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
if (index === "weekindex") {
|
||||
date.setUTCDate(date.getUTCDate() - ((date.getUTCDay() + 6) % 7));
|
||||
} else if (index === "monthindex") {
|
||||
date.setUTCDate(1);
|
||||
} else if (index === "quarterindex") {
|
||||
const month = date.getUTCMonth();
|
||||
date.setUTCMonth(month - (month % 3), 1);
|
||||
} else if (index === "semesterindex") {
|
||||
const month = date.getUTCMonth();
|
||||
date.setUTCMonth(month - (month % 6), 1);
|
||||
} else if (index === "yearindex") {
|
||||
date.setUTCMonth(0, 1);
|
||||
} else if (index === "decadeindex") {
|
||||
date.setUTCFullYear(
|
||||
Math.floor(date.getUTCFullYear() / 10) * 10,
|
||||
0,
|
||||
1,
|
||||
);
|
||||
} else if (index !== "dateindex") {
|
||||
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);
|
||||
}
|
||||
series.update(last);
|
||||
} else {
|
||||
last.time = time;
|
||||
series.update(last);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
signals.createEffect(option, (option) => {
|
||||
headingElement.innerHTML = option.title;
|
||||
|
||||
const bottomUnits = Array.from(option.bottom.keys());
|
||||
|
||||
/** @type {{ field: HTMLDivElement, selected: Accessor<Unit> } | undefined} */
|
||||
let bottomUnitSelector;
|
||||
|
||||
if (bottomUnits.length) {
|
||||
bottomUnitSelector = createChoiceField({
|
||||
defaultValue: bottomUnits[0],
|
||||
keyPrefix,
|
||||
key: "unit-1",
|
||||
choices: bottomUnits,
|
||||
toKey: (u) => u.id,
|
||||
toLabel: (u) => u.name,
|
||||
signals,
|
||||
sorted: true,
|
||||
type: "select",
|
||||
});
|
||||
|
||||
const field = bottomUnitSelector.field;
|
||||
chart.addFieldsetIfNeeded({
|
||||
id: "charts-unit-1",
|
||||
paneIndex: 1,
|
||||
position: "nw",
|
||||
createChild() {
|
||||
return field;
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// Clean up bottom pane when new option has no bottom series
|
||||
seriesListBottom.forEach((series) => series.remove());
|
||||
seriesListBottom.length = 0;
|
||||
chart.legendBottom.removeFrom(0);
|
||||
}
|
||||
|
||||
signals.createEffect(index, (index) => {
|
||||
signals.createEffect(topUnit, (topUnit) => {
|
||||
/** @type {AnySeries | undefined} */
|
||||
let series;
|
||||
|
||||
switch (topUnit) {
|
||||
case Unit.usd: {
|
||||
series = chart.addCandlestickSeries({
|
||||
metric: brk.metrics.price.usd.ohlc,
|
||||
name: "Price",
|
||||
unit: topUnit,
|
||||
order: 0,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case Unit.sats: {
|
||||
series = chart.addCandlestickSeries({
|
||||
metric: brk.metrics.price.sats.ohlc,
|
||||
name: "Price",
|
||||
unit: topUnit,
|
||||
inverse: true,
|
||||
order: 0,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!series) throw Error("Unreachable");
|
||||
|
||||
seriesListTop[0]?.remove();
|
||||
seriesListTop[0] = series;
|
||||
|
||||
signals.createEffect(
|
||||
() => ({
|
||||
latest: webSockets.kraken1dCandle.latest(),
|
||||
hasData: series.hasData(),
|
||||
}),
|
||||
({ latest, hasData }) => {
|
||||
if (!series || !latest || !hasData) return;
|
||||
printLatest({ series, unit: topUnit, index });
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {Object} args
|
||||
* @param {Map<Unit, AnyFetchedSeriesBlueprint[]>} args.blueprints
|
||||
* @param {number} args.paneIndex
|
||||
* @param {Accessor<Unit>} args.unit
|
||||
* @param {AnySeries[]} args.seriesList
|
||||
* @param {number} args.orderStart
|
||||
* @param {Legend} args.legend
|
||||
*/
|
||||
function processPane({
|
||||
blueprints,
|
||||
paneIndex,
|
||||
unit,
|
||||
seriesList,
|
||||
orderStart,
|
||||
legend,
|
||||
}) {
|
||||
signals.createEffect(unit, (unit) => {
|
||||
legend.removeFrom(orderStart);
|
||||
|
||||
seriesList.splice(orderStart).forEach((series) => {
|
||||
series.remove();
|
||||
});
|
||||
|
||||
blueprints.get(unit)?.forEach((blueprint, order) => {
|
||||
order += orderStart;
|
||||
|
||||
const options = blueprint.options;
|
||||
|
||||
// Tree-first: metric is now an accessor with .by property
|
||||
const indexes = Object.keys(blueprint.metric.by);
|
||||
|
||||
if (indexes.includes(index)) {
|
||||
switch (blueprint.type) {
|
||||
case "Baseline": {
|
||||
seriesList.push(
|
||||
chart.addBaselineSeries({
|
||||
metric: blueprint.metric,
|
||||
name: blueprint.title,
|
||||
unit,
|
||||
defaultActive: blueprint.defaultActive,
|
||||
paneIndex,
|
||||
options: {
|
||||
...options,
|
||||
topLineColor:
|
||||
blueprint.color?.() ?? blueprint.colors?.[0](),
|
||||
bottomLineColor:
|
||||
blueprint.color?.() ?? blueprint.colors?.[1](),
|
||||
},
|
||||
order,
|
||||
}),
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "Histogram": {
|
||||
seriesList.push(
|
||||
chart.addHistogramSeries({
|
||||
metric: blueprint.metric,
|
||||
name: blueprint.title,
|
||||
unit,
|
||||
color: blueprint.color,
|
||||
defaultActive: blueprint.defaultActive,
|
||||
paneIndex,
|
||||
options,
|
||||
order,
|
||||
}),
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "Candlestick": {
|
||||
seriesList.push(
|
||||
chart.addCandlestickSeries({
|
||||
metric: blueprint.metric,
|
||||
name: blueprint.title,
|
||||
unit,
|
||||
colors: blueprint.colors,
|
||||
defaultActive: blueprint.defaultActive,
|
||||
paneIndex,
|
||||
options,
|
||||
order,
|
||||
}),
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "Dots": {
|
||||
seriesList.push(
|
||||
chart.addDotsSeries({
|
||||
metric: blueprint.metric,
|
||||
color: blueprint.color,
|
||||
name: blueprint.title,
|
||||
unit,
|
||||
defaultActive: blueprint.defaultActive,
|
||||
paneIndex,
|
||||
options,
|
||||
order,
|
||||
}),
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "Line":
|
||||
case undefined:
|
||||
seriesList.push(
|
||||
chart.addLineSeries({
|
||||
metric: blueprint.metric,
|
||||
color: blueprint.color,
|
||||
name: blueprint.title,
|
||||
unit,
|
||||
defaultActive: blueprint.defaultActive,
|
||||
paneIndex,
|
||||
options,
|
||||
order,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
processPane({
|
||||
blueprints: option.top,
|
||||
paneIndex: 0,
|
||||
unit: topUnit,
|
||||
seriesList: seriesListTop,
|
||||
orderStart: 1,
|
||||
legend: chart.legendTop,
|
||||
});
|
||||
|
||||
if (bottomUnitSelector) {
|
||||
processPane({
|
||||
blueprints: option.bottom,
|
||||
paneIndex: 1,
|
||||
unit: bottomUnitSelector.selected,
|
||||
seriesList: seriesListBottom,
|
||||
orderStart: 0,
|
||||
legend: chart.legendBottom,
|
||||
});
|
||||
}
|
||||
|
||||
firstRun = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Accessor<ChartOption>} option
|
||||
*/
|
||||
function createIndexSelector(option) {
|
||||
const choices_ = /** @satisfies {ChartableIndexName[]} */ ([
|
||||
"timestamp",
|
||||
"date",
|
||||
"week",
|
||||
"month",
|
||||
"quarter",
|
||||
"semester",
|
||||
"year",
|
||||
"decade",
|
||||
]);
|
||||
|
||||
/** @type {Accessor<typeof choices_>} */
|
||||
const choices = signals.createMemo(() => {
|
||||
const o = option();
|
||||
|
||||
if (!o.top.size && !o.bottom.size) {
|
||||
return [...choices_];
|
||||
}
|
||||
const rawIndexes = new Set(
|
||||
[Array.from(o.top.values()), Array.from(o.bottom.values())]
|
||||
.flat(2)
|
||||
.filter((blueprint) => {
|
||||
const path = Object.values(blueprint.metric.by)[0]?.path ?? "";
|
||||
return !path.includes("constant_");
|
||||
})
|
||||
.flatMap((blueprint) => blueprint.metric.indexes()),
|
||||
);
|
||||
|
||||
const serializedIndexes = [...rawIndexes].flatMap((index) => {
|
||||
const c = serdeChartableIndex.serialize(index);
|
||||
return c ? [c] : [];
|
||||
});
|
||||
|
||||
return /** @type {any} */ (
|
||||
choices_.filter((choice) => serializedIndexes.includes(choice))
|
||||
);
|
||||
});
|
||||
|
||||
/** @type {ChartableIndexName} */
|
||||
const defaultIndex = "date";
|
||||
const { field, selected } = createChoiceField({
|
||||
defaultValue: defaultIndex,
|
||||
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(() =>
|
||||
serdeChartableIndex.deserialize(selected()),
|
||||
);
|
||||
|
||||
return { fieldset, index };
|
||||
}
|
||||
38
website/scripts/panes/chart/screenshot.js
Normal file
38
website/scripts/panes/chart/screenshot.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import { ios } from "../../utils/env.js";
|
||||
import { domToBlob } from "../../modules/modern-screenshot/4.6.7/dist/index.mjs";
|
||||
|
||||
/**
|
||||
* @param {Object} args
|
||||
* @param {Element} args.element
|
||||
* @param {string} args.name
|
||||
* @param {string} args.title
|
||||
*/
|
||||
export async function screenshot({ element, name, title }) {
|
||||
const blob = await domToBlob(element, {
|
||||
scale: 2,
|
||||
});
|
||||
|
||||
if (ios) {
|
||||
const file = new File(
|
||||
[blob],
|
||||
`bitview-${name}-${new Date().toJSON().split(".")[0]}.png`,
|
||||
{
|
||||
type: "image/png",
|
||||
},
|
||||
);
|
||||
|
||||
try {
|
||||
await navigator.share({
|
||||
files: [file],
|
||||
title: `${title} on ${window.document.location.hostname}`,
|
||||
});
|
||||
return;
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
}
|
||||
|
||||
const url = URL.createObjectURL(blob);
|
||||
window.open(url, "_blank");
|
||||
setTimeout(() => URL.revokeObjectURL(url), 100);
|
||||
}
|
||||
99
website/scripts/panes/explorer.js
Normal file
99
website/scripts/panes/explorer.js
Normal file
@@ -0,0 +1,99 @@
|
||||
import { randomFromArray } from "../utils/array.js";
|
||||
import { explorerElement } from "../utils/elements.js";
|
||||
|
||||
export function init() {
|
||||
const chain = window.document.createElement("div");
|
||||
chain.id = "chain";
|
||||
explorerElement.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: _color } = randomFromArray(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,
|
||||
};
|
||||
}
|
||||
0
website/scripts/panes/nav.js
Normal file
0
website/scripts/panes/nav.js
Normal file
0
website/scripts/panes/search.js
Normal file
0
website/scripts/panes/search.js
Normal file
1106
website/scripts/panes/simulation.js
Normal file
1106
website/scripts/panes/simulation.js
Normal file
File diff suppressed because it is too large
Load Diff
433
website/scripts/panes/table.js
Normal file
433
website/scripts/panes/table.js
Normal file
@@ -0,0 +1,433 @@
|
||||
// @ts-nocheck
|
||||
|
||||
import { randomFromArray } from "../utils/array.js";
|
||||
import { createButtonElement, createHeader, createSelect } from "../utils/dom.js";
|
||||
import { tableElement } from "../utils/elements.js";
|
||||
import { serdeMetrics, serdeString } from "../utils/serde.js";
|
||||
import { resetParams } from "../utils/url.js";
|
||||
|
||||
export function init() {
|
||||
tableElement.innerHTML = "wip, will hopefuly be back soon, sorry !";
|
||||
|
||||
// const parent = tableElement;
|
||||
// const { headerElement } = createHeader("Table");
|
||||
// parent.append(headerElement);
|
||||
|
||||
// const div = window.document.createElement("div");
|
||||
// parent.append(div);
|
||||
|
||||
// const table = createTable({
|
||||
// signals,
|
||||
// brk,
|
||||
// resources,
|
||||
// option,
|
||||
// });
|
||||
// div.append(table.element);
|
||||
|
||||
// const span = window.document.createElement("span");
|
||||
// span.innerHTML = "Add column";
|
||||
// div.append(
|
||||
// createButtonElement({
|
||||
// onClick: () => {
|
||||
// table.addRandomCol?.();
|
||||
// },
|
||||
// inside: span,
|
||||
// title: "Click or tap to add a column to the table",
|
||||
// }),
|
||||
// );
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @param {Object} args
|
||||
// * @param {Option} args.option
|
||||
// * @param {Signals} args.signals
|
||||
// * @param {BrkClient} args.brk
|
||||
// * @param {Resources} args.resources
|
||||
// */
|
||||
// function createTable({ brk, signals, option, resources }) {
|
||||
// const indexToMetrics = createIndexToMetrics(metricToIndexes);
|
||||
|
||||
// const serializedIndexes = createSerializedIndexes();
|
||||
// /** @type {SerializedIndex} */
|
||||
// const defaultSerializedIndex = "height";
|
||||
// const serializedIndex = /** @type {Signal<SerializedIndex>} */ (
|
||||
// signals.createSignal(
|
||||
// /** @type {SerializedIndex} */ (defaultSerializedIndex),
|
||||
// {
|
||||
// save: {
|
||||
// ...serdeString,
|
||||
// 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) {
|
||||
// resetParams(option);
|
||||
// }
|
||||
|
||||
// const possibleMetrics = indexToMetrics[index];
|
||||
|
||||
// const columns = signals.createSignal(/** @type {Metric[]} */ ([]), {
|
||||
// equals: false,
|
||||
// save: {
|
||||
// ...serdeMetrics,
|
||||
// keyPrefix: `table-${serializedIndex()}`,
|
||||
// key: `columns`,
|
||||
// },
|
||||
// });
|
||||
// columns.set((l) => l.filter((id) => possibleMetrics.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(
|
||||
// // createAnchorElement({
|
||||
// // href: "",
|
||||
// // blank: true,
|
||||
// // }),
|
||||
// // );
|
||||
// const bottom = window.document.createElement("div");
|
||||
// const unit = window.document.createElement("span");
|
||||
// if (_unit) {
|
||||
// unit.innerHTML = _unit;
|
||||
// }
|
||||
// const moveLeft = createButtonElement({
|
||||
// inside: "←",
|
||||
// title: "Move column to the left",
|
||||
// onClick: onLeft || (() => {}),
|
||||
// });
|
||||
// const moveRight = createButtonElement({
|
||||
// inside: "→",
|
||||
// title: "Move column to the right",
|
||||
// onClick: onRight || (() => {}),
|
||||
// });
|
||||
// const remove = 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({
|
||||
// ...createSelect({
|
||||
// list: serializedIndexes,
|
||||
// signal: serializedIndex,
|
||||
// }),
|
||||
// unit: "index",
|
||||
// });
|
||||
|
||||
// let from = 0;
|
||||
// let to = 0;
|
||||
|
||||
// resources
|
||||
// .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 {Metric} metric
|
||||
// * @param {number} [_colIndex]
|
||||
// */
|
||||
// function addCol(metric, _colIndex = columns().length) {
|
||||
// signals.runWithOwner(owner, () => {
|
||||
// /** @type {VoidFunction | undefined} */
|
||||
// let dispose;
|
||||
// signals.createRoot((_dispose) => {
|
||||
// dispose = _dispose;
|
||||
|
||||
// const metricOption = signals.createSignal({
|
||||
// name: metric,
|
||||
// value: metric,
|
||||
// });
|
||||
// const { select } = createSelect({
|
||||
// list: possibleMetrics.map((metric) => ({
|
||||
// name: metric,
|
||||
// value: metric,
|
||||
// })),
|
||||
// signal: metricOption,
|
||||
// });
|
||||
|
||||
// signals.createEffect(metricOption, (metricOption) => {
|
||||
// select.style.width = `${21 + 7.25 * metricOption.name.length}px`;
|
||||
// });
|
||||
|
||||
// if (_colIndex === columns().length) {
|
||||
// columns.set((l) => {
|
||||
// l.push(metric);
|
||||
// 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: serdeUnit.deserialize(metric),
|
||||
// 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(
|
||||
// () => metricOption().name,
|
||||
// (metric, prevMetric) => {
|
||||
// const unit = serdeUnit.deserialize(metric);
|
||||
// th.setUnit(unit);
|
||||
|
||||
// const vec = resources.getOrCreate(index, metric);
|
||||
|
||||
// vec.fetch({ from, to });
|
||||
|
||||
// const fetchedKey = resources.genFetchedKey({ from, to });
|
||||
|
||||
// columns.set((l) => {
|
||||
// const i = l.indexOf(prevMetric ?? metric);
|
||||
// if (i === -1) {
|
||||
// l.push(metric);
|
||||
// } else {
|
||||
// l[i] = metric;
|
||||
// }
|
||||
// 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 () => metric;
|
||||
// },
|
||||
// );
|
||||
// });
|
||||
// });
|
||||
|
||||
// signals.onCleanup(() => {
|
||||
// dispose?.();
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
|
||||
// columns().forEach((metric, colIndex) => addCol(metric, colIndex));
|
||||
|
||||
// obj.addRandomCol = function () {
|
||||
// addCol(randomFromArray(possibleMetrics));
|
||||
// };
|
||||
|
||||
// return () => index;
|
||||
// });
|
||||
|
||||
// return obj;
|
||||
// }
|
||||
|
||||
/**
|
||||
* @param {MetricToIndexes} metricToIndexes
|
||||
*/
|
||||
function createIndexToMetrics(metricToIndexes) {
|
||||
// const indexToMetrics = Object.entries(metricToIndexes).reduce(
|
||||
// (arr, [_id, indexes]) => {
|
||||
// const id = /** @type {Metric} */ (_id);
|
||||
// indexes.forEach((i) => {
|
||||
// arr[i] ??= [];
|
||||
// arr[i].push(id);
|
||||
// });
|
||||
// return arr;
|
||||
// },
|
||||
// /** @type {Metric[][]} */ (Array.from({ length: 24 })),
|
||||
// );
|
||||
// indexToMetrics.forEach((arr) => {
|
||||
// arr.sort();
|
||||
// });
|
||||
// return indexToMetrics;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} args
|
||||
* @param {number | string | Object | Array<any>} args.value
|
||||
* @param {Unit} args.unit
|
||||
*/
|
||||
function serializeValue({ value, unit }) {
|
||||
const t = typeof value;
|
||||
if (value === null) {
|
||||
return "null";
|
||||
} else if (typeof value === "string") {
|
||||
return value;
|
||||
} else if (t !== "number") {
|
||||
return JSON.stringify(value).replaceAll('"', "").slice(1, -1);
|
||||
} 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 "";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user