website: snapshot

This commit is contained in:
nym21
2026-01-22 16:21:09 +01:00
parent b557477770
commit a62a377081
49 changed files with 2101 additions and 8286 deletions

View File

@@ -3,12 +3,10 @@
*
* @import { IChartApi, ISeriesApi as _ISeriesApi, SeriesDefinition, SingleValueData as _SingleValueData, CandlestickData as _CandlestickData, BaselineData as _BaselineData, HistogramData as _HistogramData, SeriesType as LCSeriesType, IPaneApi, LineSeriesPartialOptions as _LineSeriesPartialOptions, HistogramSeriesPartialOptions as _HistogramSeriesPartialOptions, BaselineSeriesPartialOptions as _BaselineSeriesPartialOptions, CandlestickSeriesPartialOptions as _CandlestickSeriesPartialOptions, WhitespaceData, DeepPartial, ChartOptions, Time, LineData as _LineData, createChart as CreateLCChart, LineStyle, createSeriesMarkers as CreateSeriesMarkers, SeriesMarker, ISeriesMarkersPluginApi } from './modules/lightweight-charts/5.1.0/dist/typings.js'
*
* @import { Signal, Signals, Accessor } from "./signals.js";
*
* @import * as Brk from "./modules/brk-client/index.js"
* @import { BrkClient, Index, Metric, MetricData } from "./modules/brk-client/index.js"
*
* @import { Resources, MetricResource } from './resources.js'
* @import { Options } from './options/full.js'
*
* @import { PersistedValue } from './utils/persisted.js'
*

View File

