mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-05-02 02:20:00 -07:00
bitview: reorg part 2
This commit is contained in:
281
websites/bitview/scripts/core/api.js
Normal file
281
websites/bitview/scripts/core/api.js
Normal file
@@ -0,0 +1,281 @@
|
||||
import { index as serdeIndex } from "./serde";
|
||||
import { runWhenIdle } from "./scheduling";
|
||||
|
||||
/**
|
||||
* @param {Signals} signals
|
||||
* @param {Utilities} utils
|
||||
* @param {Env} env
|
||||
* @param {VecIdToIndexes} vecIdToIndexes
|
||||
*/
|
||||
export function createVecsResources(signals, utils, env, vecIdToIndexes) {
|
||||
const owner = signals.getOwner();
|
||||
|
||||
const defaultFrom = -10_000;
|
||||
const defaultTo = undefined;
|
||||
|
||||
/**
|
||||
* Defaults
|
||||
* - from: -10_000
|
||||
* - to: undefined
|
||||
*
|
||||
* @param {Object} [args]
|
||||
* @param {number} [args.from]
|
||||
* @param {number} [args.to]
|
||||
*/
|
||||
function genFetchedKey(args) {
|
||||
return `${args?.from}-${args?.to}`;
|
||||
}
|
||||
|
||||
const defaultFetchedKey = genFetchedKey({ from: defaultFrom, to: defaultTo });
|
||||
|
||||
/**
|
||||
* @template {number | OHLCTuple} [T=number]
|
||||
* @param {Index} index
|
||||
* @param {VecId} id
|
||||
*/
|
||||
function createVecResource(index, id) {
|
||||
if (env.localhost && !(id in vecIdToIndexes)) {
|
||||
throw Error(`${id} not recognized`);
|
||||
}
|
||||
|
||||
return signals.runWithOwner(owner, () => {
|
||||
/** @typedef {T extends number ? SingleValueData : CandlestickData} Value */
|
||||
|
||||
const fetchedRecord = signals.createSignal(
|
||||
/** @type {Map<string, {loading: boolean, at: Date | null, vec: Signal<T[] | null>}>} */ (
|
||||
new Map()
|
||||
),
|
||||
);
|
||||
|
||||
return {
|
||||
url: api.genUrl(index, id, defaultFrom),
|
||||
fetched: fetchedRecord,
|
||||
/**
|
||||
* Defaults
|
||||
* - from: -10_000
|
||||
* - to: undefined
|
||||
*
|
||||
* @param {Object} [args]
|
||||
* @param {number} [args.from]
|
||||
* @param {number} [args.to]
|
||||
*/
|
||||
async fetch(args) {
|
||||
const from = args?.from ?? defaultFrom;
|
||||
const to = args?.to ?? defaultTo;
|
||||
const fetchedKey = genFetchedKey({ from, to });
|
||||
if (!fetchedRecord().has(fetchedKey)) {
|
||||
fetchedRecord.set((map) => {
|
||||
map.set(fetchedKey, {
|
||||
loading: false,
|
||||
at: null,
|
||||
vec: signals.createSignal(/** @type {T[] | null} */ (null), {
|
||||
equals: false,
|
||||
}),
|
||||
});
|
||||
return map;
|
||||
});
|
||||
}
|
||||
const fetched = fetchedRecord().get(fetchedKey);
|
||||
if (!fetched) throw Error("Unreachable");
|
||||
if (fetched.loading) return fetched.vec();
|
||||
if (fetched.at) {
|
||||
const diff = new Date().getTime() - fetched.at.getTime();
|
||||
const ONE_MINUTE_IN_MS = 60_000;
|
||||
if (diff < ONE_MINUTE_IN_MS) return fetched.vec();
|
||||
}
|
||||
fetched.loading = true;
|
||||
const res = /** @type {T[] | null} */ (
|
||||
await api.fetchVec(
|
||||
(values) => {
|
||||
if (values.length || !fetched.vec()) {
|
||||
fetched.vec.set(values);
|
||||
}
|
||||
},
|
||||
index,
|
||||
id,
|
||||
from,
|
||||
to,
|
||||
)
|
||||
);
|
||||
fetched.at = new Date();
|
||||
fetched.loading = false;
|
||||
return res;
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/** @type {Map<string, NonNullable<ReturnType<typeof createVecResource>>>} */
|
||||
const map = new Map();
|
||||
|
||||
const vecs = {
|
||||
/**
|
||||
* @template {number | OHLCTuple} [T=number]
|
||||
* @param {Index} index
|
||||
* @param {VecId} id
|
||||
*/
|
||||
getOrCreate(index, id) {
|
||||
const key = `${index},${id}`;
|
||||
const found = map.get(key);
|
||||
if (found) {
|
||||
return found;
|
||||
}
|
||||
|
||||
const vec = createVecResource(index, id);
|
||||
if (!vec) throw Error("vec is undefined");
|
||||
map.set(key, /** @type {any} */ (vec));
|
||||
return vec;
|
||||
},
|
||||
genFetchedKey,
|
||||
defaultFetchedKey,
|
||||
};
|
||||
|
||||
return vecs;
|
||||
}
|
||||
/** @typedef {ReturnType<typeof createVecsResources>} VecsResources */
|
||||
/** @typedef {ReturnType<VecsResources["getOrCreate"]>} VecResource */
|
||||
|
||||
const CACHE_NAME = "api";
|
||||
const API_VECS_PREFIX = "/api/vecs";
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {(value: T) => void} callback
|
||||
* @param {string} path
|
||||
* @param {boolean} [mustBeArray]
|
||||
*/
|
||||
async function fetchApi(callback, path, mustBeArray) {
|
||||
const url = `${API_VECS_PREFIX}${path}`;
|
||||
|
||||
/** @type {T | null} */
|
||||
let cachedJson = null;
|
||||
|
||||
/** @type {Cache | undefined} */
|
||||
let cache;
|
||||
try {
|
||||
cache = await caches.open(CACHE_NAME);
|
||||
const cachedResponse = await cache.match(url);
|
||||
if (cachedResponse) {
|
||||
console.debug(`cache: ${url}`);
|
||||
const json = /** @type {T} */ await cachedResponse.json();
|
||||
cachedJson = json;
|
||||
callback(json);
|
||||
}
|
||||
} catch {}
|
||||
|
||||
if (navigator.onLine) {
|
||||
// TODO: rerun after 10s instead of returning (due to some kind of error)
|
||||
|
||||
/** @type {Response | undefined} */
|
||||
let fetchedResponse;
|
||||
try {
|
||||
fetchedResponse = await fetch(url, {
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
if (!fetchedResponse.ok) {
|
||||
throw Error;
|
||||
}
|
||||
} catch {
|
||||
return cachedJson;
|
||||
}
|
||||
|
||||
const clonedResponse = fetchedResponse.clone();
|
||||
|
||||
let fetchedJson = /** @type {T | null} */ (null);
|
||||
try {
|
||||
const f = await fetchedResponse.json();
|
||||
fetchedJson = /** @type {T} */ (
|
||||
mustBeArray && !Array.isArray(f) ? [f] : f
|
||||
);
|
||||
} catch (_) {
|
||||
return cachedJson;
|
||||
}
|
||||
|
||||
if (!fetchedJson) return cachedJson;
|
||||
|
||||
console.debug(`fetch: ${url}`);
|
||||
|
||||
if (Array.isArray(cachedJson) && Array.isArray(fetchedJson)) {
|
||||
const previousLength = cachedJson?.length || 0;
|
||||
const newLength = fetchedJson.length;
|
||||
|
||||
if (!newLength) {
|
||||
return cachedJson;
|
||||
}
|
||||
|
||||
if (previousLength && previousLength === newLength) {
|
||||
const previousLastValue = Object.values(cachedJson || []).at(-1);
|
||||
const newLastValue = Object.values(fetchedJson).at(-1);
|
||||
if (
|
||||
JSON.stringify(previousLastValue) === JSON.stringify(newLastValue)
|
||||
) {
|
||||
return cachedJson;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
callback(fetchedJson);
|
||||
|
||||
runWhenIdle(async function () {
|
||||
try {
|
||||
await cache?.put(url, clonedResponse);
|
||||
} catch (_) {}
|
||||
});
|
||||
|
||||
return fetchedJson;
|
||||
} else {
|
||||
return cachedJson;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Index} index
|
||||
* @param {VecId} vecId
|
||||
* @param {number} [from]
|
||||
* @param {number} [to]
|
||||
*/
|
||||
function genPath(index, vecId, from, to) {
|
||||
let path = `/${serdeIndex.serialize(index)}-to-${vecId.replaceAll("_", "-")}?`;
|
||||
|
||||
if (from !== undefined) {
|
||||
path += `from=${from}`;
|
||||
}
|
||||
if (to !== undefined) {
|
||||
if (!path.endsWith("?")) {
|
||||
path += `&`;
|
||||
}
|
||||
path += `to=${to}`;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
export const api = {
|
||||
/**
|
||||
* @param {Index} index
|
||||
* @param {VecId} vecId
|
||||
* @param {number} from
|
||||
*/
|
||||
genUrl(index, vecId, from) {
|
||||
return `${API_VECS_PREFIX}${genPath(index, vecId, from)}`;
|
||||
},
|
||||
/**
|
||||
* @template {number | OHLCTuple} [T=number]
|
||||
* @param {(v: T[]) => void} callback
|
||||
* @param {Index} index
|
||||
* @param {VecId} vecId
|
||||
* @param {number} [from]
|
||||
* @param {number} [to]
|
||||
*/
|
||||
fetchVec(callback, index, vecId, from, to) {
|
||||
return fetchApi(callback, genPath(index, vecId, from, to), true);
|
||||
},
|
||||
/**
|
||||
* @template {number | OHLCTuple} [T=number]
|
||||
* @param {(v: T) => void} callback
|
||||
* @param {Index} index
|
||||
* @param {VecId} vecId
|
||||
*/
|
||||
fetchLast(callback, index, vecId) {
|
||||
return fetchApi(callback, genPath(index, vecId, -1));
|
||||
},
|
||||
};
|
||||
20
websites/bitview/scripts/core/array.js
Normal file
20
websites/bitview/scripts/core/array.js
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* @param {number} start
|
||||
* @param {number} end
|
||||
*/
|
||||
export function range(start, end) {
|
||||
const range = [];
|
||||
while (start <= end) {
|
||||
range.push(start);
|
||||
start += 1;
|
||||
}
|
||||
return range;
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {T[]} array
|
||||
*/
|
||||
export function random(array) {
|
||||
return array[Math.floor(Math.random() * array.length)];
|
||||
}
|
||||
@@ -1,7 +1,13 @@
|
||||
// @ts-check
|
||||
|
||||
/** @import { IChartApi, ISeriesApi as _ISeriesApi, SeriesDefinition, SingleValueData as _SingleValueData, CandlestickData as _CandlestickData, BaselineData as _BaselineData, HistogramData as _HistogramData, SeriesType, IPaneApi, LineSeriesPartialOptions as _LineSeriesPartialOptions, HistogramSeriesPartialOptions as _HistogramSeriesPartialOptions, BaselineSeriesPartialOptions as _BaselineSeriesPartialOptions, CandlestickSeriesPartialOptions as _CandlestickSeriesPartialOptions, WhitespaceData, DeepPartial, ChartOptions, Time, LineData as _LineData } from '../packages/lightweight-charts/5.0.8/dist/typings' */
|
||||
|
||||
import {
|
||||
createHorizontalChoiceField,
|
||||
createLabeledInput,
|
||||
createSpanName,
|
||||
} from "./dom";
|
||||
import { createOklchToRGBA } from "./colors";
|
||||
import { throttle } from "./scheduling";
|
||||
|
||||
/**
|
||||
* @typedef {[number, number, number, number]} OHLCTuple
|
||||
*
|
||||
@@ -209,7 +215,7 @@ function createChartElement({
|
||||
|
||||
const activeResources = /** @type {Set<VecResource>} */ (new Set());
|
||||
ichart.subscribeCrosshairMove(
|
||||
utils.throttle(() => {
|
||||
throttle(() => {
|
||||
activeResources.forEach((v) => {
|
||||
v.fetch();
|
||||
});
|
||||
@@ -285,7 +291,7 @@ function createChartElement({
|
||||
paneIndex,
|
||||
position: "sw",
|
||||
createChild(pane) {
|
||||
const { field, selected } = utils.dom.createHorizontalChoiceField({
|
||||
const { field, selected } = createHorizontalChoiceField({
|
||||
choices: /** @type {const} */ (["lin", "log"]),
|
||||
id: utils.stringToId(`${id} ${paneIndex} ${unit}`),
|
||||
defaultValue:
|
||||
@@ -341,7 +347,7 @@ function createChartElement({
|
||||
save: {
|
||||
keyPrefix: "",
|
||||
key: id,
|
||||
...utils.serde.boolean,
|
||||
...serde.boolean,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -868,7 +874,7 @@ function createLegend({ signals, utils }) {
|
||||
}
|
||||
legends[order] = div;
|
||||
|
||||
const { input, label } = utils.dom.createLabeledInput({
|
||||
const { input, label } = createLabeledInput({
|
||||
inputId: utils.stringToId(`legend-${series.id}`),
|
||||
inputName: utils.stringToId(`selected-${series.id}`),
|
||||
inputValue: "value",
|
||||
@@ -884,7 +890,7 @@ function createLegend({ signals, utils }) {
|
||||
spanMain.classList.add("main");
|
||||
label.append(spanMain);
|
||||
|
||||
const spanName = utils.dom.createSpanName(name);
|
||||
const spanName = createSpanName(name);
|
||||
spanMain.append(spanName);
|
||||
|
||||
div.append(label);
|
||||
@@ -1044,107 +1050,6 @@ function numberToUSFormat(value, digits, options) {
|
||||
});
|
||||
}
|
||||
|
||||
function createOklchToRGBA() {
|
||||
{
|
||||
/**
|
||||
*
|
||||
* @param {readonly [number, number, number, number, number, number, number, number, number]} A
|
||||
* @param {readonly [number, number, number]} B
|
||||
* @returns
|
||||
*/
|
||||
function multiplyMatrices(A, B) {
|
||||
return /** @type {const} */ ([
|
||||
A[0] * B[0] + A[1] * B[1] + A[2] * B[2],
|
||||
A[3] * B[0] + A[4] * B[1] + A[5] * B[2],
|
||||
A[6] * B[0] + A[7] * B[1] + A[8] * B[2],
|
||||
]);
|
||||
}
|
||||
/**
|
||||
* @param {readonly [number, number, number]} param0
|
||||
*/
|
||||
function oklch2oklab([l, c, h]) {
|
||||
return /** @type {const} */ ([
|
||||
l,
|
||||
isNaN(h) ? 0 : c * Math.cos((h * Math.PI) / 180),
|
||||
isNaN(h) ? 0 : c * Math.sin((h * Math.PI) / 180),
|
||||
]);
|
||||
}
|
||||
/**
|
||||
* @param {readonly [number, number, number]} rgb
|
||||
*/
|
||||
function srgbLinear2rgb(rgb) {
|
||||
return rgb.map((c) =>
|
||||
Math.abs(c) > 0.0031308
|
||||
? (c < 0 ? -1 : 1) * (1.055 * Math.abs(c) ** (1 / 2.4) - 0.055)
|
||||
: 12.92 * c,
|
||||
);
|
||||
}
|
||||
/**
|
||||
* @param {readonly [number, number, number]} lab
|
||||
*/
|
||||
function oklab2xyz(lab) {
|
||||
const LMSg = multiplyMatrices(
|
||||
/** @type {const} */ ([
|
||||
1, 0.3963377773761749, 0.2158037573099136, 1, -0.1055613458156586,
|
||||
-0.0638541728258133, 1, -0.0894841775298119, -1.2914855480194092,
|
||||
]),
|
||||
lab,
|
||||
);
|
||||
const LMS = /** @type {[number, number, number]} */ (
|
||||
LMSg.map((val) => val ** 3)
|
||||
);
|
||||
return multiplyMatrices(
|
||||
/** @type {const} */ ([
|
||||
1.2268798758459243, -0.5578149944602171, 0.2813910456659647,
|
||||
-0.0405757452148008, 1.112286803280317, -0.0717110580655164,
|
||||
-0.0763729366746601, -0.4214933324022432, 1.5869240198367816,
|
||||
]),
|
||||
LMS,
|
||||
);
|
||||
}
|
||||
/**
|
||||
* @param {readonly [number, number, number]} xyz
|
||||
*/
|
||||
function xyz2rgbLinear(xyz) {
|
||||
return multiplyMatrices(
|
||||
[
|
||||
3.2409699419045226, -1.537383177570094, -0.4986107602930034,
|
||||
-0.9692436362808796, 1.8759675015077202, 0.04155505740717559,
|
||||
0.05563007969699366, -0.20397695888897652, 1.0569715142428786,
|
||||
],
|
||||
xyz,
|
||||
);
|
||||
}
|
||||
|
||||
/** @param {string} oklch */
|
||||
return function (oklch) {
|
||||
oklch = oklch.replace("oklch(", "");
|
||||
oklch = oklch.replace(")", "");
|
||||
let splitOklch = oklch.split(" / ");
|
||||
let alpha = 1;
|
||||
if (splitOklch.length === 2) {
|
||||
alpha = Number(splitOklch.pop()?.replace("%", "")) / 100;
|
||||
}
|
||||
splitOklch = oklch.split(" ");
|
||||
const lch = splitOklch.map((v, i) => {
|
||||
if (!i && v.includes("%")) {
|
||||
return Number(v.replace("%", "")) / 100;
|
||||
} else {
|
||||
return Number(v);
|
||||
}
|
||||
});
|
||||
const rgb = srgbLinear2rgb(
|
||||
xyz2rgbLinear(
|
||||
oklab2xyz(oklch2oklab(/** @type {[number, number, number]} */ (lch))),
|
||||
),
|
||||
).map((v) => {
|
||||
return Math.max(Math.min(Math.round(v * 255), 255), 0);
|
||||
});
|
||||
return [...rgb, alpha];
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {typeof createChartElement} CreateChartElement
|
||||
* @typedef {ReturnType<createChartElement>} Chart
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
/**
|
||||
* @import { Accessor } from "../packages/solidjs-signals/wrapper";
|
||||
*/
|
||||
|
||||
const globalComputedStyle = getComputedStyle(window.document.documentElement);
|
||||
|
||||
/**
|
||||
* @param {Accessor<boolean>} dark
|
||||
*/
|
||||
export function createColors(dark) {
|
||||
const globalComputedStyle = getComputedStyle(window.document.documentElement);
|
||||
/**
|
||||
* @param {string} color
|
||||
*/
|
||||
@@ -113,8 +108,110 @@ export function createColors(dark) {
|
||||
rose,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {ReturnType<typeof createColors>} Colors
|
||||
* @typedef {Colors["orange"]} Color
|
||||
* @typedef {keyof Colors} ColorName
|
||||
*/
|
||||
|
||||
export function createOklchToRGBA() {
|
||||
{
|
||||
/**
|
||||
*
|
||||
* @param {readonly [number, number, number, number, number, number, number, number, number]} A
|
||||
* @param {readonly [number, number, number]} B
|
||||
* @returns
|
||||
*/
|
||||
function multiplyMatrices(A, B) {
|
||||
return /** @type {const} */ ([
|
||||
A[0] * B[0] + A[1] * B[1] + A[2] * B[2],
|
||||
A[3] * B[0] + A[4] * B[1] + A[5] * B[2],
|
||||
A[6] * B[0] + A[7] * B[1] + A[8] * B[2],
|
||||
]);
|
||||
}
|
||||
/**
|
||||
* @param {readonly [number, number, number]} param0
|
||||
*/
|
||||
function oklch2oklab([l, c, h]) {
|
||||
return /** @type {const} */ ([
|
||||
l,
|
||||
isNaN(h) ? 0 : c * Math.cos((h * Math.PI) / 180),
|
||||
isNaN(h) ? 0 : c * Math.sin((h * Math.PI) / 180),
|
||||
]);
|
||||
}
|
||||
/**
|
||||
* @param {readonly [number, number, number]} rgb
|
||||
*/
|
||||
function srgbLinear2rgb(rgb) {
|
||||
return rgb.map((c) =>
|
||||
Math.abs(c) > 0.0031308
|
||||
? (c < 0 ? -1 : 1) * (1.055 * Math.abs(c) ** (1 / 2.4) - 0.055)
|
||||
: 12.92 * c,
|
||||
);
|
||||
}
|
||||
/**
|
||||
* @param {readonly [number, number, number]} lab
|
||||
*/
|
||||
function oklab2xyz(lab) {
|
||||
const LMSg = multiplyMatrices(
|
||||
/** @type {const} */ ([
|
||||
1, 0.3963377773761749, 0.2158037573099136, 1, -0.1055613458156586,
|
||||
-0.0638541728258133, 1, -0.0894841775298119, -1.2914855480194092,
|
||||
]),
|
||||
lab,
|
||||
);
|
||||
const LMS = /** @type {[number, number, number]} */ (
|
||||
LMSg.map((val) => val ** 3)
|
||||
);
|
||||
return multiplyMatrices(
|
||||
/** @type {const} */ ([
|
||||
1.2268798758459243, -0.5578149944602171, 0.2813910456659647,
|
||||
-0.0405757452148008, 1.112286803280317, -0.0717110580655164,
|
||||
-0.0763729366746601, -0.4214933324022432, 1.5869240198367816,
|
||||
]),
|
||||
LMS,
|
||||
);
|
||||
}
|
||||
/**
|
||||
* @param {readonly [number, number, number]} xyz
|
||||
*/
|
||||
function xyz2rgbLinear(xyz) {
|
||||
return multiplyMatrices(
|
||||
[
|
||||
3.2409699419045226, -1.537383177570094, -0.4986107602930034,
|
||||
-0.9692436362808796, 1.8759675015077202, 0.04155505740717559,
|
||||
0.05563007969699366, -0.20397695888897652, 1.0569715142428786,
|
||||
],
|
||||
xyz,
|
||||
);
|
||||
}
|
||||
|
||||
/** @param {string} oklch */
|
||||
return function (oklch) {
|
||||
oklch = oklch.replace("oklch(", "");
|
||||
oklch = oklch.replace(")", "");
|
||||
let splitOklch = oklch.split(" / ");
|
||||
let alpha = 1;
|
||||
if (splitOklch.length === 2) {
|
||||
alpha = Number(splitOklch.pop()?.replace("%", "")) / 100;
|
||||
}
|
||||
splitOklch = oklch.split(" ");
|
||||
const lch = splitOklch.map((v, i) => {
|
||||
if (!i && v.includes("%")) {
|
||||
return Number(v.replace("%", "")) / 100;
|
||||
} else {
|
||||
return Number(v);
|
||||
}
|
||||
});
|
||||
const rgb = srgbLinear2rgb(
|
||||
xyz2rgbLinear(
|
||||
oklab2xyz(oklch2oklab(/** @type {[number, number, number]} */ (lch))),
|
||||
),
|
||||
).map((v) => {
|
||||
return Math.max(Math.min(Math.round(v * 255), 255), 0);
|
||||
});
|
||||
return [...rgb, alpha];
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
68
websites/bitview/scripts/core/date.js
Normal file
68
websites/bitview/scripts/core/date.js
Normal file
@@ -0,0 +1,68 @@
|
||||
const ONE_DAY_IN_MS = 1000 * 60 * 60 * 24;
|
||||
|
||||
export function todayUTC() {
|
||||
const today = new Date();
|
||||
return new Date(
|
||||
Date.UTC(
|
||||
today.getUTCFullYear(),
|
||||
today.getUTCMonth(),
|
||||
today.getUTCDate(),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Date} date
|
||||
*/
|
||||
export function toString(date) {
|
||||
return date.toJSON().split("T")[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Date} date
|
||||
*/
|
||||
export function toDateIndex(date) {
|
||||
if (
|
||||
date.getUTCFullYear() === 2009 &&
|
||||
date.getUTCMonth() === 0 &&
|
||||
date.getUTCDate() === 3
|
||||
)
|
||||
return 0;
|
||||
return differenceBetween(date, new Date("2009-01-09"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Date} start
|
||||
* @param {Date} end
|
||||
*/
|
||||
export function getRange(start, end) {
|
||||
const dates = /** @type {Date[]} */ ([]);
|
||||
let currentDate = new Date(start);
|
||||
while (currentDate <= end) {
|
||||
dates.push(new Date(currentDate));
|
||||
currentDate.setUTCDate(currentDate.getUTCDate() + 1);
|
||||
}
|
||||
return dates;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Date} date1
|
||||
* @param {Date} date2
|
||||
*/
|
||||
export function differenceBetween(date1, date2) {
|
||||
return Math.abs(date1.valueOf() - date2.valueOf()) / ONE_DAY_IN_MS;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Date} oldest
|
||||
* @param {Date} youngest
|
||||
* @returns {number}
|
||||
*/
|
||||
export function getNumberOfDaysBetweenTwoDates(oldest, youngest) {
|
||||
return Math.round(
|
||||
Math.abs((youngest.getTime() - oldest.getTime()) / ONE_DAY_IN_MS),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,446 @@
|
||||
import { string as stringSerde } from "./serde";
|
||||
|
||||
/**
|
||||
* @param {string} id
|
||||
* @returns {HTMLElement}
|
||||
*/
|
||||
export function getElementById(id) {
|
||||
const element = window.document.getElementById(id);
|
||||
if (!element) throw `Element with id = "${id}" should exist`;
|
||||
return element;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} element
|
||||
*/
|
||||
export function isHidden(element) {
|
||||
return element.tagName !== "BODY" && !element.offsetParent;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {HTMLElement} element
|
||||
* @param {VoidFunction} callback
|
||||
*/
|
||||
export function onFirstIntersection(element, callback) {
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
for (let i = 0; i < entries.length; i++) {
|
||||
if (entries[i].isIntersecting) {
|
||||
callback();
|
||||
observer.disconnect();
|
||||
}
|
||||
}
|
||||
});
|
||||
observer.observe(element);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
*/
|
||||
export function createSpanName(name) {
|
||||
const spanName = window.document.createElement("span");
|
||||
spanName.classList.add("name");
|
||||
const [first, second, third] = name.split(" - ");
|
||||
spanName.innerHTML = first;
|
||||
|
||||
if (second) {
|
||||
const smallRest = window.document.createElement("small");
|
||||
smallRest.innerHTML = ` — ${second}`;
|
||||
spanName.append(smallRest);
|
||||
|
||||
if (third) {
|
||||
throw "Shouldn't have more than one dash";
|
||||
}
|
||||
}
|
||||
|
||||
return spanName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} arg
|
||||
* @param {string} arg.href
|
||||
* @param {string} arg.title
|
||||
* @param {string} [arg.text]
|
||||
* @param {boolean} [arg.blank]
|
||||
* @param {VoidFunction} [arg.onClick]
|
||||
* @param {boolean} [arg.preventDefault]
|
||||
*/
|
||||
export function createAnchorElement({
|
||||
text,
|
||||
href,
|
||||
blank,
|
||||
onClick,
|
||||
title,
|
||||
preventDefault,
|
||||
}) {
|
||||
const anchor = window.document.createElement("a");
|
||||
anchor.href = href;
|
||||
anchor.title = title.toUpperCase();
|
||||
|
||||
if (text) {
|
||||
anchor.innerText = text;
|
||||
}
|
||||
|
||||
if (blank) {
|
||||
anchor.target = "_blank";
|
||||
anchor.rel = "noopener noreferrer";
|
||||
}
|
||||
|
||||
if (onClick || preventDefault) {
|
||||
if (onClick) {
|
||||
anchor.addEventListener("click", (event) => {
|
||||
event.preventDefault();
|
||||
onClick();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return anchor;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} arg
|
||||
* @param {string | HTMLElement} arg.inside
|
||||
* @param {string} arg.title
|
||||
* @param {(event: MouseEvent) => void} arg.onClick
|
||||
*/
|
||||
export function createButtonElement({ inside: text, onClick, title }) {
|
||||
const button = window.document.createElement("button");
|
||||
|
||||
button.append(text);
|
||||
|
||||
button.title = title.toUpperCase();
|
||||
|
||||
button.addEventListener("click", onClick);
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} args
|
||||
* @param {string} args.inputName
|
||||
* @param {string} args.inputId
|
||||
* @param {string} args.inputValue
|
||||
* @param {boolean} [args.inputChecked=false]
|
||||
* @param {string} [args.title]
|
||||
* @param {'radio' | 'checkbox'} args.type
|
||||
* @param {(event: MouseEvent) => void} [args.onClick]
|
||||
*/
|
||||
export function createLabeledInput({
|
||||
inputId,
|
||||
inputName,
|
||||
inputValue,
|
||||
inputChecked = false,
|
||||
title,
|
||||
onClick,
|
||||
type,
|
||||
}) {
|
||||
const label = window.document.createElement("label");
|
||||
|
||||
inputId = inputId.toLowerCase();
|
||||
|
||||
const input = window.document.createElement("input");
|
||||
if (type === "radio") {
|
||||
input.type = "radio";
|
||||
input.name = inputName;
|
||||
} else {
|
||||
input.type = "checkbox";
|
||||
}
|
||||
input.id = inputId;
|
||||
input.value = inputValue;
|
||||
input.checked = inputChecked;
|
||||
label.append(input);
|
||||
|
||||
label.id = `${inputId}-label`;
|
||||
if (title) {
|
||||
label.title = title;
|
||||
}
|
||||
label.htmlFor = inputId;
|
||||
|
||||
if (onClick) {
|
||||
label.addEventListener("click", onClick);
|
||||
}
|
||||
|
||||
return {
|
||||
label,
|
||||
input,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} parent
|
||||
* @param {HTMLElement} child
|
||||
* @param {number} index
|
||||
*/
|
||||
export function insertElementAtIndex(parent, child, index) {
|
||||
if (!index) index = 0;
|
||||
if (index >= parent.children.length) {
|
||||
parent.appendChild(child);
|
||||
} else {
|
||||
parent.insertBefore(child, parent.children[index]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} url
|
||||
* @param {boolean} [targetBlank]
|
||||
*/
|
||||
export function open(url, targetBlank) {
|
||||
console.log(`open: ${url}`);
|
||||
const a = window.document.createElement("a");
|
||||
window.document.body.append(a);
|
||||
a.href = url;
|
||||
|
||||
if (targetBlank) {
|
||||
a.target = "_blank";
|
||||
a.rel = "noopener noreferrer";
|
||||
}
|
||||
|
||||
a.click();
|
||||
a.remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} href
|
||||
*/
|
||||
export function importStyle(href) {
|
||||
const link = document.createElement("link");
|
||||
link.href = href;
|
||||
link.type = "text/css";
|
||||
link.rel = "stylesheet";
|
||||
link.media = "screen,print";
|
||||
const head = window.document.getElementsByTagName("head")[0];
|
||||
head.appendChild(link);
|
||||
return link;
|
||||
}
|
||||
|
||||
/**
|
||||
* @template {Readonly<string[]>} T
|
||||
* @param {Object} args
|
||||
* @param {T[number]} args.defaultValue
|
||||
* @param {string} [args.id]
|
||||
* @param {T | Accessor<T>} args.choices
|
||||
* @param {string} [args.keyPrefix]
|
||||
* @param {string} args.key
|
||||
* @param {boolean} [args.sorted]
|
||||
* @param {Signals} args.signals
|
||||
*/
|
||||
export function createHorizontalChoiceField({
|
||||
id,
|
||||
choices: unsortedChoices,
|
||||
defaultValue,
|
||||
keyPrefix,
|
||||
key,
|
||||
signals,
|
||||
sorted,
|
||||
}) {
|
||||
const choices = signals.createMemo(() => {
|
||||
/** @type {T} */
|
||||
let c;
|
||||
if (typeof unsortedChoices === "function") {
|
||||
c = unsortedChoices();
|
||||
} else {
|
||||
c = unsortedChoices;
|
||||
}
|
||||
|
||||
return sorted
|
||||
? /** @type {T} */ (
|
||||
/** @type {any} */ (c.toSorted((a, b) => a.localeCompare(b)))
|
||||
)
|
||||
: c;
|
||||
});
|
||||
|
||||
/** @type {Signal<T[number]>} */
|
||||
const selected = signals.createSignal(defaultValue, {
|
||||
save: {
|
||||
...stringSerde,
|
||||
keyPrefix: keyPrefix ?? "",
|
||||
key,
|
||||
saveDefaultValue: true,
|
||||
},
|
||||
});
|
||||
|
||||
const field = window.document.createElement("div");
|
||||
field.classList.add("field");
|
||||
|
||||
const div = window.document.createElement("div");
|
||||
field.append(div);
|
||||
|
||||
signals.createEffect(choices, (choices) => {
|
||||
const s = selected();
|
||||
if (!choices.includes(s)) {
|
||||
if (choices.includes(defaultValue)) {
|
||||
selected.set(() => defaultValue);
|
||||
} else if (choices.length) {
|
||||
selected.set(() => choices[0]);
|
||||
}
|
||||
}
|
||||
|
||||
div.innerHTML = "";
|
||||
|
||||
choices.forEach((choice) => {
|
||||
const inputValue = choice;
|
||||
const { label } = createLabeledInput({
|
||||
inputId: `${id ?? key}-${choice.toLowerCase()}`,
|
||||
inputName: id ?? key,
|
||||
inputValue,
|
||||
inputChecked: inputValue === selected(),
|
||||
// title: choice,
|
||||
type: "radio",
|
||||
});
|
||||
|
||||
const text = window.document.createTextNode(choice);
|
||||
label.append(text);
|
||||
div.append(label);
|
||||
});
|
||||
});
|
||||
|
||||
field.addEventListener("change", (event) => {
|
||||
// @ts-ignore
|
||||
const value = event.target.value;
|
||||
selected.set(value);
|
||||
});
|
||||
|
||||
return { field, selected };
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} [title]
|
||||
* @param {1 | 2 | 3} [level]
|
||||
*/
|
||||
export function createHeader(title = "", level = 1) {
|
||||
const headerElement = window.document.createElement("header");
|
||||
|
||||
const headingElement = window.document.createElement(`h${level}`);
|
||||
headingElement.innerHTML = title;
|
||||
headerElement.append(headingElement);
|
||||
headingElement.style.display = "block";
|
||||
|
||||
return {
|
||||
headerElement,
|
||||
headingElement,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @template {string} Name
|
||||
* @template {string} Value
|
||||
* @template {Value | {name: Name; value: Value}} T
|
||||
* @param {T} arg
|
||||
*/
|
||||
export function createOption(arg) {
|
||||
const option = window.document.createElement("option");
|
||||
if (typeof arg === "object") {
|
||||
option.value = arg.value;
|
||||
option.innerText = arg.name;
|
||||
} else {
|
||||
option.value = arg;
|
||||
option.innerText = arg;
|
||||
}
|
||||
return option;
|
||||
}
|
||||
|
||||
/**
|
||||
* @template {string} Name
|
||||
* @template {string} Value
|
||||
* @template {Value | {name: Name; value: Value}} T
|
||||
* @param {Object} args
|
||||
* @param {string} [args.id]
|
||||
* @param {boolean} [args.deep]
|
||||
* @param {readonly ((T) | {name: string; list: T[]})[]} args.list
|
||||
* @param {Signal<T>} args.signal
|
||||
*/
|
||||
export function createSelect({ id, list, signal, deep = false }) {
|
||||
const select = window.document.createElement("select");
|
||||
|
||||
if (id) {
|
||||
select.name = id;
|
||||
select.id = id;
|
||||
}
|
||||
|
||||
/** @type {Record<string, VoidFunction>} */
|
||||
const setters = {};
|
||||
|
||||
list.forEach((anyOption, index) => {
|
||||
if (typeof anyOption === "object" && "list" in anyOption) {
|
||||
const { name, list } = anyOption;
|
||||
const optGroup = window.document.createElement("optgroup");
|
||||
optGroup.label = name;
|
||||
select.append(optGroup);
|
||||
list.forEach((option) => {
|
||||
optGroup.append(createOption(option));
|
||||
const key = /** @type {string} */ (
|
||||
typeof option === "object" ? option.value : option
|
||||
);
|
||||
setters[key] = () => signal.set(() => option);
|
||||
});
|
||||
} else {
|
||||
select.append(createOption(anyOption));
|
||||
const key = /** @type {string} */ (
|
||||
typeof anyOption === "object" ? anyOption.value : anyOption
|
||||
);
|
||||
setters[key] = () => signal.set(() => anyOption);
|
||||
}
|
||||
if (deep && index !== list.length - 1) {
|
||||
select.append(window.document.createElement("hr"));
|
||||
}
|
||||
});
|
||||
|
||||
select.addEventListener("change", () => {
|
||||
const callback = setters[select.value];
|
||||
// @ts-ignore
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
|
||||
const initialSignal = signal();
|
||||
const initialValue =
|
||||
typeof initialSignal === "object" ? initialSignal.value : initialSignal;
|
||||
select.value = String(initialValue);
|
||||
|
||||
return { select, signal };
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} args
|
||||
* @param {string} args.title
|
||||
* @param {string} args.description
|
||||
* @param {HTMLElement} args.input
|
||||
*/
|
||||
export function createFieldElement({ title, description, input }) {
|
||||
const div = window.document.createElement("div");
|
||||
|
||||
const label = window.document.createElement("label");
|
||||
div.append(label);
|
||||
|
||||
const titleElement = window.document.createElement("span");
|
||||
titleElement.innerHTML = title;
|
||||
label.append(titleElement);
|
||||
|
||||
const descriptionElement = window.document.createElement("small");
|
||||
descriptionElement.innerHTML = description;
|
||||
label.append(descriptionElement);
|
||||
|
||||
div.append(input);
|
||||
|
||||
const forId = input.id || input.firstElementChild?.id;
|
||||
|
||||
if (!forId) {
|
||||
console.log(input);
|
||||
throw `Input should've an ID`;
|
||||
}
|
||||
|
||||
label.htmlFor = forId;
|
||||
|
||||
return div;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {'left' | 'bottom' | 'top' | 'right'} position
|
||||
*/
|
||||
export function createShadow(position) {
|
||||
const div = window.document.createElement("div");
|
||||
div.classList.add(`shadow-${position}`);
|
||||
return div;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
/**
|
||||
* @param {string} id
|
||||
*/
|
||||
function getElementById(id) {
|
||||
const element = window.document.getElementById(id);
|
||||
if (!element) throw `Element with id = "${id}" should exist`;
|
||||
return element;
|
||||
}
|
||||
import { getElementById } from "./dom";
|
||||
|
||||
export default {
|
||||
head: window.document.getElementsByTagName("head")[0],
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { createPartialOptions } from "./partial";
|
||||
import {
|
||||
createButtonElement,
|
||||
createAnchorElement,
|
||||
insertElementAtIndex,
|
||||
} from "../dom";
|
||||
|
||||
/**
|
||||
* @param {Object} args
|
||||
@@ -26,7 +31,7 @@ export function initOptions({
|
||||
.filter((v) => v);
|
||||
const urlPath = urlPath_.length ? urlPath_ : undefined;
|
||||
const savedPath = /** @type {string[]} */ (
|
||||
JSON.parse(utils.storage.read(LS_SELECTED_KEY) || "[]") || []
|
||||
JSON.parse(storage.read(LS_SELECTED_KEY) || "[]") || []
|
||||
).filter((v) => v);
|
||||
console.log(savedPath);
|
||||
|
||||
@@ -83,7 +88,7 @@ export function initOptions({
|
||||
const href = option.url();
|
||||
|
||||
if (option.qrcode) {
|
||||
return utils.dom.createButtonElement({
|
||||
return createButtonElement({
|
||||
inside: option.name,
|
||||
title,
|
||||
onClick: () => {
|
||||
@@ -91,7 +96,7 @@ export function initOptions({
|
||||
},
|
||||
});
|
||||
} else {
|
||||
return utils.dom.createAnchorElement({
|
||||
return createAnchorElement({
|
||||
href,
|
||||
blank: true,
|
||||
text: option.name,
|
||||
@@ -99,7 +104,7 @@ export function initOptions({
|
||||
});
|
||||
}
|
||||
} else {
|
||||
return utils.dom.createAnchorElement({
|
||||
return createAnchorElement({
|
||||
href: `/${option.path.join("/")}`,
|
||||
title,
|
||||
text: name || option.name,
|
||||
@@ -161,7 +166,7 @@ export function initOptions({
|
||||
|
||||
if (renderLi() && _ul) {
|
||||
const li = window.document.createElement("li");
|
||||
utils.dom.insertElementAtIndex(_ul, li, partialIndex);
|
||||
insertElementAtIndex(_ul, li, partialIndex);
|
||||
return li;
|
||||
} else {
|
||||
return null;
|
||||
|
||||
50
websites/bitview/scripts/core/scheduling.js
Normal file
50
websites/bitview/scripts/core/scheduling.js
Normal file
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* @param {number} ms
|
||||
*/
|
||||
export function sleep(ms) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
}
|
||||
|
||||
export function next() {
|
||||
return sleep(0);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @template {(...args: any[]) => any} F
|
||||
* @param {F} callback
|
||||
* @param {number} [wait]
|
||||
*/
|
||||
export function throttle(callback, wait = 1000) {
|
||||
/** @type {number | null} */
|
||||
let timeoutId = null;
|
||||
/** @type {Parameters<F>} */
|
||||
let latestArgs;
|
||||
|
||||
return (/** @type {Parameters<F>} */ ...args) => {
|
||||
latestArgs = args;
|
||||
|
||||
if (!timeoutId) {
|
||||
// Otherwise it optimizes away timeoutId in Chrome and FF
|
||||
timeoutId = timeoutId;
|
||||
timeoutId = setTimeout(() => {
|
||||
callback(...latestArgs); // Execute with latest args
|
||||
timeoutId = null;
|
||||
}, wait);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {VoidFunction} callback
|
||||
* @param {number} [timeout = 1]
|
||||
*/
|
||||
export function runWhenIdle(callback, timeout = 1) {
|
||||
if ("requestIdleCallback" in window) {
|
||||
requestIdleCallback(callback);
|
||||
} else {
|
||||
setTimeout(callback, timeout);
|
||||
}
|
||||
}
|
||||
583
websites/bitview/scripts/core/serde.js
Normal file
583
websites/bitview/scripts/core/serde.js
Normal file
@@ -0,0 +1,583 @@
|
||||
const localhost = window.location.hostname === "localhost";
|
||||
|
||||
export const string = {
|
||||
/**
|
||||
* @param {string} v
|
||||
*/
|
||||
serialize(v) {
|
||||
return v;
|
||||
},
|
||||
/**
|
||||
* @param {string} v
|
||||
*/
|
||||
deserialize(v) {
|
||||
return v;
|
||||
},
|
||||
};
|
||||
|
||||
export const vecIds = {
|
||||
/**
|
||||
* @param {VecId[]} v
|
||||
*/
|
||||
serialize(v) {
|
||||
return v.join(",");
|
||||
},
|
||||
/**
|
||||
* @param {string} v
|
||||
*/
|
||||
deserialize(v) {
|
||||
return /** @type {VecId[]} */ (v.split(","));
|
||||
},
|
||||
};
|
||||
|
||||
export const number = {
|
||||
/**
|
||||
* @param {number} v
|
||||
*/
|
||||
serialize(v) {
|
||||
return String(v);
|
||||
},
|
||||
/**
|
||||
* @param {string} v
|
||||
*/
|
||||
deserialize(v) {
|
||||
return Number(v);
|
||||
},
|
||||
};
|
||||
|
||||
export const optNumber = {
|
||||
/**
|
||||
* @param {number | null} v
|
||||
*/
|
||||
serialize(v) {
|
||||
return v !== null ? String(v) : "";
|
||||
},
|
||||
/**
|
||||
* @param {string} v
|
||||
*/
|
||||
deserialize(v) {
|
||||
return v ? Number(v) : null;
|
||||
},
|
||||
};
|
||||
|
||||
export const optDate = {
|
||||
/**
|
||||
* @param {Date | null} date
|
||||
*/
|
||||
serialize(date) {
|
||||
return date !== null ? date.toString() : "";
|
||||
},
|
||||
/**
|
||||
* @param {string} v
|
||||
*/
|
||||
deserialize(v) {
|
||||
return new Date(v);
|
||||
},
|
||||
};
|
||||
|
||||
export const boolean = {
|
||||
/**
|
||||
* @param {boolean} v
|
||||
*/
|
||||
serialize(v) {
|
||||
return String(v);
|
||||
},
|
||||
/**
|
||||
* @param {string} v
|
||||
*/
|
||||
deserialize(v) {
|
||||
if (v === "true") {
|
||||
return true;
|
||||
} else if (v === "false") {
|
||||
return false;
|
||||
} else {
|
||||
throw "deser bool err";
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const index = {
|
||||
/**
|
||||
* @param {Index} v
|
||||
*/
|
||||
serialize(v) {
|
||||
switch (v) {
|
||||
case /** @satisfies {DateIndex} */ (0):
|
||||
return "dateindex";
|
||||
case /** @satisfies {DecadeIndex} */ (1):
|
||||
return "decadeindex";
|
||||
case /** @satisfies {DifficultyEpoch} */ (2):
|
||||
return "difficultyepoch";
|
||||
case /** @satisfies {EmptyOutputIndex} */ (3):
|
||||
return "emptyoutputindex";
|
||||
case /** @satisfies {HalvingEpoch} */ (4):
|
||||
return "halvingepoch";
|
||||
case /** @satisfies {Height} */ (5):
|
||||
return "height";
|
||||
case /** @satisfies {InputIndex} */ (6):
|
||||
return "inputindex";
|
||||
case /** @satisfies {MonthIndex} */ (7):
|
||||
return "monthindex";
|
||||
case /** @satisfies {OpReturnIndex} */ (8):
|
||||
return "opreturnindex";
|
||||
case /** @satisfies {OutputIndex} */ (9):
|
||||
return "outputindex";
|
||||
case /** @satisfies {P2AAddressIndex} */ (10):
|
||||
return "p2aaddressindex";
|
||||
case /** @satisfies {P2MSOutputIndex} */ (11):
|
||||
return "p2msoutputindex";
|
||||
case /** @satisfies {P2PK33AddressIndex} */ (12):
|
||||
return "p2pk33addressindex";
|
||||
case /** @satisfies {P2PK65AddressIndex} */ (13):
|
||||
return "p2pk65addressindex";
|
||||
case /** @satisfies {P2PKHAddressIndex} */ (14):
|
||||
return "p2pkhaddressindex";
|
||||
case /** @satisfies {P2SHAddressIndex} */ (15):
|
||||
return "p2shaddressindex";
|
||||
case /** @satisfies {P2TRAddressIndex} */ (16):
|
||||
return "p2traddressindex";
|
||||
case /** @satisfies {P2WPKHAddressIndex} */ (17):
|
||||
return "p2wpkhaddressindex";
|
||||
case /** @satisfies {P2WSHAddressIndex} */ (18):
|
||||
return "p2wshaddressindex";
|
||||
case /** @satisfies {QuarterIndex} */ (19):
|
||||
return "quarterindex";
|
||||
case /** @satisfies {SemesterIndex} */ (20):
|
||||
return "semesterindex";
|
||||
case /** @satisfies {TxIndex} */ (21):
|
||||
return "txindex";
|
||||
case /** @satisfies {UnknownOutputIndex} */ (22):
|
||||
return "unknownoutputindex";
|
||||
case /** @satisfies {WeekIndex} */ (23):
|
||||
return "weekindex";
|
||||
case /** @satisfies {YearIndex} */ (24):
|
||||
return "yearindex";
|
||||
case /** @satisfies {LoadedAddressIndex} */ (25):
|
||||
return "loadedaddressindex";
|
||||
case /** @satisfies {EmptyAddressIndex} */ (26):
|
||||
return "emptyaddressindex";
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const chartableIndex = {
|
||||
/**
|
||||
* @param {number} v
|
||||
* @returns {SerializedChartableIndex | null}
|
||||
*/
|
||||
serialize(v) {
|
||||
switch (v) {
|
||||
case /** @satisfies {DateIndex} */ (0):
|
||||
return "date";
|
||||
case /** @satisfies {DecadeIndex} */ (1):
|
||||
return "decade";
|
||||
case /** @satisfies {DifficultyEpoch} */ (2):
|
||||
return "epoch";
|
||||
// case /** @satisfies {HalvingEpoch} */ (4):
|
||||
// return "halving";
|
||||
case /** @satisfies {Height} */ (5):
|
||||
return "timestamp";
|
||||
case /** @satisfies {MonthIndex} */ (7):
|
||||
return "month";
|
||||
case /** @satisfies {QuarterIndex} */ (19):
|
||||
return "quarter";
|
||||
case /** @satisfies {SemesterIndex} */ (20):
|
||||
return "semester";
|
||||
case /** @satisfies {WeekIndex} */ (23):
|
||||
return "week";
|
||||
case /** @satisfies {YearIndex} */ (24):
|
||||
return "year";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @param {SerializedChartableIndex} v
|
||||
* @returns {Index}
|
||||
*/
|
||||
deserialize(v) {
|
||||
switch (v) {
|
||||
case "timestamp":
|
||||
return /** @satisfies {Height} */ (5);
|
||||
case "date":
|
||||
return /** @satisfies {DateIndex} */ (0);
|
||||
case "week":
|
||||
return /** @satisfies {WeekIndex} */ (23);
|
||||
case "epoch":
|
||||
return /** @satisfies {DifficultyEpoch} */ (2);
|
||||
case "month":
|
||||
return /** @satisfies {MonthIndex} */ (7);
|
||||
case "quarter":
|
||||
return /** @satisfies {QuarterIndex} */ (19);
|
||||
case "semester":
|
||||
return /** @satisfies {SemesterIndex} */ (20);
|
||||
case "year":
|
||||
return /** @satisfies {YearIndex} */ (24);
|
||||
case "decade":
|
||||
return /** @satisfies {DecadeIndex} */ (1);
|
||||
default:
|
||||
throw Error("todo");
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef {"" |
|
||||
* "%all" |
|
||||
* "%cmcap" |
|
||||
* "%cp+l" |
|
||||
* "%mcap" |
|
||||
* "%pnl" |
|
||||
* "%rcap" |
|
||||
* "%self" |
|
||||
* "/sec" |
|
||||
* "address data" |
|
||||
* "block" |
|
||||
* "blocks" |
|
||||
* "bool" |
|
||||
* "btc" |
|
||||
* "bytes" |
|
||||
* "cents" |
|
||||
* "coinblocks" |
|
||||
* "coindays" |
|
||||
* "constant" |
|
||||
* "count" |
|
||||
* "date" |
|
||||
* "days" |
|
||||
* "difficulty" |
|
||||
* "epoch" |
|
||||
* "gigabytes" |
|
||||
* "h/s" |
|
||||
* "hash" |
|
||||
* "height" |
|
||||
* "id" |
|
||||
* "index" |
|
||||
* "len" |
|
||||
* "locktime" |
|
||||
* "percentage" |
|
||||
* "position" |
|
||||
* "ratio" |
|
||||
* "sat/vb" |
|
||||
* "satblocks" |
|
||||
* "satdays" |
|
||||
* "sats" |
|
||||
* "sats/(ph/s)/day" |
|
||||
* "sats/(th/s)/day" |
|
||||
* "sd" |
|
||||
* "secs" |
|
||||
* "timestamp" |
|
||||
* "tx" |
|
||||
* "type" |
|
||||
* "usd" |
|
||||
* "usd/(ph/s)/day" |
|
||||
* "usd/(th/s)/day" |
|
||||
* "vb" |
|
||||
* "version" |
|
||||
* "wu" |
|
||||
* "years" |
|
||||
* "" } Unit
|
||||
*/
|
||||
|
||||
export const unit = {
|
||||
/**
|
||||
* @param {VecId} v
|
||||
*/
|
||||
deserialize(v) {
|
||||
/** @type {Unit | undefined} */
|
||||
let unit;
|
||||
|
||||
/**
|
||||
* @param {Unit} u
|
||||
*/
|
||||
function setUnit(u) {
|
||||
if (unit)
|
||||
throw Error(
|
||||
`Can't assign "${u}" to unit, "${unit}" is already assigned to "${v}"`,
|
||||
);
|
||||
unit = u;
|
||||
}
|
||||
|
||||
if (
|
||||
(!unit || localhost) &&
|
||||
(v.includes("in_sats") ||
|
||||
(v.endsWith("supply") &&
|
||||
!(v.endsWith("circulating_supply") || v.endsWith("_own_supply"))) ||
|
||||
v === "sent" ||
|
||||
v === "annualized_volume" ||
|
||||
v.endsWith("supply_half") ||
|
||||
v.endsWith("supply_breakeven") ||
|
||||
v.endsWith("supply_in_profit") ||
|
||||
v.endsWith("supply_in_loss") ||
|
||||
v.endsWith("stack") ||
|
||||
(v.endsWith("value") && !v.includes("realized")) ||
|
||||
((v.includes("coinbase") ||
|
||||
v.includes("fee") ||
|
||||
v.includes("subsidy") ||
|
||||
v.includes("rewards")) &&
|
||||
!(
|
||||
v.startsWith("is_") ||
|
||||
v.includes("_btc") ||
|
||||
v.includes("_usd") ||
|
||||
v.includes("fee_rate") ||
|
||||
v.endsWith("dominance")
|
||||
)))
|
||||
) {
|
||||
setUnit("sats");
|
||||
}
|
||||
if (
|
||||
(!unit || localhost) &&
|
||||
!v.endsWith("velocity") &&
|
||||
((v.includes("_btc") &&
|
||||
!(v.includes("0k_btc") || v.includes("1k_btc"))) ||
|
||||
v.endsWith("_btc"))
|
||||
) {
|
||||
setUnit("btc");
|
||||
}
|
||||
if ((!unit || localhost) && v === "chain") {
|
||||
setUnit("block");
|
||||
}
|
||||
if ((!unit || localhost) && v.startsWith("blocks_before")) {
|
||||
setUnit("blocks");
|
||||
}
|
||||
if (
|
||||
(!unit || localhost) &&
|
||||
(v === "emptyaddressdata" || v === "loadedaddressdata")
|
||||
) {
|
||||
setUnit("address data");
|
||||
}
|
||||
if (
|
||||
(!unit || localhost) &&
|
||||
(v === "price_high" ||
|
||||
v === "price_ohlc" ||
|
||||
v === "price_low" ||
|
||||
v === "price_close" ||
|
||||
v === "price_open" ||
|
||||
v === "price_ath" ||
|
||||
v === "market_cap" ||
|
||||
v.startsWith("price_true_range") ||
|
||||
(v.includes("_usd") && !v.endsWith("velocity")) ||
|
||||
v.includes("cointime_value") ||
|
||||
v.endsWith("_ago") ||
|
||||
v.endsWith("price_paid") ||
|
||||
v.endsWith("_price") ||
|
||||
(v.startsWith("price") && (v.endsWith("min") || v.endsWith("max"))) ||
|
||||
(v.endsWith("_cap") && !v.includes("rel_to")) ||
|
||||
v.endsWith("value_created") ||
|
||||
v.endsWith("value_destroyed") ||
|
||||
((v.includes("realized") || v.includes("true_market_mean")) &&
|
||||
!v.includes("ratio") &&
|
||||
!v.includes("rel_to")) ||
|
||||
((v.endsWith("sma") || v.includes("sma_x") || v.endsWith("ema")) &&
|
||||
!v.includes("ratio") &&
|
||||
!v.includes("sopr") &&
|
||||
!v.includes("hash_rate")) ||
|
||||
v === "ath")
|
||||
) {
|
||||
setUnit("usd");
|
||||
}
|
||||
if ((!unit || localhost) && v.endsWith("cents")) {
|
||||
setUnit("cents");
|
||||
}
|
||||
if (
|
||||
((!unit || localhost) &&
|
||||
(v.endsWith("ratio") ||
|
||||
(v.includes("ratio") &&
|
||||
(v.endsWith("sma") || v.endsWith("ema") || v.endsWith("zscore"))) ||
|
||||
v.includes("sopr") ||
|
||||
v.endsWith("_5sd") ||
|
||||
v.endsWith("1sd") ||
|
||||
v.endsWith("2sd") ||
|
||||
v.endsWith("3sd") ||
|
||||
v.endsWith("pct1") ||
|
||||
v.endsWith("pct2") ||
|
||||
v.endsWith("pct5") ||
|
||||
v.endsWith("pct95") ||
|
||||
v.endsWith("pct98") ||
|
||||
v.endsWith("pct99"))) ||
|
||||
v.includes("liveliness") ||
|
||||
v.includes("vaultedness") ||
|
||||
v == "puell_multiple" ||
|
||||
v.endsWith("velocity")
|
||||
) {
|
||||
setUnit("ratio");
|
||||
}
|
||||
if (
|
||||
(!unit || localhost) &&
|
||||
(v === "price_drawdown" ||
|
||||
v === "difficulty_adjustment" ||
|
||||
v.endsWith("inflation_rate") ||
|
||||
v.endsWith("_oscillator") ||
|
||||
v.endsWith("_dominance") ||
|
||||
v.endsWith("_returns") ||
|
||||
v.endsWith("_rebound") ||
|
||||
v.endsWith("_volatility") ||
|
||||
v.endsWith("_cagr"))
|
||||
) {
|
||||
setUnit("percentage");
|
||||
}
|
||||
if (
|
||||
(!unit || localhost) &&
|
||||
(v.endsWith("count") ||
|
||||
v.includes("_count_") ||
|
||||
v.startsWith("block_count") ||
|
||||
v.includes("blocks_mined") ||
|
||||
(v.includes("tx_v") && !v.includes("vsize")))
|
||||
) {
|
||||
setUnit("count");
|
||||
}
|
||||
if (
|
||||
(!unit || localhost) &&
|
||||
(v.startsWith("hash_rate") || v.endsWith("as_hash"))
|
||||
) {
|
||||
setUnit("h/s");
|
||||
}
|
||||
if ((!unit || localhost) && v === "pool") {
|
||||
setUnit("id");
|
||||
}
|
||||
if ((!unit || localhost) && v.includes("fee_rate")) {
|
||||
setUnit("sat/vb");
|
||||
}
|
||||
if ((!unit || localhost) && v.startsWith("is_")) {
|
||||
setUnit("bool");
|
||||
}
|
||||
if ((!unit || localhost) && v.endsWith("type")) {
|
||||
setUnit("type");
|
||||
}
|
||||
if (
|
||||
(!unit || localhost) &&
|
||||
(v === "interval" || v.startsWith("block_interval"))
|
||||
) {
|
||||
setUnit("secs");
|
||||
}
|
||||
if ((!unit || localhost) && v.endsWith("_per_sec")) {
|
||||
setUnit("/sec");
|
||||
}
|
||||
if ((!unit || localhost) && v.endsWith("locktime")) {
|
||||
setUnit("locktime");
|
||||
}
|
||||
|
||||
if ((!unit || localhost) && v.endsWith("version")) {
|
||||
setUnit("version");
|
||||
}
|
||||
if (
|
||||
(!unit || localhost) &&
|
||||
(v === "txid" ||
|
||||
(v.endsWith("bytes") && !v.endsWith("vbytes")) ||
|
||||
v.endsWith("base_size") ||
|
||||
v.endsWith("total_size") ||
|
||||
v.includes("block_size"))
|
||||
) {
|
||||
setUnit("bytes");
|
||||
}
|
||||
if ((!unit || localhost) && v.endsWith("_sd")) {
|
||||
setUnit("sd");
|
||||
}
|
||||
if ((!unit || localhost) && (v.includes("vsize") || v.includes("vbytes"))) {
|
||||
setUnit("vb");
|
||||
}
|
||||
if ((!unit || localhost) && v.includes("weight")) {
|
||||
setUnit("wu");
|
||||
}
|
||||
if ((!unit || localhost) && v.endsWith("index")) {
|
||||
setUnit("index");
|
||||
}
|
||||
if ((!unit || localhost) && (v === "date" || v === "date_fixed")) {
|
||||
setUnit("date");
|
||||
}
|
||||
if (
|
||||
(!unit || localhost) &&
|
||||
(v === "timestamp" || v === "timestamp_fixed")
|
||||
) {
|
||||
setUnit("timestamp");
|
||||
}
|
||||
if ((!unit || localhost) && v.includes("coinblocks")) {
|
||||
setUnit("coinblocks");
|
||||
}
|
||||
if ((!unit || localhost) && v.includes("coindays")) {
|
||||
setUnit("coindays");
|
||||
}
|
||||
if ((!unit || localhost) && v.includes("satblocks")) {
|
||||
setUnit("satblocks");
|
||||
}
|
||||
if ((!unit || localhost) && v.includes("satdays")) {
|
||||
setUnit("satdays");
|
||||
}
|
||||
if ((!unit || localhost) && v.endsWith("height")) {
|
||||
setUnit("height");
|
||||
}
|
||||
if ((!unit || localhost) && v.endsWith("rel_to_market_cap")) {
|
||||
setUnit("%mcap");
|
||||
}
|
||||
if ((!unit || localhost) && v.endsWith("rel_to_own_market_cap")) {
|
||||
setUnit("%cmcap");
|
||||
}
|
||||
if ((!unit || localhost) && v.endsWith("rel_to_own_total_unrealized_pnl")) {
|
||||
setUnit("%cp+l");
|
||||
}
|
||||
if ((!unit || localhost) && v.endsWith("rel_to_realized_cap")) {
|
||||
setUnit("%rcap");
|
||||
}
|
||||
if ((!unit || localhost) && v.endsWith("rel_to_circulating_supply")) {
|
||||
setUnit("%all");
|
||||
}
|
||||
if (
|
||||
(!unit || localhost) &&
|
||||
(v.includes("rel_to_realized_profit") ||
|
||||
v.includes("rel_to_realized_loss"))
|
||||
) {
|
||||
setUnit("%pnl");
|
||||
}
|
||||
if ((!unit || localhost) && v.endsWith("rel_to_own_supply")) {
|
||||
setUnit("%self");
|
||||
}
|
||||
if ((!unit || localhost) && v.endsWith("epoch")) {
|
||||
setUnit("epoch");
|
||||
}
|
||||
if ((!unit || localhost) && v === "difficulty") {
|
||||
setUnit("difficulty");
|
||||
}
|
||||
if ((!unit || localhost) && v === "blockhash") {
|
||||
setUnit("hash");
|
||||
}
|
||||
if ((!unit || localhost) && v.startsWith("hash_price_phs")) {
|
||||
setUnit("usd/(ph/s)/day");
|
||||
}
|
||||
if ((!unit || localhost) && v.startsWith("hash_price_ths")) {
|
||||
setUnit("usd/(th/s)/day");
|
||||
}
|
||||
if ((!unit || localhost) && v.startsWith("hash_value_phs")) {
|
||||
setUnit("sats/(ph/s)/day");
|
||||
}
|
||||
if ((!unit || localhost) && v.startsWith("hash_value_ths")) {
|
||||
setUnit("sats/(th/s)/day");
|
||||
}
|
||||
|
||||
if (
|
||||
(!unit || localhost) &&
|
||||
(v.includes("days_between") ||
|
||||
v.includes("days_since") ||
|
||||
v.startsWith("days_before"))
|
||||
) {
|
||||
setUnit("days");
|
||||
}
|
||||
if ((!unit || localhost) && v.includes("years_between")) {
|
||||
setUnit("years");
|
||||
}
|
||||
if ((!unit || localhost) && v == "len") {
|
||||
setUnit("len");
|
||||
}
|
||||
if ((!unit || localhost) && v == "position") {
|
||||
setUnit("position");
|
||||
}
|
||||
if ((!unit || localhost) && v.startsWith("constant")) {
|
||||
setUnit("constant");
|
||||
}
|
||||
|
||||
if (!unit) {
|
||||
console.log();
|
||||
throw Error(`Unit not set for "${v}"`);
|
||||
}
|
||||
|
||||
return /** @type {Unit} */ (unit);
|
||||
},
|
||||
};
|
||||
49
websites/bitview/scripts/core/storage.js
Normal file
49
websites/bitview/scripts/core/storage.js
Normal file
@@ -0,0 +1,49 @@
|
||||
export default {
|
||||
/**
|
||||
* @param {string} key
|
||||
*/
|
||||
readNumber(key) {
|
||||
const saved = this.read(key);
|
||||
if (saved) {
|
||||
return Number(saved);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
/**
|
||||
* @param {string} key
|
||||
*/
|
||||
readBool(key) {
|
||||
const saved = this.read(key);
|
||||
if (saved) {
|
||||
return saved === "true" || saved === "1";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
/**
|
||||
* @param {string} key
|
||||
*/
|
||||
read(key) {
|
||||
try {
|
||||
return localStorage.getItem(key);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @param {string} key
|
||||
* @param {string | boolean | null | undefined} value
|
||||
*/
|
||||
write(key, value) {
|
||||
try {
|
||||
value !== undefined && value !== null
|
||||
? localStorage.setItem(key, String(value))
|
||||
: localStorage.removeItem(key);
|
||||
} catch (_) {}
|
||||
},
|
||||
/**
|
||||
* @param {string} key
|
||||
*/
|
||||
remove(key) {
|
||||
this.write(key, undefined);
|
||||
},
|
||||
};
|
||||
101
websites/bitview/scripts/core/url.js
Normal file
101
websites/bitview/scripts/core/url.js
Normal file
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* @param {string | string[]} [pathname]
|
||||
*/
|
||||
function processPathname(pathname) {
|
||||
pathname ||= window.location.pathname;
|
||||
return Array.isArray(pathname) ? pathname.join("/") : pathname;
|
||||
}
|
||||
|
||||
export default {
|
||||
chartParamsWhitelist: ["from", "to"],
|
||||
/**
|
||||
* @param {string | string[]} pathname
|
||||
*/
|
||||
pushHistory(pathname) {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
pathname = processPathname(pathname);
|
||||
try {
|
||||
const url = `/${pathname}?${urlParams.toString()}`;
|
||||
console.log(`push history: ${url}`);
|
||||
window.history.pushState(null, "", url);
|
||||
} catch (_) {}
|
||||
},
|
||||
/**
|
||||
* @param {Object} args
|
||||
* @param {URLSearchParams} [args.urlParams]
|
||||
* @param {string | string[]} [args.pathname]
|
||||
*/
|
||||
replaceHistory({ urlParams, pathname }) {
|
||||
urlParams ||= new URLSearchParams(window.location.search);
|
||||
pathname = processPathname(pathname);
|
||||
try {
|
||||
const url = `/${pathname}?${urlParams.toString()}`;
|
||||
console.log(`replace history: ${url}`);
|
||||
window.history.replaceState(null, "", url);
|
||||
} catch (_) {}
|
||||
},
|
||||
/**
|
||||
* @param {Option} option
|
||||
*/
|
||||
resetParams(option) {
|
||||
const urlParams = new URLSearchParams();
|
||||
if (option.kind === "chart") {
|
||||
[...new URLSearchParams(window.location.search).entries()]
|
||||
.filter(([key, _]) => this.chartParamsWhitelist.includes(key))
|
||||
.forEach(([key, value]) => {
|
||||
urlParams.set(key, value);
|
||||
});
|
||||
}
|
||||
this.replaceHistory({ urlParams, pathname: option.path.join("/") });
|
||||
},
|
||||
/**
|
||||
* @param {string} key
|
||||
* @param {string | boolean | null | undefined} value
|
||||
*/
|
||||
writeParam(key, value) {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
|
||||
if (value !== null && value !== undefined) {
|
||||
urlParams.set(key, String(value));
|
||||
} else {
|
||||
urlParams.delete(key);
|
||||
}
|
||||
|
||||
this.replaceHistory({ urlParams });
|
||||
},
|
||||
/**
|
||||
* @param {string} key
|
||||
*/
|
||||
removeParam(key) {
|
||||
this.writeParam(key, undefined);
|
||||
},
|
||||
/**
|
||||
* @param {string} key
|
||||
*/
|
||||
readBoolParam(key) {
|
||||
const param = this.readParam(key);
|
||||
if (param) {
|
||||
return param === "true" || param === "1";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
/**
|
||||
* @param {string} key
|
||||
*/
|
||||
readNumberParam(key) {
|
||||
const param = this.readParam(key);
|
||||
if (param) {
|
||||
return Number(param);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} key
|
||||
* @returns {string | null}
|
||||
*/
|
||||
readParam(key) {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
return params.get(key);
|
||||
},
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user