bitview: reorg part 2

This commit is contained in:
nym21
2025-09-23 19:58:34 +02:00
parent 5b6ce5d8ee
commit d45686128e
21 changed files with 1803 additions and 1810 deletions
-2
View File
@@ -1519,8 +1519,6 @@
<!-- ------- --> <!-- ------- -->
<script> <script>
// @ts-check
const preferredColorSchemeMatchMedia = window.matchMedia( const preferredColorSchemeMatchMedia = window.matchMedia(
"(prefers-color-scheme: dark)", "(prefers-color-scheme: dark)",
); );
+281
View 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
View 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)];
}
+13 -108
View File
@@ -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 { 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 * @typedef {[number, number, number, number]} OHLCTuple
* *
@@ -209,7 +215,7 @@ function createChartElement({
const activeResources = /** @type {Set<VecResource>} */ (new Set()); const activeResources = /** @type {Set<VecResource>} */ (new Set());
ichart.subscribeCrosshairMove( ichart.subscribeCrosshairMove(
utils.throttle(() => { throttle(() => {
activeResources.forEach((v) => { activeResources.forEach((v) => {
v.fetch(); v.fetch();
}); });
@@ -285,7 +291,7 @@ function createChartElement({
paneIndex, paneIndex,
position: "sw", position: "sw",
createChild(pane) { createChild(pane) {
const { field, selected } = utils.dom.createHorizontalChoiceField({ const { field, selected } = createHorizontalChoiceField({
choices: /** @type {const} */ (["lin", "log"]), choices: /** @type {const} */ (["lin", "log"]),
id: utils.stringToId(`${id} ${paneIndex} ${unit}`), id: utils.stringToId(`${id} ${paneIndex} ${unit}`),
defaultValue: defaultValue:
@@ -341,7 +347,7 @@ function createChartElement({
save: { save: {
keyPrefix: "", keyPrefix: "",
key: id, key: id,
...utils.serde.boolean, ...serde.boolean,
}, },
}); });
@@ -868,7 +874,7 @@ function createLegend({ signals, utils }) {
} }
legends[order] = div; legends[order] = div;
const { input, label } = utils.dom.createLabeledInput({ const { input, label } = createLabeledInput({
inputId: utils.stringToId(`legend-${series.id}`), inputId: utils.stringToId(`legend-${series.id}`),
inputName: utils.stringToId(`selected-${series.id}`), inputName: utils.stringToId(`selected-${series.id}`),
inputValue: "value", inputValue: "value",
@@ -884,7 +890,7 @@ function createLegend({ signals, utils }) {
spanMain.classList.add("main"); spanMain.classList.add("main");
label.append(spanMain); label.append(spanMain);
const spanName = utils.dom.createSpanName(name); const spanName = createSpanName(name);
spanMain.append(spanName); spanMain.append(spanName);
div.append(label); 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 {typeof createChartElement} CreateChartElement
* @typedef {ReturnType<createChartElement>} Chart * @typedef {ReturnType<createChartElement>} Chart
+103 -6
View File
@@ -1,13 +1,8 @@
/**
* @import { Accessor } from "../packages/solidjs-signals/wrapper";
*/
const globalComputedStyle = getComputedStyle(window.document.documentElement);
/** /**
* @param {Accessor<boolean>} dark * @param {Accessor<boolean>} dark
*/ */
export function createColors(dark) { export function createColors(dark) {
const globalComputedStyle = getComputedStyle(window.document.documentElement);
/** /**
* @param {string} color * @param {string} color
*/ */
@@ -113,8 +108,110 @@ export function createColors(dark) {
rose, rose,
}; };
} }
/** /**
* @typedef {ReturnType<typeof createColors>} Colors * @typedef {ReturnType<typeof createColors>} Colors
* @typedef {Colors["orange"]} Color * @typedef {Colors["orange"]} Color
* @typedef {keyof Colors} ColorName * @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
View 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),
);
}
+446
View File
@@ -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 -8
View File
@@ -1,11 +1,4 @@
/** import { getElementById } from "./dom";
* @param {string} id
*/
function getElementById(id) {
const element = window.document.getElementById(id);
if (!element) throw `Element with id = "${id}" should exist`;
return element;
}
export default { export default {
head: window.document.getElementsByTagName("head")[0], head: window.document.getElementsByTagName("head")[0],
+10 -5
View File
@@ -1,4 +1,9 @@
import { createPartialOptions } from "./partial"; import { createPartialOptions } from "./partial";
import {
createButtonElement,
createAnchorElement,
insertElementAtIndex,
} from "../dom";
/** /**
* @param {Object} args * @param {Object} args
@@ -26,7 +31,7 @@ export function initOptions({
.filter((v) => v); .filter((v) => v);
const urlPath = urlPath_.length ? urlPath_ : undefined; const urlPath = urlPath_.length ? urlPath_ : undefined;
const savedPath = /** @type {string[]} */ ( const savedPath = /** @type {string[]} */ (
JSON.parse(utils.storage.read(LS_SELECTED_KEY) || "[]") || [] JSON.parse(storage.read(LS_SELECTED_KEY) || "[]") || []
).filter((v) => v); ).filter((v) => v);
console.log(savedPath); console.log(savedPath);
@@ -83,7 +88,7 @@ export function initOptions({
const href = option.url(); const href = option.url();
if (option.qrcode) { if (option.qrcode) {
return utils.dom.createButtonElement({ return createButtonElement({
inside: option.name, inside: option.name,
title, title,
onClick: () => { onClick: () => {
@@ -91,7 +96,7 @@ export function initOptions({
}, },
}); });
} else { } else {
return utils.dom.createAnchorElement({ return createAnchorElement({
href, href,
blank: true, blank: true,
text: option.name, text: option.name,
@@ -99,7 +104,7 @@ export function initOptions({
}); });
} }
} else { } else {
return utils.dom.createAnchorElement({ return createAnchorElement({
href: `/${option.path.join("/")}`, href: `/${option.path.join("/")}`,
title, title,
text: name || option.name, text: name || option.name,
@@ -161,7 +166,7 @@ export function initOptions({
if (renderLi() && _ul) { if (renderLi() && _ul) {
const li = window.document.createElement("li"); const li = window.document.createElement("li");
utils.dom.insertElementAtIndex(_ul, li, partialIndex); insertElementAtIndex(_ul, li, partialIndex);
return li; return li;
} else { } else {
return null; return null;
@@ -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
View 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
View 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
View 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
+2 -57
View File
@@ -17,6 +17,8 @@
* @import { Option, PartialChartOption, ChartOption, AnyPartialOption, ProcessedOptionAddons, OptionsTree, SimulationOption, AnySeriesBlueprint, SeriesType, AnyFetchedSeriesBlueprint, TableOption, ExplorerOption, UrlOption, PartialOptionsGroup, OptionsGroup, PartialOptionsTree } from "./core/options/partial" * @import { Option, PartialChartOption, ChartOption, AnyPartialOption, ProcessedOptionAddons, OptionsTree, SimulationOption, AnySeriesBlueprint, SeriesType, AnyFetchedSeriesBlueprint, TableOption, ExplorerOption, UrlOption, PartialOptionsGroup, OptionsGroup, PartialOptionsTree } from "./core/options/partial"
* *
* @import { WebSockets } from "./core/ws" * @import { WebSockets } from "./core/ws"
*
* @import { Unit } from "./core/serde"
*/ */
/** /**
@@ -26,63 +28,6 @@
* @typedef {typeof import("./core/elements")["default"]} Elements * @typedef {typeof import("./core/elements")["default"]} Elements
*/ */
/**
* @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
*/
// DO NOT CHANGE, Exact format is expected in `brk_bundler` // DO NOT CHANGE, Exact format is expected in `brk_bundler`
// @ts-ignore // @ts-ignore
import("./main.js"); import("./main.js");
+9 -144
View File
@@ -1,145 +1,10 @@
// @ts-check
import { createColors } from "./core/colors"; import { createColors } from "./core/colors";
import { createWebSockets } from "./core/ws"; import { createWebSockets } from "./core/ws";
import * as utils from "./core/utils"; import * as utils from "./core/utils";
import elements from "./core/elements"; import elements from "./core/elements";
import env from "./core/env"; import env from "./core/env";
import packages from "./lazy"; import packages from "./lazy";
import { onFirstIntersection, getElementById } from "./core/dom";
/**
* @param {Signals} signals
* @param {Utilities} utils
* @param {Env} env
* @param {VecIdToIndexes} vecIdToIndexes
*/
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: utils.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 utils.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 */
function initFrameSelectors() { function initFrameSelectors() {
const children = Array.from(elements.selectors.children); const children = Array.from(elements.selectors.children);
@@ -497,13 +362,13 @@ Promise.all([
} }
createMobileSwitchEffect(); createMobileSwitchEffect();
utils.dom.onFirstIntersection(elements.aside, () => onFirstIntersection(elements.aside, () =>
signals.runWithOwner(owner, initSelectedFrame), signals.runWithOwner(owner, initSelectedFrame),
); );
} }
initSelected(); initSelected();
utils.dom.onFirstIntersection(elements.nav, async () => { onFirstIntersection(elements.nav, async () => {
options.parent.set(elements.nav); options.parent.set(elements.nav);
const option = options.selected(); const option = options.selected();
@@ -578,7 +443,7 @@ Promise.all([
}); });
}); });
utils.dom.onFirstIntersection(elements.search, () => { onFirstIntersection(elements.search, () => {
console.log("search: init"); console.log("search: init");
const haystack = options.list.map((option) => option.title); const haystack = options.list.map((option) => option.title);
@@ -754,8 +619,8 @@ Promise.all([
}); });
function initShare() { function initShare() {
const shareDiv = utils.dom.getElementById("share-div"); const shareDiv = getElementById("share-div");
const shareContentDiv = utils.dom.getElementById("share-content-div"); const shareContentDiv = getElementById("share-content-div");
shareDiv.addEventListener("click", () => { shareDiv.addEventListener("click", () => {
qrcode.set(null); qrcode.set(null);
@@ -769,11 +634,11 @@ Promise.all([
packages.leanQr().then(({ generate }) => packages.leanQr().then(({ generate }) =>
signals.runWithOwner(owner, () => { signals.runWithOwner(owner, () => {
const imgQrcode = /** @type {HTMLImageElement} */ ( const imgQrcode = /** @type {HTMLImageElement} */ (
utils.dom.getElementById("share-img") getElementById("share-img")
); );
const anchor = /** @type {HTMLAnchorElement} */ ( const anchor = /** @type {HTMLAnchorElement} */ (
utils.dom.getElementById("share-anchor") getElementById("share-anchor")
); );
signals.createEffect(qrcode, (qrcode) => { signals.createEffect(qrcode, (qrcode) => {
@@ -804,7 +669,7 @@ Promise.all([
initShare(); initShare();
function initDesktopResizeBar() { function initDesktopResizeBar() {
const resizeBar = utils.dom.getElementById("resize-bar"); const resizeBar = getElementById("resize-bar");
let resize = false; let resize = false;
let startingWidth = 0; let startingWidth = 0;
let startingClientX = 0; let startingClientX = 0;
+17 -13
View File
@@ -1,4 +1,9 @@
// @ts-check import {
createShadow,
createHorizontalChoiceField,
createHeader,
} from "../core/dom";
import { throttle } from "../core/scheduling";
const keyPrefix = "chart"; const keyPrefix = "chart";
const ONE_BTC_IN_SATS = 100_000_000; const ONE_BTC_IN_SATS = 100_000_000;
@@ -7,7 +12,7 @@ const LINE = "line";
const CANDLE = "candle"; const CANDLE = "candle";
/** /**
* @typedef {"timestamp" | "date" | "week" | "d.epoch" | "month" | "quarter" | "semester" | "year" | "decade" } SerializedChartableIndex * @typedef {"timestamp" | "date" | "week" | "epoch" | "month" | "quarter" | "semester" | "year" | "decade" } SerializedChartableIndex
*/ */
/** /**
@@ -37,10 +42,10 @@ export function init({
vecIdToIndexes, vecIdToIndexes,
packages, packages,
}) { }) {
elements.charts.append(utils.dom.createShadow("left")); elements.charts.append(createShadow("left"));
elements.charts.append(utils.dom.createShadow("right")); elements.charts.append(createShadow("right"));
const { headerElement, headingElement } = utils.dom.createHeader(); const { headerElement, headingElement } = createHeader();
elements.charts.append(headerElement); elements.charts.append(headerElement);
const { index, fieldset } = createIndexSelector({ const { index, fieldset } = createIndexSelector({
@@ -99,7 +104,6 @@ export function init({
}, },
}); });
console.log(env.ios, "canShare" in navigator);
if (!(env.ios && !("canShare" in navigator))) { if (!(env.ios && !("canShare" in navigator))) {
const chartBottomRightCanvas = Array.from( const chartBottomRightCanvas = Array.from(
chart.inner.chartElement().getElementsByTagName("tr"), chart.inner.chartElement().getElementsByTagName("tr"),
@@ -136,7 +140,7 @@ export function init({
} }
chart.inner.timeScale().subscribeVisibleLogicalRangeChange( chart.inner.timeScale().subscribeVisibleLogicalRangeChange(
utils.throttle((t) => { throttle((t) => {
if (!t) return; if (!t) return;
from.set(t.from); from.set(t.from);
to.set(t.to); to.set(t.to);
@@ -146,7 +150,7 @@ export function init({
elements.charts.append(fieldset); elements.charts.append(fieldset);
const { field: seriesTypeField, selected: topSeriesType_ } = const { field: seriesTypeField, selected: topSeriesType_ } =
utils.dom.createHorizontalChoiceField({ createHorizontalChoiceField({
defaultValue: CANDLE, defaultValue: CANDLE,
keyPrefix, keyPrefix,
key: "seriestype-0", key: "seriestype-0",
@@ -172,7 +176,7 @@ export function init({
}); });
const { field: topUnitField, selected: topUnit } = const { field: topUnitField, selected: topUnit } =
utils.dom.createHorizontalChoiceField({ createHorizontalChoiceField({
defaultValue: "usd", defaultValue: "usd",
keyPrefix, keyPrefix,
key: "unit-0", key: "unit-0",
@@ -289,7 +293,7 @@ export function init({
Object.keys(option.bottom) Object.keys(option.bottom)
); );
const { field: bottomUnitField, selected: bottomUnit } = const { field: bottomUnitField, selected: bottomUnit } =
utils.dom.createHorizontalChoiceField({ createHorizontalChoiceField({
defaultValue: bottomUnits.at(0) || "", defaultValue: bottomUnits.at(0) || "",
keyPrefix, keyPrefix,
key: "unit-1", key: "unit-1",
@@ -523,12 +527,12 @@ function createIndexSelector({ option, vecIdToIndexes, signals, utils }) {
"timestamp", "timestamp",
"date", "date",
"week", "week",
"d.epoch", "epoch",
"month", "month",
"quarter", "quarter",
"semester", "semester",
"year", "year",
// "halving epoch", // "h.epoch",
"decade", "decade",
]); ]);
@@ -557,7 +561,7 @@ function createIndexSelector({ option, vecIdToIndexes, signals, utils }) {
); );
}); });
const { field, selected } = utils.dom.createHorizontalChoiceField({ const { field, selected } = createHorizontalChoiceField({
defaultValue: "date", defaultValue: "date",
keyPrefix, keyPrefix,
key: "index", key: "index",
@@ -1,4 +1,3 @@
// @ts-check
/** /**
* @param {Object} args * @param {Object} args
+41 -40
View File
@@ -1,4 +1,3 @@
// @ts-check
/** /**
* @param {Object} args * @param {Object} args
@@ -6,6 +5,8 @@
* @param {CreateChartElement} args.createChartElement * @param {CreateChartElement} args.createChartElement
* @param {Signals} args.signals * @param {Signals} args.signals
* @param {Utilities} args.utils * @param {Utilities} args.utils
* @param {Serde} args.serde
* @param {Dom} args.dom
* @param {Elements} args.elements * @param {Elements} args.elements
* @param {VecsResources} args.vecsResources * @param {VecsResources} args.vecsResources
*/ */
@@ -14,6 +15,8 @@ export function init({
elements, elements,
createChartElement, createChartElement,
signals, signals,
serde,
dom,
utils, utils,
vecsResources, vecsResources,
}) { }) {
@@ -30,7 +33,7 @@ export function init({
const simulationElement = elements.simulation; const simulationElement = elements.simulation;
const dom = { const domExtended = {
/** /**
* @param {Object} args * @param {Object} args
* @param {string} args.id * @param {string} args.id
@@ -166,7 +169,7 @@ export function init({
if (!element) throw "createResetableField element missing"; if (!element) throw "createResetableField element missing";
div.append(element); div.append(element);
const button = utils.dom.createButtonElement({ const button = dom.createButtonElement({
onClick: signal.reset, onClick: signal.reset,
inside: "Reset", inside: "Reset",
title: "Reset field", title: "Reset field",
@@ -306,7 +309,7 @@ export function init({
initial: { initial: {
amount: signals.createSignal(/** @type {number | null} */ (1000), { amount: signals.createSignal(/** @type {number | null} */ (1000), {
save: { save: {
...utils.serde.optNumber, ...serde.optNumber,
keyPrefix, keyPrefix,
key: "initial-amount", key: "initial-amount",
}, },
@@ -315,7 +318,7 @@ export function init({
topUp: { topUp: {
amount: signals.createSignal(/** @type {number | null} */ (150), { amount: signals.createSignal(/** @type {number | null} */ (150), {
save: { save: {
...utils.serde.optNumber, ...serde.optNumber,
keyPrefix, keyPrefix,
key: "top-up-amount", key: "top-up-amount",
}, },
@@ -336,14 +339,14 @@ export function init({
investment: { investment: {
initial: signals.createSignal(/** @type {number | null} */ (1000), { initial: signals.createSignal(/** @type {number | null} */ (1000), {
save: { save: {
...utils.serde.optNumber, ...serde.optNumber,
keyPrefix, keyPrefix,
key: "initial-swap", key: "initial-swap",
}, },
}), }),
recurrent: signals.createSignal(/** @type {number | null} */ (5), { recurrent: signals.createSignal(/** @type {number | null} */ (5), {
save: { save: {
...utils.serde.optNumber, ...serde.optNumber,
keyPrefix, keyPrefix,
key: "recurrent-swap", key: "recurrent-swap",
}, },
@@ -365,7 +368,7 @@ export function init({
/** @type {Date | null} */ (new Date("2021-04-15")), /** @type {Date | null} */ (new Date("2021-04-15")),
{ {
save: { save: {
...utils.serde.optDate, ...serde.optDate,
keyPrefix, keyPrefix,
key: "interval-start", key: "interval-start",
}, },
@@ -373,7 +376,7 @@ export function init({
), ),
end: signals.createSignal(/** @type {Date | null} */ (new Date()), { end: signals.createSignal(/** @type {Date | null} */ (new Date()), {
save: { save: {
...utils.serde.optDate, ...serde.optDate,
keyPrefix, keyPrefix,
key: "interval-end", key: "interval-end",
}, },
@@ -382,7 +385,7 @@ export function init({
fees: { fees: {
percentage: signals.createSignal(/** @type {number | null} */ (0.25), { percentage: signals.createSignal(/** @type {number | null} */ (0.25), {
save: { save: {
...utils.serde.optNumber, ...serde.optNumber,
keyPrefix, keyPrefix,
key: "percentage", key: "percentage",
}, },
@@ -390,9 +393,7 @@ export function init({
}, },
}; };
parametersElement.append( parametersElement.append(dom.createHeader("Save in Bitcoin").headerElement);
utils.dom.createHeader("Save in Bitcoin").headerElement,
);
/** /**
* @param {Object} param0 * @param {Object} param0
@@ -417,7 +418,7 @@ export function init({
} }
parametersElement.append( parametersElement.append(
utils.dom.createFieldElement({ dom.createFieldElement({
title: createColoredTypeHTML({ title: createColoredTypeHTML({
color: "green", color: "green",
type: "Dollars", type: "Dollars",
@@ -425,8 +426,8 @@ export function init({
}), }),
description: description:
"The amount of dollars you have ready on the exchange on day one.", "The amount of dollars you have ready on the exchange on day one.",
input: dom.createResetableInput( input: domExtended.createResetableInput(
dom.createInputDollar({ domExtended.createInputDollar({
id: "simulation-dollars-initial", id: "simulation-dollars-initial",
title: "Initial Dollar Amount", title: "Initial Dollar Amount",
signal: settings.dollars.initial.amount, signal: settings.dollars.initial.amount,
@@ -437,7 +438,7 @@ export function init({
); );
parametersElement.append( parametersElement.append(
utils.dom.createFieldElement({ dom.createFieldElement({
title: createColoredTypeHTML({ title: createColoredTypeHTML({
color: "green", color: "green",
type: "Dollars", type: "Dollars",
@@ -445,8 +446,8 @@ export function init({
}), }),
description: description:
"The frequency at which you'll top up your account at the exchange.", "The frequency at which you'll top up your account at the exchange.",
input: dom.createResetableInput( input: domExtended.createResetableInput(
utils.dom.createSelect({ dom.createSelect({
id: "top-up-frequency", id: "top-up-frequency",
list: frequencies.list, list: frequencies.list,
signal: settings.dollars.topUp.frenquency, signal: settings.dollars.topUp.frenquency,
@@ -457,7 +458,7 @@ export function init({
); );
parametersElement.append( parametersElement.append(
utils.dom.createFieldElement({ dom.createFieldElement({
title: createColoredTypeHTML({ title: createColoredTypeHTML({
color: "green", color: "green",
type: "Dollars", type: "Dollars",
@@ -465,8 +466,8 @@ export function init({
}), }),
description: description:
"The recurrent amount of dollars you'll be transfering to said exchange.", "The recurrent amount of dollars you'll be transfering to said exchange.",
input: dom.createResetableInput( input: domExtended.createResetableInput(
dom.createInputDollar({ domExtended.createInputDollar({
id: "simulation-dollars-top-up-amount", id: "simulation-dollars-top-up-amount",
title: "Top Up Dollar Amount", title: "Top Up Dollar Amount",
signal: settings.dollars.topUp.amount, signal: settings.dollars.topUp.amount,
@@ -477,7 +478,7 @@ export function init({
); );
parametersElement.append( parametersElement.append(
utils.dom.createFieldElement({ dom.createFieldElement({
title: createColoredTypeHTML({ title: createColoredTypeHTML({
color: "orange", color: "orange",
type: "Bitcoin", type: "Bitcoin",
@@ -485,8 +486,8 @@ export function init({
}), }),
description: description:
"The amount, if available, of dollars that will be used to buy Bitcoin on day one.", "The amount, if available, of dollars that will be used to buy Bitcoin on day one.",
input: dom.createResetableInput( input: domExtended.createResetableInput(
dom.createInputDollar({ domExtended.createInputDollar({
id: "simulation-bitcoin-initial-investment", id: "simulation-bitcoin-initial-investment",
title: "Initial Swap Amount", title: "Initial Swap Amount",
signal: settings.bitcoin.investment.initial, signal: settings.bitcoin.investment.initial,
@@ -497,15 +498,15 @@ export function init({
); );
parametersElement.append( parametersElement.append(
utils.dom.createFieldElement({ dom.createFieldElement({
title: createColoredTypeHTML({ title: createColoredTypeHTML({
color: "orange", color: "orange",
type: "Bitcoin", type: "Bitcoin",
text: "Investment Frequency", text: "Investment Frequency",
}), }),
description: "The frequency at which you'll be buying Bitcoin.", description: "The frequency at which you'll be buying Bitcoin.",
input: dom.createResetableInput( input: domExtended.createResetableInput(
utils.dom.createSelect({ dom.createSelect({
id: "investment-frequency", id: "investment-frequency",
list: frequencies.list, list: frequencies.list,
signal: settings.bitcoin.investment.frequency, signal: settings.bitcoin.investment.frequency,
@@ -516,7 +517,7 @@ export function init({
); );
parametersElement.append( parametersElement.append(
utils.dom.createFieldElement({ dom.createFieldElement({
title: createColoredTypeHTML({ title: createColoredTypeHTML({
color: "orange", color: "orange",
type: "Bitcoin", type: "Bitcoin",
@@ -524,8 +525,8 @@ export function init({
}), }),
description: description:
"The recurrent amount, if available, of dollars that will be used to buy Bitcoin.", "The recurrent amount, if available, of dollars that will be used to buy Bitcoin.",
input: dom.createResetableInput( input: domExtended.createResetableInput(
dom.createInputDollar({ domExtended.createInputDollar({
id: "simulation-bitcoin-recurrent-investment", id: "simulation-bitcoin-recurrent-investment",
title: "Bitcoin Recurrent Investment", title: "Bitcoin Recurrent Investment",
signal: settings.bitcoin.investment.recurrent, signal: settings.bitcoin.investment.recurrent,
@@ -536,15 +537,15 @@ export function init({
); );
parametersElement.append( parametersElement.append(
utils.dom.createFieldElement({ dom.createFieldElement({
title: createColoredTypeHTML({ title: createColoredTypeHTML({
color: "sky", color: "sky",
type: "Interval", type: "Interval",
text: "Start", text: "Start",
}), }),
description: "The first day of the simulation.", description: "The first day of the simulation.",
input: dom.createResetableInput( input: domExtended.createResetableInput(
dom.createInputDate({ domExtended.createInputDate({
id: "simulation-inverval-start", id: "simulation-inverval-start",
title: "First Simulation Date", title: "First Simulation Date",
signal: settings.interval.start, signal: settings.interval.start,
@@ -555,15 +556,15 @@ export function init({
); );
parametersElement.append( parametersElement.append(
utils.dom.createFieldElement({ dom.createFieldElement({
title: createColoredTypeHTML({ title: createColoredTypeHTML({
color: "sky", color: "sky",
type: "Interval", type: "Interval",
text: "End", text: "End",
}), }),
description: "The last day of the simulation.", description: "The last day of the simulation.",
input: dom.createResetableInput( input: domExtended.createResetableInput(
dom.createInputDate({ domExtended.createInputDate({
id: "simulation-inverval-end", id: "simulation-inverval-end",
title: "Last Simulation Day", title: "Last Simulation Day",
signal: settings.interval.end, signal: settings.interval.end,
@@ -574,15 +575,15 @@ export function init({
); );
parametersElement.append( parametersElement.append(
utils.dom.createFieldElement({ dom.createFieldElement({
title: createColoredTypeHTML({ title: createColoredTypeHTML({
color: "red", color: "red",
type: "Fees", type: "Fees",
text: "Exchange", text: "Exchange",
}), }),
description: "The amount of trading fees (in %) at the exchange.", description: "The amount of trading fees (in %) at the exchange.",
input: dom.createResetableInput( input: domExtended.createResetableInput(
dom.createInputNumberElement({ domExtended.createInputNumberElement({
id: "simulation-fees", id: "simulation-fees",
title: "Exchange Fees", title: "Exchange Fees",
signal: settings.fees.percentage, signal: settings.fees.percentage,
+9 -9
View File
@@ -1,4 +1,4 @@
// @ts-check import { createButtonElement, createSelect } from "../core/dom";
/** /**
* @param {Object} args * @param {Object} args
@@ -94,7 +94,7 @@ function createTable({
// div.append(top); // div.append(top);
// top.append(select); // top.append(select);
// top.append( // top.append(
// utils.dom.createAnchorElement({ // createAnchorElement({
// href: "", // href: "",
// blank: true, // blank: true,
// }), // }),
@@ -104,17 +104,17 @@ function createTable({
if (_unit) { if (_unit) {
unit.innerHTML = _unit; unit.innerHTML = _unit;
} }
const moveLeft = utils.dom.createButtonElement({ const moveLeft = createButtonElement({
inside: "←", inside: "←",
title: "Move column to the left", title: "Move column to the left",
onClick: onLeft || (() => {}), onClick: onLeft || (() => {}),
}); });
const moveRight = utils.dom.createButtonElement({ const moveRight = createButtonElement({
inside: "→", inside: "→",
title: "Move column to the right", title: "Move column to the right",
onClick: onRight || (() => {}), onClick: onRight || (() => {}),
}); });
const remove = utils.dom.createButtonElement({ const remove = createButtonElement({
inside: "×", inside: "×",
title: "Remove column", title: "Remove column",
onClick: onRemove || (() => {}), onClick: onRemove || (() => {}),
@@ -137,7 +137,7 @@ function createTable({
} }
addThCol({ addThCol({
...utils.dom.createSelect({ ...createSelect({
list: serializedIndexes, list: serializedIndexes,
signal: serializedIndex, signal: serializedIndex,
}), }),
@@ -188,7 +188,7 @@ function createTable({
name: vecId, name: vecId,
value: vecId, value: vecId,
}); });
const { select } = utils.dom.createSelect({ const { select } = createSelect({
list: possibleVecIds.map((vecId) => ({ list: possibleVecIds.map((vecId) => ({
name: vecId, name: vecId,
value: vecId, value: vecId,
@@ -367,7 +367,7 @@ export function init({
vecIdToIndexes, vecIdToIndexes,
}) { }) {
const parent = elements.table; const parent = elements.table;
const { headerElement } = utils.dom.createHeader("Table"); const { headerElement } = createHeader("Table");
parent.append(headerElement); parent.append(headerElement);
const div = window.document.createElement("div"); const div = window.document.createElement("div");
@@ -385,7 +385,7 @@ export function init({
const span = window.document.createElement("span"); const span = window.document.createElement("span");
span.innerHTML = "Add column"; span.innerHTML = "Add column";
div.append( div.append(
utils.dom.createButtonElement({ createButtonElement({
onClick: () => { onClick: () => {
table.addRandomCol?.(); table.addRandomCol?.();
}, },
@@ -1,4 +1,3 @@
// @ts-check
/** /**
* @import { SignalOptions } from "./0.4.11/dist/types/core/core" * @import { SignalOptions } from "./0.4.11/dist/types/core/core"