@@ -1,11 +1,13 @@
import { webSockets } from "./utils/ws.js";
import * as formatters from "./utils/format.js";
import { onFirstIntersection, getElementById, isHidden } from "./utils/dom.js";
import signals from "./signals.js";
import { BrkClient } from "./modules/brk-client/index.js";
import { initOptions } from "./options/full.js";
import ufuzzy from "./modules/leeoniya-ufuzzy/1.0.19/dist/uFuzzy.mjs";
import { init as initChart } from "./panes/chart.js";
import {
init as initChart,
setOption as setChartOption,
} from "./panes/chart.js";
import { initSearch } from "./panes/search.js";
import { next } from "./utils/timing.js";
import { replaceHistory } from "./utils/url.js";
import { removeStored, writeToStorage } from "./utils/storage.js";
@@ -19,8 +21,6 @@ import {
navElement,
navLabelElement,
searchElement,
searchInput,
searchResultsElement,
style,
} from "./utils/elements.js";
@@ -106,427 +106,223 @@ function initFrameSelectors() {
}
initFrameSelectors();
signals.createRoot(() => {
const brk = new BrkClient("https://next.bitview.space");
// const brk = new BrkClient("/");
const owner = signals.getOwner();
const brk = new BrkClient("https://next.bitview.space");
// const brk = new BrkClient("/");
console.log(`VERSION = ${brk.VERSION}`);
console.log(`VERSION = ${brk.VERSION}`);
webSockets.kraken1dCandle.onLatest((latest) => {
console.log("close:", latest.close);
window.document.title = `${latest.close.toLocaleString("en-us")} | ${window.location.host}`;
});
webSockets.kraken1dCandle.onLatest((latest) => {
console.log("close:", latest.close);
window.document.title = `${latest.close.toLocaleString("en-us")} | ${window.location.host}`;
});
// function createLastHeightResource() {
// const lastHeight = signals.createSignal(0);
// function fetchLastHeight() {
// utils.api.fetchLast(
// (h) => {
// lastHeight.set(h);
// },
// /** @satisfies {Height} */ (5),
// "height",
// );
// }
// fetchLastHeight();
// setInterval(fetchLastHeight, 10_000);
// return lastHeight;
// }
// const lastHeight = createLastHeightResource();
const options = initOptions(brk);
const options = initOptions({
signals,
brk,
});
window.addEventListener("popstate", (_event) => {
const path = window.document.location.pathname.split("/").filter((v) => v);
let folder = options.tree;
window.addEventListener("popstate", (_event) => {
const path = window.document.location.pathname.split("/").filter((v) => v);
let folder = options.tree;
while (path.length) {
const id = path.shift();
const res = folder.find((v) => id === formatters.stringToId(v.name));
if (!res) throw "Option not found";
if (path.length >= 1) {
if (!("tree" in res)) {
throw "Unreachable";
}
folder = res.tree;
} else {
if ("tree" in res) {
throw "Unreachable";
}
options.selected.set(res);
while (path.length) {
const id = path.shift();
const res = folder.find((v) => id === formatters.stringToId(v.name));
if (!res) throw "Option not found";
if (path.length >= 1) {
if (!("tree" in res)) {
throw "Unreachable";
}
folder = res.tree;
} else {
if ("tree" in res) {
throw "Unreachable";
}
options.selected.set(res);
}
});
function initSelected() {
let firstRun = true;
function initSelectedFrame() {
if (!firstRun) throw Error("Unreachable");
firstRun = false;
const owner = signals.getOwner();
const chartOption = signals.createSignal(
/** @type {ChartOption | null} */ (null),
);
let previousElement = /** @type {HTMLElement | undefined} */ (undefined);
let firstTimeLoadingChart = true;
signals.createScopedEffect(options.selected, (option) => {
/** @type {HTMLElement | undefined} */
let element;
switch (option.kind) {
case "chart": {
element = chartElement;
chartOption.set(option);
if (firstTimeLoadingChart) {
signals.runWithOwner(owner, () =>
initChart({
option: /** @type {Accessor<ChartOption>} */ (chartOption),
brk,
}),
);
}
firstTimeLoadingChart = false;
break;
}
case "link": {
return;
}
}
if (!element) throw "Element should be set";
if (element !== previousElement) {
if (previousElement) previousElement.hidden = true;
element.hidden = false;
}
if (!previousElement) {
replaceHistory({ pathname: option.path });
}
previousElement = element;
});
}
function createMobileSwitchEffect() {
let firstRun = true;
signals.createEffect(options.selected, () => {
if (!firstRun && !isHidden(asideLabelElement)) {
asideLabelElement.click();
}
firstRun = false;
});
}
createMobileSwitchEffect();
onFirstIntersection(asideElement, () =>
signals.runWithOwner(owner, initSelectedFrame),
);
}
initSelected();
});
onFirstIntersection(navElement, async () => {
options.parent.set(navElement);
function initSelected() {
let firstRun = true;
function initSelectedFrame() {
if (!firstRun) throw Error("Unreachable");
firstRun = false;
const option = options.selected();
if (!option) throw "Selected should be set by now";
const path = [...option.path];
let previousElement = /** @type {HTMLElement | undefined} */ (undefined);
let firstTimeLoadingChart = true;
/** @type {HTMLUListElement | null} */
let ul = /** @type {any} */ (null);
async function getFirstChild() {
try {
ul = /** @type {HTMLUListElement} */ (navElement.firstElementChild);
await next();
if (!ul) {
await getFirstChild();
options.selected.onChange((option) => {
/** @type {HTMLElement | undefined} */
let element;
switch (option.kind) {
case "chart": {
element = chartElement;
if (firstTimeLoadingChart) {
initChart(brk);
}
firstTimeLoadingChart = false;
setChartOption(option);
break;
}
} catch (_) {
await next();
await getFirstChild();
}
}
await getFirstChild();
if (!ul) throw Error("Unreachable");
while (path.length > 1) {
const name = path.shift();
if (!name) throw "Unreachable";
/** @type {HTMLDetailsElement[]} */
let detailsList = [];
while (!detailsList.length) {
detailsList = Array.from(ul.querySelectorAll(":scope > li > details"));
if (!detailsList.length) {
await next();
}
}
const details = detailsList.find((s) => s.dataset.name == name);
if (!details) return;
details.open = true;
ul = null;
while (!ul) {
const uls = /** @type {HTMLUListElement[]} */ (
Array.from(details.querySelectorAll(":scope > ul"))
);
if (!uls.length) {
await next();
} else if (uls.length > 1) {
throw "Shouldn't be possible";
} else {
ul = /** @type {HTMLUListElement} */ (uls.pop());
}
}
}
/** @type {HTMLAnchorElement[]} */
let anchors = [];
while (!anchors.length) {
anchors = Array.from(ul.querySelectorAll(":scope > li > a"));
if (!anchors.length) {
await next();
}
}
anchors
.find((a) => a.getAttribute("href") == window.document.location.pathname)
?.scrollIntoView({
behavior: "instant",
block: "center",
});
});
onFirstIntersection(searchElement, () => {
console.log("search: init");
const haystack = options.list.map((option) => option.title);
const RESULTS_PER_PAGE = 100;
/**
* @param {uFuzzy.SearchResult} searchResult
* @param {number} pageIndex
*/
function computeResultPage(searchResult, pageIndex) {
/** @type {{ option: Option, title: string }[]} */
let list = [];
let [indexes, _info, order] = searchResult || [null, null, null];
const minIndex = pageIndex * RESULTS_PER_PAGE;
if (indexes?.length) {
const maxIndex = Math.min(
(order || indexes).length - 1,
minIndex + RESULTS_PER_PAGE - 1,
);
list = Array(maxIndex - minIndex + 1);
for (let i = minIndex; i <= maxIndex; i++) {
let index = indexes[i];
const title = haystack[index];
list[i % 100] = {
option: options.list[index],
title,
};
}
}
return list;
}
/** @type {uFuzzy.Options} */
const config = {
intraIns: Infinity,
intraChars: `[a-z\d' ]`,
};
const fuzzyMultiInsert = /** @type {uFuzzy} */ (
ufuzzy({
intraIns: 1,
})
);
const fuzzyMultiInsertFuzzier = /** @type {uFuzzy} */ (ufuzzy(config));
const fuzzySingleError = /** @type {uFuzzy} */ (
ufuzzy({
intraMode: 1,
...config,
})
);
const fuzzySingleErrorFuzzier = /** @type {uFuzzy} */ (
ufuzzy({
intraMode: 1,
...config,
})
);
/** @type {VoidFunction | undefined} */
let dispose;
function inputEvent() {
signals.createRoot((_dispose) => {
const needle = /** @type {string} */ (searchInput.value);
dispose?.();
dispose = _dispose;
searchResultsElement.scrollTo({
top: 0,
});
if (!needle) {
searchResultsElement.innerHTML = "";
case "link": {
return;
}
}
const outOfOrder = 5;
const infoThresh = 5_000;
if (!element) throw "Element should be set";
let result = fuzzyMultiInsert?.search(
haystack,
needle,
undefined,
infoThresh,
);
if (element !== previousElement) {
if (previousElement) previousElement.hidden = true;
element.hidden = false;
}
if (!result?.[0]?.length || !result?.[1]) {
result = fuzzyMultiInsert?.search(
haystack,
needle,
outOfOrder,
infoThresh,
);
}
if (!previousElement) {
replaceHistory({ pathname: option.path });
}
if (!result?.[0]?.length || !result?.[1]) {
result = fuzzySingleError?.search(
haystack,
needle,
outOfOrder,
infoThresh,
);
}
previousElement = element;
});
}
if (!result?.[0]?.length || !result?.[1]) {
result = fuzzySingleErrorFuzzier?.search(
haystack,
needle,
outOfOrder,
infoThresh,
);
}
if (!result?.[0]?.length || !result?.[1]) {
result = fuzzyMultiInsertFuzzier?.search(
haystack,
needle,
undefined,
infoThresh,
);
}
if (!result?.[0]?.length || !result?.[1]) {
result = fuzzyMultiInsertFuzzier?.search(
haystack,
needle,
outOfOrder,
infoThresh,
);
}
searchResultsElement.innerHTML = "";
const list = computeResultPage(result, 0);
list.forEach(({ option, title }) => {
const li = window.document.createElement("li");
searchResultsElement.appendChild(li);
const element = options.createOptionElement({
option,
name: title,
});
if (element) {
li.append(element);
}
});
});
let firstMobileSwitch = true;
options.selected.onChange(() => {
if (!firstMobileSwitch && !isHidden(asideLabelElement)) {
asideLabelElement.click();
}
if (searchInput.value) {
inputEvent();
}
searchInput.addEventListener("input", inputEvent);
firstMobileSwitch = false;
});
function initDesktopResizeBar() {
const resizeBar = getElementById("resize-bar");
let resize = false;
let startingWidth = 0;
let startingClientX = 0;
onFirstIntersection(asideElement, initSelectedFrame);
}
initSelected();
const barWidthLocalStorageKey = "bar-width";
onFirstIntersection(navElement, async () => {
options.setParent(navElement);
/**
* @param {number | null} width
*/
function setBarWidth(width) {
// TODO: Check if should be a signal ??
try {
if (typeof width === "number") {
mainElement.style.width = `${width}px`;
writeToStorage(barWidthLocalStorageKey, String(width));
} else {
mainElement.style.width = style.getPropertyValue(
"--default-main-width",
);
removeStored(barWidthLocalStorageKey);
}
} catch (_) {}
const option = options.selected.value;
if (!option) throw "Selected should be set by now";
const path = [...option.path];
/** @type {HTMLUListElement | null} */
let ul = /** @type {any} */ (null);
async function getFirstChild() {
try {
ul = /** @type {HTMLUListElement} */ (navElement.firstElementChild);
await next();
if (!ul) {
await getFirstChild();
}
} catch (_) {
await next();
await getFirstChild();
}
}
await getFirstChild();
if (!ul) throw Error("Unreachable");
/**
* @param {MouseEvent} event
*/
function mouseMoveEvent(event) {
if (resize) {
setBarWidth(startingWidth + (event.clientX - startingClientX));
while (path.length > 1) {
const name = path.shift();
if (!name) throw "Unreachable";
/** @type {HTMLDetailsElement[]} */
let detailsList = [];
while (!detailsList.length) {
detailsList = Array.from(ul.querySelectorAll(":scope > li > details"));
if (!detailsList.length) {
await next();
}
}
const details = detailsList.find((s) => s.dataset.name == name);
if (!details) return;
details.open = true;
ul = null;
while (!ul) {
const uls = /** @type {HTMLUListElement[]} */ (
Array.from(details.querySelectorAll(":scope > ul"))
);
if (!uls.length) {
await next();
} else if (uls.length > 1) {
throw "Shouldn't be possible";
} else {
ul = /** @type {HTMLUListElement} */ (uls.pop());
}
}
resizeBar.addEventListener("mousedown", (event) => {
startingClientX = event.clientX;
startingWidth = mainElement.clientWidth;
resize = true;
window.document.documentElement.dataset.resize = "";
window.addEventListener("mousemove", mouseMoveEvent);
});
resizeBar.addEventListener("dblclick", () => {
setBarWidth(null);
});
const setResizeFalse = () => {
resize = false;
delete window.document.documentElement.dataset.resize;
window.removeEventListener("mousemove", mouseMoveEvent);
};
window.addEventListener("mouseup", setResizeFalse);
window.addEventListener("mouseleave", setResizeFalse);
}
initDesktopResizeBar();
/** @type {HTMLAnchorElement[]} */
let anchors = [];
while (!anchors.length) {
anchors = Array.from(ul.querySelectorAll(":scope > li > a"));
if (!anchors.length) {
await next();
}
}
anchors
.find((a) => a.getAttribute("href") == window.document.location.pathname)
?.scrollIntoView({
behavior: "instant",
block: "center",
});
});
onFirstIntersection(searchElement, () => {
initSearch(options);
});
function initDesktopResizeBar() {
const resizeBar = getElementById("resize-bar");
let resize = false;
let startingWidth = 0;
let startingClientX = 0;
const barWidthLocalStorageKey = "bar-width";
/**
* @param {number | null} width
*/
function setBarWidth(width) {
// TODO: Check if should be a signal ??
try {
if (typeof width === "number") {
mainElement.style.width = `${width}px`;
writeToStorage(barWidthLocalStorageKey, String(width));
} else {
mainElement.style.width = style.getPropertyValue(
"--default-main-width",
);
removeStored(barWidthLocalStorageKey);
}
} catch (_) {}
}
/**
* @param {MouseEvent} event
*/
function mouseMoveEvent(event) {
if (resize) {
setBarWidth(startingWidth + (event.clientX - startingClientX));
}
}
resizeBar.addEventListener("mousedown", (event) => {
startingClientX = event.clientX;
startingWidth = mainElement.clientWidth;
resize = true;
window.document.documentElement.dataset.resize = "";
window.addEventListener("mousemove", mouseMoveEvent);
});
resizeBar.addEventListener("dblclick", () => {
setBarWidth(null);
});
const setResizeFalse = () => {
resize = false;
delete window.document.documentElement.dataset.resize;
window.removeEventListener("mousemove", mouseMoveEvent);
};
window.addEventListener("mouseup", setResizeFalse);
window.addEventListener("mouseleave", setResizeFalse);
}
initDesktopResizeBar();

View File

@@ -7,11 +7,9 @@ import { collect, markUsed, logUnused } from "./unused.js";
import { setQr } from "../panes/share.js";
/**
* @param {Object} args
* @param {Signals} args.signals
* @param {BrkClient} args.brk
* @param {BrkClient} brk
*/
export function initOptions({ signals, brk }) {
export function initOptions(brk) {
collect(brk.metrics);
const LS_SELECTED_KEY = `selected_path`;
@@ -25,9 +23,6 @@ export function initOptions({ signals, brk }) {
).filter((v) => v);
console.log(savedPath);
/** @type {Signal<Option>} */
const selected = signals.createSignal(/** @type {any} */ (undefined));
const partialOptions = createPartialOptions({
brk,
});
@@ -35,16 +30,49 @@ export function initOptions({ signals, brk }) {
/** @type {Option[]} */
const list = [];
const parent = signals.createSignal(/** @type {HTMLElement | null} */ (null));
/** @type {Map<string, HTMLLIElement>} */
const liByPath = new Map();
/** @type {Set<(option: Option) => void>} */
const selectedListeners = new Set();
/**
* @param {Option | undefined} sel
*/
function updateHighlight(sel) {
if (!sel) return;
liByPath.forEach((li) => {
delete li.dataset.highlight;
});
for (let i = 1; i <= sel.path.length; i++) {
const pathKey = sel.path.slice(0, i).join("/");
const li = liByPath.get(pathKey);
if (li) li.dataset.highlight = "";
}
}
const selected = {
/** @type {Option | undefined} */
value: undefined,
/** @param {Option} v */
set(v) {
this.value = v;
updateHighlight(v);
selectedListeners.forEach((cb) => cb(v));
},
/** @param {(option: Option) => void} cb */
onChange(cb) {
selectedListeners.add(cb);
if (this.value) cb(this.value);
return () => selectedListeners.delete(cb);
},
};
/**
* @param {string[]} nodePath
*/
function isOnSelectedPath(nodePath) {
const selectedPath = selected()?.path;
const selectedPath = selected.value?.path;
return (
selectedPath &&
nodePath.length <= selectedPath.length &&
@@ -128,11 +156,6 @@ export function initOptions({ signals, brk }) {
/** @type {Option | undefined} */
let savedOption;
// ============================================
// Phase 1: Process partial tree (non-reactive)
// Transforms options, computes counts, populates list
// ============================================
/**
* @typedef {{ type: "group"; name: string; serName: string; path: string[]; count: number; children: ProcessedNode[] }} ProcessedGroup
* @typedef {{ type: "option"; option: Option; path: string[] }} ProcessedOption
@@ -267,11 +290,6 @@ export function initOptions({ signals, brk }) {
const processedTree = processPartialTree(partialOptions);
logUnused();
// ============================================
// Phase 2: Build DOM lazily (imperative)
// Uses native toggle events for lazy loading
// ============================================
/**
* @param {ProcessedNode[]} nodes
* @param {HTMLElement} parentEl
@@ -320,36 +338,19 @@ export function initOptions({ signals, brk }) {
}
}
// Single effect to kick off DOM building when parent is set
signals.createEffect(
() => parent(),
(_parent) => {
if (!_parent) return;
buildTreeDOM(processedTree, _parent);
},
);
/** @type {HTMLElement | null} */
let parentEl = null;
// Single effect for highlighting on selection change
signals.createEffect(
() => selected(),
(selected) => {
if (!selected) return;
/**
* @param {HTMLElement} el
*/
function setParent(el) {
if (parentEl) return;
parentEl = el;
buildTreeDOM(processedTree, el);
}
// Clear all existing highlights
liByPath.forEach((li) => {
delete li.dataset.highlight;
});
// Highlight selected option and parent groups
for (let i = 1; i <= selected.path.length; i++) {
const pathKey = selected.path.slice(0, i).join("/");
const li = liByPath.get(pathKey);
if (li) li.dataset.highlight = "";
}
},
);
if (!selected()) {
if (!selected.value) {
const option =
savedOption || list.find((option) => option.kind === "chart");
if (option) {
@@ -361,7 +362,7 @@ export function initOptions({ signals, brk }) {
selected,
list,
tree: /** @type {OptionsTree} */ (partialOptions),
parent,
setParent,
createOptionElement,
selectOption,
};

View File

@@ -8,35 +8,30 @@
* @property {Color} [color]
* @property {[Color, Color]} [colors]
* @property {BaselineSeriesPartialOptions} [options]
* @property {Accessor<BaselineData[]>} [data]
* @typedef {BaseSeriesBlueprint & BaselineSeriesBlueprintSpecific} BaselineSeriesBlueprint
*
* @typedef {Object} CandlestickSeriesBlueprintSpecific
* @property {"Candlestick"} type
* @property {[Color, Color]} [colors]
* @property {CandlestickSeriesPartialOptions} [options]
* @property {Accessor<CandlestickData[]>} [data]
* @typedef {BaseSeriesBlueprint & CandlestickSeriesBlueprintSpecific} CandlestickSeriesBlueprint
*
* @typedef {Object} LineSeriesBlueprintSpecific
* @property {"Line"} [type]
* @property {Color} [color]
* @property {LineSeriesPartialOptions} [options]
* @property {Accessor<LineData[]>} [data]
* @typedef {BaseSeriesBlueprint & LineSeriesBlueprintSpecific} LineSeriesBlueprint
*
* @typedef {Object} HistogramSeriesBlueprintSpecific
* @property {"Histogram"} type
* @property {Color | [Color, Color]} [color] - Single color or [positive, negative] colors (defaults to green/red)
* @property {HistogramSeriesPartialOptions} [options]
* @property {Accessor<HistogramData[]>} [data]
* @typedef {BaseSeriesBlueprint & HistogramSeriesBlueprintSpecific} HistogramSeriesBlueprint
*
* @typedef {Object} DotsSeriesBlueprintSpecific
* @property {"Dots"} type
* @property {Color} [color]
* @property {LineSeriesPartialOptions} [options]
* @property {Accessor<LineData[]>} [data]
* @typedef {BaseSeriesBlueprint & DotsSeriesBlueprintSpecific} DotsSeriesBlueprint
*
* @typedef {BaselineSeriesBlueprint | CandlestickSeriesBlueprint | LineSeriesBlueprint | HistogramSeriesBlueprint | DotsSeriesBlueprint} AnySeriesBlueprint

View File

@@ -1,99 +1,99 @@
import { randomFromArray } from "../utils/array.js";
import { explorerElement } from "../utils/elements.js";
// 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);
// 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" },
];
// // 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();
// 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})`);
// // 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 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 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);
// const spanMiner = window.document.createElement("span");
// spanMiner.innerHTML = name;
// topFaceElement.append(spanMiner);
chain.prepend(cubeElement);
}
}
// chain.prepend(cubeElement);
// }
// }
function createCube() {
const cubeElement = window.document.createElement("div");
cubeElement.classList.add("cube");
// 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 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 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);
// const topFaceElement = window.document.createElement("div");
// topFaceElement.classList.add("face");
// topFaceElement.classList.add("top");
// cubeElement.append(topFaceElement);
return {
cubeElement,
leftFaceElement,
rightFaceElement,
topFaceElement,
};
}
// return {
// cubeElement,
// leftFaceElement,
// rightFaceElement,
// topFaceElement,
// };
// }

File diff suppressed because it is too large Load Diff

View File

@@ -1,433 +1,433 @@
// @ts-nocheck
// // @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";
// 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 !";
// export function init() {
// tableElement.innerHTML = "wip, will hopefuly be back soon, sorry !";
// const parent = tableElement;
// const { headerElement } = createHeader("Table");
// parent.append(headerElement);
// // const parent = tableElement;
// // const { headerElement } = createHeader("Table");
// // parent.append(headerElement);
// const div = window.document.createElement("div");
// parent.append(div);
// // const div = window.document.createElement("div");
// // parent.append(div);
// const table = createTable({
// signals,
// brk,
// resources,
// option,
// });
// div.append(table.element);
// // 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",
// }),
// );
}
// // 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 {Option} args.option
// * @param {Signals} args.signals
// * @param {BrkClient} args.brk
// * @param {Resources} args.resources
// * @param {number | string | Object | Array<any>} args.value
// * @param {Unit} args.unit
// */
// 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);
// 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");
// }
// 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;
// } else {
// return "";
// }
// }
/**
* @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 "";
}
}

View File

@@ -1,24 +1,28 @@
import {
createShadow,
createChoiceField,
createHeader,
} from "../utils/dom.js";
import { createShadow, createChoiceField, createHeader } from "../utils/dom.js";
import { chartElement } from "../utils/elements.js";
import { serdeChartableIndex } from "../utils/serde.js";
import { Unit } from "../utils/units.js";
import signals from "../signals.js";
import { createChart } from "../chart/index.js";
import { colors } from "../chart/colors.js";
import { webSockets } from "../utils/ws.js";
const ONE_BTC_IN_SATS = 100_000_000;
/** @type {((opt: ChartOption) => void) | null} */
let _setOption = null;
/**
* @param {Object} args
* @param {Accessor<ChartOption>} args.option
* @param {BrkClient} args.brk
* @param {ChartOption} opt
*/
export function init({ option, brk }) {
export function setOption(opt) {
if (!_setOption) throw new Error("Chart not initialized");
_setOption(opt);
}
/**
* @param {BrkClient} brk
*/
export function init(brk) {
chartElement.append(createShadow("left"));
chartElement.append(createShadow("right"));
@@ -31,8 +35,8 @@ export function init({ option, brk }) {
brk,
});
// Create index selector using chart's index state
const fieldset = createIndexSelector(option, chart);
// Create index selector
const { fieldset, setChoices } = createIndexSelector(chart);
chartElement.append(fieldset);
/**
@@ -91,61 +95,62 @@ export function init({ option, brk }) {
priceSeries.update({ ...last, close });
}
// When option changes, update heading and rebuild blueprints
signals.createEffect(option, (opt) => {
// Set up the setOption function
_setOption = (opt) => {
headingElement.innerHTML = opt.title;
// Update index choices based on option
setChoices(computeChoices(opt));
blueprints = chart.setBlueprints({
top: buildTopBlueprints(opt.top),
bottom: opt.bottom,
onDataLoaded: updatePriceWithLatest,
});
});
};
// Live price update listener
webSockets.kraken1dCandle.onLatest(updatePriceWithLatest);
}
const ALL_CHOICES = /** @satisfies {ChartableIndexName[]} */ ([
"timestamp",
"date",
"week",
"month",
"quarter",
"semester",
"year",
"decade",
]);
/**
* @param {ChartOption} opt
* @returns {ChartableIndexName[]}
*/
function computeChoices(opt) {
if (!opt.top.size && !opt.bottom.size) {
return [...ALL_CHOICES];
}
const rawIndexes = new Set(
[Array.from(opt.top.values()), Array.from(opt.bottom.values())]
.flat(2)
.filter((blueprint) => {
const path = Object.values(blueprint.metric.by)[0]?.path ?? "";
return !path.includes("constant_");
})
.flatMap((blueprint) => blueprint.metric.indexes()),
);
return ALL_CHOICES.filter((choice) =>
rawIndexes.has(serdeChartableIndex.deserialize(choice)),
);
}
/**
* @param {Accessor<ChartOption>} option
* @param {Chart} chart
*/
function createIndexSelector(option, chart) {
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()),
);
return /** @type {any} */ (
choices_.filter((choice) =>
rawIndexes.has(serdeChartableIndex.deserialize(choice)),
)
);
});
function createIndexSelector(chart) {
const fieldset = window.document.createElement("fieldset");
fieldset.id = "interval";
fieldset.dataset.size = "sm";
@@ -155,7 +160,11 @@ function createIndexSelector(option, chart) {
/** @type {HTMLElement | null} */
let field = null;
signals.createEffect(choices, (newChoices) => {
/**
* @param {ChartableIndexName[]} newChoices
*/
function setChoices(newChoices) {
if (field) field.remove();
// Use preferred index if available, otherwise fall back to first choice
@@ -177,7 +186,7 @@ function createIndexSelector(option, chart) {
id: "index",
});
fieldset.append(field);
});
}
return fieldset;
return { fieldset, setChoices };
}

View File

@@ -0,0 +1,181 @@
import {
searchInput,
searchLabelElement,
searchResultsElement,
} from "../utils/elements.js";
import ufuzzy from "../modules/leeoniya-ufuzzy/1.0.19/dist/uFuzzy.mjs";
/**
* @param {Options} options
*/
export function initSearch(options) {
console.log("search: init");
const haystack = options.list.map((option) => option.title);
const RESULTS_PER_PAGE = 100;
/**
* @param {uFuzzy.SearchResult} searchResult
* @param {number} pageIndex
*/
function computeResultPage(searchResult, pageIndex) {
/** @type {{ option: Option, title: string }[]} */
let list = [];
let [indexes, _info, order] = searchResult || [null, null, null];
const minIndex = pageIndex * RESULTS_PER_PAGE;
if (indexes?.length) {
const maxIndex = Math.min(
(order || indexes).length - 1,
minIndex + RESULTS_PER_PAGE - 1,
);
list = Array(maxIndex - minIndex + 1);
for (let i = minIndex; i <= maxIndex; i++) {
let index = indexes[i];
const title = haystack[index];
list[i % 100] = {
option: options.list[index],
title,
};
}
}
return list;
}
/** @type {uFuzzy.Options} */
const config = {
intraIns: Infinity,
intraChars: `[a-z\d' ]`,
};
const fuzzyMultiInsert = /** @type {uFuzzy} */ (
ufuzzy({
intraIns: 1,
})
);
const fuzzyMultiInsertFuzzier = /** @type {uFuzzy} */ (ufuzzy(config));
const fuzzySingleError = /** @type {uFuzzy} */ (
ufuzzy({
intraMode: 1,
...config,
})
);
const fuzzySingleErrorFuzzier = /** @type {uFuzzy} */ (
ufuzzy({
intraMode: 1,
...config,
})
);
function inputEvent() {
const needle = /** @type {string} */ (searchInput.value);
searchResultsElement.scrollTo({
top: 0,
});
if (!needle) {
searchResultsElement.innerHTML = "";
return;
}
const outOfOrder = 5;
const infoThresh = 5_000;
let result = fuzzyMultiInsert?.search(
haystack,
needle,
undefined,
infoThresh,
);
if (!result?.[0]?.length || !result?.[1]) {
result = fuzzyMultiInsert?.search(
haystack,
needle,
outOfOrder,
infoThresh,
);
}
if (!result?.[0]?.length || !result?.[1]) {
result = fuzzySingleError?.search(
haystack,
needle,
outOfOrder,
infoThresh,
);
}
if (!result?.[0]?.length || !result?.[1]) {
result = fuzzySingleErrorFuzzier?.search(
haystack,
needle,
outOfOrder,
infoThresh,
);
}
if (!result?.[0]?.length || !result?.[1]) {
result = fuzzyMultiInsertFuzzier?.search(
haystack,
needle,
undefined,
infoThresh,
);
}
if (!result?.[0]?.length || !result?.[1]) {
result = fuzzyMultiInsertFuzzier?.search(
haystack,
needle,
outOfOrder,
infoThresh,
);
}
searchResultsElement.innerHTML = "";
const list = computeResultPage(result, 0);
list.forEach(({ option, title }) => {
const li = window.document.createElement("li");
searchResultsElement.appendChild(li);
const element = options.createOptionElement({
option,
name: title,
});
if (element) {
li.append(element);
}
});
}
if (searchInput.value) {
inputEvent();
}
searchInput.addEventListener("input", inputEvent);
}
document.addEventListener("keydown", (e) => {
const el = document.activeElement;
const isTextInput =
el?.tagName === "INPUT" &&
/** @type {HTMLInputElement} */ (el).type === "text";
if (e.key === "/" && !isTextInput) {
e.preventDefault();
searchLabelElement.click();
searchInput.focus();
}
});

View File

@@ -1,128 +0,0 @@
/**
* @template T
* @typedef {Object} Resource
* @property {Signal<T | null>} data
* @property {Signal<boolean>} loading
* @property {Signal<Error | null>} error
* @property {(...args: any[]) => Promise<T | null>} fetch
*/
/**
* @template T
* @typedef {Object} RangeState
* @property {Signal<MetricData<T> | null>} response
* @property {Signal<boolean>} loading
*/
/** @typedef {RangeState<unknown>} AnyRangeState */
/**
* @template T
* @typedef {Object} MetricResource
* @property {string} path
* @property {(from?: number, to?: number) => RangeState<T>} range
* @property {(from?: number, to?: number) => Promise<MetricData<T> | null>} fetch
*/
/** @typedef {MetricResource<unknown>} AnyMetricResource */
/**
* @typedef {{ createResource: typeof createResource, useMetricEndpoint: typeof useMetricEndpoint }} Resources
*/
import signals from "./signals.js";
/**
* Create a generic reactive resource wrapper for any async fetcher
* @template T
* @template {any[]} Args
* @param {(...args: Args) => Promise<T>} fetcher
* @returns {Resource<T>}
*/
function createResource(fetcher) {
const owner = signals.getOwner();
return signals.runWithOwner(owner, () => {
const data = signals.createSignal(/** @type {T | null} */ (null));
const loading = signals.createSignal(false);
const error = signals.createSignal(/** @type {Error | null} */ (null));
return {
data,
loading,
error,
/**
* @param {Args} args
*/
async fetch(...args) {
loading.set(true);
error.set(null);
try {
const result = await fetcher(...args);
data.set(() => result);
return result;
} catch (e) {
error.set(e instanceof Error ? e : new Error(String(e)));
return null;
} finally {
loading.set(false);
}
},
};
});
}
/**
* Create a reactive resource wrapper for a MetricEndpoint with multi-range support
* @template T
* @param {MetricEndpoint<T>} endpoint
* @returns {MetricResource<T>}
*/
function useMetricEndpoint(endpoint) {
const owner = signals.getOwner();
return signals.runWithOwner(owner, () => {
/** @type {Map<string, RangeState<T>>} */
const ranges = new Map();
/**
* Get or create range state
* @param {number} [from=-10000]
* @param {number} [to]
* @returns {RangeState<T>}
*/
function range(from = -10000, to) {
const key = `${from}-${to ?? ""}`;
const existing = ranges.get(key);
if (existing) return existing;
/** @type {RangeState<T>} */
const state = {
response: signals.createSignal(
/** @type {MetricData<T> | null} */ (null),
),
loading: signals.createSignal(false),
};
ranges.set(key, state);
return state;
}
return {
path: endpoint.path,
range,
/**
* Fetch data for a range
* @param {number} [start=-10000]
* @param {number} [end]
*/
async fetch(start = -10000, end) {
const r = range(start, end);
r.loading.set(true);
try {
const result = await endpoint.slice(start, end).fetch(r.response.set);
return result;
} finally {
r.loading.set(false);
}
},
};
});
}
export const resources = { createResource, useMetricEndpoint };

View File

@@ -1,134 +0,0 @@
/**
* @import { SignalOptions } from "./modules/solidjs-signals/0.6.3/dist/types/core/core.js"
* @import { getOwner as GetOwner, onCleanup as OnCleanup } from "./modules/solidjs-signals/0.6.3/dist/types/core/owner.js"
* @import { createSignal as CreateSignal, createEffect as CreateEffect, createMemo as CreateMemo, createRoot as CreateRoot, runWithOwner as RunWithOwner, Setter } from "./modules/solidjs-signals/0.6.3/dist/types/signals.js";
*/
/**
* @template T
* @typedef {() => T} Accessor
*/
/**
* @template T
* @typedef {Accessor<T> & { set: Setter<T>; reset: VoidFunction }} Signal
*/
import {
createSignal,
createEffect,
getOwner,
createMemo,
createRoot,
runWithOwner,
onCleanup,
} from "./modules/solidjs-signals/0.6.3/dist/prod.js";
import { createPersistedValue } from "./utils/persisted.js";
// let effectCount = 0;
const signals = {
createSolidSignal: /** @type {typeof CreateSignal} */ (createSignal),
createEffect: /** @type {typeof CreateEffect} */ (createEffect),
createScopedEffect: /** @type {typeof CreateEffect} */ (
// @ts-ignore
(compute, effect) => {
let dispose = /** @type {VoidFunction | null} */ (null);
if (getOwner() === null) {
throw Error("No owner");
}
function cleanup() {
if (dispose) {
dispose();
dispose = null;
// console.log("effectCount = ", --effectCount);
}
}
// @ts-ignore
createEffect(compute, (v, oldV) => {
// console.log("effectCount = ", ++effectCount);
cleanup();
signals.createRoot((_dispose) => {
dispose = _dispose;
return effect(v, oldV);
});
signals.onCleanup(cleanup);
});
signals.onCleanup(cleanup);
}
),
createMemo: /** @type {typeof CreateMemo} */ (createMemo),
createRoot: /** @type {typeof CreateRoot} */ (createRoot),
getOwner: /** @type {typeof GetOwner} */ (getOwner),
runWithOwner: /** @type {typeof RunWithOwner} */ (runWithOwner),
onCleanup: /** @type {typeof OnCleanup} */ (onCleanup),
/**
* @template T
* @param {T} initialValue
* @param {SignalOptions<T>} [options]
* @returns {Signal<T>}
*/
createSignal(initialValue, options) {
const [get, set] = this.createSolidSignal(
/** @type {any} */ (initialValue),
options,
);
// @ts-ignore
get.set = set;
// @ts-ignore
get.reset = () => set(initialValue);
// @ts-ignore
return get;
},
/**
* @template T
* @param {Object} args
* @param {T} args.defaultValue
* @param {string} args.storageKey
* @param {string} [args.urlKey]
* @param {(v: T) => string} args.serialize
* @param {(s: string) => T} args.deserialize
* @param {boolean} [args.saveDefaultValue]
* @returns {Signal<T>}
*/
createPersistedSignal({
defaultValue,
storageKey,
urlKey,
serialize,
deserialize,
saveDefaultValue = false,
}) {
const persisted = createPersistedValue({
defaultValue,
storageKey,
urlKey,
serialize,
deserialize,
saveDefaultValue,
});
const signal = this.createSignal(persisted.value);
// Sync signal changes to persisted storage
let firstRun = true;
this.createEffect(signal, (value) => {
if (firstRun) {
firstRun = false;
} else {
persisted.set(value);
}
});
return signal;
},
};
/** @typedef {typeof signals} Signals */
export default signals;