mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-04-24 14:49:58 -07:00
global: snapshot
This commit is contained in:
1238
website/scripts/chart/index.js
Normal file
1238
website/scripts/chart/index.js
Normal file
File diff suppressed because it is too large
Load Diff
100
website/scripts/chart/oklch.js
Normal file
100
website/scripts/chart/oklch.js
Normal file
@@ -0,0 +1,100 @@
|
||||
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];
|
||||
};
|
||||
}
|
||||
}
|
||||
138
website/scripts/entry.js
Normal file
138
website/scripts/entry.js
Normal file
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* @import * as _ from "./modules/leeoniya-ufuzzy/1.0.19/dist/uFuzzy.d.ts"
|
||||
*
|
||||
* @import { IChartApi, ISeriesApi as _ISeriesApi, SeriesDefinition, SingleValueData as _SingleValueData, CandlestickData as _CandlestickData, BaselineData as _BaselineData, HistogramData as _HistogramData, SeriesType as LCSeriesType, IPaneApi, LineSeriesPartialOptions as _LineSeriesPartialOptions, HistogramSeriesPartialOptions as _HistogramSeriesPartialOptions, BaselineSeriesPartialOptions as _BaselineSeriesPartialOptions, CandlestickSeriesPartialOptions as _CandlestickSeriesPartialOptions, WhitespaceData, DeepPartial, ChartOptions, Time, LineData as _LineData, createChart as CreateChart, LineStyle } from './modules/lightweight-charts/5.1.0/dist/typings.js'
|
||||
*
|
||||
* @import { Signal, Signals, Accessor } from "./signals.js";
|
||||
*
|
||||
* @import * as Brk from "./modules/brk-client/index.js"
|
||||
* @import { BrkClient, Index, Metric, MetricData } from "./modules/brk-client/index.js"
|
||||
*
|
||||
* @import { Resources, MetricResource } from './resources.js'
|
||||
*
|
||||
* @import { Valued, SingleValueData, CandlestickData, Series, AnySeries, ISeries, HistogramData, LineData, BaselineData, LineSeriesPartialOptions, BaselineSeriesPartialOptions, HistogramSeriesPartialOptions, CandlestickSeriesPartialOptions, CreateChartElement, Chart, Legend } from "./chart/index.js"
|
||||
*
|
||||
* @import { Color, ColorName, Colors } from "./utils/colors.js"
|
||||
*
|
||||
* @import { WebSockets } from "./utils/ws.js"
|
||||
*
|
||||
* @import { Option, PartialChartOption, ChartOption, AnyPartialOption, ProcessedOptionAddons, OptionsTree, SimulationOption, AnySeriesBlueprint, SeriesType, AnyFetchedSeriesBlueprint, TableOption, ExplorerOption, UrlOption, PartialOptionsGroup, OptionsGroup, PartialOptionsTree, UtxoCohortObject, AddressCohortObject, CohortObject, CohortGroupObject, FetchedLineSeriesBlueprint, FetchedBaselineSeriesBlueprint, FetchedHistogramSeriesBlueprint, PartialContext, PatternAll, PatternFull, PatternWithAdjusted, PatternWithPercentiles, PatternBasic, CohortAll, CohortFull, CohortWithAdjusted, CohortWithPercentiles, CohortBasic, CohortGroupFull, CohortGroupWithAdjusted, CohortGroupWithPercentiles, CohortGroupBasic, UtxoCohortGroupObject, AddressCohortGroupObject, FetchedDotsSeriesBlueprint, FetchedCandlestickSeriesBlueprint } from "./options/partial.js"
|
||||
*
|
||||
* @import { line as LineSeriesFn, dots as DotsSeriesFn, candlestick as CandlestickSeriesFn, baseline as BaselineSeriesFn, histogram as HistogramSeriesFn } from "./options/series.js"
|
||||
*
|
||||
* @import { UnitObject as Unit } from "./utils/units.js"
|
||||
*
|
||||
* @import { ChartableIndexName } from "./panes/chart/index.js";
|
||||
*/
|
||||
|
||||
// import uFuzzy = require("./modules/leeoniya-ufuzzy/1.0.19/dist/uFuzzy.d.ts");
|
||||
|
||||
/**
|
||||
* @typedef {[number, number, number, number]} OHLCTuple
|
||||
*
|
||||
* Brk type aliases
|
||||
* @typedef {Brk.MetricsTree_Distribution_UtxoCohorts} UtxoCohortTree
|
||||
* @typedef {Brk.MetricsTree_Distribution_AddressCohorts} AddressCohortTree
|
||||
* @typedef {Brk.MetricsTree_Distribution_UtxoCohorts_All} AllUtxoPattern
|
||||
* @typedef {Brk.MetricsTree_Distribution_UtxoCohorts_Term_Short} ShortTermPattern
|
||||
* @typedef {Brk.MetricsTree_Distribution_UtxoCohorts_Term_Long} LongTermPattern
|
||||
* @typedef {Brk._10yPattern} MaxAgePattern
|
||||
* @typedef {Brk._10yTo12yPattern} AgeRangePattern
|
||||
* @typedef {Brk._0satsPattern2} UtxoAmountPattern
|
||||
* @typedef {Brk._0satsPattern} AddressAmountPattern
|
||||
* @typedef {Brk._100btcPattern} BasicUtxoPattern
|
||||
* @typedef {Brk._0satsPattern2} EpochPattern
|
||||
* @typedef {Brk.Ratio1ySdPattern} Ratio1ySdPattern
|
||||
* @typedef {Brk.Dollars} Dollars
|
||||
* @typedef {Brk.Price111dSmaPattern} EmaRatioPattern
|
||||
* @typedef {Brk.CoinbasePattern} CoinbasePattern
|
||||
* @typedef {Brk.ActivePriceRatioPattern} ActivePriceRatioPattern
|
||||
* @typedef {Brk.UnclaimedRewardsPattern} ValuePattern
|
||||
* @typedef {Brk.AnyMetricPattern} AnyMetricPattern
|
||||
* @typedef {Brk.AnyMetricEndpointBuilder} AnyMetricEndpoint
|
||||
* @typedef {Brk.AnyMetricData} AnyMetricData
|
||||
* @typedef {Brk.AddrCountPattern} AddrCountPattern
|
||||
* @typedef {Brk.MetricsTree_Blocks_Interval} IntervalPattern
|
||||
* @typedef {Brk.MetricsTree_Supply_Circulating} SupplyPattern
|
||||
* @typedef {Brk.RelativePattern} GlobalRelativePattern
|
||||
* @typedef {Brk.RelativePattern2} OwnRelativePattern
|
||||
* @typedef {Brk.RelativePattern5} FullRelativePattern
|
||||
* @typedef {Brk.MetricsTree_Distribution_UtxoCohorts_All_Relative} AllRelativePattern
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @typedef {Brk.BlockCountPattern<T>} BlockCountPattern
|
||||
*/
|
||||
/**
|
||||
* @template T
|
||||
* @typedef {Brk.FullnessPattern<T>} FullnessPattern
|
||||
*/
|
||||
/**
|
||||
* @template T
|
||||
* @typedef {Brk.FeeRatePattern<T>} FeeRatePattern
|
||||
*/
|
||||
/**
|
||||
* @template T
|
||||
* @typedef {Brk.MetricEndpointBuilder<T>} MetricEndpoint
|
||||
*/
|
||||
/**
|
||||
* @template T
|
||||
* @typedef {Brk.DollarsPattern<T>} SizePattern
|
||||
*/
|
||||
/**
|
||||
* @template T
|
||||
* @typedef {Brk.CountPattern2<T>} CountStatsPattern
|
||||
*/
|
||||
/**
|
||||
* @typedef {Brk.MetricsTree_Blocks_Size} BlockSizePattern
|
||||
*/
|
||||
/**
|
||||
* Stats pattern union - accepts both CountStatsPattern and BlockSizePattern
|
||||
* @typedef {CountStatsPattern<any> | BlockSizePattern} AnyStatsPattern
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @typedef {InstanceType<typeof BrkClient>["INDEXES"]} Indexes
|
||||
* @typedef {Indexes[number]} IndexName
|
||||
* @typedef {InstanceType<typeof BrkClient>["POOL_ID_TO_POOL_NAME"]} PoolIdToPoolName
|
||||
* @typedef {keyof PoolIdToPoolName} PoolId
|
||||
*
|
||||
* Tree branch types
|
||||
* @typedef {Brk.MetricsTree_Market} Market
|
||||
* @typedef {Brk.MetricsTree_Market_MovingAverage} MarketMovingAverage
|
||||
* @typedef {Brk.MetricsTree_Market_Dca} MarketDca
|
||||
*
|
||||
* Pattern unions by cohort type
|
||||
* @typedef {AllUtxoPattern | AgeRangePattern | UtxoAmountPattern} UtxoCohortPattern
|
||||
* @typedef {AddressAmountPattern} AddressCohortPattern
|
||||
* @typedef {UtxoCohortPattern | AddressCohortPattern} CohortPattern
|
||||
*
|
||||
* Relative pattern capability types
|
||||
* @typedef {GlobalRelativePattern | FullRelativePattern} RelativeWithMarketCap
|
||||
* @typedef {OwnRelativePattern | FullRelativePattern} RelativeWithOwnMarketCap
|
||||
* @typedef {OwnRelativePattern | FullRelativePattern | AllRelativePattern} RelativeWithOwnPnl
|
||||
*
|
||||
* Capability-based pattern groupings (patterns that have specific properties)
|
||||
* @typedef {AllUtxoPattern | AgeRangePattern | UtxoAmountPattern} PatternWithRealizedPrice
|
||||
* @typedef {AllUtxoPattern} PatternWithFullRealized
|
||||
* @typedef {AllUtxoPattern | AgeRangePattern | UtxoAmountPattern} PatternWithNupl
|
||||
* @typedef {AllUtxoPattern | AgeRangePattern | UtxoAmountPattern} PatternWithCostBasis
|
||||
* @typedef {AllUtxoPattern | AgeRangePattern | UtxoAmountPattern} PatternWithActivity
|
||||
* @typedef {AllUtxoPattern | AgeRangePattern} PatternWithCostBasisPercentiles
|
||||
*
|
||||
* Cohort objects with specific pattern capabilities
|
||||
* @typedef {{ name: string, title: string, color: Color, tree: PatternWithRealizedPrice }} CohortWithRealizedPrice
|
||||
* @typedef {{ name: string, title: string, color: Color, tree: PatternWithFullRealized }} CohortWithFullRealized
|
||||
* @typedef {{ name: string, title: string, color: Color, tree: PatternWithNupl }} CohortWithNupl
|
||||
* @typedef {{ name: string, title: string, color: Color, tree: PatternWithCostBasis }} CohortWithCostBasis
|
||||
* @typedef {{ name: string, title: string, color: Color, tree: PatternWithActivity }} CohortWithActivity
|
||||
* @typedef {{ name: string, title: string, color: Color, tree: PatternWithCostBasisPercentiles }} CohortWithCostBasisPercentiles
|
||||
*
|
||||
* Generic tree node type for walking
|
||||
* @typedef {AnyMetricPattern | Record<string, unknown>} TreeNode
|
||||
*
|
||||
* Chartable index IDs (subset of IndexName that can be charted)
|
||||
* @typedef {"height" | "dateindex" | "weekindex" | "monthindex" | "quarterindex" | "semesterindex" | "yearindex" | "decadeindex"} ChartableIndex
|
||||
*/
|
||||
649
website/scripts/main.js
Normal file
649
website/scripts/main.js
Normal file
@@ -0,0 +1,649 @@
|
||||
import { createColors } from "./utils/colors.js";
|
||||
import { webSockets } from "./utils/ws.js";
|
||||
import * as formatters from "./utils/format.js";
|
||||
import { onFirstIntersection, getElementById, isHidden } from "./utils/dom.js";
|
||||
import signals from "./signals.js";
|
||||
import { BrkClient } from "./modules/brk-client/index.js";
|
||||
import { initOptions } from "./options/full.js";
|
||||
import ufuzzy from "./modules/leeoniya-ufuzzy/1.0.19/dist/uFuzzy.mjs";
|
||||
import * as leanQr from "./modules/lean-qr/2.7.1/index.mjs";
|
||||
import { init as initExplorer } from "./panes/explorer.js";
|
||||
import { init as initChart } from "./panes/chart/index.js";
|
||||
import { init as initTable } from "./panes/table.js";
|
||||
import { init as initSimulation } from "./panes/simulation.js";
|
||||
import { next } from "./utils/timing.js";
|
||||
import { replaceHistory } from "./utils/url.js";
|
||||
import { removeStored, writeToStorage } from "./utils/storage.js";
|
||||
import {
|
||||
asideElement,
|
||||
asideLabelElement,
|
||||
bodyElement,
|
||||
chartElement,
|
||||
explorerElement,
|
||||
frameSelectorsElement,
|
||||
mainElement,
|
||||
navElement,
|
||||
navLabelElement,
|
||||
searchElement,
|
||||
searchInput,
|
||||
searchResultsElement,
|
||||
simulationElement,
|
||||
style,
|
||||
tableElement,
|
||||
} from "./utils/elements.js";
|
||||
|
||||
function initFrameSelectors() {
|
||||
const children = Array.from(frameSelectorsElement.children);
|
||||
|
||||
/** @type {HTMLElement | undefined} */
|
||||
let focusedFrame = undefined;
|
||||
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const element = children[i];
|
||||
|
||||
switch (element.tagName) {
|
||||
case "LABEL": {
|
||||
element.addEventListener("click", () => {
|
||||
const inputId = element.getAttribute("for");
|
||||
|
||||
if (!inputId) {
|
||||
console.log(element, element.getAttribute("for"));
|
||||
throw "Input id in label not found";
|
||||
}
|
||||
|
||||
const input = window.document.getElementById(inputId);
|
||||
|
||||
if (!input || !("value" in input)) {
|
||||
throw "Not input or no value";
|
||||
}
|
||||
|
||||
const frame = window.document.getElementById(
|
||||
/** @type {string} */ (input.value),
|
||||
);
|
||||
|
||||
if (!frame) {
|
||||
console.log(input.value);
|
||||
throw "Frame element doesn't exist";
|
||||
}
|
||||
|
||||
if (frame === focusedFrame) {
|
||||
return;
|
||||
}
|
||||
|
||||
frame.hidden = false;
|
||||
if (focusedFrame) {
|
||||
focusedFrame.hidden = true;
|
||||
}
|
||||
focusedFrame = frame;
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
asideLabelElement.click();
|
||||
|
||||
// When going from mobile view to desktop view, if selected frame was open, go to the nav frame
|
||||
new IntersectionObserver((entries) => {
|
||||
for (let i = 0; i < entries.length; i++) {
|
||||
if (
|
||||
!entries[i].isIntersecting &&
|
||||
entries[i].target === asideLabelElement &&
|
||||
focusedFrame == asideElement
|
||||
) {
|
||||
navLabelElement.click();
|
||||
}
|
||||
}
|
||||
}).observe(asideLabelElement);
|
||||
|
||||
function setAsideParent() {
|
||||
const { clientWidth } = window.document.documentElement;
|
||||
const MEDIUM_WIDTH = 768;
|
||||
if (clientWidth >= MEDIUM_WIDTH) {
|
||||
asideElement.parentElement !== bodyElement &&
|
||||
bodyElement.append(asideElement);
|
||||
} else {
|
||||
asideElement.parentElement !== mainElement &&
|
||||
mainElement.append(asideElement);
|
||||
}
|
||||
}
|
||||
|
||||
setAsideParent();
|
||||
|
||||
window.addEventListener("resize", setAsideParent);
|
||||
}
|
||||
initFrameSelectors();
|
||||
|
||||
signals.createRoot(() => {
|
||||
const brk = new BrkClient("/");
|
||||
const owner = signals.getOwner();
|
||||
|
||||
console.log(`VERSION = ${brk.VERSION}`);
|
||||
|
||||
function initDark() {
|
||||
const preferredColorSchemeMatchMedia = window.matchMedia(
|
||||
"(prefers-color-scheme: dark)",
|
||||
);
|
||||
const dark = signals.createSignal(preferredColorSchemeMatchMedia.matches);
|
||||
preferredColorSchemeMatchMedia.addEventListener("change", ({ matches }) => {
|
||||
dark.set(matches);
|
||||
});
|
||||
return dark;
|
||||
}
|
||||
const dark = initDark();
|
||||
|
||||
const qrcode = signals.createSignal(/** @type {string | null} */ (null));
|
||||
|
||||
signals.createEffect(webSockets.kraken1dCandle.latest, (latest) => {
|
||||
if (latest) {
|
||||
console.log("close:", latest.close);
|
||||
window.document.title = `${latest.close.toLocaleString("en-us")} | ${window.location.host}`;
|
||||
}
|
||||
});
|
||||
|
||||
// function createLastHeightResource() {
|
||||
// const lastHeight = signals.createSignal(0);
|
||||
// function fetchLastHeight() {
|
||||
// utils.api.fetchLast(
|
||||
// (h) => {
|
||||
// lastHeight.set(h);
|
||||
// },
|
||||
// /** @satisfies {Height} */ (5),
|
||||
// "height",
|
||||
// );
|
||||
// }
|
||||
// fetchLastHeight();
|
||||
// setInterval(fetchLastHeight, 10_000);
|
||||
// return lastHeight;
|
||||
// }
|
||||
// const lastHeight = createLastHeightResource();
|
||||
|
||||
const colors = createColors(dark);
|
||||
|
||||
const options = initOptions({
|
||||
colors,
|
||||
signals,
|
||||
brk,
|
||||
qrcode,
|
||||
});
|
||||
|
||||
window.addEventListener("popstate", (_event) => {
|
||||
const path = window.document.location.pathname.split("/").filter((v) => v);
|
||||
let folder = options.tree;
|
||||
|
||||
while (path.length) {
|
||||
const id = path.shift();
|
||||
const res = folder.find((v) => id === formatters.stringToId(v.name));
|
||||
if (!res) throw "Option not found";
|
||||
if (path.length >= 1) {
|
||||
if (!("tree" in res)) {
|
||||
throw "Unreachable";
|
||||
}
|
||||
folder = res.tree;
|
||||
} else {
|
||||
if ("tree" in res) {
|
||||
throw "Unreachable";
|
||||
}
|
||||
options.selected.set(res);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function initSelected() {
|
||||
let firstRun = true;
|
||||
function initSelectedFrame() {
|
||||
if (!firstRun) throw Error("Unreachable");
|
||||
firstRun = false;
|
||||
|
||||
const owner = signals.getOwner();
|
||||
|
||||
const chartOption = signals.createSignal(
|
||||
/** @type {ChartOption | null} */ (null),
|
||||
);
|
||||
const simOption = signals.createSignal(
|
||||
/** @type {SimulationOption | null} */ (null),
|
||||
);
|
||||
|
||||
let previousElement = /** @type {HTMLElement | undefined} */ (undefined);
|
||||
let firstTimeLoadingChart = true;
|
||||
let firstTimeLoadingTable = true;
|
||||
let firstTimeLoadingSimulation = true;
|
||||
let firstTimeLoadingExplorer = true;
|
||||
|
||||
signals.createEffect(options.selected, (option) => {
|
||||
/** @type {HTMLElement} */
|
||||
let element;
|
||||
|
||||
switch (option.kind) {
|
||||
case "explorer": {
|
||||
element = explorerElement;
|
||||
|
||||
if (firstTimeLoadingExplorer) {
|
||||
signals.runWithOwner(owner, () => initExplorer());
|
||||
}
|
||||
firstTimeLoadingExplorer = false;
|
||||
|
||||
break;
|
||||
}
|
||||
case "chart": {
|
||||
element = chartElement;
|
||||
|
||||
chartOption.set(option);
|
||||
|
||||
if (firstTimeLoadingChart) {
|
||||
signals.runWithOwner(owner, () =>
|
||||
initChart({
|
||||
colors,
|
||||
option: /** @type {Accessor<ChartOption>} */ (chartOption),
|
||||
brk,
|
||||
}),
|
||||
);
|
||||
}
|
||||
firstTimeLoadingChart = false;
|
||||
|
||||
break;
|
||||
}
|
||||
case "table": {
|
||||
element = tableElement;
|
||||
|
||||
if (firstTimeLoadingTable) {
|
||||
signals.runWithOwner(owner, () => initTable());
|
||||
}
|
||||
firstTimeLoadingTable = false;
|
||||
|
||||
break;
|
||||
}
|
||||
case "simulation": {
|
||||
element = simulationElement;
|
||||
|
||||
simOption.set(option);
|
||||
|
||||
if (firstTimeLoadingSimulation) {
|
||||
signals.runWithOwner(owner, () =>
|
||||
initSimulation({
|
||||
colors,
|
||||
}),
|
||||
);
|
||||
}
|
||||
firstTimeLoadingSimulation = false;
|
||||
|
||||
break;
|
||||
}
|
||||
case "url": {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (element !== previousElement) {
|
||||
if (previousElement) previousElement.hidden = true;
|
||||
element.hidden = false;
|
||||
}
|
||||
|
||||
if (!previousElement) {
|
||||
replaceHistory({ pathname: option.path });
|
||||
}
|
||||
|
||||
previousElement = element;
|
||||
});
|
||||
}
|
||||
|
||||
function createMobileSwitchEffect() {
|
||||
let firstRun = true;
|
||||
signals.createEffect(options.selected, () => {
|
||||
if (!firstRun && !isHidden(asideLabelElement)) {
|
||||
asideLabelElement.click();
|
||||
}
|
||||
firstRun = false;
|
||||
});
|
||||
}
|
||||
createMobileSwitchEffect();
|
||||
|
||||
onFirstIntersection(asideElement, () =>
|
||||
signals.runWithOwner(owner, initSelectedFrame),
|
||||
);
|
||||
}
|
||||
initSelected();
|
||||
|
||||
onFirstIntersection(navElement, async () => {
|
||||
options.parent.set(navElement);
|
||||
|
||||
const option = options.selected();
|
||||
if (!option) throw "Selected should be set by now";
|
||||
const path = [...option.path];
|
||||
|
||||
/** @type {HTMLUListElement | null} */
|
||||
let ul = /** @type {any} */ (null);
|
||||
async function getFirstChild() {
|
||||
try {
|
||||
ul = /** @type {HTMLUListElement} */ (navElement.firstElementChild);
|
||||
await next();
|
||||
if (!ul) {
|
||||
await getFirstChild();
|
||||
}
|
||||
} catch (_) {
|
||||
await next();
|
||||
await getFirstChild();
|
||||
}
|
||||
}
|
||||
await getFirstChild();
|
||||
if (!ul) throw Error("Unreachable");
|
||||
|
||||
while (path.length > 1) {
|
||||
const name = path.shift();
|
||||
if (!name) throw "Unreachable";
|
||||
/** @type {HTMLDetailsElement[]} */
|
||||
let detailsList = [];
|
||||
while (!detailsList.length) {
|
||||
detailsList = Array.from(ul.querySelectorAll(":scope > li > details"));
|
||||
if (!detailsList.length) {
|
||||
await next();
|
||||
}
|
||||
}
|
||||
const details = detailsList.find((s) => s.dataset.name == name);
|
||||
if (!details) return;
|
||||
details.open = true;
|
||||
ul = null;
|
||||
while (!ul) {
|
||||
const uls = /** @type {HTMLUListElement[]} */ (
|
||||
Array.from(details.querySelectorAll(":scope > ul"))
|
||||
);
|
||||
if (!uls.length) {
|
||||
await next();
|
||||
} else if (uls.length > 1) {
|
||||
throw "Shouldn't be possible";
|
||||
} else {
|
||||
ul = /** @type {HTMLUListElement} */ (uls.pop());
|
||||
}
|
||||
}
|
||||
}
|
||||
/** @type {HTMLAnchorElement[]} */
|
||||
let anchors = [];
|
||||
while (!anchors.length) {
|
||||
anchors = Array.from(ul.querySelectorAll(":scope > li > a"));
|
||||
if (!anchors.length) {
|
||||
await next();
|
||||
}
|
||||
}
|
||||
anchors
|
||||
.find((a) => a.getAttribute("href") == window.document.location.pathname)
|
||||
?.scrollIntoView({
|
||||
behavior: "instant",
|
||||
block: "center",
|
||||
});
|
||||
});
|
||||
|
||||
onFirstIntersection(searchElement, () => {
|
||||
console.log("search: init");
|
||||
|
||||
const haystack = options.list.map((option) => option.title);
|
||||
|
||||
const RESULTS_PER_PAGE = 100;
|
||||
|
||||
/**
|
||||
* @param {uFuzzy.SearchResult} searchResult
|
||||
* @param {number} pageIndex
|
||||
*/
|
||||
function computeResultPage(searchResult, pageIndex) {
|
||||
/** @type {{ option: Option, title: string }[]} */
|
||||
let list = [];
|
||||
|
||||
let [indexes, _info, order] = searchResult || [null, null, null];
|
||||
|
||||
const minIndex = pageIndex * RESULTS_PER_PAGE;
|
||||
|
||||
if (indexes?.length) {
|
||||
const maxIndex = Math.min(
|
||||
(order || indexes).length - 1,
|
||||
minIndex + RESULTS_PER_PAGE - 1,
|
||||
);
|
||||
|
||||
list = Array(maxIndex - minIndex + 1);
|
||||
|
||||
for (let i = minIndex; i <= maxIndex; i++) {
|
||||
let index = indexes[i];
|
||||
|
||||
const title = haystack[index];
|
||||
|
||||
list[i % 100] = {
|
||||
option: options.list[index],
|
||||
title,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/** @type {uFuzzy.Options} */
|
||||
const config = {
|
||||
intraIns: Infinity,
|
||||
intraChars: `[a-z\d' ]`,
|
||||
};
|
||||
|
||||
const fuzzyMultiInsert = /** @type {uFuzzy} */ (
|
||||
ufuzzy({
|
||||
intraIns: 1,
|
||||
})
|
||||
);
|
||||
const fuzzyMultiInsertFuzzier = /** @type {uFuzzy} */ (ufuzzy(config));
|
||||
const fuzzySingleError = /** @type {uFuzzy} */ (
|
||||
ufuzzy({
|
||||
intraMode: 1,
|
||||
...config,
|
||||
})
|
||||
);
|
||||
const fuzzySingleErrorFuzzier = /** @type {uFuzzy} */ (
|
||||
ufuzzy({
|
||||
intraMode: 1,
|
||||
...config,
|
||||
})
|
||||
);
|
||||
|
||||
/** @type {VoidFunction | undefined} */
|
||||
let dispose;
|
||||
|
||||
function inputEvent() {
|
||||
signals.createRoot((_dispose) => {
|
||||
const needle = /** @type {string} */ (searchInput.value);
|
||||
|
||||
dispose?.();
|
||||
|
||||
dispose = _dispose;
|
||||
|
||||
searchResultsElement.scrollTo({
|
||||
top: 0,
|
||||
});
|
||||
|
||||
if (!needle) {
|
||||
searchResultsElement.innerHTML = "";
|
||||
return;
|
||||
}
|
||||
|
||||
const outOfOrder = 5;
|
||||
const infoThresh = 5_000;
|
||||
|
||||
let result = fuzzyMultiInsert?.search(
|
||||
haystack,
|
||||
needle,
|
||||
undefined,
|
||||
infoThresh,
|
||||
);
|
||||
|
||||
if (!result?.[0]?.length || !result?.[1]) {
|
||||
result = fuzzyMultiInsert?.search(
|
||||
haystack,
|
||||
needle,
|
||||
outOfOrder,
|
||||
infoThresh,
|
||||
);
|
||||
}
|
||||
|
||||
if (!result?.[0]?.length || !result?.[1]) {
|
||||
result = fuzzySingleError?.search(
|
||||
haystack,
|
||||
needle,
|
||||
outOfOrder,
|
||||
infoThresh,
|
||||
);
|
||||
}
|
||||
|
||||
if (!result?.[0]?.length || !result?.[1]) {
|
||||
result = fuzzySingleErrorFuzzier?.search(
|
||||
haystack,
|
||||
needle,
|
||||
outOfOrder,
|
||||
infoThresh,
|
||||
);
|
||||
}
|
||||
|
||||
if (!result?.[0]?.length || !result?.[1]) {
|
||||
result = fuzzyMultiInsertFuzzier?.search(
|
||||
haystack,
|
||||
needle,
|
||||
undefined,
|
||||
infoThresh,
|
||||
);
|
||||
}
|
||||
|
||||
if (!result?.[0]?.length || !result?.[1]) {
|
||||
result = fuzzyMultiInsertFuzzier?.search(
|
||||
haystack,
|
||||
needle,
|
||||
outOfOrder,
|
||||
infoThresh,
|
||||
);
|
||||
}
|
||||
|
||||
searchResultsElement.innerHTML = "";
|
||||
|
||||
const list = computeResultPage(result, 0);
|
||||
|
||||
list.forEach(({ option, title }) => {
|
||||
const li = window.document.createElement("li");
|
||||
searchResultsElement.appendChild(li);
|
||||
|
||||
const element = options.createOptionElement({
|
||||
option,
|
||||
name: title,
|
||||
qrcode,
|
||||
});
|
||||
|
||||
if (element) {
|
||||
li.append(element);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (searchInput.value) {
|
||||
inputEvent();
|
||||
}
|
||||
|
||||
searchInput.addEventListener("input", inputEvent);
|
||||
});
|
||||
|
||||
function initShare() {
|
||||
const shareDiv = getElementById("share-div");
|
||||
const shareContentDiv = getElementById("share-content-div");
|
||||
|
||||
shareDiv.addEventListener("click", () => {
|
||||
qrcode.set(null);
|
||||
});
|
||||
|
||||
shareContentDiv.addEventListener("click", (event) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
signals.runWithOwner(owner, () => {
|
||||
const imgQrcode = /** @type {HTMLImageElement} */ (
|
||||
getElementById("share-img")
|
||||
);
|
||||
|
||||
const anchor = /** @type {HTMLAnchorElement} */ (
|
||||
getElementById("share-anchor")
|
||||
);
|
||||
|
||||
signals.createEffect(qrcode, (qrcode) => {
|
||||
if (!qrcode) {
|
||||
shareDiv.hidden = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const href = qrcode;
|
||||
anchor.href = href;
|
||||
anchor.innerText =
|
||||
(href.startsWith("http")
|
||||
? href.split("//").at(-1)
|
||||
: href.split(":").at(-1)) || "";
|
||||
|
||||
imgQrcode.src =
|
||||
leanQr.generate(/** @type {any} */ (href))?.toDataURL({
|
||||
// @ts-ignore
|
||||
padX: 0,
|
||||
padY: 0,
|
||||
}) || "";
|
||||
|
||||
shareDiv.hidden = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
initShare();
|
||||
|
||||
function initDesktopResizeBar() {
|
||||
const resizeBar = getElementById("resize-bar");
|
||||
let resize = false;
|
||||
let startingWidth = 0;
|
||||
let startingClientX = 0;
|
||||
|
||||
const barWidthLocalStorageKey = "bar-width";
|
||||
|
||||
/**
|
||||
* @param {number | null} width
|
||||
*/
|
||||
function setBarWidth(width) {
|
||||
// TODO: Check if should be a signal ??
|
||||
try {
|
||||
if (typeof width === "number") {
|
||||
mainElement.style.width = `${width}px`;
|
||||
writeToStorage(barWidthLocalStorageKey, String(width));
|
||||
} else {
|
||||
mainElement.style.width = style.getPropertyValue(
|
||||
"--default-main-width",
|
||||
);
|
||||
removeStored(barWidthLocalStorageKey);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
function mouseMoveEvent(event) {
|
||||
if (resize) {
|
||||
setBarWidth(startingWidth + (event.clientX - startingClientX));
|
||||
}
|
||||
}
|
||||
|
||||
resizeBar.addEventListener("mousedown", (event) => {
|
||||
startingClientX = event.clientX;
|
||||
startingWidth = mainElement.clientWidth;
|
||||
resize = true;
|
||||
window.document.documentElement.dataset.resize = "";
|
||||
window.addEventListener("mousemove", mouseMoveEvent);
|
||||
});
|
||||
|
||||
resizeBar.addEventListener("dblclick", () => {
|
||||
setBarWidth(null);
|
||||
});
|
||||
|
||||
const setResizeFalse = () => {
|
||||
resize = false;
|
||||
delete window.document.documentElement.dataset.resize;
|
||||
window.removeEventListener("mousemove", mouseMoveEvent);
|
||||
};
|
||||
window.addEventListener("mouseup", setResizeFalse);
|
||||
window.addEventListener("mouseleave", setResizeFalse);
|
||||
}
|
||||
initDesktopResizeBar();
|
||||
});
|
||||
1
website/scripts/modules
Symbolic link
1
website/scripts/modules
Symbolic link
@@ -0,0 +1 @@
|
||||
../../modules
|
||||
821
website/scripts/options/chain.js
Normal file
821
website/scripts/options/chain.js
Normal file
@@ -0,0 +1,821 @@
|
||||
/** Chain section builder - typed tree-based patterns */
|
||||
|
||||
import { Unit } from "../utils/units.js";
|
||||
|
||||
/**
|
||||
* Create Chain section
|
||||
* @param {PartialContext} ctx
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
export function createChainSection(ctx) {
|
||||
const {
|
||||
colors,
|
||||
brk,
|
||||
line,
|
||||
dots,
|
||||
createPriceLine,
|
||||
fromSizePattern,
|
||||
fromFullnessPattern,
|
||||
fromFeeRatePattern,
|
||||
fromCoinbasePattern,
|
||||
fromValuePattern,
|
||||
fromBlockCountWithUnit,
|
||||
fromIntervalPattern,
|
||||
fromSupplyPattern,
|
||||
} = ctx;
|
||||
const {
|
||||
blocks,
|
||||
transactions,
|
||||
pools,
|
||||
inputs,
|
||||
outputs,
|
||||
market,
|
||||
scripts,
|
||||
supply,
|
||||
} = brk.metrics;
|
||||
|
||||
// Build pools tree dynamically
|
||||
const poolEntries = Object.entries(pools.vecs);
|
||||
const poolsTree = poolEntries.map(([key, pool]) => {
|
||||
const poolName =
|
||||
brk.POOL_ID_TO_POOL_NAME[
|
||||
/** @type {keyof typeof brk.POOL_ID_TO_POOL_NAME} */ (key.toLowerCase())
|
||||
] || key;
|
||||
return {
|
||||
name: poolName,
|
||||
tree: [
|
||||
{
|
||||
name: "Dominance",
|
||||
title: `Mining Dominance of ${poolName}`,
|
||||
bottom: [
|
||||
line({
|
||||
metric: pool._24hDominance,
|
||||
name: "24h",
|
||||
color: colors.orange,
|
||||
unit: Unit.percentage,
|
||||
defaultActive: false,
|
||||
}),
|
||||
line({
|
||||
metric: pool._1wDominance,
|
||||
name: "1w",
|
||||
color: colors.red,
|
||||
unit: Unit.percentage,
|
||||
defaultActive: false,
|
||||
}),
|
||||
line({
|
||||
metric: pool._1mDominance,
|
||||
name: "1m",
|
||||
unit: Unit.percentage,
|
||||
}),
|
||||
line({
|
||||
metric: pool._1yDominance,
|
||||
name: "1y",
|
||||
color: colors.lime,
|
||||
unit: Unit.percentage,
|
||||
defaultActive: false,
|
||||
}),
|
||||
line({
|
||||
metric: pool.dominance,
|
||||
name: "all time",
|
||||
color: colors.teal,
|
||||
unit: Unit.percentage,
|
||||
defaultActive: false,
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Blocks mined",
|
||||
title: `Blocks mined by ${poolName}`,
|
||||
bottom: [
|
||||
line({
|
||||
metric: pool.blocksMined.sum,
|
||||
name: "Sum",
|
||||
unit: Unit.count,
|
||||
}),
|
||||
line({
|
||||
metric: pool.blocksMined.cumulative,
|
||||
name: "Cumulative",
|
||||
color: colors.blue,
|
||||
unit: Unit.count,
|
||||
}),
|
||||
line({
|
||||
metric: pool._1wBlocksMined,
|
||||
name: "1w Sum",
|
||||
color: colors.red,
|
||||
unit: Unit.count,
|
||||
defaultActive: false,
|
||||
}),
|
||||
line({
|
||||
metric: pool._1mBlocksMined,
|
||||
name: "1m Sum",
|
||||
color: colors.pink,
|
||||
unit: Unit.count,
|
||||
defaultActive: false,
|
||||
}),
|
||||
line({
|
||||
metric: pool._1yBlocksMined,
|
||||
name: "1y Sum",
|
||||
color: colors.purple,
|
||||
unit: Unit.count,
|
||||
defaultActive: false,
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Rewards",
|
||||
title: `Rewards collected by ${poolName}`,
|
||||
bottom: [
|
||||
...fromValuePattern(
|
||||
pool.coinbase,
|
||||
"coinbase",
|
||||
colors.orange,
|
||||
colors.red,
|
||||
),
|
||||
...fromValuePattern(
|
||||
pool.subsidy,
|
||||
"subsidy",
|
||||
colors.lime,
|
||||
colors.emerald,
|
||||
),
|
||||
...fromValuePattern(pool.fee, "fee", colors.cyan, colors.indigo),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Days since block",
|
||||
title: `Days since ${poolName} mined a block`,
|
||||
bottom: [
|
||||
line({
|
||||
metric: pool.daysSinceBlock,
|
||||
name: "Since block",
|
||||
unit: Unit.days,
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
name: "Chain",
|
||||
tree: [
|
||||
// Block
|
||||
{
|
||||
name: "Block",
|
||||
tree: [
|
||||
{
|
||||
name: "Count",
|
||||
title: "Block Count",
|
||||
bottom: [
|
||||
...fromBlockCountWithUnit(
|
||||
blocks.count.blockCount,
|
||||
"Block",
|
||||
Unit.count,
|
||||
),
|
||||
line({
|
||||
metric: blocks.count.blockCountTarget,
|
||||
name: "Target",
|
||||
color: colors.gray,
|
||||
unit: Unit.count,
|
||||
options: { lineStyle: 4 },
|
||||
}),
|
||||
line({
|
||||
metric: blocks.count._1wBlockCount,
|
||||
name: "1w sum",
|
||||
color: colors.red,
|
||||
unit: Unit.count,
|
||||
defaultActive: false,
|
||||
}),
|
||||
line({
|
||||
metric: blocks.count._1mBlockCount,
|
||||
name: "1m sum",
|
||||
color: colors.pink,
|
||||
unit: Unit.count,
|
||||
defaultActive: false,
|
||||
}),
|
||||
line({
|
||||
metric: blocks.count._1yBlockCount,
|
||||
name: "1y sum",
|
||||
color: colors.purple,
|
||||
unit: Unit.count,
|
||||
defaultActive: false,
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Interval",
|
||||
title: "Block Interval",
|
||||
bottom: [
|
||||
...fromIntervalPattern(blocks.interval, "Interval", Unit.secs),
|
||||
createPriceLine({ unit: Unit.secs, name: "Target", number: 600 }),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Size",
|
||||
title: "Block Size",
|
||||
bottom: [
|
||||
...fromSizePattern(blocks.size, "Size", Unit.bytes),
|
||||
...fromFullnessPattern(blocks.vbytes, "Vbytes", Unit.vb),
|
||||
...fromFullnessPattern(blocks.weight, "Weight", Unit.wu),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Transaction
|
||||
{
|
||||
name: "Transaction",
|
||||
tree: [
|
||||
{
|
||||
name: "Count",
|
||||
title: "Transaction Count",
|
||||
bottom: fromFullnessPattern(
|
||||
transactions.count.txCount,
|
||||
"Count",
|
||||
Unit.count,
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Volume",
|
||||
title: "Transaction Volume",
|
||||
bottom: [
|
||||
line({
|
||||
metric: transactions.volume.sentSum.sats,
|
||||
name: "Sent",
|
||||
unit: Unit.sats,
|
||||
}),
|
||||
line({
|
||||
metric: transactions.volume.sentSum.bitcoin,
|
||||
name: "Sent",
|
||||
unit: Unit.btc,
|
||||
}),
|
||||
line({
|
||||
metric: transactions.volume.sentSum.dollars,
|
||||
name: "Sent",
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
line({
|
||||
metric: transactions.volume.annualizedVolume.sats,
|
||||
name: "annualized",
|
||||
color: colors.red,
|
||||
unit: Unit.sats,
|
||||
defaultActive: false,
|
||||
}),
|
||||
line({
|
||||
metric: transactions.volume.annualizedVolume.bitcoin,
|
||||
name: "annualized",
|
||||
color: colors.red,
|
||||
unit: Unit.btc,
|
||||
defaultActive: false,
|
||||
}),
|
||||
line({
|
||||
metric: transactions.volume.annualizedVolume.dollars,
|
||||
name: "annualized",
|
||||
color: colors.lime,
|
||||
unit: Unit.usd,
|
||||
defaultActive: false,
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Size",
|
||||
title: "Transaction Size",
|
||||
bottom: [
|
||||
...fromFeeRatePattern(
|
||||
transactions.size.weight,
|
||||
"weight",
|
||||
Unit.wu,
|
||||
),
|
||||
...fromFeeRatePattern(transactions.size.vsize, "vsize", Unit.vb),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Versions",
|
||||
title: "Transaction Versions",
|
||||
bottom: [
|
||||
...fromBlockCountWithUnit(
|
||||
transactions.versions.v1,
|
||||
"v1",
|
||||
Unit.count,
|
||||
colors.orange,
|
||||
colors.red,
|
||||
),
|
||||
...fromBlockCountWithUnit(
|
||||
transactions.versions.v2,
|
||||
"v2",
|
||||
Unit.count,
|
||||
colors.cyan,
|
||||
colors.blue,
|
||||
),
|
||||
...fromBlockCountWithUnit(
|
||||
transactions.versions.v3,
|
||||
"v3",
|
||||
Unit.count,
|
||||
colors.lime,
|
||||
colors.green,
|
||||
),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Velocity",
|
||||
title: "Transactions Velocity",
|
||||
bottom: [
|
||||
line({
|
||||
metric: supply.velocity.btc,
|
||||
name: "bitcoin",
|
||||
unit: Unit.ratio,
|
||||
}),
|
||||
line({
|
||||
metric: supply.velocity.usd,
|
||||
name: "dollars",
|
||||
color: colors.emerald,
|
||||
unit: Unit.ratio,
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Speed",
|
||||
title: "Transactions Per Second",
|
||||
bottom: [
|
||||
line({
|
||||
metric: transactions.volume.txPerSec,
|
||||
name: "Transactions",
|
||||
unit: Unit.perSec,
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Input
|
||||
{
|
||||
name: "Input",
|
||||
tree: [
|
||||
{
|
||||
name: "Count",
|
||||
title: "Transaction Input Count",
|
||||
bottom: [...fromSizePattern(inputs.count, "Input", Unit.count)],
|
||||
},
|
||||
{
|
||||
name: "Speed",
|
||||
title: "Inputs Per Second",
|
||||
bottom: [
|
||||
line({
|
||||
metric: transactions.volume.inputsPerSec,
|
||||
name: "Inputs",
|
||||
unit: Unit.perSec,
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Output
|
||||
{
|
||||
name: "Output",
|
||||
tree: [
|
||||
{
|
||||
name: "Count",
|
||||
title: "Transaction Output Count",
|
||||
bottom: [
|
||||
...fromSizePattern(
|
||||
outputs.count.totalCount,
|
||||
"Output",
|
||||
Unit.count,
|
||||
),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Speed",
|
||||
title: "Outputs Per Second",
|
||||
bottom: [
|
||||
line({
|
||||
metric: transactions.volume.outputsPerSec,
|
||||
name: "Outputs",
|
||||
unit: Unit.perSec,
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
name: "UTXO",
|
||||
tree: [
|
||||
{
|
||||
name: "Count",
|
||||
title: "UTXO Count",
|
||||
bottom: [
|
||||
line({
|
||||
metric: outputs.count.utxoCount,
|
||||
name: "Count",
|
||||
unit: Unit.count,
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Coinbase
|
||||
{
|
||||
name: "Coinbase",
|
||||
title: "Coinbase Rewards",
|
||||
bottom: fromCoinbasePattern(blocks.rewards.coinbase, "Coinbase"),
|
||||
},
|
||||
|
||||
// Subsidy
|
||||
{
|
||||
name: "Subsidy",
|
||||
title: "Block Subsidy",
|
||||
bottom: [
|
||||
...fromCoinbasePattern(blocks.rewards.subsidy, "Subsidy"),
|
||||
line({
|
||||
metric: blocks.rewards.subsidyDominance,
|
||||
name: "Dominance",
|
||||
color: colors.purple,
|
||||
unit: Unit.percentage,
|
||||
defaultActive: false,
|
||||
}),
|
||||
],
|
||||
},
|
||||
|
||||
// Fee
|
||||
{
|
||||
name: "Fee",
|
||||
tree: [
|
||||
{
|
||||
name: "Total",
|
||||
title: "Transaction Fees",
|
||||
bottom: [
|
||||
line({
|
||||
metric: transactions.fees.fee.sats.sum,
|
||||
name: "Sum",
|
||||
unit: Unit.sats,
|
||||
}),
|
||||
line({
|
||||
metric: transactions.fees.fee.sats.cumulative,
|
||||
name: "Cumulative",
|
||||
color: colors.blue,
|
||||
unit: Unit.sats,
|
||||
defaultActive: false,
|
||||
}),
|
||||
line({
|
||||
metric: transactions.fees.fee.bitcoin.sum,
|
||||
name: "Sum",
|
||||
unit: Unit.btc,
|
||||
}),
|
||||
line({
|
||||
metric: transactions.fees.fee.bitcoin.cumulative,
|
||||
name: "Cumulative",
|
||||
color: colors.blue,
|
||||
unit: Unit.btc,
|
||||
defaultActive: false,
|
||||
}),
|
||||
line({
|
||||
metric: transactions.fees.fee.dollars.sum,
|
||||
name: "Sum",
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
line({
|
||||
metric: transactions.fees.fee.dollars.cumulative,
|
||||
name: "Cumulative",
|
||||
color: colors.blue,
|
||||
unit: Unit.usd,
|
||||
defaultActive: false,
|
||||
}),
|
||||
line({
|
||||
metric: blocks.rewards.feeDominance,
|
||||
name: "Dominance",
|
||||
color: colors.purple,
|
||||
unit: Unit.percentage,
|
||||
defaultActive: false,
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Rate",
|
||||
title: "Fee Rate",
|
||||
bottom: [
|
||||
line({
|
||||
metric: transactions.fees.feeRate.median,
|
||||
name: "Median",
|
||||
color: colors.purple,
|
||||
unit: Unit.feeRate,
|
||||
}),
|
||||
line({
|
||||
metric: transactions.fees.feeRate.average,
|
||||
name: "Average",
|
||||
color: colors.blue,
|
||||
unit: Unit.feeRate,
|
||||
defaultActive: false,
|
||||
}),
|
||||
line({
|
||||
metric: transactions.fees.feeRate.min,
|
||||
name: "Min",
|
||||
color: colors.red,
|
||||
unit: Unit.feeRate,
|
||||
defaultActive: false,
|
||||
}),
|
||||
line({
|
||||
metric: transactions.fees.feeRate.max,
|
||||
name: "Max",
|
||||
color: colors.green,
|
||||
unit: Unit.feeRate,
|
||||
defaultActive: false,
|
||||
}),
|
||||
line({
|
||||
metric: transactions.fees.feeRate.pct10,
|
||||
name: "pct10",
|
||||
color: colors.rose,
|
||||
unit: Unit.feeRate,
|
||||
defaultActive: false,
|
||||
}),
|
||||
line({
|
||||
metric: transactions.fees.feeRate.pct25,
|
||||
name: "pct25",
|
||||
color: colors.pink,
|
||||
unit: Unit.feeRate,
|
||||
defaultActive: false,
|
||||
}),
|
||||
line({
|
||||
metric: transactions.fees.feeRate.pct75,
|
||||
name: "pct75",
|
||||
color: colors.violet,
|
||||
unit: Unit.feeRate,
|
||||
defaultActive: false,
|
||||
}),
|
||||
line({
|
||||
metric: transactions.fees.feeRate.pct90,
|
||||
name: "pct90",
|
||||
color: colors.fuchsia,
|
||||
unit: Unit.feeRate,
|
||||
defaultActive: false,
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Mining
|
||||
{
|
||||
name: "Mining",
|
||||
tree: [
|
||||
{
|
||||
name: "Hashrate",
|
||||
title: "Network Hashrate",
|
||||
bottom: [
|
||||
dots({
|
||||
metric: blocks.mining.hashRate,
|
||||
name: "Hashrate",
|
||||
unit: Unit.hashRate,
|
||||
}),
|
||||
line({
|
||||
metric: blocks.mining.hashRate1wSma,
|
||||
name: "1w SMA",
|
||||
color: colors.red,
|
||||
unit: Unit.hashRate,
|
||||
defaultActive: false,
|
||||
}),
|
||||
line({
|
||||
metric: blocks.mining.hashRate1mSma,
|
||||
name: "1m SMA",
|
||||
color: colors.orange,
|
||||
unit: Unit.hashRate,
|
||||
defaultActive: false,
|
||||
}),
|
||||
line({
|
||||
metric: blocks.mining.hashRate2mSma,
|
||||
name: "2m SMA",
|
||||
color: colors.yellow,
|
||||
unit: Unit.hashRate,
|
||||
defaultActive: false,
|
||||
}),
|
||||
line({
|
||||
metric: blocks.mining.hashRate1ySma,
|
||||
name: "1y SMA",
|
||||
color: colors.lime,
|
||||
unit: Unit.hashRate,
|
||||
defaultActive: false,
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Difficulty",
|
||||
title: "Network Difficulty",
|
||||
bottom: [
|
||||
line({
|
||||
metric: blocks.difficulty.raw,
|
||||
name: "Difficulty",
|
||||
unit: Unit.difficulty,
|
||||
}),
|
||||
line({
|
||||
metric: blocks.difficulty.adjustment,
|
||||
name: "Adjustment",
|
||||
color: colors.orange,
|
||||
unit: Unit.percentage,
|
||||
defaultActive: false,
|
||||
}),
|
||||
line({
|
||||
metric: blocks.difficulty.asHash,
|
||||
name: "As hash",
|
||||
color: colors.default,
|
||||
unit: Unit.hashRate,
|
||||
defaultActive: false,
|
||||
options: { lineStyle: 1 },
|
||||
}),
|
||||
line({
|
||||
metric: blocks.difficulty.blocksBeforeNextAdjustment,
|
||||
name: "Blocks until adj.",
|
||||
color: colors.indigo,
|
||||
unit: Unit.blocks,
|
||||
defaultActive: false,
|
||||
}),
|
||||
line({
|
||||
metric: blocks.difficulty.daysBeforeNextAdjustment,
|
||||
name: "Days until adj.",
|
||||
color: colors.purple,
|
||||
unit: Unit.days,
|
||||
defaultActive: false,
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Hash Price",
|
||||
title: "Hash Price",
|
||||
bottom: [
|
||||
line({
|
||||
metric: blocks.mining.hashPriceThs,
|
||||
name: "TH/s",
|
||||
color: colors.emerald,
|
||||
unit: Unit.usdPerThsPerDay,
|
||||
}),
|
||||
line({
|
||||
metric: blocks.mining.hashPricePhs,
|
||||
name: "PH/s",
|
||||
color: colors.emerald,
|
||||
unit: Unit.usdPerPhsPerDay,
|
||||
}),
|
||||
line({
|
||||
metric: blocks.mining.hashPriceRebound,
|
||||
name: "Rebound",
|
||||
color: colors.yellow,
|
||||
unit: Unit.percentage,
|
||||
}),
|
||||
line({
|
||||
metric: blocks.mining.hashPriceThsMin,
|
||||
name: "TH/s Min",
|
||||
color: colors.red,
|
||||
unit: Unit.usdPerThsPerDay,
|
||||
options: { lineStyle: 1 },
|
||||
}),
|
||||
line({
|
||||
metric: blocks.mining.hashPricePhsMin,
|
||||
name: "PH/s Min",
|
||||
color: colors.red,
|
||||
unit: Unit.usdPerPhsPerDay,
|
||||
options: { lineStyle: 1 },
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Hash Value",
|
||||
title: "Hash Value",
|
||||
bottom: [
|
||||
line({
|
||||
metric: blocks.mining.hashValueThs,
|
||||
name: "TH/s",
|
||||
color: colors.orange,
|
||||
unit: Unit.satsPerThsPerDay,
|
||||
}),
|
||||
line({
|
||||
metric: blocks.mining.hashValuePhs,
|
||||
name: "PH/s",
|
||||
color: colors.orange,
|
||||
unit: Unit.satsPerPhsPerDay,
|
||||
}),
|
||||
line({
|
||||
metric: blocks.mining.hashValueRebound,
|
||||
name: "Rebound",
|
||||
color: colors.yellow,
|
||||
unit: Unit.percentage,
|
||||
}),
|
||||
line({
|
||||
metric: blocks.mining.hashValueThsMin,
|
||||
name: "TH/s Min",
|
||||
color: colors.red,
|
||||
unit: Unit.satsPerThsPerDay,
|
||||
options: { lineStyle: 1 },
|
||||
}),
|
||||
line({
|
||||
metric: blocks.mining.hashValuePhsMin,
|
||||
name: "PH/s Min",
|
||||
color: colors.red,
|
||||
unit: Unit.satsPerPhsPerDay,
|
||||
options: { lineStyle: 1 },
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Halving",
|
||||
title: "Halving Info",
|
||||
bottom: [
|
||||
line({
|
||||
metric: blocks.halving.blocksBeforeNextHalving,
|
||||
name: "Blocks until halving",
|
||||
unit: Unit.blocks,
|
||||
}),
|
||||
line({
|
||||
metric: blocks.halving.daysBeforeNextHalving,
|
||||
name: "Days until halving",
|
||||
color: colors.orange,
|
||||
unit: Unit.days,
|
||||
}),
|
||||
line({
|
||||
metric: blocks.halving.epoch,
|
||||
name: "Halving epoch",
|
||||
color: colors.purple,
|
||||
unit: Unit.epoch,
|
||||
defaultActive: false,
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Puell Multiple",
|
||||
title: "Puell Multiple",
|
||||
bottom: [
|
||||
line({
|
||||
metric: market.indicators.puellMultiple,
|
||||
name: "Puell Multiple",
|
||||
unit: Unit.ratio,
|
||||
}),
|
||||
createPriceLine({ unit: Unit.ratio, number: 1 }),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Pools
|
||||
{
|
||||
name: "Pools",
|
||||
tree: poolsTree,
|
||||
},
|
||||
|
||||
// Unspendable
|
||||
{
|
||||
name: "Unspendable",
|
||||
tree: [
|
||||
{
|
||||
name: "Supply",
|
||||
title: "Unspendable Supply",
|
||||
bottom: fromValuePattern(supply.burned.unspendable, "Supply"),
|
||||
},
|
||||
{
|
||||
name: "OP_RETURN",
|
||||
tree: [
|
||||
{
|
||||
name: "Outputs",
|
||||
title: "OP_RETURN Outputs",
|
||||
bottom: fromFullnessPattern(
|
||||
scripts.count.opreturn,
|
||||
"Count",
|
||||
Unit.count,
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Supply",
|
||||
title: "OP_RETURN Supply",
|
||||
bottom: fromValuePattern(supply.burned.opreturn, "Supply"),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Supply
|
||||
{
|
||||
name: "Supply",
|
||||
title: "Circulating Supply",
|
||||
bottom: fromSupplyPattern(supply.circulating, "Supply"),
|
||||
},
|
||||
|
||||
// Inflation
|
||||
{
|
||||
name: "Inflation",
|
||||
title: "Inflation Rate",
|
||||
bottom: [
|
||||
line({
|
||||
metric: supply.inflation,
|
||||
name: "Rate",
|
||||
unit: Unit.percentage,
|
||||
}),
|
||||
],
|
||||
},
|
||||
|
||||
// Unclaimed Rewards
|
||||
{
|
||||
name: "Unclaimed Rewards",
|
||||
title: "Unclaimed Block Rewards",
|
||||
bottom: fromValuePattern(blocks.rewards.unclaimedRewards, "Unclaimed"),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
400
website/scripts/options/cohorts/address.js
Normal file
400
website/scripts/options/cohorts/address.js
Normal file
@@ -0,0 +1,400 @@
|
||||
/**
|
||||
* Address cohort folder builder
|
||||
* Creates option trees for address-based cohorts (has addrCount)
|
||||
* Address cohorts use _0satsPattern which has CostBasisPattern (no percentiles)
|
||||
*/
|
||||
|
||||
import { Unit } from "../../utils/units.js";
|
||||
import {
|
||||
createSingleSupplySeries,
|
||||
createGroupedSupplyTotalSeries,
|
||||
createGroupedSupplyInProfitSeries,
|
||||
createGroupedSupplyInLossSeries,
|
||||
createUtxoCountSeries,
|
||||
createAddressCountSeries,
|
||||
createRealizedPriceSeries,
|
||||
createRealizedPriceRatioSeries,
|
||||
} from "./shared.js";
|
||||
|
||||
/**
|
||||
* Create a cohort folder for address cohorts
|
||||
* Includes address count section (addrCount exists on AddressCohortObject)
|
||||
* @param {PartialContext} ctx
|
||||
* @param {AddressCohortObject | AddressCohortGroupObject} args
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
export function createAddressCohortFolder(ctx, args) {
|
||||
const list = "list" in args ? args.list : [args];
|
||||
const useGroupName = "list" in args;
|
||||
const isSingle = !("list" in args);
|
||||
|
||||
const title = args.title ? `${useGroupName ? "by" : "of"} ${args.title}` : "";
|
||||
|
||||
return {
|
||||
name: args.name || "all",
|
||||
tree: [
|
||||
// Supply section
|
||||
isSingle
|
||||
? {
|
||||
name: "supply",
|
||||
title: `Supply ${title}`,
|
||||
bottom: createSingleSupplySeries(
|
||||
ctx,
|
||||
/** @type {AddressCohortObject} */ (args),
|
||||
),
|
||||
}
|
||||
: {
|
||||
name: "supply",
|
||||
tree: [
|
||||
{
|
||||
name: "total",
|
||||
title: `Supply ${title}`,
|
||||
bottom: createGroupedSupplyTotalSeries(ctx, list),
|
||||
},
|
||||
{
|
||||
name: "in profit",
|
||||
title: `Supply In Profit ${title}`,
|
||||
bottom: createGroupedSupplyInProfitSeries(ctx, list),
|
||||
},
|
||||
{
|
||||
name: "in loss",
|
||||
title: `Supply In Loss ${title}`,
|
||||
bottom: createGroupedSupplyInLossSeries(ctx, list),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// UTXO count
|
||||
{
|
||||
name: "utxo count",
|
||||
title: `UTXO Count ${title}`,
|
||||
bottom: createUtxoCountSeries(ctx, list, useGroupName),
|
||||
},
|
||||
|
||||
// Address count (ADDRESS COHORTS ONLY - fully type safe!)
|
||||
{
|
||||
name: "address count",
|
||||
title: `Address Count ${title}`,
|
||||
bottom: createAddressCountSeries(ctx, list, useGroupName),
|
||||
},
|
||||
|
||||
// Realized section
|
||||
{
|
||||
name: "Realized",
|
||||
tree: [
|
||||
...(useGroupName
|
||||
? [
|
||||
{
|
||||
name: "Price",
|
||||
title: `Realized Price ${title}`,
|
||||
top: createRealizedPriceSeries(ctx, list),
|
||||
},
|
||||
{
|
||||
name: "Ratio",
|
||||
title: `Realized Price Ratio ${title}`,
|
||||
bottom: createRealizedPriceRatioSeries(ctx, list),
|
||||
},
|
||||
]
|
||||
: createRealizedPriceOptions(
|
||||
ctx,
|
||||
/** @type {AddressCohortObject} */ (args),
|
||||
title,
|
||||
)),
|
||||
{
|
||||
name: "capitalization",
|
||||
title: `Realized Capitalization ${title}`,
|
||||
bottom: createRealizedCapWithExtras(ctx, list, args, useGroupName),
|
||||
},
|
||||
...(!useGroupName
|
||||
? createRealizedPnlSection(
|
||||
ctx,
|
||||
/** @type {AddressCohortObject} */ (args),
|
||||
title,
|
||||
)
|
||||
: []),
|
||||
],
|
||||
},
|
||||
|
||||
// Unrealized section
|
||||
...createUnrealizedSection(ctx, list, useGroupName, title),
|
||||
|
||||
// Cost basis section (no percentiles for address cohorts)
|
||||
...createCostBasisSection(ctx, list, useGroupName, title),
|
||||
|
||||
// Activity section
|
||||
...createActivitySection(ctx, list, useGroupName, title),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create realized price options for single cohort
|
||||
* @param {PartialContext} ctx
|
||||
* @param {AddressCohortObject} args
|
||||
* @param {string} title
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
function createRealizedPriceOptions(ctx, args, title) {
|
||||
const { line } = ctx;
|
||||
const { tree, color } = args;
|
||||
|
||||
return [
|
||||
{
|
||||
name: "price",
|
||||
title: `Realized Price ${title}`,
|
||||
top: [
|
||||
line({
|
||||
metric: tree.realized.realizedPrice,
|
||||
name: "Realized",
|
||||
color,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create realized cap with extras
|
||||
* @param {PartialContext} ctx
|
||||
* @param {readonly AddressCohortObject[]} list
|
||||
* @param {AddressCohortObject | AddressCohortGroupObject} args
|
||||
* @param {boolean} useGroupName
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
function createRealizedCapWithExtras(ctx, list, args, useGroupName) {
|
||||
const { line, baseline, createPriceLine } = ctx;
|
||||
const isSingle = !("list" in args);
|
||||
|
||||
return list.flatMap(({ color, name, tree }) => [
|
||||
line({
|
||||
metric: tree.realized.realizedCap,
|
||||
name: useGroupName ? name : "Capitalization",
|
||||
color,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
...(isSingle
|
||||
? [
|
||||
baseline({
|
||||
metric: tree.realized.realizedCap30dDelta,
|
||||
name: "30d Change",
|
||||
unit: Unit.usd,
|
||||
defaultActive: false,
|
||||
}),
|
||||
createPriceLine({ unit: Unit.usd, defaultActive: false }),
|
||||
]
|
||||
: []),
|
||||
// RealizedPattern (address cohorts) doesn't have realizedCapRelToOwnMarketCap
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create realized PnL section for single cohort
|
||||
* @param {PartialContext} ctx
|
||||
* @param {AddressCohortObject} args
|
||||
* @param {string} title
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
function createRealizedPnlSection(ctx, args, title) {
|
||||
const { colors, line } = ctx;
|
||||
const { realized } = args.tree;
|
||||
|
||||
return [
|
||||
{
|
||||
name: "pnl",
|
||||
title: `Realized Profit And Loss ${title}`,
|
||||
bottom: [
|
||||
line({
|
||||
metric: realized.realizedProfit.sum,
|
||||
name: "Profit",
|
||||
color: colors.green,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
line({
|
||||
metric: realized.realizedLoss.sum,
|
||||
name: "Loss",
|
||||
color: colors.red,
|
||||
unit: Unit.usd,
|
||||
defaultActive: false,
|
||||
}),
|
||||
// RealizedPattern (address cohorts) doesn't have realizedProfitToLossRatio
|
||||
line({
|
||||
metric: realized.totalRealizedPnl,
|
||||
name: "Total",
|
||||
color: colors.default,
|
||||
defaultActive: false,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
line({
|
||||
metric: realized.negRealizedLoss.sum,
|
||||
name: "Negative Loss",
|
||||
color: colors.red,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
line({
|
||||
metric: realized.negRealizedLoss.cumulative,
|
||||
name: "Negative Loss",
|
||||
color: colors.red,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create unrealized section
|
||||
* @param {PartialContext} ctx
|
||||
* @param {readonly AddressCohortObject[]} list
|
||||
* @param {boolean} useGroupName
|
||||
* @param {string} title
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
function createUnrealizedSection(ctx, list, useGroupName, title) {
|
||||
const { colors, line, baseline } = ctx;
|
||||
|
||||
return [
|
||||
{
|
||||
name: "Unrealized",
|
||||
tree: [
|
||||
{
|
||||
name: "nupl",
|
||||
title: `Net Unrealized Profit/Loss ${title}`,
|
||||
bottom: list.flatMap(({ color, name, tree }) => [
|
||||
baseline({
|
||||
metric: tree.unrealized.netUnrealizedPnl,
|
||||
name: useGroupName ? name : "NUPL",
|
||||
color: useGroupName ? color : [colors.red, colors.green],
|
||||
unit: Unit.ratio,
|
||||
options: { baseValue: { price: 0 } },
|
||||
}),
|
||||
]),
|
||||
},
|
||||
{
|
||||
name: "profit",
|
||||
title: `Unrealized Profit ${title}`,
|
||||
bottom: list.flatMap(({ color, name, tree }) => [
|
||||
line({
|
||||
metric: tree.unrealized.unrealizedProfit,
|
||||
name: useGroupName ? name : "Profit",
|
||||
color,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
]),
|
||||
},
|
||||
{
|
||||
name: "loss",
|
||||
title: `Unrealized Loss ${title}`,
|
||||
bottom: list.flatMap(({ color, name, tree }) => [
|
||||
line({
|
||||
metric: tree.unrealized.unrealizedLoss,
|
||||
name: useGroupName ? name : "Loss",
|
||||
color,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
]),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create cost basis section (no percentiles for address cohorts)
|
||||
* @param {PartialContext} ctx
|
||||
* @param {readonly AddressCohortObject[]} list
|
||||
* @param {boolean} useGroupName
|
||||
* @param {string} title
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
function createCostBasisSection(ctx, list, useGroupName, title) {
|
||||
const { line } = ctx;
|
||||
|
||||
return [
|
||||
{
|
||||
name: "Cost Basis",
|
||||
tree: [
|
||||
{
|
||||
name: "min",
|
||||
title: `Min Cost Basis ${title}`,
|
||||
top: list.map(({ color, name, tree }) =>
|
||||
line({
|
||||
metric: tree.costBasis.min,
|
||||
name: useGroupName ? name : "Min",
|
||||
color,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "max",
|
||||
title: `Max Cost Basis ${title}`,
|
||||
top: list.map(({ color, name, tree }) =>
|
||||
line({
|
||||
metric: tree.costBasis.max,
|
||||
name: useGroupName ? name : "Max",
|
||||
color,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create activity section
|
||||
* @param {PartialContext} ctx
|
||||
* @param {readonly AddressCohortObject[]} list
|
||||
* @param {boolean} useGroupName
|
||||
* @param {string} title
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
function createActivitySection(ctx, list, useGroupName, title) {
|
||||
const { line } = ctx;
|
||||
|
||||
return [
|
||||
{
|
||||
name: "Activity",
|
||||
tree: [
|
||||
{
|
||||
name: "coinblocks destroyed",
|
||||
title: `Coinblocks Destroyed ${title}`,
|
||||
bottom: list.flatMap(({ color, name, tree }) => [
|
||||
line({
|
||||
metric: tree.activity.coinblocksDestroyed.sum,
|
||||
name: useGroupName ? name : "Coinblocks",
|
||||
color,
|
||||
unit: Unit.coinblocks,
|
||||
}),
|
||||
line({
|
||||
metric: tree.activity.coinblocksDestroyed.cumulative,
|
||||
name: useGroupName ? name : "Coinblocks",
|
||||
color,
|
||||
unit: Unit.coinblocks,
|
||||
}),
|
||||
]),
|
||||
},
|
||||
{
|
||||
name: "coindays destroyed",
|
||||
title: `Coindays Destroyed ${title}`,
|
||||
bottom: list.flatMap(({ color, name, tree }) => [
|
||||
line({
|
||||
metric: tree.activity.coindaysDestroyed.sum,
|
||||
name: useGroupName ? name : "Coindays",
|
||||
color,
|
||||
unit: Unit.coindays,
|
||||
}),
|
||||
line({
|
||||
metric: tree.activity.coindaysDestroyed.cumulative,
|
||||
name: useGroupName ? name : "Coindays",
|
||||
color,
|
||||
unit: Unit.coindays,
|
||||
}),
|
||||
]),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
229
website/scripts/options/cohorts/data.js
Normal file
229
website/scripts/options/cohorts/data.js
Normal file
@@ -0,0 +1,229 @@
|
||||
/** Build cohort data arrays from brk.metrics */
|
||||
|
||||
import {
|
||||
termColors,
|
||||
maxAgeColors,
|
||||
minAgeColors,
|
||||
ageRangeColors,
|
||||
epochColors,
|
||||
geAmountColors,
|
||||
ltAmountColors,
|
||||
amountRangeColors,
|
||||
spendableTypeColors,
|
||||
} from "../colors/index.js";
|
||||
|
||||
/**
|
||||
* @template {Record<string, any>} T
|
||||
* @param {T} obj
|
||||
* @returns {[keyof T & string, T[keyof T & string]][]}
|
||||
*/
|
||||
const entries = (obj) =>
|
||||
/** @type {[keyof T & string, T[keyof T & string]][]} */ (
|
||||
Object.entries(obj)
|
||||
);
|
||||
|
||||
/**
|
||||
* Build all cohort data from brk tree
|
||||
* @param {Colors} colors
|
||||
* @param {BrkClient} brk
|
||||
*/
|
||||
export function buildCohortData(colors, brk) {
|
||||
const utxoCohorts = brk.metrics.distribution.utxoCohorts;
|
||||
const addressCohorts = brk.metrics.distribution.addressCohorts;
|
||||
const {
|
||||
TERM_NAMES,
|
||||
EPOCH_NAMES,
|
||||
MAX_AGE_NAMES,
|
||||
MIN_AGE_NAMES,
|
||||
AGE_RANGE_NAMES,
|
||||
GE_AMOUNT_NAMES,
|
||||
LT_AMOUNT_NAMES,
|
||||
AMOUNT_RANGE_NAMES,
|
||||
SPENDABLE_TYPE_NAMES,
|
||||
} = brk;
|
||||
|
||||
// Base cohort representing "all" - CohortAll (adjustedSopr + percentiles but no RelToMarketCap)
|
||||
/** @type {CohortAll} */
|
||||
const cohortAll = {
|
||||
name: "",
|
||||
title: "",
|
||||
color: colors.orange,
|
||||
tree: utxoCohorts.all,
|
||||
};
|
||||
|
||||
// Term cohorts - split because short is CohortFull, long is CohortWithPercentiles
|
||||
const shortNames = TERM_NAMES.short;
|
||||
/** @type {CohortFull} */
|
||||
const termShort = {
|
||||
name: shortNames.short,
|
||||
title: shortNames.long,
|
||||
color: colors[termColors.short],
|
||||
tree: utxoCohorts.term.short,
|
||||
};
|
||||
|
||||
const longNames = TERM_NAMES.long;
|
||||
/** @type {CohortWithPercentiles} */
|
||||
const termLong = {
|
||||
name: longNames.short,
|
||||
title: longNames.long,
|
||||
color: colors[termColors.long],
|
||||
tree: utxoCohorts.term.long,
|
||||
};
|
||||
|
||||
// Max age cohorts (up to X time) - CohortWithAdjusted (adjustedSopr only)
|
||||
/** @type {readonly CohortWithAdjusted[]} */
|
||||
const upToDate = entries(utxoCohorts.maxAge).map(([key, tree]) => {
|
||||
const names = MAX_AGE_NAMES[key];
|
||||
return {
|
||||
name: names.short,
|
||||
title: names.long,
|
||||
color: colors[maxAgeColors[key]],
|
||||
tree,
|
||||
};
|
||||
});
|
||||
|
||||
// Min age cohorts (from X time) - CohortBasic (neither adjustedSopr nor percentiles)
|
||||
/** @type {readonly CohortBasic[]} */
|
||||
const fromDate = entries(utxoCohorts.minAge).map(([key, tree]) => {
|
||||
const names = MIN_AGE_NAMES[key];
|
||||
return {
|
||||
name: names.short,
|
||||
title: names.long,
|
||||
color: colors[minAgeColors[key]],
|
||||
tree,
|
||||
};
|
||||
});
|
||||
|
||||
// Age range cohorts - CohortWithPercentiles (percentiles only)
|
||||
/** @type {readonly CohortWithPercentiles[]} */
|
||||
const dateRange = entries(utxoCohorts.ageRange).map(([key, tree]) => {
|
||||
const names = AGE_RANGE_NAMES[key];
|
||||
return {
|
||||
name: names.short,
|
||||
title: names.long,
|
||||
color: colors[ageRangeColors[key]],
|
||||
tree,
|
||||
};
|
||||
});
|
||||
|
||||
// Epoch cohorts - CohortBasic (neither adjustedSopr nor percentiles)
|
||||
/** @type {readonly CohortBasic[]} */
|
||||
const epoch = entries(utxoCohorts.epoch).map(([key, tree]) => {
|
||||
const names = EPOCH_NAMES[key];
|
||||
return {
|
||||
name: names.short,
|
||||
title: names.long,
|
||||
color: colors[epochColors[key]],
|
||||
tree,
|
||||
};
|
||||
});
|
||||
|
||||
// UTXOs above amount - CohortBasic (neither adjustedSopr nor percentiles)
|
||||
/** @type {readonly CohortBasic[]} */
|
||||
const utxosAboveAmount = entries(utxoCohorts.geAmount).map(([key, tree]) => {
|
||||
const names = GE_AMOUNT_NAMES[key];
|
||||
return {
|
||||
name: names.short,
|
||||
title: names.long,
|
||||
color: colors[geAmountColors[key]],
|
||||
tree,
|
||||
};
|
||||
});
|
||||
|
||||
// Addresses above amount
|
||||
/** @type {readonly AddressCohortObject[]} */
|
||||
const addressesAboveAmount = entries(addressCohorts.geAmount).map(
|
||||
([key, tree]) => {
|
||||
const names = GE_AMOUNT_NAMES[key];
|
||||
return {
|
||||
name: names.short,
|
||||
title: names.long,
|
||||
color: colors[geAmountColors[key]],
|
||||
tree,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// UTXOs under amount - CohortBasic (neither adjustedSopr nor percentiles)
|
||||
/** @type {readonly CohortBasic[]} */
|
||||
const utxosUnderAmount = entries(utxoCohorts.ltAmount).map(([key, tree]) => {
|
||||
const names = LT_AMOUNT_NAMES[key];
|
||||
return {
|
||||
name: names.short,
|
||||
title: names.long,
|
||||
color: colors[ltAmountColors[key]],
|
||||
tree,
|
||||
};
|
||||
});
|
||||
|
||||
// Addresses under amount
|
||||
/** @type {readonly AddressCohortObject[]} */
|
||||
const addressesUnderAmount = entries(addressCohorts.ltAmount).map(
|
||||
([key, tree]) => {
|
||||
const names = LT_AMOUNT_NAMES[key];
|
||||
return {
|
||||
name: names.short,
|
||||
title: names.long,
|
||||
color: colors[ltAmountColors[key]],
|
||||
tree,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// UTXOs amount ranges - CohortBasic (neither adjustedSopr nor percentiles)
|
||||
/** @type {readonly CohortBasic[]} */
|
||||
const utxosAmountRanges = entries(utxoCohorts.amountRange).map(
|
||||
([key, tree]) => {
|
||||
const names = AMOUNT_RANGE_NAMES[key];
|
||||
return {
|
||||
name: names.short,
|
||||
title: names.long,
|
||||
color: colors[amountRangeColors[key]],
|
||||
tree,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// Addresses amount ranges
|
||||
/** @type {readonly AddressCohortObject[]} */
|
||||
const addressesAmountRanges = entries(addressCohorts.amountRange).map(
|
||||
([key, tree]) => {
|
||||
const names = AMOUNT_RANGE_NAMES[key];
|
||||
return {
|
||||
name: names.short,
|
||||
title: names.long,
|
||||
color: colors[amountRangeColors[key]],
|
||||
tree,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// Spendable type cohorts - CohortBasic (neither adjustedSopr nor percentiles)
|
||||
/** @type {readonly CohortBasic[]} */
|
||||
const type = entries(utxoCohorts.type).map(([key, tree]) => {
|
||||
const names = SPENDABLE_TYPE_NAMES[key];
|
||||
return {
|
||||
name: names.short,
|
||||
title: names.long,
|
||||
color: colors[spendableTypeColors[key]],
|
||||
tree,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
cohortAll,
|
||||
termShort,
|
||||
termLong,
|
||||
upToDate,
|
||||
fromDate,
|
||||
dateRange,
|
||||
epoch,
|
||||
utxosAboveAmount,
|
||||
addressesAboveAmount,
|
||||
utxosUnderAmount,
|
||||
addressesUnderAmount,
|
||||
utxosAmountRanges,
|
||||
addressesAmountRanges,
|
||||
type,
|
||||
};
|
||||
}
|
||||
31
website/scripts/options/cohorts/index.js
Normal file
31
website/scripts/options/cohorts/index.js
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Cohort module - exports all cohort-related functionality
|
||||
*/
|
||||
|
||||
// Cohort data builder
|
||||
export { buildCohortData } from "./data.js";
|
||||
|
||||
// Cohort folder builders (type-safe!)
|
||||
export {
|
||||
createCohortFolderAll,
|
||||
createCohortFolderFull,
|
||||
createCohortFolderWithAdjusted,
|
||||
createCohortFolderWithPercentiles,
|
||||
createCohortFolderBasic,
|
||||
} from "./utxo.js";
|
||||
export { createAddressCohortFolder } from "./address.js";
|
||||
|
||||
// Shared helpers
|
||||
export {
|
||||
createSingleSupplySeries,
|
||||
createGroupedSupplyTotalSeries,
|
||||
createGroupedSupplyInProfitSeries,
|
||||
createGroupedSupplyInLossSeries,
|
||||
createUtxoCountSeries,
|
||||
createAddressCountSeries,
|
||||
createRealizedPriceSeries,
|
||||
createRealizedPriceRatioSeries,
|
||||
createRealizedCapSeries,
|
||||
createCostBasisMinMaxSeries,
|
||||
createCostBasisPercentilesSeries,
|
||||
} from "./shared.js";
|
||||
417
website/scripts/options/cohorts/shared.js
Normal file
417
website/scripts/options/cohorts/shared.js
Normal file
@@ -0,0 +1,417 @@
|
||||
/** Shared cohort chart section builders */
|
||||
|
||||
import { Unit } from "../../utils/units.js";
|
||||
|
||||
/**
|
||||
* Create supply section for a single cohort
|
||||
* @param {PartialContext} ctx
|
||||
* @param {CohortObject} cohort
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
export function createSingleSupplySeries(ctx, cohort) {
|
||||
const { colors, line, createPriceLine } = ctx;
|
||||
const { tree } = cohort;
|
||||
|
||||
return [
|
||||
line({
|
||||
metric: tree.supply.total.sats,
|
||||
name: "Supply",
|
||||
color: colors.default,
|
||||
unit: Unit.sats,
|
||||
}),
|
||||
line({
|
||||
metric: tree.supply.total.bitcoin,
|
||||
name: "Supply",
|
||||
color: colors.default,
|
||||
unit: Unit.btc,
|
||||
}),
|
||||
line({
|
||||
metric: tree.supply.total.dollars,
|
||||
name: "Supply",
|
||||
color: colors.default,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
...("supplyRelToCirculatingSupply" in tree.relative
|
||||
? [
|
||||
line({
|
||||
metric: tree.relative.supplyRelToCirculatingSupply,
|
||||
name: "Supply",
|
||||
color: colors.default,
|
||||
unit: Unit.pctSupply,
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
line({
|
||||
metric: tree.unrealized.supplyInProfit.sats,
|
||||
name: "In Profit",
|
||||
color: colors.green,
|
||||
unit: Unit.sats,
|
||||
}),
|
||||
line({
|
||||
metric: tree.unrealized.supplyInProfit.bitcoin,
|
||||
name: "In Profit",
|
||||
color: colors.green,
|
||||
unit: Unit.btc,
|
||||
}),
|
||||
line({
|
||||
metric: tree.unrealized.supplyInProfit.dollars,
|
||||
name: "In Profit",
|
||||
color: colors.green,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
line({
|
||||
metric: tree.unrealized.supplyInLoss.sats,
|
||||
name: "In Loss",
|
||||
color: colors.red,
|
||||
unit: Unit.sats,
|
||||
}),
|
||||
line({
|
||||
metric: tree.unrealized.supplyInLoss.bitcoin,
|
||||
name: "In Loss",
|
||||
color: colors.red,
|
||||
unit: Unit.btc,
|
||||
}),
|
||||
line({
|
||||
metric: tree.unrealized.supplyInLoss.dollars,
|
||||
name: "In Loss",
|
||||
color: colors.red,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
line({
|
||||
metric: tree.supply.halved.sats,
|
||||
name: "half",
|
||||
color: colors.gray,
|
||||
unit: Unit.sats,
|
||||
options: { lineStyle: 4 },
|
||||
}),
|
||||
line({
|
||||
metric: tree.supply.halved.bitcoin,
|
||||
name: "half",
|
||||
color: colors.gray,
|
||||
unit: Unit.btc,
|
||||
options: { lineStyle: 4 },
|
||||
}),
|
||||
line({
|
||||
metric: tree.supply.halved.dollars,
|
||||
name: "half",
|
||||
color: colors.gray,
|
||||
unit: Unit.usd,
|
||||
options: { lineStyle: 4 },
|
||||
}),
|
||||
...("supplyInProfitRelToCirculatingSupply" in tree.relative
|
||||
? [
|
||||
line({
|
||||
metric: tree.relative.supplyInProfitRelToCirculatingSupply,
|
||||
name: "In Profit",
|
||||
color: colors.green,
|
||||
unit: Unit.pctSupply,
|
||||
}),
|
||||
line({
|
||||
metric: tree.relative.supplyInLossRelToCirculatingSupply,
|
||||
name: "In Loss",
|
||||
color: colors.red,
|
||||
unit: Unit.pctSupply,
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
line({
|
||||
metric: tree.relative.supplyInProfitRelToOwnSupply,
|
||||
name: "In Profit",
|
||||
color: colors.green,
|
||||
unit: Unit.pctOwn,
|
||||
}),
|
||||
line({
|
||||
metric: tree.relative.supplyInLossRelToOwnSupply,
|
||||
name: "In Loss",
|
||||
color: colors.red,
|
||||
unit: Unit.pctOwn,
|
||||
}),
|
||||
createPriceLine({
|
||||
unit: Unit.pctOwn,
|
||||
number: 100,
|
||||
lineStyle: 0,
|
||||
color: colors.default,
|
||||
}),
|
||||
createPriceLine({ unit: Unit.pctOwn, number: 50 }),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create supply total series for grouped cohorts
|
||||
* @param {PartialContext} ctx
|
||||
* @param {readonly CohortObject[]} list
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
export function createGroupedSupplyTotalSeries(ctx, list) {
|
||||
const { line, brk } = ctx;
|
||||
const constant100 = brk.metrics.constants.constant100;
|
||||
|
||||
return list.flatMap(({ color, name, tree }) => [
|
||||
line({ metric: tree.supply.total.sats, name, color, unit: Unit.sats }),
|
||||
line({ metric: tree.supply.total.bitcoin, name, color, unit: Unit.btc }),
|
||||
line({ metric: tree.supply.total.dollars, name, color, unit: Unit.usd }),
|
||||
"supplyRelToCirculatingSupply" in tree.relative
|
||||
? line({
|
||||
metric: tree.relative.supplyRelToCirculatingSupply,
|
||||
name,
|
||||
color,
|
||||
unit: Unit.pctSupply,
|
||||
})
|
||||
: line({ metric: constant100, name, color, unit: Unit.pctSupply }),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create supply in profit series for grouped cohorts
|
||||
* @param {PartialContext} ctx
|
||||
* @param {readonly CohortObject[]} list
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
export function createGroupedSupplyInProfitSeries(ctx, list) {
|
||||
const { line } = ctx;
|
||||
|
||||
return list.flatMap(({ color, name, tree }) => [
|
||||
line({
|
||||
metric: tree.unrealized.supplyInProfit.sats,
|
||||
name,
|
||||
color,
|
||||
unit: Unit.sats,
|
||||
}),
|
||||
line({
|
||||
metric: tree.unrealized.supplyInProfit.bitcoin,
|
||||
name,
|
||||
color,
|
||||
unit: Unit.btc,
|
||||
}),
|
||||
line({
|
||||
metric: tree.unrealized.supplyInProfit.dollars,
|
||||
name,
|
||||
color,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
...("supplyInProfitRelToCirculatingSupply" in tree.relative
|
||||
? [
|
||||
line({
|
||||
metric: tree.relative.supplyInProfitRelToCirculatingSupply,
|
||||
name,
|
||||
color,
|
||||
unit: Unit.pctSupply,
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create supply in loss series for grouped cohorts
|
||||
* @param {PartialContext} ctx
|
||||
* @param {readonly CohortObject[]} list
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
export function createGroupedSupplyInLossSeries(ctx, list) {
|
||||
const { line } = ctx;
|
||||
|
||||
return list.flatMap(({ color, name, tree }) => [
|
||||
line({
|
||||
metric: tree.unrealized.supplyInLoss.sats,
|
||||
name,
|
||||
color,
|
||||
unit: Unit.sats,
|
||||
}),
|
||||
line({
|
||||
metric: tree.unrealized.supplyInLoss.bitcoin,
|
||||
name,
|
||||
color,
|
||||
unit: Unit.btc,
|
||||
}),
|
||||
line({
|
||||
metric: tree.unrealized.supplyInLoss.dollars,
|
||||
name,
|
||||
color,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
...("supplyInLossRelToCirculatingSupply" in tree.relative
|
||||
? [
|
||||
line({
|
||||
metric: tree.relative.supplyInLossRelToCirculatingSupply,
|
||||
name,
|
||||
color,
|
||||
unit: Unit.pctSupply,
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create UTXO count series
|
||||
* @param {PartialContext} ctx
|
||||
* @param {readonly CohortObject[]} list
|
||||
* @param {boolean} useGroupName
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
export function createUtxoCountSeries(ctx, list, useGroupName) {
|
||||
const { line } = ctx;
|
||||
|
||||
return list.flatMap(({ color, name, tree }) => [
|
||||
line({
|
||||
metric: tree.outputs.utxoCount,
|
||||
name: useGroupName ? name : "Count",
|
||||
color,
|
||||
unit: Unit.count,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create address count series (for address cohorts only)
|
||||
* @param {PartialContext} ctx
|
||||
* @param {readonly AddressCohortObject[]} list
|
||||
* @param {boolean} useGroupName
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
export function createAddressCountSeries(ctx, list, useGroupName) {
|
||||
const { line, colors } = ctx;
|
||||
|
||||
return list.flatMap(({ color, name, tree }) => [
|
||||
line({
|
||||
metric: tree.addrCount,
|
||||
name: useGroupName ? name : "Count",
|
||||
color: useGroupName ? color : colors.orange,
|
||||
unit: Unit.count,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create realized price series for grouped cohorts
|
||||
* @param {PartialContext} ctx
|
||||
* @param {readonly CohortObject[]} list
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
export function createRealizedPriceSeries(ctx, list) {
|
||||
const { line } = ctx;
|
||||
|
||||
return list.map(({ color, name, tree }) =>
|
||||
line({ metric: tree.realized.realizedPrice, name, color, unit: Unit.usd }),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create realized price ratio series for grouped cohorts
|
||||
* @param {PartialContext} ctx
|
||||
* @param {readonly CohortObject[]} list
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
export function createRealizedPriceRatioSeries(ctx, list) {
|
||||
const { line, createPriceLine } = ctx;
|
||||
|
||||
return [
|
||||
...list.map(({ color, name, tree }) =>
|
||||
line({
|
||||
metric: tree.realized.realizedPriceExtra.ratio,
|
||||
name,
|
||||
color,
|
||||
unit: Unit.ratio,
|
||||
}),
|
||||
),
|
||||
createPriceLine({ unit: Unit.ratio, number: 1 }),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create realized capitalization series
|
||||
* @param {PartialContext} ctx
|
||||
* @param {readonly CohortObject[]} list
|
||||
* @param {boolean} useGroupName
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
export function createRealizedCapSeries(ctx, list, useGroupName) {
|
||||
const { line } = ctx;
|
||||
|
||||
return list.flatMap(({ color, name, tree }) => [
|
||||
line({
|
||||
metric: tree.realized.realizedCap,
|
||||
name: useGroupName ? name : "Capitalization",
|
||||
color,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create cost basis min/max series (available on all cohorts)
|
||||
* @param {PartialContext} ctx
|
||||
* @param {readonly CohortObject[]} list
|
||||
* @param {boolean} useGroupName
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
export function createCostBasisMinMaxSeries(ctx, list, useGroupName) {
|
||||
const { line } = ctx;
|
||||
|
||||
return list.flatMap(({ color, name, tree }) => [
|
||||
line({
|
||||
metric: tree.costBasis.min,
|
||||
name: useGroupName ? `${name} min` : "Min",
|
||||
color,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
line({
|
||||
metric: tree.costBasis.max,
|
||||
name: useGroupName ? `${name} max` : "Max",
|
||||
color,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create cost basis percentile series (only for cohorts with CostBasisPattern2)
|
||||
* @param {PartialContext} ctx
|
||||
* @param {readonly CohortWithCostBasisPercentiles[]} list
|
||||
* @param {boolean} useGroupName
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
export function createCostBasisPercentilesSeries(ctx, list, useGroupName) {
|
||||
const { line } = ctx;
|
||||
|
||||
return list.flatMap(({ color, name, tree }) => {
|
||||
const percentiles = tree.costBasis.percentiles;
|
||||
return [
|
||||
line({
|
||||
metric: percentiles.pct10,
|
||||
name: useGroupName ? `${name} p10` : "p10",
|
||||
color,
|
||||
unit: Unit.usd,
|
||||
defaultActive: false,
|
||||
}),
|
||||
line({
|
||||
metric: percentiles.pct25,
|
||||
name: useGroupName ? `${name} p25` : "p25",
|
||||
color,
|
||||
unit: Unit.usd,
|
||||
defaultActive: false,
|
||||
}),
|
||||
line({
|
||||
metric: percentiles.pct50,
|
||||
name: useGroupName ? `${name} p50` : "p50",
|
||||
color,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
line({
|
||||
metric: percentiles.pct75,
|
||||
name: useGroupName ? `${name} p75` : "p75",
|
||||
color,
|
||||
unit: Unit.usd,
|
||||
defaultActive: false,
|
||||
}),
|
||||
line({
|
||||
metric: percentiles.pct90,
|
||||
name: useGroupName ? `${name} p90` : "p90",
|
||||
color,
|
||||
unit: Unit.usd,
|
||||
defaultActive: false,
|
||||
}),
|
||||
];
|
||||
});
|
||||
}
|
||||
2246
website/scripts/options/cohorts/utxo.js
Normal file
2246
website/scripts/options/cohorts/utxo.js
Normal file
File diff suppressed because it is too large
Load Diff
488
website/scripts/options/cointime.js
Normal file
488
website/scripts/options/cointime.js
Normal file
@@ -0,0 +1,488 @@
|
||||
/** Cointime section builder - typed tree-based patterns */
|
||||
|
||||
import { Unit } from "../utils/units.js";
|
||||
|
||||
/**
|
||||
* Create price with ratio options for cointime prices
|
||||
* @param {PartialContext} ctx
|
||||
* @param {Object} args
|
||||
* @param {string} args.title
|
||||
* @param {string} args.legend
|
||||
* @param {AnyMetricPattern} args.price
|
||||
* @param {ActivePriceRatioPattern} args.ratio
|
||||
* @param {Color} [args.color]
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
function createCointimePriceWithRatioOptions(
|
||||
ctx,
|
||||
{ title, legend, price, ratio, color },
|
||||
) {
|
||||
const { line, colors, createPriceLine } = ctx;
|
||||
|
||||
// Percentile USD mappings
|
||||
const percentileUsdMap = [
|
||||
{ name: "pct99", prop: ratio.ratioPct99Usd, color: colors.rose },
|
||||
{ name: "pct98", prop: ratio.ratioPct98Usd, color: colors.pink },
|
||||
{ name: "pct95", prop: ratio.ratioPct95Usd, color: colors.fuchsia },
|
||||
{ name: "pct5", prop: ratio.ratioPct5Usd, color: colors.cyan },
|
||||
{ name: "pct2", prop: ratio.ratioPct2Usd, color: colors.sky },
|
||||
{ name: "pct1", prop: ratio.ratioPct1Usd, color: colors.blue },
|
||||
];
|
||||
|
||||
// Percentile ratio mappings
|
||||
const percentileMap = [
|
||||
{ name: "pct99", prop: ratio.ratioPct99, color: colors.rose },
|
||||
{ name: "pct98", prop: ratio.ratioPct98, color: colors.pink },
|
||||
{ name: "pct95", prop: ratio.ratioPct95, color: colors.fuchsia },
|
||||
{ name: "pct5", prop: ratio.ratioPct5, color: colors.cyan },
|
||||
{ name: "pct2", prop: ratio.ratioPct2, color: colors.sky },
|
||||
{ name: "pct1", prop: ratio.ratioPct1, color: colors.blue },
|
||||
];
|
||||
|
||||
// SD patterns by window
|
||||
const sdPatterns = [
|
||||
{ nameAddon: "all", titleAddon: "", sd: ratio.ratioSd },
|
||||
{ nameAddon: "4y", titleAddon: "4y", sd: ratio.ratio4ySd },
|
||||
{ nameAddon: "2y", titleAddon: "2y", sd: ratio.ratio2ySd },
|
||||
{ nameAddon: "1y", titleAddon: "1y", sd: ratio.ratio1ySd },
|
||||
];
|
||||
|
||||
/** @param {Ratio1ySdPattern} sd */
|
||||
const getSdBands = (sd) => [
|
||||
{ name: "0σ", prop: sd._0sdUsd, color: colors.lime },
|
||||
{ name: "+0.5σ", prop: sd.p05sdUsd, color: colors.yellow },
|
||||
{ name: "+1σ", prop: sd.p1sdUsd, color: colors.amber },
|
||||
{ name: "+1.5σ", prop: sd.p15sdUsd, color: colors.orange },
|
||||
{ name: "+2σ", prop: sd.p2sdUsd, color: colors.red },
|
||||
{ name: "+2.5σ", prop: sd.p25sdUsd, color: colors.rose },
|
||||
{ name: "+3σ", prop: sd.p3sd, color: colors.pink },
|
||||
{ name: "−0.5σ", prop: sd.m05sdUsd, color: colors.teal },
|
||||
{ name: "−1σ", prop: sd.m1sdUsd, color: colors.cyan },
|
||||
{ name: "−1.5σ", prop: sd.m15sdUsd, color: colors.sky },
|
||||
{ name: "−2σ", prop: sd.m2sdUsd, color: colors.blue },
|
||||
{ name: "−2.5σ", prop: sd.m25sdUsd, color: colors.indigo },
|
||||
{ name: "−3σ", prop: sd.m3sd, color: colors.violet },
|
||||
];
|
||||
|
||||
return [
|
||||
{
|
||||
name: "price",
|
||||
title,
|
||||
top: [line({ metric: price, name: legend, color, unit: Unit.usd })],
|
||||
},
|
||||
{
|
||||
name: "Ratio",
|
||||
title: `${title} Ratio`,
|
||||
top: [
|
||||
line({ metric: price, name: legend, color, unit: Unit.usd }),
|
||||
...percentileUsdMap.map(({ name: pctName, prop, color: pctColor }) =>
|
||||
line({
|
||||
metric: prop,
|
||||
name: pctName,
|
||||
color: pctColor,
|
||||
defaultActive: false,
|
||||
unit: Unit.usd,
|
||||
options: { lineStyle: 1 },
|
||||
}),
|
||||
),
|
||||
],
|
||||
bottom: [
|
||||
line({ metric: ratio.ratio, name: "Ratio", color, unit: Unit.ratio }),
|
||||
line({
|
||||
metric: ratio.ratio1wSma,
|
||||
name: "1w SMA",
|
||||
color: colors.lime,
|
||||
unit: Unit.ratio,
|
||||
}),
|
||||
line({
|
||||
metric: ratio.ratio1mSma,
|
||||
name: "1m SMA",
|
||||
color: colors.teal,
|
||||
unit: Unit.ratio,
|
||||
}),
|
||||
line({
|
||||
metric: ratio.ratio1ySd.sma,
|
||||
name: "1y SMA",
|
||||
color: colors.sky,
|
||||
unit: Unit.ratio,
|
||||
}),
|
||||
line({
|
||||
metric: ratio.ratio2ySd.sma,
|
||||
name: "2y SMA",
|
||||
color: colors.indigo,
|
||||
unit: Unit.ratio,
|
||||
}),
|
||||
line({
|
||||
metric: ratio.ratio4ySd.sma,
|
||||
name: "4y SMA",
|
||||
color: colors.purple,
|
||||
unit: Unit.ratio,
|
||||
}),
|
||||
line({
|
||||
metric: ratio.ratioSd.sma,
|
||||
name: "All SMA",
|
||||
color: colors.rose,
|
||||
unit: Unit.ratio,
|
||||
}),
|
||||
...percentileMap.map(({ name: pctName, prop, color: pctColor }) =>
|
||||
line({
|
||||
metric: prop,
|
||||
name: pctName,
|
||||
color: pctColor,
|
||||
defaultActive: false,
|
||||
unit: Unit.ratio,
|
||||
options: { lineStyle: 1 },
|
||||
}),
|
||||
),
|
||||
createPriceLine({ unit: Unit.ratio, number: 1 }),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "ZScores",
|
||||
tree: sdPatterns.map(({ nameAddon, titleAddon, sd }) => ({
|
||||
name: nameAddon,
|
||||
title: `${title} ${titleAddon} Z-Score`,
|
||||
top: getSdBands(sd).map(({ name: bandName, prop, color: bandColor }) =>
|
||||
line({
|
||||
metric: prop,
|
||||
name: bandName,
|
||||
color: bandColor,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
),
|
||||
bottom: [
|
||||
line({ metric: sd.zscore, name: "Z-Score", color, unit: Unit.sd }),
|
||||
createPriceLine({ unit: Unit.sd, number: 3 }),
|
||||
createPriceLine({ unit: Unit.sd, number: 2 }),
|
||||
createPriceLine({ unit: Unit.sd, number: 1 }),
|
||||
createPriceLine({ unit: Unit.sd, number: 0 }),
|
||||
createPriceLine({ unit: Unit.sd, number: -1 }),
|
||||
createPriceLine({ unit: Unit.sd, number: -2 }),
|
||||
createPriceLine({ unit: Unit.sd, number: -3 }),
|
||||
],
|
||||
})),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Cointime section
|
||||
* @param {PartialContext} ctx
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
export function createCointimeSection(ctx) {
|
||||
const { colors, brk, line } = ctx;
|
||||
const { cointime, distribution, supply } = brk.metrics;
|
||||
const { pricing, cap, activity, supply: cointimeSupply, adjusted } = cointime;
|
||||
const { all } = distribution.utxoCohorts;
|
||||
|
||||
// Cointime prices data
|
||||
const cointimePrices = [
|
||||
{
|
||||
price: pricing.trueMarketMean,
|
||||
ratio: pricing.trueMarketMeanRatio,
|
||||
name: "True market mean",
|
||||
title: "true market mean",
|
||||
color: colors.blue,
|
||||
},
|
||||
{
|
||||
price: pricing.vaultedPrice,
|
||||
ratio: pricing.vaultedPriceRatio,
|
||||
name: "Vaulted",
|
||||
title: "vaulted price",
|
||||
color: colors.lime,
|
||||
},
|
||||
{
|
||||
price: pricing.activePrice,
|
||||
ratio: pricing.activePriceRatio,
|
||||
name: "Active",
|
||||
title: "active price",
|
||||
color: colors.rose,
|
||||
},
|
||||
{
|
||||
price: pricing.cointimePrice,
|
||||
ratio: pricing.cointimePriceRatio,
|
||||
name: "cointime",
|
||||
title: "cointime price",
|
||||
color: colors.yellow,
|
||||
},
|
||||
];
|
||||
|
||||
// Cointime capitalizations data
|
||||
const cointimeCapitalizations = [
|
||||
{
|
||||
metric: cap.vaultedCap,
|
||||
name: "vaulted",
|
||||
title: "vaulted Capitalization",
|
||||
color: colors.lime,
|
||||
},
|
||||
{
|
||||
metric: cap.activeCap,
|
||||
name: "active",
|
||||
title: "active Capitalization",
|
||||
color: colors.rose,
|
||||
},
|
||||
{
|
||||
metric: cap.cointimeCap,
|
||||
name: "cointime",
|
||||
title: "cointime Capitalization",
|
||||
color: colors.yellow,
|
||||
},
|
||||
{
|
||||
metric: cap.investorCap,
|
||||
name: "investor",
|
||||
title: "investor Capitalization",
|
||||
color: colors.fuchsia,
|
||||
},
|
||||
{
|
||||
metric: cap.thermoCap,
|
||||
name: "thermo",
|
||||
title: "thermo Capitalization",
|
||||
color: colors.emerald,
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
name: "Cointime",
|
||||
tree: [
|
||||
// Prices
|
||||
{
|
||||
name: "Prices",
|
||||
tree: [
|
||||
{
|
||||
name: "Compare",
|
||||
title: "Compare Cointime Prices",
|
||||
top: cointimePrices.map(({ price, name, color }) =>
|
||||
line({ metric: price, name, color, unit: Unit.usd }),
|
||||
),
|
||||
},
|
||||
...cointimePrices.map(({ price, ratio, name, color, title }) => ({
|
||||
name,
|
||||
tree: createCointimePriceWithRatioOptions(ctx, {
|
||||
price,
|
||||
ratio,
|
||||
legend: name,
|
||||
color,
|
||||
title,
|
||||
}),
|
||||
})),
|
||||
],
|
||||
},
|
||||
|
||||
// Capitalization
|
||||
{
|
||||
name: "Capitalization",
|
||||
tree: [
|
||||
{
|
||||
name: "Compare",
|
||||
title: "Compare Cointime Capitalizations",
|
||||
bottom: [
|
||||
line({
|
||||
metric: supply.marketCap,
|
||||
name: "Market",
|
||||
color: colors.default,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
line({
|
||||
metric: all.realized.realizedCap,
|
||||
name: "Realized",
|
||||
color: colors.orange,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
...cointimeCapitalizations.map(({ metric, name, color }) =>
|
||||
line({ metric, name, color, unit: Unit.usd }),
|
||||
),
|
||||
],
|
||||
},
|
||||
...cointimeCapitalizations.map(({ metric, name, color, title }) => ({
|
||||
name,
|
||||
title,
|
||||
bottom: [
|
||||
line({ metric, name, color, unit: Unit.usd }),
|
||||
line({
|
||||
metric: supply.marketCap,
|
||||
name: "Market",
|
||||
color: colors.default,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
line({
|
||||
metric: all.realized.realizedCap,
|
||||
name: "Realized",
|
||||
color: colors.orange,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
],
|
||||
})),
|
||||
],
|
||||
},
|
||||
|
||||
// Supply
|
||||
{
|
||||
name: "Supply",
|
||||
title: "Cointime Supply",
|
||||
bottom: [
|
||||
// All supply (different pattern structure)
|
||||
line({
|
||||
metric: all.supply.total.sats,
|
||||
name: "All",
|
||||
color: colors.orange,
|
||||
unit: Unit.sats,
|
||||
}),
|
||||
line({
|
||||
metric: all.supply.total.bitcoin,
|
||||
name: "All",
|
||||
color: colors.orange,
|
||||
unit: Unit.btc,
|
||||
}),
|
||||
line({
|
||||
metric: all.supply.total.dollars,
|
||||
name: "All",
|
||||
color: colors.orange,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
// Cointime supplies (ActiveSupplyPattern)
|
||||
.../** @type {const} */ ([
|
||||
[cointimeSupply.vaultedSupply, "Vaulted", colors.lime],
|
||||
[cointimeSupply.activeSupply, "Active", colors.rose],
|
||||
]).flatMap(([supplyItem, name, color]) => [
|
||||
line({ metric: supplyItem.sats, name, color, unit: Unit.sats }),
|
||||
line({ metric: supplyItem.bitcoin, name, color, unit: Unit.btc }),
|
||||
line({ metric: supplyItem.dollars, name, color, unit: Unit.usd }),
|
||||
]),
|
||||
],
|
||||
},
|
||||
|
||||
// Liveliness & Vaultedness
|
||||
{
|
||||
name: "Liveliness & Vaultedness",
|
||||
title: "Liveliness & Vaultedness",
|
||||
bottom: [
|
||||
line({
|
||||
metric: activity.liveliness,
|
||||
name: "Liveliness",
|
||||
color: colors.rose,
|
||||
unit: Unit.ratio,
|
||||
}),
|
||||
line({
|
||||
metric: activity.vaultedness,
|
||||
name: "Vaultedness",
|
||||
color: colors.lime,
|
||||
unit: Unit.ratio,
|
||||
}),
|
||||
line({
|
||||
metric: activity.activityToVaultednessRatio,
|
||||
name: "Liveliness / Vaultedness",
|
||||
color: colors.purple,
|
||||
unit: Unit.ratio,
|
||||
}),
|
||||
],
|
||||
},
|
||||
|
||||
// Coinblocks
|
||||
{
|
||||
name: "Coinblocks",
|
||||
title: "Coinblocks",
|
||||
bottom: [
|
||||
// Destroyed comes from the all cohort's activity
|
||||
line({
|
||||
metric: all.activity.coinblocksDestroyed.sum,
|
||||
name: "Destroyed",
|
||||
color: colors.red,
|
||||
unit: Unit.coinblocks,
|
||||
}),
|
||||
line({
|
||||
metric: all.activity.coinblocksDestroyed.cumulative,
|
||||
name: "Cumulative Destroyed",
|
||||
color: colors.red,
|
||||
defaultActive: false,
|
||||
unit: Unit.coinblocks,
|
||||
}),
|
||||
// Created and stored from cointime
|
||||
line({
|
||||
metric: activity.coinblocksCreated.sum,
|
||||
name: "Created",
|
||||
color: colors.orange,
|
||||
unit: Unit.coinblocks,
|
||||
}),
|
||||
line({
|
||||
metric: activity.coinblocksCreated.cumulative,
|
||||
name: "Cumulative Created",
|
||||
color: colors.orange,
|
||||
defaultActive: false,
|
||||
unit: Unit.coinblocks,
|
||||
}),
|
||||
line({
|
||||
metric: activity.coinblocksStored.sum,
|
||||
name: "Stored",
|
||||
color: colors.green,
|
||||
unit: Unit.coinblocks,
|
||||
}),
|
||||
line({
|
||||
metric: activity.coinblocksStored.cumulative,
|
||||
name: "Cumulative Stored",
|
||||
color: colors.green,
|
||||
defaultActive: false,
|
||||
unit: Unit.coinblocks,
|
||||
}),
|
||||
],
|
||||
},
|
||||
|
||||
// Adjusted metrics
|
||||
{
|
||||
name: "Adjusted",
|
||||
tree: [
|
||||
// Inflation
|
||||
{
|
||||
name: "Inflation",
|
||||
title: "Cointime-Adjusted Inflation Rate",
|
||||
bottom: [
|
||||
line({
|
||||
metric: supply.inflation,
|
||||
name: "Base",
|
||||
color: colors.orange,
|
||||
unit: Unit.percentage,
|
||||
}),
|
||||
line({
|
||||
metric: adjusted.cointimeAdjInflationRate,
|
||||
name: "Adjusted",
|
||||
color: colors.purple,
|
||||
unit: Unit.percentage,
|
||||
}),
|
||||
],
|
||||
},
|
||||
// Velocity
|
||||
{
|
||||
name: "Velocity",
|
||||
title: "Cointime-Adjusted Transactions Velocity",
|
||||
bottom: [
|
||||
line({
|
||||
metric: supply.velocity.btc,
|
||||
name: "BTC",
|
||||
color: colors.orange,
|
||||
unit: Unit.ratio,
|
||||
}),
|
||||
line({
|
||||
metric: adjusted.cointimeAdjTxBtcVelocity,
|
||||
name: "Adj. BTC",
|
||||
color: colors.red,
|
||||
unit: Unit.ratio,
|
||||
}),
|
||||
line({
|
||||
metric: supply.velocity.usd,
|
||||
name: "USD",
|
||||
color: colors.emerald,
|
||||
unit: Unit.ratio,
|
||||
}),
|
||||
line({
|
||||
metric: adjusted.cointimeAdjTxUsdVelocity,
|
||||
name: "Adj. USD",
|
||||
color: colors.lime,
|
||||
unit: Unit.ratio,
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
152
website/scripts/options/colors/cohorts.js
Normal file
152
website/scripts/options/colors/cohorts.js
Normal file
@@ -0,0 +1,152 @@
|
||||
/** Cohort color mappings */
|
||||
|
||||
/** @type {Readonly<Record<string, ColorName>>} */
|
||||
export const termColors = {
|
||||
short: "yellow",
|
||||
long: "fuchsia",
|
||||
};
|
||||
|
||||
/** @type {Readonly<Record<string, ColorName>>} */
|
||||
export const maxAgeColors = {
|
||||
_1w: "red",
|
||||
_1m: "orange",
|
||||
_2m: "amber",
|
||||
_3m: "yellow",
|
||||
_4m: "lime",
|
||||
_5m: "green",
|
||||
_6m: "teal",
|
||||
_1y: "sky",
|
||||
_2y: "indigo",
|
||||
_3y: "violet",
|
||||
_4y: "purple",
|
||||
_5y: "fuchsia",
|
||||
_6y: "pink",
|
||||
_7y: "red",
|
||||
_8y: "orange",
|
||||
_10y: "amber",
|
||||
_12y: "yellow",
|
||||
_15y: "lime",
|
||||
};
|
||||
|
||||
/** @type {Readonly<Record<string, ColorName>>} */
|
||||
export const minAgeColors = {
|
||||
_1d: "red",
|
||||
_1w: "orange",
|
||||
_1m: "yellow",
|
||||
_2m: "lime",
|
||||
_3m: "green",
|
||||
_4m: "teal",
|
||||
_5m: "cyan",
|
||||
_6m: "blue",
|
||||
_1y: "indigo",
|
||||
_2y: "violet",
|
||||
_3y: "purple",
|
||||
_4y: "fuchsia",
|
||||
_5y: "pink",
|
||||
_6y: "rose",
|
||||
_7y: "red",
|
||||
_8y: "orange",
|
||||
_10y: "yellow",
|
||||
_12y: "lime",
|
||||
};
|
||||
|
||||
/** @type {Readonly<Record<string, ColorName>>} */
|
||||
export const ageRangeColors = {
|
||||
upTo1d: "pink",
|
||||
_1dTo1w: "red",
|
||||
_1wTo1m: "orange",
|
||||
_1mTo2m: "yellow",
|
||||
_2mTo3m: "yellow",
|
||||
_3mTo4m: "lime",
|
||||
_4mTo5m: "lime",
|
||||
_5mTo6m: "lime",
|
||||
_6mTo1y: "green",
|
||||
_1yTo2y: "cyan",
|
||||
_2yTo3y: "blue",
|
||||
_3yTo4y: "indigo",
|
||||
_4yTo5y: "violet",
|
||||
_5yTo6y: "purple",
|
||||
_6yTo7y: "purple",
|
||||
_7yTo8y: "fuchsia",
|
||||
_8yTo10y: "fuchsia",
|
||||
_10yTo12y: "pink",
|
||||
_12yTo15y: "red",
|
||||
from15y: "orange",
|
||||
};
|
||||
|
||||
/** @type {Readonly<Record<string, ColorName>>} */
|
||||
export const epochColors = {
|
||||
_0: "red",
|
||||
_1: "yellow",
|
||||
_2: "orange",
|
||||
_3: "lime",
|
||||
_4: "green",
|
||||
};
|
||||
|
||||
/** @type {Readonly<Record<string, ColorName>>} */
|
||||
export const geAmountColors = {
|
||||
_1sat: "orange",
|
||||
_10sats: "orange",
|
||||
_100sats: "yellow",
|
||||
_1kSats: "lime",
|
||||
_10kSats: "green",
|
||||
_100kSats: "cyan",
|
||||
_1mSats: "blue",
|
||||
_10mSats: "indigo",
|
||||
_1btc: "purple",
|
||||
_10btc: "violet",
|
||||
_100btc: "fuchsia",
|
||||
_1kBtc: "pink",
|
||||
_10kBtc: "red",
|
||||
};
|
||||
|
||||
/** @type {Readonly<Record<string, ColorName>>} */
|
||||
export const ltAmountColors = {
|
||||
_10sats: "orange",
|
||||
_100sats: "yellow",
|
||||
_1kSats: "lime",
|
||||
_10kSats: "green",
|
||||
_100kSats: "cyan",
|
||||
_1mSats: "blue",
|
||||
_10mSats: "indigo",
|
||||
_1btc: "purple",
|
||||
_10btc: "violet",
|
||||
_100btc: "fuchsia",
|
||||
_1kBtc: "pink",
|
||||
_10kBtc: "red",
|
||||
_100kBtc: "orange",
|
||||
};
|
||||
|
||||
/** @type {Readonly<Record<string, ColorName>>} */
|
||||
export const amountRangeColors = {
|
||||
_0sats: "red",
|
||||
_1satTo10sats: "orange",
|
||||
_10satsTo100sats: "yellow",
|
||||
_100satsTo1kSats: "lime",
|
||||
_1kSatsTo10kSats: "green",
|
||||
_10kSatsTo100kSats: "cyan",
|
||||
_100kSatsTo1mSats: "blue",
|
||||
_1mSatsTo10mSats: "indigo",
|
||||
_10mSatsTo1btc: "purple",
|
||||
_1btcTo10btc: "violet",
|
||||
_10btcTo100btc: "fuchsia",
|
||||
_100btcTo1kBtc: "pink",
|
||||
_1kBtcTo10kBtc: "red",
|
||||
_10kBtcTo100kBtc: "orange",
|
||||
_100kBtcOrMore: "yellow",
|
||||
};
|
||||
|
||||
/** @type {Readonly<Record<string, ColorName>>} */
|
||||
export const spendableTypeColors = {
|
||||
p2pk65: "red",
|
||||
p2pk33: "orange",
|
||||
p2pkh: "yellow",
|
||||
p2ms: "lime",
|
||||
p2sh: "green",
|
||||
p2wpkh: "teal",
|
||||
p2wsh: "blue",
|
||||
p2tr: "indigo",
|
||||
p2a: "purple",
|
||||
unknown: "violet",
|
||||
empty: "fuchsia",
|
||||
};
|
||||
14
website/scripts/options/colors/index.js
Normal file
14
website/scripts/options/colors/index.js
Normal file
@@ -0,0 +1,14 @@
|
||||
// Re-export all color mappings
|
||||
export {
|
||||
termColors,
|
||||
maxAgeColors,
|
||||
minAgeColors,
|
||||
ageRangeColors,
|
||||
epochColors,
|
||||
geAmountColors,
|
||||
ltAmountColors,
|
||||
amountRangeColors,
|
||||
spendableTypeColors,
|
||||
} from "./cohorts.js";
|
||||
|
||||
export { averageColors, dcaColors } from "./misc.js";
|
||||
42
website/scripts/options/colors/misc.js
Normal file
42
website/scripts/options/colors/misc.js
Normal file
@@ -0,0 +1,42 @@
|
||||
/** Miscellaneous color mappings for DCA and averages */
|
||||
|
||||
/**
|
||||
* Moving average period colors
|
||||
* Format: [periodId, days, colorName]
|
||||
* @type {readonly [string, number, ColorName][]}
|
||||
*/
|
||||
export const averageColors = [
|
||||
["1w", 7, "red"],
|
||||
["8d", 8, "orange"],
|
||||
["13d", 13, "amber"],
|
||||
["21d", 21, "yellow"],
|
||||
["1m", 30, "lime"],
|
||||
["34d", 34, "green"],
|
||||
["55d", 55, "emerald"],
|
||||
["89d", 89, "teal"],
|
||||
["144d", 144, "cyan"],
|
||||
["200d", 200, "sky"],
|
||||
["1y", 365, "blue"],
|
||||
["2y", 730, "indigo"],
|
||||
["200w", 1400, "violet"],
|
||||
["4y", 1460, "purple"],
|
||||
];
|
||||
|
||||
/**
|
||||
* DCA class colors by year
|
||||
* Format: [year, colorName, defaultActive]
|
||||
* @type {readonly [number, ColorName, boolean][]}
|
||||
*/
|
||||
export const dcaColors = [
|
||||
[2015, "pink", false],
|
||||
[2016, "red", false],
|
||||
[2017, "orange", true],
|
||||
[2018, "yellow", true],
|
||||
[2019, "green", true],
|
||||
[2020, "teal", true],
|
||||
[2021, "sky", true],
|
||||
[2022, "blue", true],
|
||||
[2023, "purple", true],
|
||||
[2024, "fuchsia", true],
|
||||
[2025, "pink", true],
|
||||
];
|
||||
118
website/scripts/options/constants.js
Normal file
118
website/scripts/options/constants.js
Normal file
@@ -0,0 +1,118 @@
|
||||
/** Constant helpers for creating price lines and reference lines */
|
||||
|
||||
import { line } from "./series.js";
|
||||
|
||||
/**
|
||||
* Get constant pattern by number dynamically from tree
|
||||
* Examples: 0 → constant0, 38.2 → constant382, -1 → constantMinus1
|
||||
* @param {BrkClient["metrics"]["constants"]} constants
|
||||
* @param {number} num
|
||||
* @returns {AnyMetricPattern}
|
||||
*/
|
||||
export function getConstant(constants, num) {
|
||||
const key =
|
||||
num >= 0
|
||||
? `constant${String(num).replace(".", "")}`
|
||||
: `constantMinus${Math.abs(num)}`;
|
||||
const constant = /** @type {AnyMetricPattern | undefined} */ (
|
||||
/** @type {Record<string, AnyMetricPattern>} */ (constants)[key]
|
||||
);
|
||||
if (!constant) throw new Error(`Unknown constant: ${num} (key: ${key})`);
|
||||
return constant;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a price line series (horizontal reference line)
|
||||
* @param {Object} args
|
||||
* @param {BrkClient["metrics"]["constants"]} args.constants
|
||||
* @param {Colors} args.colors
|
||||
* @param {number} [args.number]
|
||||
* @param {string} [args.name]
|
||||
* @param {boolean} [args.defaultActive]
|
||||
* @param {number} [args.lineStyle]
|
||||
* @param {Color} [args.color]
|
||||
* @param {Unit} args.unit
|
||||
* @returns {FetchedLineSeriesBlueprint}
|
||||
*/
|
||||
export function createPriceLine({
|
||||
constants,
|
||||
colors,
|
||||
number = 0,
|
||||
unit,
|
||||
defaultActive,
|
||||
color,
|
||||
name,
|
||||
lineStyle,
|
||||
}) {
|
||||
return {
|
||||
metric: getConstant(constants, number),
|
||||
title: name ?? `${number}`,
|
||||
unit,
|
||||
defaultActive,
|
||||
color: color ?? colors.gray,
|
||||
options: {
|
||||
lineStyle: lineStyle ?? 4,
|
||||
lastValueVisible: false,
|
||||
crosshairMarkerVisible: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create multiple price lines from an array of numbers
|
||||
* @param {Object} args
|
||||
* @param {BrkClient["metrics"]["constants"]} args.constants
|
||||
* @param {Colors} args.colors
|
||||
* @param {number[]} args.numbers
|
||||
* @param {Unit} args.unit
|
||||
* @returns {FetchedLineSeriesBlueprint[]}
|
||||
*/
|
||||
export function createPriceLines({ constants, colors, numbers, unit }) {
|
||||
return numbers.map((number) => ({
|
||||
metric: getConstant(constants, number),
|
||||
title: `${number}`,
|
||||
unit,
|
||||
defaultActive: !number,
|
||||
color: colors.gray,
|
||||
options: {
|
||||
lineStyle: 4,
|
||||
lastValueVisible: false,
|
||||
crosshairMarkerVisible: false,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a constant line series
|
||||
* @param {Object} args
|
||||
* @param {Colors} args.colors
|
||||
* @param {AnyMetricPattern} args.constant
|
||||
* @param {string} args.name
|
||||
* @param {Unit} args.unit
|
||||
* @param {Color} [args.color]
|
||||
* @param {number} [args.lineStyle]
|
||||
* @param {boolean} [args.defaultActive]
|
||||
* @returns {FetchedLineSeriesBlueprint}
|
||||
*/
|
||||
export function constantLine({
|
||||
colors,
|
||||
constant,
|
||||
name,
|
||||
unit,
|
||||
color,
|
||||
lineStyle,
|
||||
defaultActive,
|
||||
}) {
|
||||
return line({
|
||||
metric: constant,
|
||||
name,
|
||||
unit,
|
||||
defaultActive,
|
||||
color: color ?? colors.gray,
|
||||
options: {
|
||||
lineStyle: lineStyle ?? 4,
|
||||
lastValueVisible: false,
|
||||
crosshairMarkerVisible: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
96
website/scripts/options/context.js
Normal file
96
website/scripts/options/context.js
Normal file
@@ -0,0 +1,96 @@
|
||||
import {
|
||||
line,
|
||||
dots,
|
||||
candlestick,
|
||||
baseline,
|
||||
histogram,
|
||||
fromBlockCount,
|
||||
fromBitcoin,
|
||||
fromBlockSize,
|
||||
fromSizePattern,
|
||||
fromFullnessPattern,
|
||||
fromFeeRatePattern,
|
||||
fromCoinbasePattern,
|
||||
fromValuePattern,
|
||||
fromBitcoinPatternWithUnit,
|
||||
fromBlockCountWithUnit,
|
||||
fromIntervalPattern,
|
||||
fromSupplyPattern,
|
||||
} from "./series.js";
|
||||
import {
|
||||
createPriceLine,
|
||||
createPriceLines,
|
||||
constantLine,
|
||||
} from "./constants.js";
|
||||
|
||||
/**
|
||||
* Create a context object with all dependencies for building partial options
|
||||
* @param {Object} args
|
||||
* @param {Colors} args.colors
|
||||
* @param {BrkClient} args.brk
|
||||
* @returns {PartialContext}
|
||||
*/
|
||||
export function createContext({ colors, brk }) {
|
||||
const constants = brk.metrics.constants;
|
||||
|
||||
return {
|
||||
colors,
|
||||
brk,
|
||||
|
||||
// Series helpers
|
||||
line,
|
||||
dots,
|
||||
candlestick,
|
||||
baseline,
|
||||
histogram,
|
||||
fromBlockCount: (pattern, title, color) =>
|
||||
fromBlockCount(colors, pattern, title, color),
|
||||
fromBitcoin: (pattern, title, color) =>
|
||||
fromBitcoin(colors, pattern, title, color),
|
||||
fromBlockSize: (pattern, title, color) =>
|
||||
fromBlockSize(colors, pattern, title, color),
|
||||
fromSizePattern: (pattern, title, unit) =>
|
||||
fromSizePattern(colors, pattern, title, unit),
|
||||
fromFullnessPattern: (pattern, title, unit) =>
|
||||
fromFullnessPattern(colors, pattern, title, unit),
|
||||
fromFeeRatePattern: (pattern, title, unit) =>
|
||||
fromFeeRatePattern(colors, pattern, title, unit),
|
||||
fromCoinbasePattern: (pattern, title) =>
|
||||
fromCoinbasePattern(colors, pattern, title),
|
||||
fromValuePattern: (pattern, title, sumColor, cumulativeColor) =>
|
||||
fromValuePattern(colors, pattern, title, sumColor, cumulativeColor),
|
||||
fromBitcoinPatternWithUnit: (
|
||||
pattern,
|
||||
title,
|
||||
unit,
|
||||
sumColor,
|
||||
cumulativeColor,
|
||||
) =>
|
||||
fromBitcoinPatternWithUnit(
|
||||
colors,
|
||||
pattern,
|
||||
title,
|
||||
unit,
|
||||
sumColor,
|
||||
cumulativeColor,
|
||||
),
|
||||
fromBlockCountWithUnit: (pattern, title, unit, sumColor, cumulativeColor) =>
|
||||
fromBlockCountWithUnit(
|
||||
colors,
|
||||
pattern,
|
||||
title,
|
||||
unit,
|
||||
sumColor,
|
||||
cumulativeColor,
|
||||
),
|
||||
fromIntervalPattern: (pattern, title, unit, color) =>
|
||||
fromIntervalPattern(colors, pattern, title, unit, color),
|
||||
fromSupplyPattern: (pattern, title, color) =>
|
||||
fromSupplyPattern(colors, pattern, title, color),
|
||||
|
||||
createPriceLine: (args) => createPriceLine({ constants, colors, ...args }),
|
||||
createPriceLines: (args) =>
|
||||
createPriceLines({ constants, colors, ...args }),
|
||||
constantLine: (args) => constantLine({ colors, ...args }),
|
||||
};
|
||||
}
|
||||
378
website/scripts/options/full.js
Normal file
378
website/scripts/options/full.js
Normal file
@@ -0,0 +1,378 @@
|
||||
import { createPartialOptions } from "./partial.js";
|
||||
import {
|
||||
createButtonElement,
|
||||
createAnchorElement,
|
||||
insertElementAtIndex,
|
||||
} from "../utils/dom.js";
|
||||
import { pushHistory, resetParams } from "../utils/url.js";
|
||||
import { readStored, writeToStorage } from "../utils/storage.js";
|
||||
import { stringToId } from "../utils/format.js";
|
||||
import { collect, markUsed, logUnused } from "./unused.js";
|
||||
|
||||
/**
|
||||
* @param {Object} args
|
||||
* @param {Colors} args.colors
|
||||
* @param {Signals} args.signals
|
||||
* @param {BrkClient} args.brk
|
||||
* @param {Signal<string | null>} args.qrcode
|
||||
*/
|
||||
export function initOptions({ colors, signals, brk, qrcode }) {
|
||||
collect(brk.metrics);
|
||||
|
||||
const LS_SELECTED_KEY = `selected_path`;
|
||||
|
||||
const urlPath_ = window.document.location.pathname
|
||||
.split("/")
|
||||
.filter((v) => v);
|
||||
const urlPath = urlPath_.length ? urlPath_ : undefined;
|
||||
const savedPath = /** @type {string[]} */ (
|
||||
JSON.parse(readStored(LS_SELECTED_KEY) || "[]") || []
|
||||
).filter((v) => v);
|
||||
console.log(savedPath);
|
||||
|
||||
/** @type {Signal<Option>} */
|
||||
const selected = signals.createSignal(/** @type {any} */ (undefined));
|
||||
|
||||
const partialOptions = createPartialOptions({
|
||||
colors,
|
||||
brk,
|
||||
});
|
||||
|
||||
/** @type {Option[]} */
|
||||
const list = [];
|
||||
|
||||
const parent = signals.createSignal(/** @type {HTMLElement | null} */ (null));
|
||||
|
||||
/**
|
||||
* @param {AnyFetchedSeriesBlueprint[]} [arr]
|
||||
*/
|
||||
function arrayToMap(arr = []) {
|
||||
/** @type {Map<Unit, AnyFetchedSeriesBlueprint[]>} */
|
||||
const map = new Map();
|
||||
for (const blueprint of arr || []) {
|
||||
if (!blueprint.metric) {
|
||||
throw new Error(
|
||||
`Blueprint missing metric: ${JSON.stringify(blueprint)}`,
|
||||
);
|
||||
}
|
||||
if (!blueprint.unit) {
|
||||
throw new Error(`Blueprint missing unit: ${blueprint.title}`);
|
||||
}
|
||||
markUsed(blueprint.metric);
|
||||
const unit = blueprint.unit;
|
||||
if (!map.has(unit)) {
|
||||
map.set(unit, []);
|
||||
}
|
||||
map.get(unit)?.push(blueprint);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Option} option
|
||||
*/
|
||||
function selectOption(option) {
|
||||
pushHistory(option.path);
|
||||
resetParams(option);
|
||||
writeToStorage(LS_SELECTED_KEY, JSON.stringify(option.path));
|
||||
selected.set(option);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} args
|
||||
* @param {Option} args.option
|
||||
* @param {Signal<string | null>} args.qrcode
|
||||
* @param {string} [args.name]
|
||||
*/
|
||||
function createOptionElement({ option, name, qrcode }) {
|
||||
const title = option.title;
|
||||
if (option.kind === "url") {
|
||||
const href = option.url();
|
||||
|
||||
if (option.qrcode) {
|
||||
return createButtonElement({
|
||||
inside: option.name,
|
||||
title,
|
||||
onClick: () => {
|
||||
qrcode.set(option.url);
|
||||
},
|
||||
});
|
||||
} else {
|
||||
return createAnchorElement({
|
||||
href,
|
||||
blank: true,
|
||||
text: option.name,
|
||||
title,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
return createAnchorElement({
|
||||
href: `/${option.path.join("/")}`,
|
||||
title,
|
||||
text: name || option.name,
|
||||
onClick: () => {
|
||||
selectOption(option);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {Option | undefined} */
|
||||
let savedOption;
|
||||
|
||||
/**
|
||||
* @param {PartialOptionsTree} partialTree
|
||||
* @param {Accessor<HTMLElement | null>} parent
|
||||
* @param {string[] | undefined} parentPath
|
||||
* @returns {Accessor<number>}
|
||||
*/
|
||||
function recursiveProcessPartialTree(
|
||||
partialTree,
|
||||
parent,
|
||||
parentPath = [],
|
||||
depth = 0,
|
||||
) {
|
||||
/** @type {Accessor<number>[]} */
|
||||
const listForSum = [];
|
||||
|
||||
const ul = signals.createMemo(
|
||||
// @ts_ignore
|
||||
(_previous) => {
|
||||
const previous = /** @type {HTMLUListElement | null} */ (_previous);
|
||||
previous?.remove();
|
||||
|
||||
const _parent = parent();
|
||||
if (_parent) {
|
||||
if ("open" in _parent && !_parent.open) {
|
||||
throw "Set accesor to null instead";
|
||||
}
|
||||
|
||||
const ul = window.document.createElement("ul");
|
||||
_parent.append(ul);
|
||||
return ul;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
null,
|
||||
);
|
||||
|
||||
partialTree.forEach((anyPartial, partialIndex) => {
|
||||
const renderLi = signals.createSignal(true);
|
||||
|
||||
const li = signals.createMemo((_previous) => {
|
||||
const previous = _previous;
|
||||
previous?.remove();
|
||||
|
||||
const _ul = ul();
|
||||
|
||||
if (renderLi() && _ul) {
|
||||
const li = window.document.createElement("li");
|
||||
insertElementAtIndex(_ul, li, partialIndex);
|
||||
return li;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}, /** @type {HTMLLIElement | null} */ (null));
|
||||
|
||||
if ("tree" in anyPartial) {
|
||||
/** @type {Omit<OptionsGroup, keyof PartialOptionsGroup>} */
|
||||
const groupAddons = {};
|
||||
|
||||
Object.assign(anyPartial, groupAddons);
|
||||
|
||||
const passedDetails = signals.createSignal(
|
||||
/** @type {HTMLDivElement | HTMLDetailsElement | null} */ (null),
|
||||
);
|
||||
|
||||
const serName = stringToId(anyPartial.name);
|
||||
const path = [...parentPath, serName];
|
||||
const childOptionsCount = recursiveProcessPartialTree(
|
||||
anyPartial.tree,
|
||||
passedDetails,
|
||||
path,
|
||||
depth + 1,
|
||||
);
|
||||
|
||||
listForSum.push(childOptionsCount);
|
||||
|
||||
signals.createEffect(li, (li) => {
|
||||
if (!li) {
|
||||
passedDetails.set(null);
|
||||
return;
|
||||
}
|
||||
|
||||
signals.createEffect(selected, (selected) => {
|
||||
if (
|
||||
path.length <= selected.path.length &&
|
||||
path.every((v, i) => selected.path.at(i) === v)
|
||||
) {
|
||||
li.dataset.highlight = "";
|
||||
} else {
|
||||
delete li.dataset.highlight;
|
||||
}
|
||||
});
|
||||
|
||||
const details = window.document.createElement("details");
|
||||
details.dataset.name = serName;
|
||||
li.appendChild(details);
|
||||
|
||||
const summary = window.document.createElement("summary");
|
||||
details.append(summary);
|
||||
summary.append(anyPartial.name);
|
||||
|
||||
const supCount = window.document.createElement("sup");
|
||||
summary.append(supCount);
|
||||
|
||||
signals.createEffect(childOptionsCount, (childOptionsCount) => {
|
||||
supCount.innerHTML = childOptionsCount.toLocaleString("en-us");
|
||||
});
|
||||
|
||||
details.addEventListener("toggle", () => {
|
||||
const open = details.open;
|
||||
|
||||
if (open) {
|
||||
passedDetails.set(details);
|
||||
} else {
|
||||
passedDetails.set(null);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function createRenderLiEffect() {
|
||||
signals.createEffect(childOptionsCount, (count) => {
|
||||
renderLi.set(!!count);
|
||||
});
|
||||
}
|
||||
createRenderLiEffect();
|
||||
} else {
|
||||
const option = /** @type {Option} */ (anyPartial);
|
||||
|
||||
const name = option.name;
|
||||
const path = [...parentPath, stringToId(option.name)];
|
||||
|
||||
if ("kind" in anyPartial && anyPartial.kind === "explorer") {
|
||||
Object.assign(
|
||||
option,
|
||||
/** @satisfies {ExplorerOption} */ ({
|
||||
kind: anyPartial.kind,
|
||||
path,
|
||||
name,
|
||||
title: option.title,
|
||||
}),
|
||||
);
|
||||
} else if ("kind" in anyPartial && anyPartial.kind === "table") {
|
||||
Object.assign(
|
||||
option,
|
||||
/** @satisfies {TableOption} */ ({
|
||||
kind: anyPartial.kind,
|
||||
path,
|
||||
name,
|
||||
title: option.title,
|
||||
}),
|
||||
);
|
||||
} else if ("kind" in anyPartial && anyPartial.kind === "simulation") {
|
||||
Object.assign(
|
||||
option,
|
||||
/** @satisfies {SimulationOption} */ ({
|
||||
kind: anyPartial.kind,
|
||||
path,
|
||||
name,
|
||||
title: anyPartial.title,
|
||||
}),
|
||||
);
|
||||
} else if ("url" in anyPartial) {
|
||||
Object.assign(
|
||||
option,
|
||||
/** @satisfies {UrlOption} */ ({
|
||||
kind: "url",
|
||||
path,
|
||||
name,
|
||||
title: name,
|
||||
qrcode: !!anyPartial.qrcode,
|
||||
url: anyPartial.url,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
const title = option.title || option.name;
|
||||
Object.assign(
|
||||
option,
|
||||
/** @satisfies {ChartOption} */ ({
|
||||
kind: "chart",
|
||||
name,
|
||||
title,
|
||||
path,
|
||||
top: arrayToMap(anyPartial.top),
|
||||
bottom: arrayToMap(anyPartial.bottom),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
list.push(option);
|
||||
|
||||
if (urlPath) {
|
||||
const sameAsURLPath =
|
||||
urlPath.length === path.length &&
|
||||
urlPath.every((val, i) => val === path[i]);
|
||||
if (sameAsURLPath) {
|
||||
selected.set(option);
|
||||
}
|
||||
} else if (savedPath) {
|
||||
const sameAsSavedPath =
|
||||
savedPath.length === path.length &&
|
||||
savedPath.every((val, i) => val === path[i]);
|
||||
if (sameAsSavedPath) {
|
||||
savedOption = option;
|
||||
}
|
||||
}
|
||||
|
||||
signals.createEffect(li, (li) => {
|
||||
if (!li) {
|
||||
return;
|
||||
}
|
||||
|
||||
signals.createEffect(selected, (selected) => {
|
||||
if (selected === option) {
|
||||
li.dataset.highlight = "";
|
||||
} else {
|
||||
delete li.dataset.highlight;
|
||||
}
|
||||
});
|
||||
|
||||
const element = createOptionElement({
|
||||
option,
|
||||
qrcode,
|
||||
});
|
||||
|
||||
li.append(element);
|
||||
});
|
||||
|
||||
listForSum.push(() => 1);
|
||||
}
|
||||
});
|
||||
|
||||
return signals.createMemo(() =>
|
||||
listForSum.reduce((acc, s) => acc + s(), 0),
|
||||
);
|
||||
}
|
||||
recursiveProcessPartialTree(partialOptions, parent);
|
||||
logUnused();
|
||||
|
||||
if (!selected()) {
|
||||
const option =
|
||||
savedOption || list.find((option) => option.kind === "chart");
|
||||
if (option) {
|
||||
selected.set(option);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
selected,
|
||||
list,
|
||||
tree: /** @type {OptionsTree} */ (partialOptions),
|
||||
parent,
|
||||
createOptionElement,
|
||||
selectOption,
|
||||
};
|
||||
}
|
||||
/** @typedef {ReturnType<typeof initOptions>} Options */
|
||||
237
website/scripts/options/market/averages.js
Normal file
237
website/scripts/options/market/averages.js
Normal file
@@ -0,0 +1,237 @@
|
||||
/** Moving averages section */
|
||||
|
||||
import { Unit } from "../../utils/units.js";
|
||||
import { periodIdToName } from "./utils.js";
|
||||
|
||||
/**
|
||||
* Build averages data array from market patterns
|
||||
* @param {Colors} colors
|
||||
* @param {MarketMovingAverage} ma
|
||||
*/
|
||||
export function buildAverages(colors, ma) {
|
||||
return /** @type {const} */ ([
|
||||
["1w", 7, "red", ma.price1wSma, ma.price1wEma],
|
||||
["8d", 8, "orange", ma.price8dSma, ma.price8dEma],
|
||||
["13d", 13, "amber", ma.price13dSma, ma.price13dEma],
|
||||
["21d", 21, "yellow", ma.price21dSma, ma.price21dEma],
|
||||
["1m", 30, "lime", ma.price1mSma, ma.price1mEma],
|
||||
["34d", 34, "green", ma.price34dSma, ma.price34dEma],
|
||||
["55d", 55, "emerald", ma.price55dSma, ma.price55dEma],
|
||||
["89d", 89, "teal", ma.price89dSma, ma.price89dEma],
|
||||
["144d", 144, "cyan", ma.price144dSma, ma.price144dEma],
|
||||
["200d", 200, "sky", ma.price200dSma, ma.price200dEma],
|
||||
["1y", 365, "blue", ma.price1ySma, ma.price1yEma],
|
||||
["2y", 730, "indigo", ma.price2ySma, ma.price2yEma],
|
||||
["200w", 1400, "violet", ma.price200wSma, ma.price200wEma],
|
||||
["4y", 1460, "purple", ma.price4ySma, ma.price4yEma],
|
||||
]).map(([id, days, colorKey, sma, ema]) => ({
|
||||
id,
|
||||
name: periodIdToName(id, true),
|
||||
days,
|
||||
color: colors[colorKey],
|
||||
sma,
|
||||
ema,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create price with ratio options (for moving averages)
|
||||
* @param {PartialContext} ctx
|
||||
* @param {Object} args
|
||||
* @param {string} args.title
|
||||
* @param {string} args.legend
|
||||
* @param {EmaRatioPattern} args.ratio
|
||||
* @param {Color} [args.color]
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
export function createPriceWithRatioOptions(
|
||||
ctx,
|
||||
{ title, legend, ratio, color },
|
||||
) {
|
||||
const { line, colors, createPriceLine } = ctx;
|
||||
const priceMetric = ratio.price;
|
||||
|
||||
const percentileUsdMap = [
|
||||
{ name: "pct99", prop: ratio.ratioPct99Usd, color: colors.rose },
|
||||
{ name: "pct98", prop: ratio.ratioPct98Usd, color: colors.pink },
|
||||
{ name: "pct95", prop: ratio.ratioPct95Usd, color: colors.fuchsia },
|
||||
{ name: "pct5", prop: ratio.ratioPct5Usd, color: colors.cyan },
|
||||
{ name: "pct2", prop: ratio.ratioPct2Usd, color: colors.sky },
|
||||
{ name: "pct1", prop: ratio.ratioPct1Usd, color: colors.blue },
|
||||
];
|
||||
|
||||
const percentileMap = [
|
||||
{ name: "pct99", prop: ratio.ratioPct99, color: colors.rose },
|
||||
{ name: "pct98", prop: ratio.ratioPct98, color: colors.pink },
|
||||
{ name: "pct95", prop: ratio.ratioPct95, color: colors.fuchsia },
|
||||
{ name: "pct5", prop: ratio.ratioPct5, color: colors.cyan },
|
||||
{ name: "pct2", prop: ratio.ratioPct2, color: colors.sky },
|
||||
{ name: "pct1", prop: ratio.ratioPct1, color: colors.blue },
|
||||
];
|
||||
|
||||
const sdPatterns = [
|
||||
{ nameAddon: "all", titleAddon: "", sd: ratio.ratioSd },
|
||||
{ nameAddon: "4y", titleAddon: "4y", sd: ratio.ratio4ySd },
|
||||
{ nameAddon: "2y", titleAddon: "2y", sd: ratio.ratio2ySd },
|
||||
{ nameAddon: "1y", titleAddon: "1y", sd: ratio.ratio1ySd },
|
||||
];
|
||||
|
||||
/** @param {Ratio1ySdPattern} sd */
|
||||
const getSdBands = (sd) => [
|
||||
{ name: "0σ", prop: sd._0sdUsd, color: colors.lime },
|
||||
{ name: "+0.5σ", prop: sd.p05sdUsd, color: colors.yellow },
|
||||
{ name: "+1σ", prop: sd.p1sdUsd, color: colors.amber },
|
||||
{ name: "+1.5σ", prop: sd.p15sdUsd, color: colors.orange },
|
||||
{ name: "+2σ", prop: sd.p2sdUsd, color: colors.red },
|
||||
{ name: "+2.5σ", prop: sd.p25sdUsd, color: colors.rose },
|
||||
{ name: "+3σ", prop: sd.p3sd, color: colors.pink },
|
||||
{ name: "−0.5σ", prop: sd.m05sdUsd, color: colors.teal },
|
||||
{ name: "−1σ", prop: sd.m1sdUsd, color: colors.cyan },
|
||||
{ name: "−1.5σ", prop: sd.m15sdUsd, color: colors.sky },
|
||||
{ name: "−2σ", prop: sd.m2sdUsd, color: colors.blue },
|
||||
{ name: "−2.5σ", prop: sd.m25sdUsd, color: colors.indigo },
|
||||
{ name: "−3σ", prop: sd.m3sd, color: colors.violet },
|
||||
];
|
||||
|
||||
return [
|
||||
{
|
||||
name: "price",
|
||||
title,
|
||||
top: [line({ metric: priceMetric, name: legend, color, unit: Unit.usd })],
|
||||
},
|
||||
{
|
||||
name: "Ratio",
|
||||
title: `${title} Ratio`,
|
||||
top: [
|
||||
line({ metric: priceMetric, name: legend, color, unit: Unit.usd }),
|
||||
...percentileUsdMap.map(({ name: pctName, prop, color: pctColor }) =>
|
||||
line({
|
||||
metric: prop,
|
||||
name: pctName,
|
||||
color: pctColor,
|
||||
defaultActive: false,
|
||||
unit: Unit.usd,
|
||||
options: { lineStyle: 1 },
|
||||
}),
|
||||
),
|
||||
],
|
||||
bottom: [
|
||||
line({ metric: ratio.ratio, name: "Ratio", color, unit: Unit.ratio }),
|
||||
line({
|
||||
metric: ratio.ratio1wSma,
|
||||
name: "1w SMA",
|
||||
color: colors.lime,
|
||||
unit: Unit.ratio,
|
||||
}),
|
||||
line({
|
||||
metric: ratio.ratio1mSma,
|
||||
name: "1m SMA",
|
||||
color: colors.teal,
|
||||
unit: Unit.ratio,
|
||||
}),
|
||||
line({
|
||||
metric: ratio.ratio1ySd.sma,
|
||||
name: "1y SMA",
|
||||
color: colors.sky,
|
||||
unit: Unit.ratio,
|
||||
}),
|
||||
line({
|
||||
metric: ratio.ratio2ySd.sma,
|
||||
name: "2y SMA",
|
||||
color: colors.indigo,
|
||||
unit: Unit.ratio,
|
||||
}),
|
||||
line({
|
||||
metric: ratio.ratio4ySd.sma,
|
||||
name: "4y SMA",
|
||||
color: colors.purple,
|
||||
unit: Unit.ratio,
|
||||
}),
|
||||
line({
|
||||
metric: ratio.ratioSd.sma,
|
||||
name: "All SMA",
|
||||
color: colors.rose,
|
||||
unit: Unit.ratio,
|
||||
}),
|
||||
...percentileMap.map(({ name: pctName, prop, color: pctColor }) =>
|
||||
line({
|
||||
metric: prop,
|
||||
name: pctName,
|
||||
color: pctColor,
|
||||
defaultActive: false,
|
||||
unit: Unit.ratio,
|
||||
options: { lineStyle: 1 },
|
||||
}),
|
||||
),
|
||||
createPriceLine({ unit: Unit.ratio, number: 1 }),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "ZScores",
|
||||
tree: sdPatterns.map(({ nameAddon, titleAddon, sd }) => ({
|
||||
name: nameAddon,
|
||||
title: `${title} ${titleAddon} Z-Score`,
|
||||
top: getSdBands(sd).map(({ name: bandName, prop, color: bandColor }) =>
|
||||
line({
|
||||
metric: prop,
|
||||
name: bandName,
|
||||
color: bandColor,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
),
|
||||
bottom: [
|
||||
line({ metric: sd.zscore, name: "Z-Score", color, unit: Unit.sd }),
|
||||
createPriceLine({ unit: Unit.sd, number: 3 }),
|
||||
createPriceLine({ unit: Unit.sd, number: 2 }),
|
||||
createPriceLine({ unit: Unit.sd, number: 1 }),
|
||||
createPriceLine({ unit: Unit.sd, number: 0 }),
|
||||
createPriceLine({ unit: Unit.sd, number: -1 }),
|
||||
createPriceLine({ unit: Unit.sd, number: -2 }),
|
||||
createPriceLine({ unit: Unit.sd, number: -3 }),
|
||||
],
|
||||
})),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Averages section
|
||||
* @param {PartialContext} ctx
|
||||
* @param {ReturnType<typeof buildAverages>} averages
|
||||
*/
|
||||
export function createAveragesSection(ctx, averages) {
|
||||
const { line } = ctx;
|
||||
|
||||
return {
|
||||
name: "Averages",
|
||||
tree: [
|
||||
{ nameAddon: "Simple", metricAddon: /** @type {const} */ ("sma") },
|
||||
{ nameAddon: "Exponential", metricAddon: /** @type {const} */ ("ema") },
|
||||
].map(({ nameAddon, metricAddon }) => ({
|
||||
name: nameAddon,
|
||||
tree: [
|
||||
{
|
||||
name: "Compare",
|
||||
title: `Market Price ${nameAddon} Moving Averages`,
|
||||
top: averages.map(({ id, color, sma, ema }) =>
|
||||
line({
|
||||
metric: (metricAddon === "sma" ? sma : ema).price,
|
||||
name: id,
|
||||
color,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
),
|
||||
},
|
||||
...averages.map(({ name, color, sma, ema }) => ({
|
||||
name,
|
||||
tree: createPriceWithRatioOptions(ctx, {
|
||||
ratio: metricAddon === "sma" ? sma : ema,
|
||||
title: `${name} Market Price ${nameAddon} Moving Average`,
|
||||
legend: "average",
|
||||
color,
|
||||
}),
|
||||
})),
|
||||
],
|
||||
})),
|
||||
};
|
||||
}
|
||||
118
website/scripts/options/market/index.js
Normal file
118
website/scripts/options/market/index.js
Normal file
@@ -0,0 +1,118 @@
|
||||
/** Market section - Main entry point */
|
||||
|
||||
import { localhost } from "../../utils/env.js";
|
||||
import { Unit } from "../../utils/units.js";
|
||||
import { buildAverages, createAveragesSection } from "./averages.js";
|
||||
import { createPerformanceSection } from "./performance.js";
|
||||
import { createIndicatorsSection } from "./indicators/index.js";
|
||||
import { createInvestingSection } from "./investing.js";
|
||||
|
||||
/**
|
||||
* Create Market section
|
||||
* @param {PartialContext} ctx
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
export function createMarketSection(ctx) {
|
||||
const { colors, brk, line, candlestick } = ctx;
|
||||
const { market, supply, price } = brk.metrics;
|
||||
const {
|
||||
movingAverage,
|
||||
ath,
|
||||
returns,
|
||||
volatility,
|
||||
range,
|
||||
dca,
|
||||
lookback,
|
||||
indicators,
|
||||
} = market;
|
||||
|
||||
const averages = buildAverages(colors, movingAverage);
|
||||
|
||||
return {
|
||||
name: "Market",
|
||||
tree: [
|
||||
// Price
|
||||
{
|
||||
name: "Price",
|
||||
title: "Bitcoin Price",
|
||||
...(localhost && {
|
||||
top: [
|
||||
candlestick({
|
||||
metric: price.oracle.ohlcDollars,
|
||||
name: "Oracle",
|
||||
unit: Unit.usd,
|
||||
colors: [colors.cyan, colors.purple],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
},
|
||||
|
||||
// Capitalization
|
||||
{
|
||||
name: "Capitalization",
|
||||
title: "Market Capitalization",
|
||||
bottom: [
|
||||
line({
|
||||
metric: supply.marketCap,
|
||||
name: "Capitalization",
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
],
|
||||
},
|
||||
|
||||
// All Time High
|
||||
{
|
||||
name: "All Time High",
|
||||
title: "All Time High",
|
||||
top: [line({ metric: ath.priceAth, name: "ATH", unit: Unit.usd })],
|
||||
bottom: [
|
||||
line({
|
||||
metric: ath.priceDrawdown,
|
||||
name: "Drawdown",
|
||||
color: colors.red,
|
||||
unit: Unit.percentage,
|
||||
}),
|
||||
line({
|
||||
metric: ath.daysSincePriceAth,
|
||||
name: "Since",
|
||||
unit: Unit.days,
|
||||
}),
|
||||
line({
|
||||
metric: ath.yearsSincePriceAth,
|
||||
name: "Since",
|
||||
unit: Unit.years,
|
||||
}),
|
||||
line({
|
||||
metric: ath.maxDaysBetweenPriceAths,
|
||||
name: "Max",
|
||||
color: colors.red,
|
||||
unit: Unit.days,
|
||||
}),
|
||||
line({
|
||||
metric: ath.maxYearsBetweenPriceAths,
|
||||
name: "Max",
|
||||
color: colors.red,
|
||||
unit: Unit.years,
|
||||
}),
|
||||
],
|
||||
},
|
||||
|
||||
// Averages
|
||||
createAveragesSection(ctx, averages),
|
||||
|
||||
// Performance
|
||||
createPerformanceSection(ctx, returns),
|
||||
|
||||
// Indicators
|
||||
createIndicatorsSection(ctx, {
|
||||
volatility,
|
||||
range,
|
||||
movingAverage,
|
||||
indicators,
|
||||
}),
|
||||
|
||||
// Investing
|
||||
createInvestingSection(ctx, { dca, lookback, returns }),
|
||||
],
|
||||
};
|
||||
}
|
||||
90
website/scripts/options/market/indicators/bands.js
Normal file
90
website/scripts/options/market/indicators/bands.js
Normal file
@@ -0,0 +1,90 @@
|
||||
/** Bands indicators (MinMax, Mayer Multiple) */
|
||||
|
||||
import { Unit } from "../../../utils/units.js";
|
||||
|
||||
/**
|
||||
* Create Bands section
|
||||
* @param {PartialContext} ctx
|
||||
* @param {Object} args
|
||||
* @param {Market["range"]} args.range
|
||||
* @param {Market["movingAverage"]} args.movingAverage
|
||||
*/
|
||||
export function createBandsSection(ctx, { range, movingAverage }) {
|
||||
const { line, colors } = ctx;
|
||||
|
||||
return {
|
||||
name: "Bands",
|
||||
tree: [
|
||||
{
|
||||
name: "MinMax",
|
||||
tree: [
|
||||
{
|
||||
id: "1w",
|
||||
title: "1 Week",
|
||||
min: range.price1wMin,
|
||||
max: range.price1wMax,
|
||||
},
|
||||
{
|
||||
id: "2w",
|
||||
title: "2 Week",
|
||||
min: range.price2wMin,
|
||||
max: range.price2wMax,
|
||||
},
|
||||
{
|
||||
id: "1m",
|
||||
title: "1 Month",
|
||||
min: range.price1mMin,
|
||||
max: range.price1mMax,
|
||||
},
|
||||
{
|
||||
id: "1y",
|
||||
title: "1 Year",
|
||||
min: range.price1yMin,
|
||||
max: range.price1yMax,
|
||||
},
|
||||
].map(({ id, title, min, max }) => ({
|
||||
name: id,
|
||||
title: `Bitcoin Price ${title} MinMax Bands`,
|
||||
top: [
|
||||
line({
|
||||
metric: min,
|
||||
name: "Min",
|
||||
color: colors.red,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
line({
|
||||
metric: max,
|
||||
name: "Max",
|
||||
color: colors.green,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
],
|
||||
})),
|
||||
},
|
||||
{
|
||||
name: "Mayer Multiple",
|
||||
title: "Mayer Multiple",
|
||||
top: [
|
||||
line({
|
||||
metric: movingAverage.price200dSma.price,
|
||||
name: "200d SMA",
|
||||
color: colors.yellow,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
line({
|
||||
metric: movingAverage.price200dSmaX24,
|
||||
name: "200d SMA x2.4",
|
||||
color: colors.green,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
line({
|
||||
metric: movingAverage.price200dSmaX08,
|
||||
name: "200d SMA x0.8",
|
||||
color: colors.red,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
27
website/scripts/options/market/indicators/index.js
Normal file
27
website/scripts/options/market/indicators/index.js
Normal file
@@ -0,0 +1,27 @@
|
||||
/** Indicators section - Main entry point */
|
||||
|
||||
import { createMomentumSection } from "./momentum.js";
|
||||
import { createVolatilitySection } from "./volatility.js";
|
||||
import { createBandsSection } from "./bands.js";
|
||||
import { createOnchainSection } from "./onchain.js";
|
||||
|
||||
/**
|
||||
* Create Indicators section
|
||||
* @param {PartialContext} ctx
|
||||
* @param {Object} args
|
||||
* @param {Market["volatility"]} args.volatility
|
||||
* @param {Market["range"]} args.range
|
||||
* @param {Market["movingAverage"]} args.movingAverage
|
||||
* @param {Market["indicators"]} args.indicators
|
||||
*/
|
||||
export function createIndicatorsSection(ctx, { volatility, range, movingAverage, indicators }) {
|
||||
return {
|
||||
name: "Indicators",
|
||||
tree: [
|
||||
createMomentumSection(ctx, indicators),
|
||||
createVolatilitySection(ctx, { volatility, range }),
|
||||
createBandsSection(ctx, { range, movingAverage }),
|
||||
createOnchainSection(ctx, { indicators, movingAverage }),
|
||||
],
|
||||
};
|
||||
}
|
||||
111
website/scripts/options/market/indicators/momentum.js
Normal file
111
website/scripts/options/market/indicators/momentum.js
Normal file
@@ -0,0 +1,111 @@
|
||||
/** Momentum indicators (RSI, StochRSI, Stochastic, MACD) */
|
||||
|
||||
import { Unit } from "../../../utils/units.js";
|
||||
|
||||
/**
|
||||
* Create Momentum section
|
||||
* @param {PartialContext} ctx
|
||||
* @param {Market["indicators"]} indicators
|
||||
*/
|
||||
export function createMomentumSection(ctx, indicators) {
|
||||
const { line, histogram, colors, createPriceLine } = ctx;
|
||||
|
||||
return {
|
||||
name: "Momentum",
|
||||
tree: [
|
||||
{
|
||||
name: "RSI",
|
||||
title: "Relative Strength Index (14d)",
|
||||
bottom: [
|
||||
line({
|
||||
metric: indicators.rsi14d,
|
||||
name: "RSI",
|
||||
color: colors.indigo,
|
||||
unit: Unit.index,
|
||||
}),
|
||||
line({
|
||||
metric: indicators.rsi14dMin,
|
||||
name: "Min",
|
||||
color: colors.red,
|
||||
defaultActive: false,
|
||||
unit: Unit.index,
|
||||
}),
|
||||
line({
|
||||
metric: indicators.rsi14dMax,
|
||||
name: "Max",
|
||||
color: colors.green,
|
||||
defaultActive: false,
|
||||
unit: Unit.index,
|
||||
}),
|
||||
createPriceLine({ unit: Unit.index, number: 70 }),
|
||||
createPriceLine({
|
||||
unit: Unit.index,
|
||||
number: 50,
|
||||
defaultActive: false,
|
||||
}),
|
||||
createPriceLine({ unit: Unit.index, number: 30 }),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "StochRSI",
|
||||
title: "Stochastic RSI",
|
||||
bottom: [
|
||||
// line({
|
||||
// metric: indicators.stochRsi,
|
||||
// name: "Stoch RSI",
|
||||
// color: colors.purple,
|
||||
// unit: Unit.index,
|
||||
// }),
|
||||
line({
|
||||
metric: indicators.stochRsiK,
|
||||
name: "K",
|
||||
color: colors.blue,
|
||||
unit: Unit.index,
|
||||
}),
|
||||
line({
|
||||
metric: indicators.stochRsiD,
|
||||
name: "D",
|
||||
color: colors.orange,
|
||||
unit: Unit.index,
|
||||
}),
|
||||
createPriceLine({ unit: Unit.index, number: 80 }),
|
||||
createPriceLine({ unit: Unit.index, number: 20 }),
|
||||
],
|
||||
},
|
||||
// {
|
||||
// name: "Stochastic",
|
||||
// title: "Stochastic Oscillator",
|
||||
// bottom: [
|
||||
// line({ metric: indicators.stochK, name: "K", color: colors.blue, unit: Unit.index }),
|
||||
// line({ metric: indicators.stochD, name: "D", color: colors.orange, unit: Unit.index }),
|
||||
// createPriceLine({ unit: Unit.index, number: 80 }),
|
||||
// createPriceLine({ unit: Unit.index, number: 20 }),
|
||||
// ],
|
||||
// },
|
||||
{
|
||||
name: "MACD",
|
||||
title: "Moving Average Convergence Divergence",
|
||||
bottom: [
|
||||
line({
|
||||
metric: indicators.macdLine,
|
||||
name: "MACD",
|
||||
color: colors.blue,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
line({
|
||||
metric: indicators.macdSignal,
|
||||
name: "Signal",
|
||||
color: colors.orange,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
histogram({
|
||||
metric: indicators.macdHistogram,
|
||||
name: "Histogram",
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
createPriceLine({ unit: Unit.usd }),
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
83
website/scripts/options/market/indicators/onchain.js
Normal file
83
website/scripts/options/market/indicators/onchain.js
Normal file
@@ -0,0 +1,83 @@
|
||||
/** On-chain indicators (Pi Cycle, Puell, NVT, Gini) */
|
||||
|
||||
import { Unit } from "../../../utils/units.js";
|
||||
|
||||
/**
|
||||
* Create On-chain section
|
||||
* @param {PartialContext} ctx
|
||||
* @param {Object} args
|
||||
* @param {Market["indicators"]} args.indicators
|
||||
* @param {Market["movingAverage"]} args.movingAverage
|
||||
*/
|
||||
export function createOnchainSection(ctx, { indicators, movingAverage }) {
|
||||
const { line, colors, createPriceLine } = ctx;
|
||||
|
||||
return {
|
||||
name: "On-chain",
|
||||
tree: [
|
||||
{
|
||||
name: "Pi Cycle",
|
||||
title: "Pi Cycle Top Indicator",
|
||||
top: [
|
||||
line({
|
||||
metric: movingAverage.price111dSma.price,
|
||||
name: "111d SMA",
|
||||
color: colors.green,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
line({
|
||||
metric: movingAverage.price350dSmaX2,
|
||||
name: "350d SMA x2",
|
||||
color: colors.red,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
],
|
||||
bottom: [
|
||||
line({
|
||||
metric: indicators.piCycle,
|
||||
name: "Pi Cycle",
|
||||
color: colors.purple,
|
||||
unit: Unit.ratio,
|
||||
}),
|
||||
createPriceLine({ unit: Unit.ratio, number: 1 }),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Puell Multiple",
|
||||
title: "Puell Multiple",
|
||||
bottom: [
|
||||
line({
|
||||
metric: indicators.puellMultiple,
|
||||
name: "Puell",
|
||||
color: colors.green,
|
||||
unit: Unit.ratio,
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "NVT",
|
||||
title: "Network Value to Transactions Ratio",
|
||||
bottom: [
|
||||
line({
|
||||
metric: indicators.nvt,
|
||||
name: "NVT",
|
||||
color: colors.orange,
|
||||
unit: Unit.ratio,
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Gini",
|
||||
title: "Gini Coefficient",
|
||||
bottom: [
|
||||
line({
|
||||
metric: indicators.gini,
|
||||
name: "Gini",
|
||||
color: colors.red,
|
||||
unit: Unit.ratio,
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
120
website/scripts/options/market/indicators/volatility.js
Normal file
120
website/scripts/options/market/indicators/volatility.js
Normal file
@@ -0,0 +1,120 @@
|
||||
/** Volatility indicators (Index, True Range, Choppiness, Sharpe, Sortino) */
|
||||
|
||||
import { Unit } from "../../../utils/units.js";
|
||||
|
||||
/**
|
||||
* Create Volatility section
|
||||
* @param {PartialContext} ctx
|
||||
* @param {Object} args
|
||||
* @param {Market["volatility"]} args.volatility
|
||||
* @param {Market["range"]} args.range
|
||||
*/
|
||||
export function createVolatilitySection(ctx, { volatility, range }) {
|
||||
const { line, colors, createPriceLine } = ctx;
|
||||
|
||||
return {
|
||||
name: "Volatility",
|
||||
tree: [
|
||||
{
|
||||
name: "Index",
|
||||
title: "Bitcoin Price Volatility Index",
|
||||
bottom: [
|
||||
line({
|
||||
metric: volatility.price1wVolatility,
|
||||
name: "1w",
|
||||
color: colors.red,
|
||||
unit: Unit.percentage,
|
||||
}),
|
||||
line({
|
||||
metric: volatility.price1mVolatility,
|
||||
name: "1m",
|
||||
color: colors.orange,
|
||||
unit: Unit.percentage,
|
||||
}),
|
||||
line({
|
||||
metric: volatility.price1yVolatility,
|
||||
name: "1y",
|
||||
color: colors.lime,
|
||||
unit: Unit.percentage,
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "True Range",
|
||||
title: "Bitcoin Price True Range",
|
||||
bottom: [
|
||||
line({
|
||||
metric: range.priceTrueRange,
|
||||
name: "Value",
|
||||
color: colors.yellow,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Choppiness",
|
||||
title: "Bitcoin Price Choppiness Index",
|
||||
bottom: [
|
||||
line({
|
||||
metric: range.price2wChoppinessIndex,
|
||||
name: "2w",
|
||||
color: colors.red,
|
||||
unit: Unit.index,
|
||||
}),
|
||||
createPriceLine({ unit: Unit.index, number: 61.8 }),
|
||||
createPriceLine({ unit: Unit.index, number: 38.2 }),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Sharpe Ratio",
|
||||
title: "Sharpe Ratio",
|
||||
bottom: [
|
||||
line({
|
||||
metric: volatility.sharpe1w,
|
||||
name: "1w",
|
||||
color: colors.red,
|
||||
unit: Unit.ratio,
|
||||
}),
|
||||
line({
|
||||
metric: volatility.sharpe1m,
|
||||
name: "1m",
|
||||
color: colors.orange,
|
||||
unit: Unit.ratio,
|
||||
}),
|
||||
line({
|
||||
metric: volatility.sharpe1y,
|
||||
name: "1y",
|
||||
color: colors.lime,
|
||||
unit: Unit.ratio,
|
||||
}),
|
||||
createPriceLine({ unit: Unit.ratio }),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Sortino Ratio",
|
||||
title: "Sortino Ratio",
|
||||
bottom: [
|
||||
line({
|
||||
metric: volatility.sortino1w,
|
||||
name: "1w",
|
||||
color: colors.red,
|
||||
unit: Unit.ratio,
|
||||
}),
|
||||
line({
|
||||
metric: volatility.sortino1m,
|
||||
name: "1m",
|
||||
color: colors.orange,
|
||||
unit: Unit.ratio,
|
||||
}),
|
||||
line({
|
||||
metric: volatility.sortino1y,
|
||||
name: "1y",
|
||||
color: colors.lime,
|
||||
unit: Unit.ratio,
|
||||
}),
|
||||
createPriceLine({ unit: Unit.ratio }),
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
284
website/scripts/options/market/investing.js
Normal file
284
website/scripts/options/market/investing.js
Normal file
@@ -0,0 +1,284 @@
|
||||
/** Investing section (DCA) */
|
||||
|
||||
import { Unit } from "../../utils/units.js";
|
||||
import { periodIdToName } from "./utils.js";
|
||||
|
||||
/**
|
||||
* Build DCA classes data array
|
||||
* @param {Colors} colors
|
||||
* @param {MarketDca} dca
|
||||
*/
|
||||
export function buildDcaClasses(colors, dca) {
|
||||
return /** @type {const} */ ([
|
||||
[2025, "pink", true],
|
||||
[2024, "fuchsia", true],
|
||||
[2023, "purple", true],
|
||||
[2022, "blue", true],
|
||||
[2021, "sky", true],
|
||||
[2020, "teal", true],
|
||||
[2019, "green", true],
|
||||
[2018, "yellow", true],
|
||||
[2017, "orange", true],
|
||||
[2016, "red", false],
|
||||
[2015, "pink", false],
|
||||
]).map(([year, colorKey, defaultActive]) => ({
|
||||
year,
|
||||
color: colors[colorKey],
|
||||
defaultActive,
|
||||
costBasis: dca.classAveragePrice[`_${year}`],
|
||||
returns: dca.classReturns[`_${year}`],
|
||||
stack: dca.classStack[`_${year}`],
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Investing section
|
||||
* @param {PartialContext} ctx
|
||||
* @param {Object} args
|
||||
* @param {Market["dca"]} args.dca
|
||||
* @param {Market["lookback"]} args.lookback
|
||||
* @param {Market["returns"]} args.returns
|
||||
*/
|
||||
export function createInvestingSection(ctx, { dca, lookback, returns }) {
|
||||
const { line, baseline, colors, createPriceLine } = ctx;
|
||||
const dcaClasses = buildDcaClasses(colors, dca);
|
||||
|
||||
return {
|
||||
name: "Investing",
|
||||
tree: [
|
||||
// DCA vs Lump sum
|
||||
{
|
||||
name: "DCA vs Lump sum",
|
||||
tree: /** @type {const} */ ([
|
||||
["1w", "_1w"],
|
||||
["1m", "_1m"],
|
||||
["3m", "_3m"],
|
||||
["6m", "_6m"],
|
||||
["1y", "_1y"],
|
||||
["2y", "_2y"],
|
||||
["3y", "_3y"],
|
||||
["4y", "_4y"],
|
||||
["5y", "_5y"],
|
||||
["6y", "_6y"],
|
||||
["8y", "_8y"],
|
||||
["10y", "_10y"],
|
||||
]).map(([id, key]) => {
|
||||
const name = periodIdToName(id, true);
|
||||
const priceAgo = lookback[key];
|
||||
const priceReturns = returns.priceReturns[key];
|
||||
const dcaCostBasis = dca.periodAveragePrice[key];
|
||||
const dcaReturns = dca.periodReturns[key];
|
||||
const dcaStack = dca.periodStack[key];
|
||||
const lumpSumStack = dca.periodLumpSumStack[key];
|
||||
return {
|
||||
name,
|
||||
tree: [
|
||||
{
|
||||
name: "Cost basis",
|
||||
title: `${name} DCA vs Lump Sum (Cost Basis)`,
|
||||
top: [
|
||||
line({
|
||||
metric: dcaCostBasis,
|
||||
name: "DCA",
|
||||
color: colors.green,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
line({
|
||||
metric: priceAgo,
|
||||
name: "Lump sum",
|
||||
color: colors.orange,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Returns",
|
||||
title: `${name} DCA vs Lump Sum (Returns)`,
|
||||
bottom: [
|
||||
baseline({
|
||||
metric: dcaReturns,
|
||||
name: "DCA",
|
||||
unit: Unit.percentage,
|
||||
}),
|
||||
baseline({
|
||||
metric: priceReturns,
|
||||
name: "Lump sum",
|
||||
color: [colors.lime, colors.red],
|
||||
unit: Unit.percentage,
|
||||
}),
|
||||
createPriceLine({ unit: Unit.percentage }),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Stack",
|
||||
title: `${name} DCA vs Lump Sum Stack ($100/day)`,
|
||||
bottom: [
|
||||
line({
|
||||
metric: dcaStack.sats,
|
||||
name: "DCA",
|
||||
color: colors.green,
|
||||
unit: Unit.sats,
|
||||
}),
|
||||
line({
|
||||
metric: dcaStack.bitcoin,
|
||||
name: "DCA",
|
||||
color: colors.green,
|
||||
unit: Unit.btc,
|
||||
}),
|
||||
line({
|
||||
metric: dcaStack.dollars,
|
||||
name: "DCA",
|
||||
color: colors.green,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
line({
|
||||
metric: lumpSumStack.sats,
|
||||
name: "Lump sum",
|
||||
color: colors.orange,
|
||||
unit: Unit.sats,
|
||||
}),
|
||||
line({
|
||||
metric: lumpSumStack.bitcoin,
|
||||
name: "Lump sum",
|
||||
color: colors.orange,
|
||||
unit: Unit.btc,
|
||||
}),
|
||||
line({
|
||||
metric: lumpSumStack.dollars,
|
||||
name: "Lump sum",
|
||||
color: colors.orange,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}),
|
||||
},
|
||||
|
||||
// DCA classes
|
||||
{
|
||||
name: "DCA classes",
|
||||
tree: [
|
||||
// Comparison charts (all years overlaid)
|
||||
{
|
||||
name: "Compare",
|
||||
tree: [
|
||||
{
|
||||
name: "Cost basis",
|
||||
title: "DCA Cost Basis by Year",
|
||||
top: dcaClasses.map(
|
||||
({ year, color, defaultActive, costBasis }) =>
|
||||
line({
|
||||
metric: costBasis,
|
||||
name: `${year}`,
|
||||
color,
|
||||
defaultActive,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Returns",
|
||||
title: "DCA Returns by Year",
|
||||
bottom: dcaClasses.map(
|
||||
({ year, color, defaultActive, returns }) =>
|
||||
baseline({
|
||||
metric: returns,
|
||||
name: `${year}`,
|
||||
color,
|
||||
defaultActive,
|
||||
unit: Unit.percentage,
|
||||
}),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Stack",
|
||||
title: "DCA Stack by Year ($100/day)",
|
||||
bottom: dcaClasses.flatMap(
|
||||
({ year, color, defaultActive, stack }) => [
|
||||
line({
|
||||
metric: stack.sats,
|
||||
name: `${year}`,
|
||||
color,
|
||||
defaultActive,
|
||||
unit: Unit.sats,
|
||||
}),
|
||||
line({
|
||||
metric: stack.bitcoin,
|
||||
name: `${year}`,
|
||||
color,
|
||||
defaultActive,
|
||||
unit: Unit.btc,
|
||||
}),
|
||||
line({
|
||||
metric: stack.dollars,
|
||||
name: `${year}`,
|
||||
color,
|
||||
defaultActive,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
],
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
// Individual year charts
|
||||
...dcaClasses.map(({ year, color, costBasis, returns, stack }) => ({
|
||||
name: `${year}`,
|
||||
tree: [
|
||||
{
|
||||
name: "Cost basis",
|
||||
title: `DCA Class ${year} Cost Basis`,
|
||||
top: [
|
||||
line({
|
||||
metric: costBasis,
|
||||
name: "Cost basis",
|
||||
color,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Returns",
|
||||
title: `DCA Class ${year} Returns`,
|
||||
bottom: [
|
||||
baseline({
|
||||
metric: returns,
|
||||
name: "Returns",
|
||||
color,
|
||||
unit: Unit.percentage,
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Stack",
|
||||
title: `DCA Class ${year} Stack ($100/day)`,
|
||||
bottom: [
|
||||
line({
|
||||
metric: stack.sats,
|
||||
name: "Stack",
|
||||
color,
|
||||
unit: Unit.sats,
|
||||
}),
|
||||
line({
|
||||
metric: stack.bitcoin,
|
||||
name: "Stack",
|
||||
color,
|
||||
unit: Unit.btc,
|
||||
}),
|
||||
line({
|
||||
metric: stack.dollars,
|
||||
name: "Stack",
|
||||
color,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
})),
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
58
website/scripts/options/market/performance.js
Normal file
58
website/scripts/options/market/performance.js
Normal file
@@ -0,0 +1,58 @@
|
||||
/** Performance section */
|
||||
|
||||
import { Unit } from "../../utils/units.js";
|
||||
import { periodIdToName } from "./utils.js";
|
||||
|
||||
/**
|
||||
* Create Performance section
|
||||
* @param {PartialContext} ctx
|
||||
* @param {Market["returns"]} returns
|
||||
*/
|
||||
export function createPerformanceSection(ctx, returns) {
|
||||
const { colors, baseline, createPriceLine } = ctx;
|
||||
|
||||
return {
|
||||
name: "Performance",
|
||||
tree: /** @type {const} */ ([
|
||||
["1d", "_1d", undefined],
|
||||
["1w", "_1w", undefined],
|
||||
["1m", "_1m", undefined],
|
||||
["3m", "_3m", undefined],
|
||||
["6m", "_6m", undefined],
|
||||
["1y", "_1y", undefined],
|
||||
["2y", "_2y", "_2y"],
|
||||
["3y", "_3y", "_3y"],
|
||||
["4y", "_4y", "_4y"],
|
||||
["5y", "_5y", "_5y"],
|
||||
["6y", "_6y", "_6y"],
|
||||
["8y", "_8y", "_8y"],
|
||||
["10y", "_10y", "_10y"],
|
||||
]).map(([id, returnKey, cagrKey]) => {
|
||||
const priceReturns = returns.priceReturns[returnKey];
|
||||
const cagr = cagrKey ? returns.cagr[cagrKey] : undefined;
|
||||
const name = periodIdToName(id, true);
|
||||
return {
|
||||
name,
|
||||
title: `${name} Performance`,
|
||||
bottom: [
|
||||
baseline({
|
||||
metric: priceReturns,
|
||||
name: "Total",
|
||||
unit: Unit.percentage,
|
||||
}),
|
||||
...(cagr
|
||||
? [
|
||||
baseline({
|
||||
metric: cagr,
|
||||
name: "CAGR",
|
||||
color: [colors.lime, colors.pink],
|
||||
unit: Unit.percentage,
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
createPriceLine({ unit: Unit.percentage }),
|
||||
],
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
23
website/scripts/options/market/utils.js
Normal file
23
website/scripts/options/market/utils.js
Normal file
@@ -0,0 +1,23 @@
|
||||
/** Market utilities */
|
||||
|
||||
/**
|
||||
* Convert period ID to readable name
|
||||
* @param {string} id
|
||||
* @param {boolean} [compoundAdjective]
|
||||
*/
|
||||
export function periodIdToName(id, compoundAdjective) {
|
||||
const num = parseInt(id);
|
||||
const s = compoundAdjective || num === 1 ? "" : "s";
|
||||
switch (id.slice(-1)) {
|
||||
case "d":
|
||||
return `${num} day${s}`;
|
||||
case "w":
|
||||
return `${num} week${s}`;
|
||||
case "m":
|
||||
return `${num} month${s}`;
|
||||
case "y":
|
||||
return `${num} year${s}`;
|
||||
default:
|
||||
return id;
|
||||
}
|
||||
}
|
||||
362
website/scripts/options/partial.js
Normal file
362
website/scripts/options/partial.js
Normal file
@@ -0,0 +1,362 @@
|
||||
/** Partial options - Main entry point */
|
||||
|
||||
import { localhost } from "../utils/env.js";
|
||||
import { createContext } from "./context.js";
|
||||
import {
|
||||
buildCohortData,
|
||||
createCohortFolderAll,
|
||||
createCohortFolderFull,
|
||||
createCohortFolderWithAdjusted,
|
||||
createCohortFolderWithPercentiles,
|
||||
createCohortFolderBasic,
|
||||
createAddressCohortFolder,
|
||||
} from "./cohorts/index.js";
|
||||
import { createMarketSection } from "./market/index.js";
|
||||
import { createChainSection } from "./chain.js";
|
||||
import { createCointimeSection } from "./cointime.js";
|
||||
|
||||
// Re-export types for external consumers
|
||||
export * from "./types.js";
|
||||
|
||||
/**
|
||||
* Create partial options tree
|
||||
* @param {Object} args
|
||||
* @param {Colors} args.colors
|
||||
* @param {BrkClient} args.brk
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
export function createPartialOptions({ colors, brk }) {
|
||||
// Create context with all helpers
|
||||
const ctx = createContext({ colors, brk });
|
||||
|
||||
// Build cohort data
|
||||
const {
|
||||
cohortAll,
|
||||
termShort,
|
||||
termLong,
|
||||
upToDate,
|
||||
fromDate,
|
||||
dateRange,
|
||||
epoch,
|
||||
utxosAboveAmount,
|
||||
addressesAboveAmount,
|
||||
utxosUnderAmount,
|
||||
addressesUnderAmount,
|
||||
utxosAmountRanges,
|
||||
addressesAmountRanges,
|
||||
type,
|
||||
} = buildCohortData(colors, brk);
|
||||
|
||||
// Helpers to map cohorts by capability type
|
||||
/** @param {CohortWithAdjusted} cohort */
|
||||
const mapWithAdjusted = (cohort) => createCohortFolderWithAdjusted(ctx, cohort);
|
||||
/** @param {CohortWithPercentiles} cohort */
|
||||
const mapWithPercentiles = (cohort) => createCohortFolderWithPercentiles(ctx, cohort);
|
||||
/** @param {CohortBasic} cohort */
|
||||
const mapBasic = (cohort) => createCohortFolderBasic(ctx, cohort);
|
||||
/** @param {AddressCohortObject} cohort */
|
||||
const mapAddressCohorts = (cohort) => createAddressCohortFolder(ctx, cohort);
|
||||
|
||||
return [
|
||||
// Debug explorer (localhost only)
|
||||
...(localhost
|
||||
? [
|
||||
{
|
||||
kind: /** @type {const} */ ("explorer"),
|
||||
name: "Explorer",
|
||||
title: "Debug explorer",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
|
||||
// Charts section
|
||||
{
|
||||
name: "Charts",
|
||||
tree: [
|
||||
// Market section
|
||||
createMarketSection(ctx),
|
||||
|
||||
// Chain section
|
||||
createChainSection(ctx),
|
||||
|
||||
// Cohorts section
|
||||
{
|
||||
name: "Cohorts",
|
||||
tree: [
|
||||
// All UTXOs - CohortAll (adjustedSopr + percentiles but no RelToMarketCap)
|
||||
createCohortFolderAll(ctx, cohortAll),
|
||||
|
||||
// Terms (STH/LTH) - Short is Full, Long is WithPercentiles
|
||||
{
|
||||
name: "terms",
|
||||
tree: [
|
||||
// Individual cohorts with their specific capabilities
|
||||
createCohortFolderFull(ctx, termShort),
|
||||
createCohortFolderWithPercentiles(ctx, termLong),
|
||||
],
|
||||
},
|
||||
|
||||
// Epochs - CohortBasic (neither adjustedSopr nor percentiles)
|
||||
{
|
||||
name: "Epochs",
|
||||
tree: [
|
||||
createCohortFolderBasic(ctx, {
|
||||
name: "Compare",
|
||||
title: "Epoch",
|
||||
list: epoch,
|
||||
}),
|
||||
...epoch.map(mapBasic),
|
||||
],
|
||||
},
|
||||
|
||||
// Types - CohortBasic
|
||||
{
|
||||
name: "types",
|
||||
tree: [
|
||||
createCohortFolderBasic(ctx, {
|
||||
name: "Compare",
|
||||
title: "Type",
|
||||
list: type,
|
||||
}),
|
||||
...type.map(mapBasic),
|
||||
],
|
||||
},
|
||||
|
||||
// UTXOs Up to age - CohortWithAdjusted (adjustedSopr only)
|
||||
{
|
||||
name: "UTXOs Up to age",
|
||||
tree: [
|
||||
createCohortFolderWithAdjusted(ctx, {
|
||||
name: "Compare",
|
||||
title: "UTXOs Up To Age",
|
||||
list: upToDate,
|
||||
}),
|
||||
...upToDate.map(mapWithAdjusted),
|
||||
],
|
||||
},
|
||||
|
||||
// UTXOs from age - CohortBasic
|
||||
{
|
||||
name: "UTXOs from age",
|
||||
tree: [
|
||||
createCohortFolderBasic(ctx, {
|
||||
name: "Compare",
|
||||
title: "UTXOs from age",
|
||||
list: fromDate,
|
||||
}),
|
||||
...fromDate.map(mapBasic),
|
||||
],
|
||||
},
|
||||
|
||||
// UTXOs age ranges - CohortWithPercentiles (percentiles only)
|
||||
{
|
||||
name: "UTXOs age Ranges",
|
||||
tree: [
|
||||
createCohortFolderWithPercentiles(ctx, {
|
||||
name: "Compare",
|
||||
title: "UTXOs Age Range",
|
||||
list: dateRange,
|
||||
}),
|
||||
...dateRange.map(mapWithPercentiles),
|
||||
],
|
||||
},
|
||||
|
||||
// UTXOs under amounts - CohortBasic
|
||||
{
|
||||
name: "UTXOs under amounts",
|
||||
tree: [
|
||||
createCohortFolderBasic(ctx, {
|
||||
name: "Compare",
|
||||
title: "UTXOs under amount",
|
||||
list: utxosUnderAmount,
|
||||
}),
|
||||
...utxosUnderAmount.map(mapBasic),
|
||||
],
|
||||
},
|
||||
|
||||
// UTXOs above amounts - CohortBasic
|
||||
{
|
||||
name: "UTXOs Above Amounts",
|
||||
tree: [
|
||||
createCohortFolderBasic(ctx, {
|
||||
name: "Compare",
|
||||
title: "UTXOs Above Amount",
|
||||
list: utxosAboveAmount,
|
||||
}),
|
||||
...utxosAboveAmount.map(mapBasic),
|
||||
],
|
||||
},
|
||||
|
||||
// UTXOs between amounts - CohortBasic
|
||||
{
|
||||
name: "UTXOs between amounts",
|
||||
tree: [
|
||||
createCohortFolderBasic(ctx, {
|
||||
name: "Compare",
|
||||
title: "UTXOs between amounts",
|
||||
list: utxosAmountRanges,
|
||||
}),
|
||||
...utxosAmountRanges.map(mapBasic),
|
||||
],
|
||||
},
|
||||
|
||||
// Addresses under amount (TYPE SAFE - uses createAddressCohortFolder!)
|
||||
{
|
||||
name: "Addresses under amount",
|
||||
tree: [
|
||||
createAddressCohortFolder(ctx, {
|
||||
name: "Compare",
|
||||
title: "Addresses under Amount",
|
||||
list: addressesUnderAmount,
|
||||
}),
|
||||
...addressesUnderAmount.map(mapAddressCohorts),
|
||||
],
|
||||
},
|
||||
|
||||
// Addresses above amount (TYPE SAFE - uses createAddressCohortFolder!)
|
||||
{
|
||||
name: "Addresses above amount",
|
||||
tree: [
|
||||
createAddressCohortFolder(ctx, {
|
||||
name: "Compare",
|
||||
title: "Addresses above amount",
|
||||
list: addressesAboveAmount,
|
||||
}),
|
||||
...addressesAboveAmount.map(mapAddressCohorts),
|
||||
],
|
||||
},
|
||||
|
||||
// Addresses between amounts (TYPE SAFE - uses createAddressCohortFolder!)
|
||||
{
|
||||
name: "Addresses between amounts",
|
||||
tree: [
|
||||
createAddressCohortFolder(ctx, {
|
||||
name: "Compare",
|
||||
title: "Addresses between amounts",
|
||||
list: addressesAmountRanges,
|
||||
}),
|
||||
...addressesAmountRanges.map(mapAddressCohorts),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Cointime section
|
||||
createCointimeSection(ctx),
|
||||
],
|
||||
},
|
||||
|
||||
// Table section
|
||||
{
|
||||
kind: /** @type {const} */ ("table"),
|
||||
title: "Table",
|
||||
name: "Table",
|
||||
},
|
||||
|
||||
// Simulations section
|
||||
{
|
||||
name: "Simulations",
|
||||
tree: [
|
||||
{
|
||||
kind: /** @type {const} */ ("simulation"),
|
||||
name: "Save In Bitcoin",
|
||||
title: "Save In Bitcoin",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Tools section
|
||||
{
|
||||
name: "Tools",
|
||||
tree: [
|
||||
{
|
||||
name: "Documentation",
|
||||
tree: [
|
||||
{
|
||||
name: "API",
|
||||
url: () => "/api",
|
||||
title: "API documentation",
|
||||
},
|
||||
{
|
||||
name: "MCP",
|
||||
url: () =>
|
||||
"https://github.com/bitcoinresearchkit/brk/blob/main/crates/brk_mcp/README.md#brk_mcp",
|
||||
title: "Model Context Protocol documentation",
|
||||
},
|
||||
{
|
||||
name: "Crate",
|
||||
url: () => "/crate",
|
||||
title: "View on crates.io",
|
||||
},
|
||||
{
|
||||
name: "Source",
|
||||
url: () => "/github",
|
||||
title: "Source code and issues",
|
||||
},
|
||||
{
|
||||
name: "Changelog",
|
||||
url: () => "/changelog",
|
||||
title: "Release notes and changelog",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Hosting",
|
||||
tree: [
|
||||
{
|
||||
name: "Status",
|
||||
url: () => "/status",
|
||||
title: "Service status and uptime",
|
||||
},
|
||||
{
|
||||
name: "Self-host",
|
||||
url: () => "/install",
|
||||
title: "Install and run yourself",
|
||||
},
|
||||
{
|
||||
name: "Service",
|
||||
url: () => "/service",
|
||||
title: "Hosted service offering",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Community",
|
||||
tree: [
|
||||
{
|
||||
name: "Discord",
|
||||
url: () => "/discord",
|
||||
title: "Join the Discord server",
|
||||
},
|
||||
{
|
||||
name: "GitHub",
|
||||
url: () => "/github",
|
||||
title: "Source code and issues",
|
||||
},
|
||||
{
|
||||
name: "Nostr",
|
||||
url: () => "/nostr",
|
||||
title: "Follow on Nostr",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Donate
|
||||
{
|
||||
name: "Donate",
|
||||
qrcode: true,
|
||||
url: () => "bitcoin:bc1q098zsm89m7kgyze338vfejhpdt92ua9p3peuve",
|
||||
title: "Bitcoin address for donations",
|
||||
},
|
||||
|
||||
// Share
|
||||
{
|
||||
name: "Share",
|
||||
qrcode: true,
|
||||
url: () => window.location.href,
|
||||
title: "Share",
|
||||
},
|
||||
];
|
||||
}
|
||||
736
website/scripts/options/series.js
Normal file
736
website/scripts/options/series.js
Normal file
@@ -0,0 +1,736 @@
|
||||
/** Series helpers for creating chart series blueprints */
|
||||
|
||||
import { Unit } from "../utils/units.js";
|
||||
|
||||
/**
|
||||
* Create a Line series
|
||||
* @param {Object} args
|
||||
* @param {AnyMetricPattern} args.metric
|
||||
* @param {string} args.name
|
||||
* @param {Unit} args.unit
|
||||
* @param {Color} [args.color]
|
||||
* @param {boolean} [args.defaultActive]
|
||||
* @param {LineSeriesPartialOptions} [args.options]
|
||||
* @returns {FetchedLineSeriesBlueprint}
|
||||
*/
|
||||
export function line({ metric, name, color, defaultActive, unit, options }) {
|
||||
return {
|
||||
metric,
|
||||
title: name,
|
||||
color,
|
||||
unit,
|
||||
defaultActive,
|
||||
options,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Dots series (line with only point markers visible)
|
||||
* @param {Object} args
|
||||
* @param {AnyMetricPattern} args.metric
|
||||
* @param {string} args.name
|
||||
* @param {Unit} args.unit
|
||||
* @param {Color} [args.color]
|
||||
* @param {boolean} [args.defaultActive]
|
||||
* @param {LineSeriesPartialOptions} [args.options]
|
||||
* @returns {FetchedDotsSeriesBlueprint}
|
||||
*/
|
||||
export function dots({ metric, name, color, defaultActive, unit, options }) {
|
||||
return {
|
||||
type: /** @type {const} */ ("Dots"),
|
||||
metric,
|
||||
title: name,
|
||||
color,
|
||||
unit,
|
||||
defaultActive,
|
||||
options,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Candlestick series
|
||||
* @param {Object} args
|
||||
* @param {AnyMetricPattern} args.metric
|
||||
* @param {string} args.name
|
||||
* @param {Unit} args.unit
|
||||
* @param {[Color, Color]} [args.colors] - [upColor, downColor] for legend
|
||||
* @param {boolean} [args.defaultActive]
|
||||
* @param {CandlestickSeriesPartialOptions} [args.options]
|
||||
* @returns {FetchedCandlestickSeriesBlueprint}
|
||||
*/
|
||||
export function candlestick({
|
||||
metric,
|
||||
name,
|
||||
colors,
|
||||
defaultActive,
|
||||
unit,
|
||||
options,
|
||||
}) {
|
||||
return {
|
||||
type: /** @type {const} */ ("Candlestick"),
|
||||
metric,
|
||||
title: name,
|
||||
colors,
|
||||
unit,
|
||||
defaultActive,
|
||||
options,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Baseline series
|
||||
* @param {Object} args
|
||||
* @param {AnyMetricPattern} args.metric
|
||||
* @param {string} args.name
|
||||
* @param {Unit} args.unit
|
||||
* @param {Color | [Color, Color]} [args.color]
|
||||
* @param {boolean} [args.defaultActive]
|
||||
* @param {BaselineSeriesPartialOptions} [args.options]
|
||||
* @returns {FetchedBaselineSeriesBlueprint}
|
||||
*/
|
||||
export function baseline({
|
||||
metric,
|
||||
name,
|
||||
color,
|
||||
defaultActive,
|
||||
unit,
|
||||
options,
|
||||
}) {
|
||||
const isTuple = Array.isArray(color);
|
||||
return {
|
||||
type: /** @type {const} */ ("Baseline"),
|
||||
metric,
|
||||
title: name,
|
||||
color: isTuple ? undefined : color,
|
||||
colors: isTuple ? color : undefined,
|
||||
unit,
|
||||
defaultActive,
|
||||
options,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Histogram series
|
||||
* @param {Object} args
|
||||
* @param {AnyMetricPattern} args.metric
|
||||
* @param {string} args.name
|
||||
* @param {Unit} args.unit
|
||||
* @param {Color | [Color, Color]} [args.color]
|
||||
* @param {boolean} [args.defaultActive]
|
||||
* @param {HistogramSeriesPartialOptions} [args.options]
|
||||
* @returns {FetchedHistogramSeriesBlueprint}
|
||||
*/
|
||||
export function histogram({
|
||||
metric,
|
||||
name,
|
||||
color,
|
||||
defaultActive,
|
||||
unit,
|
||||
options,
|
||||
}) {
|
||||
return {
|
||||
type: /** @type {const} */ ("Histogram"),
|
||||
metric,
|
||||
title: name,
|
||||
color,
|
||||
unit,
|
||||
defaultActive,
|
||||
options,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create series from a BlockCountPattern ({ base, sum, cumulative })
|
||||
* @param {Colors} colors
|
||||
* @param {BlockCountPattern<any>} pattern
|
||||
* @param {string} title
|
||||
* @param {Color} [color]
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
export function fromBlockCount(colors, pattern, title, color) {
|
||||
return [
|
||||
{ metric: pattern.sum, title, color: color ?? colors.default },
|
||||
{
|
||||
metric: pattern.cumulative,
|
||||
title: `${title} (cum.)`,
|
||||
color: colors.cyan,
|
||||
defaultActive: false,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create series from a FullnessPattern ({ base, sum, cumulative, average, min, max, percentiles })
|
||||
* @param {Colors} colors
|
||||
* @param {FullnessPattern<any>} pattern
|
||||
* @param {string} title
|
||||
* @param {Color} [color]
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
export function fromBitcoin(colors, pattern, title, color) {
|
||||
return [
|
||||
{ metric: pattern.base, title, color: color ?? colors.default },
|
||||
{ metric: pattern.average, title: "Average", defaultActive: false },
|
||||
{
|
||||
metric: pattern.max,
|
||||
title: "Max",
|
||||
color: colors.pink,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.min,
|
||||
title: "Min",
|
||||
color: colors.green,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.median,
|
||||
title: "Median",
|
||||
color: colors.amber,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.pct75,
|
||||
title: "pct75",
|
||||
color: colors.red,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.pct25,
|
||||
title: "pct25",
|
||||
color: colors.yellow,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.pct90,
|
||||
title: "pct90",
|
||||
color: colors.rose,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.pct10,
|
||||
title: "pct10",
|
||||
color: colors.lime,
|
||||
defaultActive: false,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create series from a SizePattern ({ sum, cumulative, average, min, max, percentiles })
|
||||
* @param {Colors} colors
|
||||
* @param {AnyStatsPattern} pattern
|
||||
* @param {string} title
|
||||
* @param {Color} [color]
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
export function fromBlockSize(colors, pattern, title, color) {
|
||||
return [
|
||||
{ metric: pattern.sum, title, color: color ?? colors.default },
|
||||
{ metric: pattern.average, title: "Average", defaultActive: false },
|
||||
{
|
||||
metric: pattern.cumulative,
|
||||
title: `${title} (cum.)`,
|
||||
color: colors.cyan,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.max,
|
||||
title: "Max",
|
||||
color: colors.pink,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.min,
|
||||
title: "Min",
|
||||
color: colors.green,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.median,
|
||||
title: "Median",
|
||||
color: colors.amber,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.pct75,
|
||||
title: "pct75",
|
||||
color: colors.red,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.pct25,
|
||||
title: "pct25",
|
||||
color: colors.yellow,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.pct90,
|
||||
title: "pct90",
|
||||
color: colors.rose,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.pct10,
|
||||
title: "pct10",
|
||||
color: colors.lime,
|
||||
defaultActive: false,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create series from a SizePattern ({ average, sum, cumulative, min, max, percentiles })
|
||||
* @param {Colors} colors
|
||||
* @param {AnyStatsPattern} pattern
|
||||
* @param {string} title
|
||||
* @param {Unit} unit
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
export function fromSizePattern(colors, pattern, title, unit) {
|
||||
return [
|
||||
{ metric: pattern.average, title: `${title} avg`, unit },
|
||||
{
|
||||
metric: pattern.sum,
|
||||
title: `${title} sum`,
|
||||
color: colors.blue,
|
||||
unit,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.cumulative,
|
||||
title: `${title} cumulative`,
|
||||
color: colors.indigo,
|
||||
unit,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.min,
|
||||
title: `${title} min`,
|
||||
color: colors.red,
|
||||
unit,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.max,
|
||||
title: `${title} max`,
|
||||
color: colors.green,
|
||||
unit,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.pct10,
|
||||
title: `${title} pct10`,
|
||||
color: colors.rose,
|
||||
unit,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.pct25,
|
||||
title: `${title} pct25`,
|
||||
color: colors.pink,
|
||||
unit,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.median,
|
||||
title: `${title} median`,
|
||||
color: colors.purple,
|
||||
unit,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.pct75,
|
||||
title: `${title} pct75`,
|
||||
color: colors.violet,
|
||||
unit,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.pct90,
|
||||
title: `${title} pct90`,
|
||||
color: colors.fuchsia,
|
||||
unit,
|
||||
defaultActive: false,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create series from a FullnessPattern ({ base, average, sum, cumulative, min, max, percentiles })
|
||||
* @param {Colors} colors
|
||||
* @param {FullnessPattern<any>} pattern
|
||||
* @param {string} title
|
||||
* @param {Unit} unit
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
export function fromFullnessPattern(colors, pattern, title, unit) {
|
||||
return [
|
||||
{ metric: pattern.base, title, unit },
|
||||
{
|
||||
metric: pattern.average,
|
||||
title: `${title} avg`,
|
||||
color: colors.purple,
|
||||
unit,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.min,
|
||||
title: `${title} min`,
|
||||
color: colors.red,
|
||||
unit,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.max,
|
||||
title: `${title} max`,
|
||||
color: colors.green,
|
||||
unit,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.pct10,
|
||||
title: `${title} pct10`,
|
||||
color: colors.rose,
|
||||
unit,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.pct25,
|
||||
title: `${title} pct25`,
|
||||
color: colors.pink,
|
||||
unit,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.median,
|
||||
title: `${title} median`,
|
||||
color: colors.violet,
|
||||
unit,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.pct75,
|
||||
title: `${title} pct75`,
|
||||
color: colors.fuchsia,
|
||||
unit,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.pct90,
|
||||
title: `${title} pct90`,
|
||||
color: colors.amber,
|
||||
unit,
|
||||
defaultActive: false,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create series from a FeeRatePattern ({ average, min, max, percentiles })
|
||||
* @param {Colors} colors
|
||||
* @param {FeeRatePattern<any>} pattern
|
||||
* @param {string} title
|
||||
* @param {Unit} unit
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
export function fromFeeRatePattern(colors, pattern, title, unit) {
|
||||
return [
|
||||
{ metric: pattern.average, title: `${title} avg`, unit },
|
||||
{
|
||||
metric: pattern.min,
|
||||
title: `${title} min`,
|
||||
color: colors.red,
|
||||
unit,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.max,
|
||||
title: `${title} max`,
|
||||
color: colors.green,
|
||||
unit,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.pct10,
|
||||
title: `${title} pct10`,
|
||||
color: colors.rose,
|
||||
unit,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.pct25,
|
||||
title: `${title} pct25`,
|
||||
color: colors.pink,
|
||||
unit,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.median,
|
||||
title: `${title} median`,
|
||||
color: colors.purple,
|
||||
unit,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.pct75,
|
||||
title: `${title} pct75`,
|
||||
color: colors.violet,
|
||||
unit,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.pct90,
|
||||
title: `${title} pct90`,
|
||||
color: colors.fuchsia,
|
||||
unit,
|
||||
defaultActive: false,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create series from a CoinbasePattern ({ sats, bitcoin, dollars } each as FullnessPattern)
|
||||
* @param {Colors} colors
|
||||
* @param {CoinbasePattern} pattern
|
||||
* @param {string} title
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
export function fromCoinbasePattern(colors, pattern, title) {
|
||||
return [
|
||||
...fromFullnessPattern(colors, pattern.sats, title, Unit.sats),
|
||||
...fromFullnessPattern(colors, pattern.bitcoin, title, Unit.btc),
|
||||
...fromFullnessPattern(colors, pattern.dollars, title, Unit.usd),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create series from a ValuePattern ({ sats, bitcoin, dollars } each as BlockCountPattern with sum + cumulative)
|
||||
* @param {Colors} colors
|
||||
* @param {ValuePattern} pattern
|
||||
* @param {string} title
|
||||
* @param {Color} [sumColor]
|
||||
* @param {Color} [cumulativeColor]
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
export function fromValuePattern(
|
||||
colors,
|
||||
pattern,
|
||||
title,
|
||||
sumColor,
|
||||
cumulativeColor,
|
||||
) {
|
||||
return [
|
||||
{
|
||||
metric: pattern.sats.sum,
|
||||
title,
|
||||
color: sumColor,
|
||||
unit: Unit.sats,
|
||||
},
|
||||
{
|
||||
metric: pattern.sats.cumulative,
|
||||
title: `${title} cumulative`,
|
||||
color: cumulativeColor ?? colors.blue,
|
||||
unit: Unit.sats,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.bitcoin.sum,
|
||||
title,
|
||||
color: sumColor,
|
||||
unit: Unit.btc,
|
||||
},
|
||||
{
|
||||
metric: pattern.bitcoin.cumulative,
|
||||
title: `${title} cumulative`,
|
||||
color: cumulativeColor ?? colors.blue,
|
||||
unit: Unit.btc,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.dollars.sum,
|
||||
title,
|
||||
color: sumColor,
|
||||
unit: Unit.usd,
|
||||
},
|
||||
{
|
||||
metric: pattern.dollars.cumulative,
|
||||
title: `${title} cumulative`,
|
||||
color: cumulativeColor ?? colors.blue,
|
||||
unit: Unit.usd,
|
||||
defaultActive: false,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create sum/cumulative series from a BitcoinPattern ({ sum, cumulative }) with explicit unit and colors
|
||||
* @param {Colors} colors
|
||||
* @param {{ sum: AnyMetricPattern, cumulative: AnyMetricPattern }} pattern
|
||||
* @param {string} title
|
||||
* @param {Unit} unit
|
||||
* @param {Color} [sumColor]
|
||||
* @param {Color} [cumulativeColor]
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
export function fromBitcoinPatternWithUnit(
|
||||
colors,
|
||||
pattern,
|
||||
title,
|
||||
unit,
|
||||
sumColor,
|
||||
cumulativeColor,
|
||||
) {
|
||||
return [
|
||||
{
|
||||
metric: pattern.sum,
|
||||
title: `${title} sum`,
|
||||
color: sumColor,
|
||||
unit,
|
||||
},
|
||||
{
|
||||
metric: pattern.cumulative,
|
||||
title: `${title} cumulative`,
|
||||
color: cumulativeColor ?? colors.blue,
|
||||
unit,
|
||||
defaultActive: false,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create sum/cumulative series from a BlockCountPattern with explicit unit and colors
|
||||
* @param {Colors} colors
|
||||
* @param {BlockCountPattern<any>} pattern
|
||||
* @param {string} title
|
||||
* @param {Unit} unit
|
||||
* @param {Color} [sumColor]
|
||||
* @param {Color} [cumulativeColor]
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
export function fromBlockCountWithUnit(
|
||||
colors,
|
||||
pattern,
|
||||
title,
|
||||
unit,
|
||||
sumColor,
|
||||
cumulativeColor,
|
||||
) {
|
||||
return [
|
||||
{
|
||||
metric: pattern.sum,
|
||||
title: `${title} sum`,
|
||||
color: sumColor,
|
||||
unit,
|
||||
},
|
||||
{
|
||||
metric: pattern.cumulative,
|
||||
title: `${title} cumulative`,
|
||||
color: cumulativeColor ?? colors.blue,
|
||||
unit,
|
||||
defaultActive: false,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create series from an IntervalPattern (base + average/min/max/median/percentiles, no sum/cumulative)
|
||||
* @param {Colors} colors
|
||||
* @param {IntervalPattern} pattern
|
||||
* @param {string} title
|
||||
* @param {Unit} unit
|
||||
* @param {Color} [color]
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
export function fromIntervalPattern(colors, pattern, title, unit, color) {
|
||||
return [
|
||||
{ metric: pattern.base, title, color, unit },
|
||||
{
|
||||
metric: pattern.average,
|
||||
title: `${title} avg`,
|
||||
color: colors.purple,
|
||||
unit,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.min,
|
||||
title: `${title} min`,
|
||||
color: colors.red,
|
||||
unit,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.max,
|
||||
title: `${title} max`,
|
||||
color: colors.green,
|
||||
unit,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.median,
|
||||
title: `${title} median`,
|
||||
color: colors.violet,
|
||||
unit,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.pct10,
|
||||
title: `${title} pct10`,
|
||||
color: colors.rose,
|
||||
unit,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.pct25,
|
||||
title: `${title} pct25`,
|
||||
color: colors.pink,
|
||||
unit,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.pct75,
|
||||
title: `${title} pct75`,
|
||||
color: colors.fuchsia,
|
||||
unit,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.pct90,
|
||||
title: `${title} pct90`,
|
||||
color: colors.amber,
|
||||
unit,
|
||||
defaultActive: false,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create series from a SupplyPattern (sats/bitcoin/dollars, no sum/cumulative)
|
||||
* @param {Colors} colors
|
||||
* @param {SupplyPattern} pattern
|
||||
* @param {string} title
|
||||
* @param {Color} [color]
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
export function fromSupplyPattern(colors, pattern, title, color) {
|
||||
return [
|
||||
{
|
||||
metric: pattern.sats,
|
||||
title,
|
||||
color: color ?? colors.default,
|
||||
unit: Unit.sats,
|
||||
},
|
||||
{
|
||||
metric: pattern.bitcoin,
|
||||
title,
|
||||
color: color ?? colors.default,
|
||||
unit: Unit.btc,
|
||||
},
|
||||
{
|
||||
metric: pattern.dollars,
|
||||
title,
|
||||
color: color ?? colors.default,
|
||||
unit: Unit.usd,
|
||||
},
|
||||
];
|
||||
}
|
||||
267
website/scripts/options/types.js
Normal file
267
website/scripts/options/types.js
Normal file
@@ -0,0 +1,267 @@
|
||||
/**
|
||||
* @typedef {Object} BaseSeriesBlueprint
|
||||
* @property {string} title
|
||||
* @property {boolean} [defaultActive]
|
||||
*
|
||||
* @typedef {Object} BaselineSeriesBlueprintSpecific
|
||||
* @property {"Baseline"} type
|
||||
* @property {Color} [color]
|
||||
* @property {[Color, Color]} [colors]
|
||||
* @property {BaselineSeriesPartialOptions} [options]
|
||||
* @property {Accessor<BaselineData[]>} [data]
|
||||
* @typedef {BaseSeriesBlueprint & BaselineSeriesBlueprintSpecific} BaselineSeriesBlueprint
|
||||
*
|
||||
* @typedef {Object} CandlestickSeriesBlueprintSpecific
|
||||
* @property {"Candlestick"} type
|
||||
* @property {[Color, Color]} [colors]
|
||||
* @property {CandlestickSeriesPartialOptions} [options]
|
||||
* @property {Accessor<CandlestickData[]>} [data]
|
||||
* @typedef {BaseSeriesBlueprint & CandlestickSeriesBlueprintSpecific} CandlestickSeriesBlueprint
|
||||
*
|
||||
* @typedef {Object} LineSeriesBlueprintSpecific
|
||||
* @property {"Line"} [type]
|
||||
* @property {Color} [color]
|
||||
* @property {LineSeriesPartialOptions} [options]
|
||||
* @property {Accessor<LineData[]>} [data]
|
||||
* @typedef {BaseSeriesBlueprint & LineSeriesBlueprintSpecific} LineSeriesBlueprint
|
||||
*
|
||||
* @typedef {Object} HistogramSeriesBlueprintSpecific
|
||||
* @property {"Histogram"} type
|
||||
* @property {Color | [Color, Color]} [color] - Single color or [positive, negative] colors (defaults to green/red)
|
||||
* @property {HistogramSeriesPartialOptions} [options]
|
||||
* @property {Accessor<HistogramData[]>} [data]
|
||||
* @typedef {BaseSeriesBlueprint & HistogramSeriesBlueprintSpecific} HistogramSeriesBlueprint
|
||||
*
|
||||
* @typedef {Object} DotsSeriesBlueprintSpecific
|
||||
* @property {"Dots"} type
|
||||
* @property {Color} [color]
|
||||
* @property {LineSeriesPartialOptions} [options]
|
||||
* @property {Accessor<LineData[]>} [data]
|
||||
* @typedef {BaseSeriesBlueprint & DotsSeriesBlueprintSpecific} DotsSeriesBlueprint
|
||||
*
|
||||
* @typedef {BaselineSeriesBlueprint | CandlestickSeriesBlueprint | LineSeriesBlueprint | HistogramSeriesBlueprint | DotsSeriesBlueprint} AnySeriesBlueprint
|
||||
*
|
||||
* @typedef {AnySeriesBlueprint["type"]} SeriesType
|
||||
*
|
||||
* @typedef {{ metric: AnyMetricPattern, unit?: Unit }} FetchedAnySeriesOptions
|
||||
*
|
||||
* @typedef {BaselineSeriesBlueprint & FetchedAnySeriesOptions} FetchedBaselineSeriesBlueprint
|
||||
* @typedef {CandlestickSeriesBlueprint & FetchedAnySeriesOptions} FetchedCandlestickSeriesBlueprint
|
||||
* @typedef {LineSeriesBlueprint & FetchedAnySeriesOptions} FetchedLineSeriesBlueprint
|
||||
* @typedef {HistogramSeriesBlueprint & FetchedAnySeriesOptions} FetchedHistogramSeriesBlueprint
|
||||
* @typedef {DotsSeriesBlueprint & FetchedAnySeriesOptions} FetchedDotsSeriesBlueprint
|
||||
* @typedef {AnySeriesBlueprint & FetchedAnySeriesOptions} AnyFetchedSeriesBlueprint
|
||||
*
|
||||
* @typedef {Object} PartialOption
|
||||
* @property {string} name
|
||||
*
|
||||
* @typedef {Object} ProcessedOptionAddons
|
||||
* @property {string} title
|
||||
* @property {string[]} path
|
||||
*
|
||||
* @typedef {Object} PartialExplorerOptionSpecific
|
||||
* @property {"explorer"} kind
|
||||
* @property {string} title
|
||||
*
|
||||
* @typedef {PartialOption & PartialExplorerOptionSpecific} PartialExplorerOption
|
||||
*
|
||||
* @typedef {Required<PartialExplorerOption> & ProcessedOptionAddons} ExplorerOption
|
||||
*
|
||||
* @typedef {Object} PartialChartOptionSpecific
|
||||
* @property {"chart"} [kind]
|
||||
* @property {string} title
|
||||
* @property {AnyFetchedSeriesBlueprint[]} [top]
|
||||
* @property {AnyFetchedSeriesBlueprint[]} [bottom]
|
||||
*
|
||||
* @typedef {PartialOption & PartialChartOptionSpecific} PartialChartOption
|
||||
*
|
||||
* @typedef {Object} ProcessedChartOptionAddons
|
||||
* @property {Map<Unit, AnyFetchedSeriesBlueprint[]>} top
|
||||
* @property {Map<Unit, AnyFetchedSeriesBlueprint[]>} bottom
|
||||
*
|
||||
* @typedef {Required<Omit<PartialChartOption, "top" | "bottom">> & ProcessedChartOptionAddons & ProcessedOptionAddons} ChartOption
|
||||
*
|
||||
* @typedef {Object} PartialTableOptionSpecific
|
||||
* @property {"table"} kind
|
||||
* @property {string} title
|
||||
*
|
||||
* @typedef {PartialOption & PartialTableOptionSpecific} PartialTableOption
|
||||
*
|
||||
* @typedef {Required<PartialTableOption> & ProcessedOptionAddons} TableOption
|
||||
*
|
||||
* @typedef {Object} PartialSimulationOptionSpecific
|
||||
* @property {"simulation"} kind
|
||||
* @property {string} title
|
||||
*
|
||||
* @typedef {PartialOption & PartialSimulationOptionSpecific} PartialSimulationOption
|
||||
*
|
||||
* @typedef {Required<PartialSimulationOption> & ProcessedOptionAddons} SimulationOption
|
||||
*
|
||||
* @typedef {Object} PartialUrlOptionSpecific
|
||||
* @property {"url"} [kind]
|
||||
* @property {() => string} url
|
||||
* @property {string} title
|
||||
* @property {boolean} [qrcode]
|
||||
*
|
||||
* @typedef {PartialOption & PartialUrlOptionSpecific} PartialUrlOption
|
||||
*
|
||||
* @typedef {Required<PartialUrlOption> & ProcessedOptionAddons} UrlOption
|
||||
*
|
||||
* @typedef {PartialExplorerOption | PartialChartOption | PartialTableOption | PartialSimulationOption | PartialUrlOption} AnyPartialOption
|
||||
*
|
||||
* @typedef {ExplorerOption | ChartOption | TableOption | SimulationOption | UrlOption} Option
|
||||
*
|
||||
* @typedef {(AnyPartialOption | PartialOptionsGroup)[]} PartialOptionsTree
|
||||
*
|
||||
* @typedef {Object} PartialOptionsGroup
|
||||
* @property {string} name
|
||||
* @property {PartialOptionsTree} tree
|
||||
*
|
||||
* @typedef {Object} OptionsGroup
|
||||
* @property {string} name
|
||||
* @property {OptionsTree} tree
|
||||
*
|
||||
* @typedef {(Option | OptionsGroup)[]} OptionsTree
|
||||
*
|
||||
* @typedef {Object} UtxoCohortObject
|
||||
* @property {string} name
|
||||
* @property {string} title
|
||||
* @property {Color} color
|
||||
* @property {UtxoCohortPattern} tree
|
||||
*
|
||||
* ============================================================================
|
||||
* UTXO Cohort Pattern Types (based on brk client patterns)
|
||||
* ============================================================================
|
||||
*
|
||||
* Patterns with adjustedSopr + percentiles + RelToMarketCap:
|
||||
* - ShortTermPattern (term.short)
|
||||
* @typedef {ShortTermPattern} PatternFull
|
||||
*
|
||||
* The "All" pattern is special - has adjustedSopr + percentiles but NO RelToMarketCap
|
||||
* @typedef {AllUtxoPattern} PatternAll
|
||||
*
|
||||
* Patterns with adjustedSopr only (RealizedPattern4, CostBasisPattern):
|
||||
* - MaxAgePattern (maxAge.*)
|
||||
* @typedef {MaxAgePattern} PatternWithAdjusted
|
||||
*
|
||||
* Patterns with percentiles only (RealizedPattern2, CostBasisPattern2):
|
||||
* - LongTermPattern (term.long)
|
||||
* - AgeRangePattern (ageRange.*)
|
||||
* @typedef {LongTermPattern | AgeRangePattern} PatternWithPercentiles
|
||||
*
|
||||
* Patterns with neither (RealizedPattern/2, CostBasisPattern):
|
||||
* - BasicUtxoPattern (minAge.*, geAmount.*, ltAmount.*)
|
||||
* - EpochPattern (epoch.*)
|
||||
* @typedef {BasicUtxoPattern | EpochPattern} PatternBasic
|
||||
*
|
||||
* ============================================================================
|
||||
* Cohort Object Types (by capability)
|
||||
* ============================================================================
|
||||
*
|
||||
* All cohort: adjustedSopr + percentiles but NO RelToMarketCap (special)
|
||||
* @typedef {Object} CohortAll
|
||||
* @property {string} name
|
||||
* @property {string} title
|
||||
* @property {Color} color
|
||||
* @property {PatternAll} tree
|
||||
*
|
||||
* Full cohort: adjustedSopr + percentiles + RelToMarketCap (term.short)
|
||||
* @typedef {Object} CohortFull
|
||||
* @property {string} name
|
||||
* @property {string} title
|
||||
* @property {Color} color
|
||||
* @property {PatternFull} tree
|
||||
*
|
||||
* Cohort with adjustedSopr only (maxAge.*)
|
||||
* @typedef {Object} CohortWithAdjusted
|
||||
* @property {string} name
|
||||
* @property {string} title
|
||||
* @property {Color} color
|
||||
* @property {PatternWithAdjusted} tree
|
||||
*
|
||||
* Cohort with percentiles only (term.long, ageRange.*)
|
||||
* @typedef {Object} CohortWithPercentiles
|
||||
* @property {string} name
|
||||
* @property {string} title
|
||||
* @property {Color} color
|
||||
* @property {PatternWithPercentiles} tree
|
||||
*
|
||||
* Basic cohort: neither (minAge.*, epoch.*, amount cohorts)
|
||||
* @typedef {Object} CohortBasic
|
||||
* @property {string} name
|
||||
* @property {string} title
|
||||
* @property {Color} color
|
||||
* @property {PatternBasic} tree
|
||||
*
|
||||
* ============================================================================
|
||||
* Cohort Group Types (by capability)
|
||||
* ============================================================================
|
||||
*
|
||||
* @typedef {Object} CohortGroupFull
|
||||
* @property {string} name
|
||||
* @property {string} title
|
||||
* @property {readonly CohortFull[]} list
|
||||
*
|
||||
* @typedef {Object} CohortGroupWithAdjusted
|
||||
* @property {string} name
|
||||
* @property {string} title
|
||||
* @property {readonly CohortWithAdjusted[]} list
|
||||
*
|
||||
* @typedef {Object} CohortGroupWithPercentiles
|
||||
* @property {string} name
|
||||
* @property {string} title
|
||||
* @property {readonly CohortWithPercentiles[]} list
|
||||
*
|
||||
* @typedef {Object} CohortGroupBasic
|
||||
* @property {string} name
|
||||
* @property {string} title
|
||||
* @property {readonly CohortBasic[]} list
|
||||
*
|
||||
* @typedef {Object} UtxoCohortGroupObject
|
||||
* @property {string} name
|
||||
* @property {string} title
|
||||
* @property {readonly UtxoCohortObject[]} list
|
||||
*
|
||||
* @typedef {Object} AddressCohortObject
|
||||
* @property {string} name
|
||||
* @property {string} title
|
||||
* @property {Color} color
|
||||
* @property {AddressCohortPattern} tree
|
||||
*
|
||||
* @typedef {UtxoCohortObject | AddressCohortObject} CohortObject
|
||||
*
|
||||
*
|
||||
* @typedef {Object} AddressCohortGroupObject
|
||||
* @property {string} name
|
||||
* @property {string} title
|
||||
* @property {readonly AddressCohortObject[]} list
|
||||
*
|
||||
* @typedef {UtxoCohortGroupObject | AddressCohortGroupObject} CohortGroupObject
|
||||
*
|
||||
* @typedef {Object} PartialContext
|
||||
* @property {Colors} colors
|
||||
* @property {BrkClient} brk
|
||||
* @property {LineSeriesFn} line
|
||||
* @property {DotsSeriesFn} dots
|
||||
* @property {CandlestickSeriesFn} candlestick
|
||||
* @property {BaselineSeriesFn} baseline
|
||||
* @property {HistogramSeriesFn} histogram
|
||||
* @property {(pattern: BlockCountPattern<any>, title: string, color?: Color) => AnyFetchedSeriesBlueprint[]} fromBlockCount
|
||||
* @property {(pattern: FullnessPattern<any>, title: string, color?: Color) => AnyFetchedSeriesBlueprint[]} fromBitcoin
|
||||
* @property {(pattern: AnyStatsPattern, title: string, color?: Color) => AnyFetchedSeriesBlueprint[]} fromBlockSize
|
||||
* @property {(pattern: AnyStatsPattern, title: string, unit: Unit) => AnyFetchedSeriesBlueprint[]} fromSizePattern
|
||||
* @property {(pattern: FullnessPattern<any>, title: string, unit: Unit) => AnyFetchedSeriesBlueprint[]} fromFullnessPattern
|
||||
* @property {(pattern: FeeRatePattern<any>, title: string, unit: Unit) => AnyFetchedSeriesBlueprint[]} fromFeeRatePattern
|
||||
* @property {(pattern: CoinbasePattern, title: string) => AnyFetchedSeriesBlueprint[]} fromCoinbasePattern
|
||||
* @property {(pattern: ValuePattern, title: string, sumColor?: Color, cumulativeColor?: Color) => AnyFetchedSeriesBlueprint[]} fromValuePattern
|
||||
* @property {(pattern: { sum: AnyMetricPattern, cumulative: AnyMetricPattern }, title: string, unit: Unit, sumColor?: Color, cumulativeColor?: Color) => AnyFetchedSeriesBlueprint[]} fromBitcoinPatternWithUnit
|
||||
* @property {(pattern: BlockCountPattern<any>, title: string, unit: Unit, sumColor?: Color, cumulativeColor?: Color) => AnyFetchedSeriesBlueprint[]} fromBlockCountWithUnit
|
||||
* @property {(pattern: IntervalPattern, title: string, unit: Unit, color?: Color) => AnyFetchedSeriesBlueprint[]} fromIntervalPattern
|
||||
* @property {(pattern: SupplyPattern, title: string, color?: Color) => AnyFetchedSeriesBlueprint[]} fromSupplyPattern
|
||||
* @property {(args: { number?: number, name?: string, defaultActive?: boolean, lineStyle?: LineStyle, color?: Color, unit: Unit }) => FetchedLineSeriesBlueprint} createPriceLine
|
||||
* @property {(args: { numbers: number[], unit: Unit }) => FetchedLineSeriesBlueprint[]} createPriceLines
|
||||
* @property {(args: { constant: AnyMetricPattern, name: string, unit: Unit, color?: Color, lineStyle?: number, defaultActive?: boolean }) => FetchedLineSeriesBlueprint} constantLine
|
||||
*/
|
||||
|
||||
// Re-export for type consumers
|
||||
export {};
|
||||
44
website/scripts/options/unused.js
Normal file
44
website/scripts/options/unused.js
Normal file
@@ -0,0 +1,44 @@
|
||||
/** Track unused metrics (dev only) */
|
||||
|
||||
import { localhost } from "../utils/env.js";
|
||||
|
||||
/** @type {Set<AnyMetricPattern> | null} */
|
||||
export const unused = localhost ? new Set() : null;
|
||||
|
||||
/**
|
||||
* Walk and collect AnyMetricPatterns
|
||||
* @param {TreeNode | null | undefined} node
|
||||
* @param {Set<AnyMetricPattern>} set
|
||||
*/
|
||||
function walk(node, set) {
|
||||
if (node && "by" in node) {
|
||||
set.add(/** @type {AnyMetricPattern} */ (node));
|
||||
} else if (node && typeof node === "object") {
|
||||
for (const value of Object.values(node)) {
|
||||
walk(/** @type {TreeNode | null | undefined} */ (value), set);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect all AnyMetricPatterns from tree
|
||||
* @param {TreeNode} tree
|
||||
*/
|
||||
export function collect(tree) {
|
||||
if (unused) walk(tree, unused);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a metric as used
|
||||
* @param {AnyMetricPattern} metric
|
||||
*/
|
||||
export function markUsed(metric) {
|
||||
unused?.delete(metric);
|
||||
}
|
||||
|
||||
/** Log unused metrics to console */
|
||||
export function logUnused() {
|
||||
if (!unused?.size) return;
|
||||
const paths = [...unused].map((m) => Object.values(m.by)[0].path);
|
||||
console.warn("Unused metrics:", paths);
|
||||
}
|
||||
525
website/scripts/panes/chart/index.js
Normal file
525
website/scripts/panes/chart/index.js
Normal file
@@ -0,0 +1,525 @@
|
||||
import {
|
||||
createShadow,
|
||||
createChoiceField,
|
||||
createHeader,
|
||||
} from "../../utils/dom.js";
|
||||
import { chartElement } from "../../utils/elements.js";
|
||||
import { ios, canShare } from "../../utils/env.js";
|
||||
import { serdeChartableIndex, serdeOptNumber } from "../../utils/serde.js";
|
||||
import { throttle } from "../../utils/timing.js";
|
||||
import { Unit } from "../../utils/units.js";
|
||||
import signals from "../../signals.js";
|
||||
import { createChartElement } from "../../chart/index.js";
|
||||
import { webSockets } from "../../utils/ws.js";
|
||||
|
||||
const keyPrefix = "chart";
|
||||
const ONE_BTC_IN_SATS = 100_000_000;
|
||||
|
||||
/**
|
||||
* @typedef {"timestamp" | "date" | "week" | "month" | "quarter" | "semester" | "year" | "decade" } ChartableIndexName
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {Object} args
|
||||
* @param {Colors} args.colors
|
||||
* @param {Accessor<ChartOption>} args.option
|
||||
* @param {BrkClient} args.brk
|
||||
*/
|
||||
export function init({ colors, option, brk }) {
|
||||
chartElement.append(createShadow("left"));
|
||||
chartElement.append(createShadow("right"));
|
||||
|
||||
const { headerElement, headingElement } = createHeader();
|
||||
chartElement.append(headerElement);
|
||||
|
||||
const { index, fieldset } = createIndexSelector(option);
|
||||
|
||||
const TIMERANGE_LS_KEY = signals.createMemo(
|
||||
() => `chart-timerange-${index()}`,
|
||||
);
|
||||
|
||||
let firstRun = true;
|
||||
|
||||
const from = signals.createSignal(/** @type {number | null} */ (null), {
|
||||
save: {
|
||||
...serdeOptNumber,
|
||||
keyPrefix: TIMERANGE_LS_KEY,
|
||||
key: "from",
|
||||
serializeParam: firstRun,
|
||||
},
|
||||
});
|
||||
const to = signals.createSignal(/** @type {number | null} */ (null), {
|
||||
save: {
|
||||
...serdeOptNumber,
|
||||
keyPrefix: TIMERANGE_LS_KEY,
|
||||
key: "to",
|
||||
serializeParam: firstRun,
|
||||
},
|
||||
});
|
||||
|
||||
const chart = createChartElement({
|
||||
parent: chartElement,
|
||||
signals,
|
||||
colors,
|
||||
id: "charts",
|
||||
brk,
|
||||
index,
|
||||
timeScaleSetCallback: (unknownTimeScaleCallback) => {
|
||||
// TODO: Although it mostly works in practice, need to make it more robust, there is no guarantee that this runs in order and wait for `from` and `to` to update when `index` and thus `TIMERANGE_LS_KEY` is updated
|
||||
// Need to have the right values before the update
|
||||
|
||||
const from_ = from();
|
||||
const to_ = to();
|
||||
if (from_ !== null && to_ !== null) {
|
||||
chart.inner.timeScale().setVisibleLogicalRange({
|
||||
from: from_,
|
||||
to: to_,
|
||||
});
|
||||
} else {
|
||||
unknownTimeScaleCallback();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if (!(ios && !canShare)) {
|
||||
const chartBottomRightCanvas = Array.from(
|
||||
chart.inner.chartElement().getElementsByTagName("tr"),
|
||||
).at(-1)?.lastChild?.firstChild?.firstChild;
|
||||
if (chartBottomRightCanvas) {
|
||||
const domain = window.document.createElement("p");
|
||||
domain.innerText = `${window.location.host}`;
|
||||
domain.id = "domain";
|
||||
const screenshotButton = window.document.createElement("button");
|
||||
screenshotButton.id = "screenshot";
|
||||
const camera = "[ ◉¯]";
|
||||
screenshotButton.innerHTML = camera;
|
||||
screenshotButton.title = "Screenshot";
|
||||
chartBottomRightCanvas.replaceWith(screenshotButton);
|
||||
screenshotButton.addEventListener("click", () => {
|
||||
import("./screenshot").then(async ({ screenshot }) => {
|
||||
chartElement.dataset.screenshot = "true";
|
||||
chartElement.append(domain);
|
||||
try {
|
||||
await screenshot({
|
||||
element: chartElement,
|
||||
name: option().path.join("-"),
|
||||
title: option().title,
|
||||
});
|
||||
} catch {}
|
||||
chartElement.removeChild(domain);
|
||||
chartElement.dataset.screenshot = "false";
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
chart.inner.timeScale().subscribeVisibleLogicalRangeChange(
|
||||
throttle((t) => {
|
||||
if (!t) return;
|
||||
from.set(t.from);
|
||||
to.set(t.to);
|
||||
}, 250),
|
||||
);
|
||||
|
||||
chartElement.append(fieldset);
|
||||
|
||||
const { field: topUnitField, selected: topUnit } = createChoiceField({
|
||||
defaultValue: Unit.usd,
|
||||
keyPrefix,
|
||||
key: "unit-0",
|
||||
choices: [Unit.usd, Unit.sats],
|
||||
toKey: (u) => u.id,
|
||||
toLabel: (u) => u.name,
|
||||
signals,
|
||||
sorted: true,
|
||||
type: "select",
|
||||
});
|
||||
|
||||
chart.addFieldsetIfNeeded({
|
||||
id: "charts-unit-0",
|
||||
paneIndex: 0,
|
||||
position: "nw",
|
||||
createChild() {
|
||||
return topUnitField;
|
||||
},
|
||||
});
|
||||
|
||||
const seriesListTop = /** @type {AnySeries[]} */ ([]);
|
||||
const seriesListBottom = /** @type {AnySeries[]} */ ([]);
|
||||
|
||||
/**
|
||||
* @param {Object} params
|
||||
* @param {AnySeries} params.series
|
||||
* @param {Unit} params.unit
|
||||
* @param {IndexName} params.index
|
||||
*/
|
||||
function printLatest({ series, unit, index }) {
|
||||
const _latest = webSockets.kraken1dCandle.latest();
|
||||
|
||||
if (!_latest) return;
|
||||
|
||||
const latest = { ..._latest };
|
||||
|
||||
if (unit === Unit.sats) {
|
||||
latest.open = Math.floor(ONE_BTC_IN_SATS / latest.open);
|
||||
latest.high = Math.floor(ONE_BTC_IN_SATS / latest.high);
|
||||
latest.low = Math.floor(ONE_BTC_IN_SATS / latest.low);
|
||||
latest.close = Math.floor(ONE_BTC_IN_SATS / latest.close);
|
||||
}
|
||||
|
||||
const last_ = series.getData().at(-1);
|
||||
if (!last_) return;
|
||||
const last = { ...last_ };
|
||||
|
||||
if ("close" in last) {
|
||||
last.close = latest.close;
|
||||
}
|
||||
if ("value" in last) {
|
||||
last.value = latest.close;
|
||||
}
|
||||
const date = new Date(/** @type {number} */ (latest.time) * 1000);
|
||||
|
||||
switch (index) {
|
||||
case "height":
|
||||
case "difficultyepoch":
|
||||
case "halvingepoch": {
|
||||
if ("close" in last) {
|
||||
last.low = Math.min(last.low, latest.close);
|
||||
last.high = Math.max(last.high, latest.close);
|
||||
}
|
||||
series.update(last);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
if (index === "weekindex") {
|
||||
date.setUTCDate(date.getUTCDate() - ((date.getUTCDay() + 6) % 7));
|
||||
} else if (index === "monthindex") {
|
||||
date.setUTCDate(1);
|
||||
} else if (index === "quarterindex") {
|
||||
const month = date.getUTCMonth();
|
||||
date.setUTCMonth(month - (month % 3), 1);
|
||||
} else if (index === "semesterindex") {
|
||||
const month = date.getUTCMonth();
|
||||
date.setUTCMonth(month - (month % 6), 1);
|
||||
} else if (index === "yearindex") {
|
||||
date.setUTCMonth(0, 1);
|
||||
} else if (index === "decadeindex") {
|
||||
date.setUTCFullYear(
|
||||
Math.floor(date.getUTCFullYear() / 10) * 10,
|
||||
0,
|
||||
1,
|
||||
);
|
||||
} else if (index !== "dateindex") {
|
||||
throw Error("Unsupported");
|
||||
}
|
||||
|
||||
const time = date.valueOf() / 1000;
|
||||
|
||||
if (time === last.time) {
|
||||
if ("close" in last) {
|
||||
last.low = Math.min(last.low, latest.low);
|
||||
last.high = Math.max(last.high, latest.high);
|
||||
}
|
||||
series.update(last);
|
||||
} else {
|
||||
last.time = time;
|
||||
series.update(last);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
signals.createEffect(option, (option) => {
|
||||
headingElement.innerHTML = option.title;
|
||||
|
||||
const bottomUnits = Array.from(option.bottom.keys());
|
||||
|
||||
/** @type {{ field: HTMLDivElement, selected: Accessor<Unit> } | undefined} */
|
||||
let bottomUnitSelector;
|
||||
|
||||
if (bottomUnits.length) {
|
||||
bottomUnitSelector = createChoiceField({
|
||||
defaultValue: bottomUnits[0],
|
||||
keyPrefix,
|
||||
key: "unit-1",
|
||||
choices: bottomUnits,
|
||||
toKey: (u) => u.id,
|
||||
toLabel: (u) => u.name,
|
||||
signals,
|
||||
sorted: true,
|
||||
type: "select",
|
||||
});
|
||||
|
||||
const field = bottomUnitSelector.field;
|
||||
chart.addFieldsetIfNeeded({
|
||||
id: "charts-unit-1",
|
||||
paneIndex: 1,
|
||||
position: "nw",
|
||||
createChild() {
|
||||
return field;
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// Clean up bottom pane when new option has no bottom series
|
||||
seriesListBottom.forEach((series) => series.remove());
|
||||
seriesListBottom.length = 0;
|
||||
chart.legendBottom.removeFrom(0);
|
||||
}
|
||||
|
||||
signals.createEffect(index, (index) => {
|
||||
signals.createEffect(topUnit, (topUnit) => {
|
||||
/** @type {AnySeries | undefined} */
|
||||
let series;
|
||||
|
||||
switch (topUnit) {
|
||||
case Unit.usd: {
|
||||
series = chart.addCandlestickSeries({
|
||||
metric: brk.metrics.price.usd.ohlc,
|
||||
name: "Price",
|
||||
unit: topUnit,
|
||||
order: 0,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case Unit.sats: {
|
||||
series = chart.addCandlestickSeries({
|
||||
metric: brk.metrics.price.sats.ohlc,
|
||||
name: "Price",
|
||||
unit: topUnit,
|
||||
inverse: true,
|
||||
order: 0,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!series) throw Error("Unreachable");
|
||||
|
||||
seriesListTop[0]?.remove();
|
||||
seriesListTop[0] = series;
|
||||
|
||||
signals.createEffect(
|
||||
() => ({
|
||||
latest: webSockets.kraken1dCandle.latest(),
|
||||
hasData: series.hasData(),
|
||||
}),
|
||||
({ latest, hasData }) => {
|
||||
if (!series || !latest || !hasData) return;
|
||||
printLatest({ series, unit: topUnit, index });
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {Object} args
|
||||
* @param {Map<Unit, AnyFetchedSeriesBlueprint[]>} args.blueprints
|
||||
* @param {number} args.paneIndex
|
||||
* @param {Accessor<Unit>} args.unit
|
||||
* @param {AnySeries[]} args.seriesList
|
||||
* @param {number} args.orderStart
|
||||
* @param {Legend} args.legend
|
||||
*/
|
||||
function processPane({
|
||||
blueprints,
|
||||
paneIndex,
|
||||
unit,
|
||||
seriesList,
|
||||
orderStart,
|
||||
legend,
|
||||
}) {
|
||||
signals.createEffect(unit, (unit) => {
|
||||
legend.removeFrom(orderStart);
|
||||
|
||||
seriesList.splice(orderStart).forEach((series) => {
|
||||
series.remove();
|
||||
});
|
||||
|
||||
blueprints.get(unit)?.forEach((blueprint, order) => {
|
||||
order += orderStart;
|
||||
|
||||
const options = blueprint.options;
|
||||
|
||||
// Tree-first: metric is now an accessor with .by property
|
||||
const indexes = Object.keys(blueprint.metric.by);
|
||||
|
||||
if (indexes.includes(index)) {
|
||||
switch (blueprint.type) {
|
||||
case "Baseline": {
|
||||
seriesList.push(
|
||||
chart.addBaselineSeries({
|
||||
metric: blueprint.metric,
|
||||
name: blueprint.title,
|
||||
unit,
|
||||
defaultActive: blueprint.defaultActive,
|
||||
paneIndex,
|
||||
options: {
|
||||
...options,
|
||||
topLineColor:
|
||||
blueprint.color?.() ?? blueprint.colors?.[0](),
|
||||
bottomLineColor:
|
||||
blueprint.color?.() ?? blueprint.colors?.[1](),
|
||||
},
|
||||
order,
|
||||
}),
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "Histogram": {
|
||||
seriesList.push(
|
||||
chart.addHistogramSeries({
|
||||
metric: blueprint.metric,
|
||||
name: blueprint.title,
|
||||
unit,
|
||||
color: blueprint.color,
|
||||
defaultActive: blueprint.defaultActive,
|
||||
paneIndex,
|
||||
options,
|
||||
order,
|
||||
}),
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "Candlestick": {
|
||||
seriesList.push(
|
||||
chart.addCandlestickSeries({
|
||||
metric: blueprint.metric,
|
||||
name: blueprint.title,
|
||||
unit,
|
||||
colors: blueprint.colors,
|
||||
defaultActive: blueprint.defaultActive,
|
||||
paneIndex,
|
||||
options,
|
||||
order,
|
||||
}),
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "Dots": {
|
||||
seriesList.push(
|
||||
chart.addDotsSeries({
|
||||
metric: blueprint.metric,
|
||||
color: blueprint.color,
|
||||
name: blueprint.title,
|
||||
unit,
|
||||
defaultActive: blueprint.defaultActive,
|
||||
paneIndex,
|
||||
options,
|
||||
order,
|
||||
}),
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "Line":
|
||||
case undefined:
|
||||
seriesList.push(
|
||||
chart.addLineSeries({
|
||||
metric: blueprint.metric,
|
||||
color: blueprint.color,
|
||||
name: blueprint.title,
|
||||
unit,
|
||||
defaultActive: blueprint.defaultActive,
|
||||
paneIndex,
|
||||
options,
|
||||
order,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
processPane({
|
||||
blueprints: option.top,
|
||||
paneIndex: 0,
|
||||
unit: topUnit,
|
||||
seriesList: seriesListTop,
|
||||
orderStart: 1,
|
||||
legend: chart.legendTop,
|
||||
});
|
||||
|
||||
if (bottomUnitSelector) {
|
||||
processPane({
|
||||
blueprints: option.bottom,
|
||||
paneIndex: 1,
|
||||
unit: bottomUnitSelector.selected,
|
||||
seriesList: seriesListBottom,
|
||||
orderStart: 0,
|
||||
legend: chart.legendBottom,
|
||||
});
|
||||
}
|
||||
|
||||
firstRun = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Accessor<ChartOption>} option
|
||||
*/
|
||||
function createIndexSelector(option) {
|
||||
const choices_ = /** @satisfies {ChartableIndexName[]} */ ([
|
||||
"timestamp",
|
||||
"date",
|
||||
"week",
|
||||
"month",
|
||||
"quarter",
|
||||
"semester",
|
||||
"year",
|
||||
"decade",
|
||||
]);
|
||||
|
||||
/** @type {Accessor<typeof choices_>} */
|
||||
const choices = signals.createMemo(() => {
|
||||
const o = option();
|
||||
|
||||
if (!o.top.size && !o.bottom.size) {
|
||||
return [...choices_];
|
||||
}
|
||||
const rawIndexes = new Set(
|
||||
[Array.from(o.top.values()), Array.from(o.bottom.values())]
|
||||
.flat(2)
|
||||
.filter((blueprint) => {
|
||||
const path = Object.values(blueprint.metric.by)[0]?.path ?? "";
|
||||
return !path.includes("constant_");
|
||||
})
|
||||
.flatMap((blueprint) => blueprint.metric.indexes()),
|
||||
);
|
||||
|
||||
const serializedIndexes = [...rawIndexes].flatMap((index) => {
|
||||
const c = serdeChartableIndex.serialize(index);
|
||||
return c ? [c] : [];
|
||||
});
|
||||
|
||||
return /** @type {any} */ (
|
||||
choices_.filter((choice) => serializedIndexes.includes(choice))
|
||||
);
|
||||
});
|
||||
|
||||
/** @type {ChartableIndexName} */
|
||||
const defaultIndex = "date";
|
||||
const { field, selected } = createChoiceField({
|
||||
defaultValue: defaultIndex,
|
||||
keyPrefix,
|
||||
key: "index",
|
||||
choices,
|
||||
id: "index",
|
||||
signals,
|
||||
});
|
||||
|
||||
const fieldset = window.document.createElement("fieldset");
|
||||
fieldset.id = "interval";
|
||||
|
||||
const screenshotSpan = window.document.createElement("span");
|
||||
screenshotSpan.innerText = "interval:";
|
||||
fieldset.append(screenshotSpan);
|
||||
|
||||
fieldset.append(field);
|
||||
fieldset.dataset.size = "sm";
|
||||
|
||||
const index = signals.createMemo(() =>
|
||||
serdeChartableIndex.deserialize(selected()),
|
||||
);
|
||||
|
||||
return { fieldset, index };
|
||||
}
|
||||
38
website/scripts/panes/chart/screenshot.js
Normal file
38
website/scripts/panes/chart/screenshot.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import { ios } from "../../utils/env.js";
|
||||
import { domToBlob } from "../../modules/modern-screenshot/4.6.7/dist/index.mjs";
|
||||
|
||||
/**
|
||||
* @param {Object} args
|
||||
* @param {Element} args.element
|
||||
* @param {string} args.name
|
||||
* @param {string} args.title
|
||||
*/
|
||||
export async function screenshot({ element, name, title }) {
|
||||
const blob = await domToBlob(element, {
|
||||
scale: 2,
|
||||
});
|
||||
|
||||
if (ios) {
|
||||
const file = new File(
|
||||
[blob],
|
||||
`bitview-${name}-${new Date().toJSON().split(".")[0]}.png`,
|
||||
{
|
||||
type: "image/png",
|
||||
},
|
||||
);
|
||||
|
||||
try {
|
||||
await navigator.share({
|
||||
files: [file],
|
||||
title: `${title} on ${window.document.location.hostname}`,
|
||||
});
|
||||
return;
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
}
|
||||
|
||||
const url = URL.createObjectURL(blob);
|
||||
window.open(url, "_blank");
|
||||
setTimeout(() => URL.revokeObjectURL(url), 100);
|
||||
}
|
||||
99
website/scripts/panes/explorer.js
Normal file
99
website/scripts/panes/explorer.js
Normal file
@@ -0,0 +1,99 @@
|
||||
import { randomFromArray } from "../utils/array.js";
|
||||
import { explorerElement } from "../utils/elements.js";
|
||||
|
||||
export function init() {
|
||||
const chain = window.document.createElement("div");
|
||||
chain.id = "chain";
|
||||
explorerElement.append(chain);
|
||||
|
||||
// vecsResources.getOrCreate(/** @satisfies {Height}*/ (5), "height");
|
||||
//
|
||||
const miners = [
|
||||
{ name: "Foundry USA", color: "orange" },
|
||||
{ name: "Via BTC", color: "teal" },
|
||||
{ name: "Ant Pool", color: "emerald" },
|
||||
{ name: "F2Pool", color: "indigo" },
|
||||
{ name: "Spider Pool", color: "yellow" },
|
||||
{ name: "Mara Pool", color: "amber" },
|
||||
{ name: "SEC Pool", color: "violet" },
|
||||
{ name: "Luxor", color: "orange" },
|
||||
{ name: "Brains Pool", color: "cyan" },
|
||||
];
|
||||
|
||||
for (let i = 0; i <= 10; i++) {
|
||||
const { name, color: _color } = randomFromArray(miners);
|
||||
const { cubeElement, leftFaceElement, rightFaceElement, topFaceElement } =
|
||||
createCube();
|
||||
|
||||
// cubeElement.style.setProperty("--color", `var(--${color})`);
|
||||
|
||||
const heightElement = window.document.createElement("p");
|
||||
const height = (1_000_002 - i).toString();
|
||||
const prefixLength = 7 - height.length;
|
||||
const spanPrefix = window.document.createElement("span");
|
||||
spanPrefix.style.opacity = "0.5";
|
||||
spanPrefix.style.userSelect = "none";
|
||||
heightElement.append(spanPrefix);
|
||||
spanPrefix.innerHTML = "#" + "0".repeat(prefixLength);
|
||||
const spanHeight = window.document.createElement("span");
|
||||
heightElement.append(spanHeight);
|
||||
spanHeight.innerHTML = height;
|
||||
rightFaceElement.append(heightElement);
|
||||
|
||||
const feesElement = window.document.createElement("div");
|
||||
feesElement.classList.add("fees");
|
||||
leftFaceElement.append(feesElement);
|
||||
const averageFeeElement = window.document.createElement("p");
|
||||
feesElement.append(averageFeeElement);
|
||||
averageFeeElement.innerHTML = `~1.41`;
|
||||
const feeRangeElement = window.document.createElement("p");
|
||||
feesElement.append(feeRangeElement);
|
||||
const minFeeElement = window.document.createElement("span");
|
||||
minFeeElement.innerHTML = `0.11`;
|
||||
feeRangeElement.append(minFeeElement);
|
||||
const dashElement = window.document.createElement("span");
|
||||
dashElement.style.opacity = "0.5";
|
||||
dashElement.innerHTML = `-`;
|
||||
feeRangeElement.append(dashElement);
|
||||
const maxFeeElement = window.document.createElement("span");
|
||||
maxFeeElement.innerHTML = `12.1`;
|
||||
feeRangeElement.append(maxFeeElement);
|
||||
const feeUnitElement = window.document.createElement("p");
|
||||
feesElement.append(feeUnitElement);
|
||||
feeUnitElement.style.opacity = "0.5";
|
||||
feeUnitElement.innerHTML = `sat/vB`;
|
||||
|
||||
const spanMiner = window.document.createElement("span");
|
||||
spanMiner.innerHTML = name;
|
||||
topFaceElement.append(spanMiner);
|
||||
|
||||
chain.prepend(cubeElement);
|
||||
}
|
||||
}
|
||||
|
||||
function createCube() {
|
||||
const cubeElement = window.document.createElement("div");
|
||||
cubeElement.classList.add("cube");
|
||||
|
||||
const rightFaceElement = window.document.createElement("div");
|
||||
rightFaceElement.classList.add("face");
|
||||
rightFaceElement.classList.add("right");
|
||||
cubeElement.append(rightFaceElement);
|
||||
|
||||
const leftFaceElement = window.document.createElement("div");
|
||||
leftFaceElement.classList.add("face");
|
||||
leftFaceElement.classList.add("left");
|
||||
cubeElement.append(leftFaceElement);
|
||||
|
||||
const topFaceElement = window.document.createElement("div");
|
||||
topFaceElement.classList.add("face");
|
||||
topFaceElement.classList.add("top");
|
||||
cubeElement.append(topFaceElement);
|
||||
|
||||
return {
|
||||
cubeElement,
|
||||
leftFaceElement,
|
||||
rightFaceElement,
|
||||
topFaceElement,
|
||||
};
|
||||
}
|
||||
0
website/scripts/panes/nav.js
Normal file
0
website/scripts/panes/nav.js
Normal file
0
website/scripts/panes/search.js
Normal file
0
website/scripts/panes/search.js
Normal file
1106
website/scripts/panes/simulation.js
Normal file
1106
website/scripts/panes/simulation.js
Normal file
File diff suppressed because it is too large
Load Diff
433
website/scripts/panes/table.js
Normal file
433
website/scripts/panes/table.js
Normal file
@@ -0,0 +1,433 @@
|
||||
// @ts-nocheck
|
||||
|
||||
import { randomFromArray } from "../utils/array.js";
|
||||
import { createButtonElement, createHeader, createSelect } from "../utils/dom.js";
|
||||
import { tableElement } from "../utils/elements.js";
|
||||
import { serdeMetrics, serdeString } from "../utils/serde.js";
|
||||
import { resetParams } from "../utils/url.js";
|
||||
|
||||
export function init() {
|
||||
tableElement.innerHTML = "wip, will hopefuly be back soon, sorry !";
|
||||
|
||||
// const parent = tableElement;
|
||||
// const { headerElement } = createHeader("Table");
|
||||
// parent.append(headerElement);
|
||||
|
||||
// const div = window.document.createElement("div");
|
||||
// parent.append(div);
|
||||
|
||||
// const table = createTable({
|
||||
// signals,
|
||||
// brk,
|
||||
// resources,
|
||||
// option,
|
||||
// });
|
||||
// div.append(table.element);
|
||||
|
||||
// const span = window.document.createElement("span");
|
||||
// span.innerHTML = "Add column";
|
||||
// div.append(
|
||||
// createButtonElement({
|
||||
// onClick: () => {
|
||||
// table.addRandomCol?.();
|
||||
// },
|
||||
// inside: span,
|
||||
// title: "Click or tap to add a column to the table",
|
||||
// }),
|
||||
// );
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @param {Object} args
|
||||
// * @param {Option} args.option
|
||||
// * @param {Signals} args.signals
|
||||
// * @param {BrkClient} args.brk
|
||||
// * @param {Resources} args.resources
|
||||
// */
|
||||
// function createTable({ brk, signals, option, resources }) {
|
||||
// const indexToMetrics = createIndexToMetrics(metricToIndexes);
|
||||
|
||||
// const serializedIndexes = createSerializedIndexes();
|
||||
// /** @type {SerializedIndex} */
|
||||
// const defaultSerializedIndex = "height";
|
||||
// const serializedIndex = /** @type {Signal<SerializedIndex>} */ (
|
||||
// signals.createSignal(
|
||||
// /** @type {SerializedIndex} */ (defaultSerializedIndex),
|
||||
// {
|
||||
// save: {
|
||||
// ...serdeString,
|
||||
// keyPrefix: "table",
|
||||
// key: "index",
|
||||
// },
|
||||
// },
|
||||
// )
|
||||
// );
|
||||
// const index = signals.createMemo(() =>
|
||||
// serializedIndexToIndex(serializedIndex()),
|
||||
// );
|
||||
|
||||
// const table = window.document.createElement("table");
|
||||
// const obj = {
|
||||
// element: table,
|
||||
// /** @type {VoidFunction | undefined} */
|
||||
// addRandomCol: undefined,
|
||||
// };
|
||||
|
||||
// signals.createEffect(index, (index, prevIndex) => {
|
||||
// if (prevIndex !== undefined) {
|
||||
// resetParams(option);
|
||||
// }
|
||||
|
||||
// const possibleMetrics = indexToMetrics[index];
|
||||
|
||||
// const columns = signals.createSignal(/** @type {Metric[]} */ ([]), {
|
||||
// equals: false,
|
||||
// save: {
|
||||
// ...serdeMetrics,
|
||||
// keyPrefix: `table-${serializedIndex()}`,
|
||||
// key: `columns`,
|
||||
// },
|
||||
// });
|
||||
// columns.set((l) => l.filter((id) => possibleMetrics.includes(id)));
|
||||
|
||||
// signals.createEffect(columns, (columns) => {
|
||||
// console.log(columns);
|
||||
// });
|
||||
|
||||
// table.innerHTML = "";
|
||||
// const thead = window.document.createElement("thead");
|
||||
// table.append(thead);
|
||||
// const trHead = window.document.createElement("tr");
|
||||
// thead.append(trHead);
|
||||
// const tbody = window.document.createElement("tbody");
|
||||
// table.append(tbody);
|
||||
|
||||
// const rowElements = signals.createSignal(
|
||||
// /** @type {HTMLTableRowElement[]} */ ([]),
|
||||
// );
|
||||
|
||||
// /**
|
||||
// * @param {Object} args
|
||||
// * @param {HTMLSelectElement} args.select
|
||||
// * @param {Unit} [args.unit]
|
||||
// * @param {(event: MouseEvent) => void} [args.onLeft]
|
||||
// * @param {(event: MouseEvent) => void} [args.onRight]
|
||||
// * @param {(event: MouseEvent) => void} [args.onRemove]
|
||||
// */
|
||||
// function addThCol({ select, onLeft, onRight, onRemove, unit: _unit }) {
|
||||
// const th = window.document.createElement("th");
|
||||
// th.scope = "col";
|
||||
// trHead.append(th);
|
||||
// const div = window.document.createElement("div");
|
||||
// div.append(select);
|
||||
// // const top = window.document.createElement("div");
|
||||
// // div.append(top);
|
||||
// // top.append(select);
|
||||
// // top.append(
|
||||
// // createAnchorElement({
|
||||
// // href: "",
|
||||
// // blank: true,
|
||||
// // }),
|
||||
// // );
|
||||
// const bottom = window.document.createElement("div");
|
||||
// const unit = window.document.createElement("span");
|
||||
// if (_unit) {
|
||||
// unit.innerHTML = _unit;
|
||||
// }
|
||||
// const moveLeft = createButtonElement({
|
||||
// inside: "←",
|
||||
// title: "Move column to the left",
|
||||
// onClick: onLeft || (() => {}),
|
||||
// });
|
||||
// const moveRight = createButtonElement({
|
||||
// inside: "→",
|
||||
// title: "Move column to the right",
|
||||
// onClick: onRight || (() => {}),
|
||||
// });
|
||||
// const remove = createButtonElement({
|
||||
// inside: "×",
|
||||
// title: "Remove column",
|
||||
// onClick: onRemove || (() => {}),
|
||||
// });
|
||||
// bottom.append(unit);
|
||||
// bottom.append(moveLeft);
|
||||
// bottom.append(moveRight);
|
||||
// bottom.append(remove);
|
||||
// div.append(bottom);
|
||||
// th.append(div);
|
||||
// return {
|
||||
// element: th,
|
||||
// /**
|
||||
// * @param {Unit} _unit
|
||||
// */
|
||||
// setUnit(_unit) {
|
||||
// unit.innerHTML = _unit;
|
||||
// },
|
||||
// };
|
||||
// }
|
||||
|
||||
// addThCol({
|
||||
// ...createSelect({
|
||||
// list: serializedIndexes,
|
||||
// signal: serializedIndex,
|
||||
// }),
|
||||
// unit: "index",
|
||||
// });
|
||||
|
||||
// let from = 0;
|
||||
// let to = 0;
|
||||
|
||||
// resources
|
||||
// .getOrCreate(index, serializedIndex())
|
||||
// .fetch()
|
||||
// .then((vec) => {
|
||||
// if (!vec) return;
|
||||
// from = /** @type {number} */ (vec[0]);
|
||||
// to = /** @type {number} */ (vec.at(-1)) + 1;
|
||||
// const trs = /** @type {HTMLTableRowElement[]} */ ([]);
|
||||
// for (let i = vec.length - 1; i >= 0; i--) {
|
||||
// const value = vec[i];
|
||||
// const tr = window.document.createElement("tr");
|
||||
// trs.push(tr);
|
||||
// tbody.append(tr);
|
||||
// const th = window.document.createElement("th");
|
||||
// th.innerHTML = serializeValue({
|
||||
// value,
|
||||
// unit: "index",
|
||||
// });
|
||||
// th.scope = "row";
|
||||
// tr.append(th);
|
||||
// }
|
||||
// rowElements.set(() => trs);
|
||||
// });
|
||||
|
||||
// const owner = signals.getOwner();
|
||||
|
||||
// /**
|
||||
// * @param {Metric} metric
|
||||
// * @param {number} [_colIndex]
|
||||
// */
|
||||
// function addCol(metric, _colIndex = columns().length) {
|
||||
// signals.runWithOwner(owner, () => {
|
||||
// /** @type {VoidFunction | undefined} */
|
||||
// let dispose;
|
||||
// signals.createRoot((_dispose) => {
|
||||
// dispose = _dispose;
|
||||
|
||||
// const metricOption = signals.createSignal({
|
||||
// name: metric,
|
||||
// value: metric,
|
||||
// });
|
||||
// const { select } = createSelect({
|
||||
// list: possibleMetrics.map((metric) => ({
|
||||
// name: metric,
|
||||
// value: metric,
|
||||
// })),
|
||||
// signal: metricOption,
|
||||
// });
|
||||
|
||||
// signals.createEffect(metricOption, (metricOption) => {
|
||||
// select.style.width = `${21 + 7.25 * metricOption.name.length}px`;
|
||||
// });
|
||||
|
||||
// if (_colIndex === columns().length) {
|
||||
// columns.set((l) => {
|
||||
// l.push(metric);
|
||||
// return l;
|
||||
// });
|
||||
// }
|
||||
|
||||
// const colIndex = signals.createSignal(_colIndex);
|
||||
|
||||
// /**
|
||||
// * @param {boolean} right
|
||||
// * @returns {(event: MouseEvent) => void}
|
||||
// */
|
||||
// function createMoveColumnFunction(right) {
|
||||
// return () => {
|
||||
// const oldColIndex = colIndex();
|
||||
// const newColIndex = oldColIndex + (right ? 1 : -1);
|
||||
|
||||
// const currentTh = /** @type {HTMLTableCellElement} */ (
|
||||
// trHead.childNodes[oldColIndex + 1]
|
||||
// );
|
||||
// const oterTh = /** @type {HTMLTableCellElement} */ (
|
||||
// trHead.childNodes[newColIndex + 1]
|
||||
// );
|
||||
|
||||
// if (right) {
|
||||
// oterTh.after(currentTh);
|
||||
// } else {
|
||||
// oterTh.before(currentTh);
|
||||
// }
|
||||
|
||||
// columns.set((l) => {
|
||||
// [l[oldColIndex], l[newColIndex]] = [
|
||||
// l[newColIndex],
|
||||
// l[oldColIndex],
|
||||
// ];
|
||||
// return l;
|
||||
// });
|
||||
|
||||
// const rows = rowElements();
|
||||
// for (let i = 0; i < rows.length; i++) {
|
||||
// const element = rows[i].childNodes[oldColIndex + 1];
|
||||
// const sibling = rows[i].childNodes[newColIndex + 1];
|
||||
// const temp = element.textContent;
|
||||
// element.textContent = sibling.textContent;
|
||||
// sibling.textContent = temp;
|
||||
// }
|
||||
// };
|
||||
// }
|
||||
|
||||
// const th = addThCol({
|
||||
// select,
|
||||
// unit: serdeUnit.deserialize(metric),
|
||||
// onLeft: createMoveColumnFunction(false),
|
||||
// onRight: createMoveColumnFunction(true),
|
||||
// onRemove: () => {
|
||||
// const ci = colIndex();
|
||||
// trHead.childNodes[ci + 1].remove();
|
||||
// columns.set((l) => {
|
||||
// l.splice(ci, 1);
|
||||
// return l;
|
||||
// });
|
||||
// const rows = rowElements();
|
||||
// for (let i = 0; i < rows.length; i++) {
|
||||
// rows[i].childNodes[ci + 1].remove();
|
||||
// }
|
||||
// dispose?.();
|
||||
// },
|
||||
// });
|
||||
|
||||
// signals.createEffect(columns, () => {
|
||||
// colIndex.set(Array.from(trHead.children).indexOf(th.element) - 1);
|
||||
// });
|
||||
|
||||
// console.log(colIndex());
|
||||
|
||||
// signals.createEffect(rowElements, (rowElements) => {
|
||||
// if (!rowElements.length) return;
|
||||
// for (let i = 0; i < rowElements.length; i++) {
|
||||
// const td = window.document.createElement("td");
|
||||
// rowElements[i].append(td);
|
||||
// }
|
||||
|
||||
// signals.createEffect(
|
||||
// () => metricOption().name,
|
||||
// (metric, prevMetric) => {
|
||||
// const unit = serdeUnit.deserialize(metric);
|
||||
// th.setUnit(unit);
|
||||
|
||||
// const vec = resources.getOrCreate(index, metric);
|
||||
|
||||
// vec.fetch({ from, to });
|
||||
|
||||
// const fetchedKey = resources.genFetchedKey({ from, to });
|
||||
|
||||
// columns.set((l) => {
|
||||
// const i = l.indexOf(prevMetric ?? metric);
|
||||
// if (i === -1) {
|
||||
// l.push(metric);
|
||||
// } else {
|
||||
// l[i] = metric;
|
||||
// }
|
||||
// return l;
|
||||
// });
|
||||
|
||||
// signals.createEffect(
|
||||
// () => vec.fetched().get(fetchedKey)?.vec(),
|
||||
// (vec) => {
|
||||
// if (!vec?.length) return;
|
||||
|
||||
// const thIndex = colIndex() + 1;
|
||||
|
||||
// for (let i = 0; i < rowElements.length; i++) {
|
||||
// const iRev = vec.length - 1 - i;
|
||||
// const value = vec[iRev];
|
||||
// // @ts-ignore
|
||||
// rowElements[i].childNodes[thIndex].innerHTML =
|
||||
// serializeValue({
|
||||
// value,
|
||||
// unit,
|
||||
// });
|
||||
// }
|
||||
// },
|
||||
// );
|
||||
|
||||
// return () => metric;
|
||||
// },
|
||||
// );
|
||||
// });
|
||||
// });
|
||||
|
||||
// signals.onCleanup(() => {
|
||||
// dispose?.();
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
|
||||
// columns().forEach((metric, colIndex) => addCol(metric, colIndex));
|
||||
|
||||
// obj.addRandomCol = function () {
|
||||
// addCol(randomFromArray(possibleMetrics));
|
||||
// };
|
||||
|
||||
// return () => index;
|
||||
// });
|
||||
|
||||
// return obj;
|
||||
// }
|
||||
|
||||
/**
|
||||
* @param {MetricToIndexes} metricToIndexes
|
||||
*/
|
||||
function createIndexToMetrics(metricToIndexes) {
|
||||
// const indexToMetrics = Object.entries(metricToIndexes).reduce(
|
||||
// (arr, [_id, indexes]) => {
|
||||
// const id = /** @type {Metric} */ (_id);
|
||||
// indexes.forEach((i) => {
|
||||
// arr[i] ??= [];
|
||||
// arr[i].push(id);
|
||||
// });
|
||||
// return arr;
|
||||
// },
|
||||
// /** @type {Metric[][]} */ (Array.from({ length: 24 })),
|
||||
// );
|
||||
// indexToMetrics.forEach((arr) => {
|
||||
// arr.sort();
|
||||
// });
|
||||
// return indexToMetrics;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} args
|
||||
* @param {number | string | Object | Array<any>} args.value
|
||||
* @param {Unit} args.unit
|
||||
*/
|
||||
function serializeValue({ value, unit }) {
|
||||
const t = typeof value;
|
||||
if (value === null) {
|
||||
return "null";
|
||||
} else if (typeof value === "string") {
|
||||
return value;
|
||||
} else if (t !== "number") {
|
||||
return JSON.stringify(value).replaceAll('"', "").slice(1, -1);
|
||||
} else if (value !== 18446744073709552000) {
|
||||
if (unit === "usd" || unit === "difficulty" || unit === "sat/vb") {
|
||||
return value.toLocaleString("en-us", {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
});
|
||||
} else if (unit === "btc") {
|
||||
return value.toLocaleString("en-us", {
|
||||
minimumFractionDigits: 8,
|
||||
maximumFractionDigits: 8,
|
||||
});
|
||||
} else {
|
||||
return value.toLocaleString("en-us");
|
||||
}
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
130
website/scripts/resources.js
Normal file
130
website/scripts/resources.js
Normal file
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* @template T
|
||||
* @typedef {Object} Resource
|
||||
* @property {Signal<T | null>} data
|
||||
* @property {Signal<boolean>} loading
|
||||
* @property {Signal<Error | null>} error
|
||||
* @property {(...args: any[]) => Promise<T | null>} fetch
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @typedef {Object} RangeState
|
||||
* @property {Signal<MetricData<T> | null>} response
|
||||
* @property {Signal<boolean>} loading
|
||||
*/
|
||||
/** @typedef {RangeState<unknown>} AnyRangeState */
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @typedef {Object} MetricResource
|
||||
* @property {string} path
|
||||
* @property {(from?: number, to?: number) => RangeState<T>} range
|
||||
* @property {(from?: number, to?: number) => Promise<MetricData<T> | null>} fetch
|
||||
*/
|
||||
/** @typedef {MetricResource<unknown>} AnyMetricResource */
|
||||
|
||||
/**
|
||||
* @typedef {{ createResource: typeof createResource, useMetricEndpoint: typeof useMetricEndpoint }} Resources
|
||||
*/
|
||||
|
||||
import signals from "./signals.js";
|
||||
|
||||
/**
|
||||
* Create a generic reactive resource wrapper for any async fetcher
|
||||
* @template T
|
||||
* @template {any[]} Args
|
||||
* @param {(...args: Args) => Promise<T>} fetcher
|
||||
* @returns {Resource<T>}
|
||||
*/
|
||||
function createResource(fetcher) {
|
||||
const owner = signals.getOwner();
|
||||
return signals.runWithOwner(owner, () => {
|
||||
const data = signals.createSignal(/** @type {T | null} */ (null));
|
||||
const loading = signals.createSignal(false);
|
||||
const error = signals.createSignal(/** @type {Error | null} */ (null));
|
||||
|
||||
return {
|
||||
data,
|
||||
loading,
|
||||
error,
|
||||
/**
|
||||
* @param {Args} args
|
||||
*/
|
||||
async fetch(...args) {
|
||||
loading.set(true);
|
||||
error.set(null);
|
||||
try {
|
||||
const result = await fetcher(...args);
|
||||
data.set(() => result);
|
||||
return result;
|
||||
} catch (e) {
|
||||
error.set(e instanceof Error ? e : new Error(String(e)));
|
||||
return null;
|
||||
} finally {
|
||||
loading.set(false);
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a reactive resource wrapper for a MetricEndpoint with multi-range support
|
||||
* @template T
|
||||
* @param {MetricEndpoint<T>} endpoint
|
||||
* @returns {MetricResource<T>}
|
||||
*/
|
||||
function useMetricEndpoint(endpoint) {
|
||||
const owner = signals.getOwner();
|
||||
return signals.runWithOwner(owner, () => {
|
||||
/** @type {Map<string, RangeState<T>>} */
|
||||
const ranges = new Map();
|
||||
|
||||
/**
|
||||
* Get or create range state
|
||||
* @param {number} [from=-10000]
|
||||
* @param {number} [to]
|
||||
* @returns {RangeState<T>}
|
||||
*/
|
||||
function range(from = -10000, to) {
|
||||
const key = `${from}-${to ?? ""}`;
|
||||
const existing = ranges.get(key);
|
||||
if (existing) return existing;
|
||||
|
||||
/** @type {RangeState<T>} */
|
||||
const state = {
|
||||
response: signals.createSignal(
|
||||
/** @type {MetricData<T> | null} */ (null),
|
||||
),
|
||||
loading: signals.createSignal(false),
|
||||
};
|
||||
ranges.set(key, state);
|
||||
return state;
|
||||
}
|
||||
|
||||
return {
|
||||
path: endpoint.path,
|
||||
range,
|
||||
/**
|
||||
* Fetch data for a range
|
||||
* @param {number} [start=-10000]
|
||||
* @param {number} [end]
|
||||
*/
|
||||
async fetch(start = -10000, end) {
|
||||
const r = range(start, end);
|
||||
r.loading.set(true);
|
||||
try {
|
||||
const result = await endpoint
|
||||
.slice(start, end)
|
||||
.fetch(r.response.set);
|
||||
return result;
|
||||
} finally {
|
||||
r.loading.set(false);
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export const resources = { createResource, useMetricEndpoint };
|
||||
218
website/scripts/signals.js
Normal file
218
website/scripts/signals.js
Normal file
@@ -0,0 +1,218 @@
|
||||
/**
|
||||
* @import { SignalOptions } from "./modules/solidjs-signals/0.6.3/dist/types/core/core.js"
|
||||
* @import { getOwner as GetOwner, onCleanup as OnCleanup } from "./modules/solidjs-signals/0.6.3/dist/types/core/owner.js"
|
||||
* @import { createSignal as CreateSignal, createEffect as CreateEffect, createMemo as CreateMemo, createRoot as CreateRoot, runWithOwner as RunWithOwner, Setter } from "./modules/solidjs-signals/0.6.3/dist/types/signals.js";
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @typedef {() => T} Accessor
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @typedef {Accessor<T> & { set: Setter<T>; reset: VoidFunction }} Signal
|
||||
*/
|
||||
|
||||
import {
|
||||
createSignal,
|
||||
createEffect,
|
||||
getOwner,
|
||||
createMemo,
|
||||
createRoot,
|
||||
runWithOwner,
|
||||
onCleanup,
|
||||
} from "./modules/solidjs-signals/0.6.3/dist/prod.js";
|
||||
|
||||
// let effectCount = 0;
|
||||
|
||||
const signals = {
|
||||
createSolidSignal: /** @type {typeof CreateSignal} */ (createSignal),
|
||||
createSolidEffect: /** @type {typeof CreateEffect} */ (createEffect),
|
||||
createEffect: /** @type {typeof CreateEffect} */ (
|
||||
// @ts-ignore
|
||||
(compute, effect) => {
|
||||
let dispose = /** @type {VoidFunction | null} */ (null);
|
||||
|
||||
if (getOwner() === null) {
|
||||
throw Error("No owner");
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
if (dispose) {
|
||||
dispose();
|
||||
dispose = null;
|
||||
// console.log("effectCount = ", --effectCount);
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
createEffect(compute, (v, oldV) => {
|
||||
// console.log("effectCount = ", ++effectCount);
|
||||
cleanup();
|
||||
signals.createRoot((_dispose) => {
|
||||
dispose = _dispose;
|
||||
return effect(v, oldV);
|
||||
});
|
||||
signals.onCleanup(cleanup);
|
||||
});
|
||||
signals.onCleanup(cleanup);
|
||||
}
|
||||
),
|
||||
createMemo: /** @type {typeof CreateMemo} */ (createMemo),
|
||||
createRoot: /** @type {typeof CreateRoot} */ (createRoot),
|
||||
getOwner: /** @type {typeof GetOwner} */ (getOwner),
|
||||
runWithOwner: /** @type {typeof RunWithOwner} */ (runWithOwner),
|
||||
onCleanup: /** @type {typeof OnCleanup} */ (onCleanup),
|
||||
/**
|
||||
* @template T
|
||||
* @param {T} initialValue
|
||||
* @param {SignalOptions<T> & {save?: {keyPrefix: string | Accessor<string>; key: string; serialize: (v: T) => string; deserialize: (v: string) => T; serializeParam?: boolean; saveDefaultValue?: boolean}}} [options]
|
||||
* @returns {Signal<T>}
|
||||
*/
|
||||
createSignal(initialValue, options) {
|
||||
const [get, set] = this.createSolidSignal(
|
||||
/** @type {any} */ (initialValue),
|
||||
options,
|
||||
);
|
||||
|
||||
// @ts-ignore
|
||||
get.set = set;
|
||||
|
||||
// @ts-ignore
|
||||
get.reset = () => set(initialValue);
|
||||
|
||||
if (options?.save) {
|
||||
const save = options.save;
|
||||
|
||||
const paramKey = save.key;
|
||||
const storageKey = this.createMemo(
|
||||
() =>
|
||||
`${
|
||||
typeof save.keyPrefix === "string"
|
||||
? save.keyPrefix
|
||||
: save.keyPrefix()
|
||||
}-${paramKey}`,
|
||||
);
|
||||
|
||||
// /** @type { ((this: Window, ev: PopStateEvent) => any) | undefined} */
|
||||
// let popstateCallback;
|
||||
|
||||
let serialized = /** @type {string | null} */ (null);
|
||||
if (options.save.serializeParam !== false) {
|
||||
serialized = new URLSearchParams(window.location.search).get(paramKey);
|
||||
|
||||
// popstateCallback =
|
||||
// /** @type {(this: Window, ev: PopStateEvent) => any} */ (
|
||||
// (_) => {
|
||||
// serialized = new URLSearchParams(window.location.search).get(
|
||||
// paramKey,
|
||||
// );
|
||||
// set(() =>
|
||||
// serialized ? save.deserialize(serialized) : initialValue,
|
||||
// );
|
||||
// }
|
||||
// );
|
||||
// if (!popstateCallback) throw "Unreachable";
|
||||
// window.addEventListener("popstate", popstateCallback);
|
||||
// signals.onCleanup(() => {
|
||||
// if (popstateCallback)
|
||||
// window.removeEventListener("popstate", popstateCallback);
|
||||
// });
|
||||
}
|
||||
if (serialized === null) {
|
||||
try {
|
||||
serialized = localStorage.getItem(storageKey());
|
||||
} catch (_) {}
|
||||
}
|
||||
if (serialized) {
|
||||
set(() => (serialized ? save.deserialize(serialized) : initialValue));
|
||||
}
|
||||
|
||||
let firstRun1 = true;
|
||||
this.createEffect(storageKey, (storageKey) => {
|
||||
if (!firstRun1) {
|
||||
try {
|
||||
serialized = localStorage.getItem(storageKey);
|
||||
set(() =>
|
||||
serialized ? save.deserialize(serialized) : initialValue,
|
||||
);
|
||||
} catch (_) {}
|
||||
}
|
||||
firstRun1 = false;
|
||||
});
|
||||
|
||||
let firstRun2 = true;
|
||||
this.createEffect(get, (value) => {
|
||||
if (!save) return;
|
||||
|
||||
if (!firstRun2) {
|
||||
try {
|
||||
if (
|
||||
value !== undefined &&
|
||||
value !== null &&
|
||||
(initialValue === undefined ||
|
||||
initialValue === null ||
|
||||
save.saveDefaultValue ||
|
||||
save.serialize(value) !== save.serialize(initialValue))
|
||||
) {
|
||||
localStorage.setItem(storageKey(), save.serialize(value));
|
||||
} else {
|
||||
localStorage.removeItem(storageKey());
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
if (
|
||||
value !== undefined &&
|
||||
value !== null &&
|
||||
(initialValue === undefined ||
|
||||
initialValue === null ||
|
||||
save.saveDefaultValue ||
|
||||
save.serialize(value) !== save.serialize(initialValue))
|
||||
) {
|
||||
writeParam(paramKey, save.serialize(value));
|
||||
} else {
|
||||
removeParam(paramKey);
|
||||
}
|
||||
|
||||
firstRun2 = false;
|
||||
});
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
return get;
|
||||
},
|
||||
};
|
||||
/** @typedef {typeof signals} Signals */
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
* @param {string | undefined} value
|
||||
*/
|
||||
function writeParam(key, value) {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
|
||||
if (value !== undefined) {
|
||||
urlParams.set(key, String(value));
|
||||
} else {
|
||||
urlParams.delete(key);
|
||||
}
|
||||
|
||||
try {
|
||||
window.history.replaceState(
|
||||
null,
|
||||
"",
|
||||
`${window.location.pathname}?${urlParams.toString()}`,
|
||||
);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
*/
|
||||
function removeParam(key) {
|
||||
writeParam(key, undefined);
|
||||
}
|
||||
|
||||
export default signals;
|
||||
20
website/scripts/utils/array.js
Normal file
20
website/scripts/utils/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 randomFromArray(array) {
|
||||
return array[Math.floor(Math.random() * array.length)];
|
||||
}
|
||||
116
website/scripts/utils/colors.js
Normal file
116
website/scripts/utils/colors.js
Normal file
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* @param {Accessor<boolean>} dark
|
||||
*/
|
||||
export function createColors(dark) {
|
||||
const globalComputedStyle = getComputedStyle(window.document.documentElement);
|
||||
/**
|
||||
* @param {string} color
|
||||
*/
|
||||
function getColor(color) {
|
||||
return globalComputedStyle.getPropertyValue(`--${color}`);
|
||||
}
|
||||
function red() {
|
||||
return getColor("red");
|
||||
}
|
||||
function orange() {
|
||||
return getColor("orange");
|
||||
}
|
||||
function amber() {
|
||||
return getColor("amber");
|
||||
}
|
||||
function yellow() {
|
||||
return getColor("yellow");
|
||||
}
|
||||
function avocado() {
|
||||
return getColor("avocado");
|
||||
}
|
||||
function lime() {
|
||||
return getColor("lime");
|
||||
}
|
||||
function green() {
|
||||
return getColor("green");
|
||||
}
|
||||
function emerald() {
|
||||
return getColor("emerald");
|
||||
}
|
||||
function teal() {
|
||||
return getColor("teal");
|
||||
}
|
||||
function cyan() {
|
||||
return getColor("cyan");
|
||||
}
|
||||
function sky() {
|
||||
return getColor("sky");
|
||||
}
|
||||
function blue() {
|
||||
return getColor("blue");
|
||||
}
|
||||
function indigo() {
|
||||
return getColor("indigo");
|
||||
}
|
||||
function violet() {
|
||||
return getColor("violet");
|
||||
}
|
||||
function purple() {
|
||||
return getColor("purple");
|
||||
}
|
||||
function fuchsia() {
|
||||
return getColor("fuchsia");
|
||||
}
|
||||
function pink() {
|
||||
return getColor("pink");
|
||||
}
|
||||
function rose() {
|
||||
return getColor("rose");
|
||||
}
|
||||
function gray() {
|
||||
return getColor("gray");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} property
|
||||
*/
|
||||
function getLightDarkValue(property) {
|
||||
const value = globalComputedStyle.getPropertyValue(property);
|
||||
const [light, _dark] = value.slice(11, -1).split(", ");
|
||||
return dark() ? _dark : light;
|
||||
}
|
||||
|
||||
function textColor() {
|
||||
return getLightDarkValue("--color");
|
||||
}
|
||||
function borderColor() {
|
||||
return getLightDarkValue("--border-color");
|
||||
}
|
||||
|
||||
return {
|
||||
default: textColor,
|
||||
gray,
|
||||
border: borderColor,
|
||||
|
||||
red,
|
||||
orange,
|
||||
amber,
|
||||
yellow,
|
||||
avocado,
|
||||
lime,
|
||||
green,
|
||||
emerald,
|
||||
teal,
|
||||
cyan,
|
||||
sky,
|
||||
blue,
|
||||
indigo,
|
||||
violet,
|
||||
purple,
|
||||
fuchsia,
|
||||
pink,
|
||||
rose,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {ReturnType<typeof createColors>} Colors
|
||||
* @typedef {Colors["orange"]} Color
|
||||
* @typedef {keyof Colors} ColorName
|
||||
*/
|
||||
58
website/scripts/utils/date.js
Normal file
58
website/scripts/utils/date.js
Normal file
@@ -0,0 +1,58 @@
|
||||
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 dateToDateIndex(date) {
|
||||
if (
|
||||
date.getUTCFullYear() === 2009 &&
|
||||
date.getUTCMonth() === 0 &&
|
||||
date.getUTCDate() === 3
|
||||
)
|
||||
return 0;
|
||||
return differenceBetweenDates(date, new Date("2009-01-09"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Date} start
|
||||
* @param {Date} end
|
||||
*/
|
||||
export function createDateRange(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 differenceBetweenDates(date1, date2) {
|
||||
return Math.abs(date1.valueOf() - date2.valueOf()) / ONE_DAY_IN_MS;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Date} date1
|
||||
* @param {Date} date2
|
||||
*/
|
||||
export function roundedDifferenceBetweenDates(date1, date2) {
|
||||
return Math.round(differenceBetweenDates(date1, date2));
|
||||
}
|
||||
514
website/scripts/utils/dom.js
Normal file
514
website/scripts/utils/dom.js
Normal file
@@ -0,0 +1,514 @@
|
||||
/**
|
||||
* @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 T
|
||||
* @param {Object} args
|
||||
* @param {T} args.defaultValue
|
||||
* @param {string} [args.id]
|
||||
* @param {readonly T[] | Accessor<readonly T[]>} args.choices
|
||||
* @param {string} [args.keyPrefix]
|
||||
* @param {string} args.key
|
||||
* @param {boolean} [args.sorted]
|
||||
* @param {Signals} args.signals
|
||||
* @param {(choice: T) => string} [args.toKey] - Extract string key for storage (defaults to identity for strings)
|
||||
* @param {(choice: T) => string} [args.toLabel] - Extract display label (defaults to identity for strings)
|
||||
* @param {"radio" | "select"} [args.type] - Render as radio buttons or select dropdown
|
||||
*/
|
||||
export function createChoiceField({
|
||||
id,
|
||||
choices: unsortedChoices,
|
||||
defaultValue,
|
||||
keyPrefix,
|
||||
key,
|
||||
signals,
|
||||
sorted,
|
||||
toKey = /** @type {(choice: T) => string} */ ((/** @type {any} */ c) => c),
|
||||
toLabel = /** @type {(choice: T) => string} */ ((/** @type {any} */ c) => c),
|
||||
type = /** @type {const} */ ("radio"),
|
||||
}) {
|
||||
const defaultKey = toKey(defaultValue);
|
||||
|
||||
const choices = signals.createMemo(() => {
|
||||
/** @type {readonly T[]} */
|
||||
let c;
|
||||
if (typeof unsortedChoices === "function") {
|
||||
c = unsortedChoices();
|
||||
} else {
|
||||
c = unsortedChoices;
|
||||
}
|
||||
|
||||
return sorted
|
||||
? /** @type {readonly T[]} */ (
|
||||
/** @type {any} */ (
|
||||
c.toSorted((a, b) => toLabel(a).localeCompare(toLabel(b)))
|
||||
)
|
||||
)
|
||||
: c;
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {string} storedKey
|
||||
* @returns {T}
|
||||
*/
|
||||
function fromKey(storedKey) {
|
||||
const found = choices().find((c) => toKey(c) === storedKey);
|
||||
return found ?? defaultValue;
|
||||
}
|
||||
|
||||
/** @type {Signal<T>} */
|
||||
const selected = signals.createSignal(defaultValue, {
|
||||
save: {
|
||||
serialize: (v) => toKey(v),
|
||||
deserialize: (s) => fromKey(s),
|
||||
keyPrefix: keyPrefix ?? "",
|
||||
key,
|
||||
saveDefaultValue: true,
|
||||
},
|
||||
});
|
||||
|
||||
const field = window.document.createElement("div");
|
||||
field.classList.add("field");
|
||||
|
||||
const div = window.document.createElement("div");
|
||||
field.append(div);
|
||||
|
||||
/** @type {HTMLElement | null} */
|
||||
let remainingSmall = null;
|
||||
if (type === "select") {
|
||||
remainingSmall = window.document.createElement("small");
|
||||
field.append(remainingSmall);
|
||||
}
|
||||
|
||||
signals.createEffect(choices, (choices) => {
|
||||
const s = selected();
|
||||
const sKey = toKey(s);
|
||||
const keys = choices.map(toKey);
|
||||
if (!keys.includes(sKey)) {
|
||||
if (keys.includes(defaultKey)) {
|
||||
selected.set(() => defaultValue);
|
||||
} else if (choices.length) {
|
||||
selected.set(() => choices[0]);
|
||||
}
|
||||
}
|
||||
|
||||
div.innerHTML = "";
|
||||
|
||||
if (choices.length === 1) {
|
||||
const span = window.document.createElement("span");
|
||||
span.textContent = toLabel(choices[0]);
|
||||
div.append(span);
|
||||
|
||||
if (remainingSmall) {
|
||||
remainingSmall.hidden = true;
|
||||
}
|
||||
} else if (type === "select") {
|
||||
const select = window.document.createElement("select");
|
||||
select.id = id ?? key;
|
||||
select.name = id ?? key;
|
||||
|
||||
choices.forEach((choice) => {
|
||||
const option = window.document.createElement("option");
|
||||
option.value = toKey(choice);
|
||||
option.textContent = toLabel(choice);
|
||||
if (toKey(choice) === sKey) {
|
||||
option.selected = true;
|
||||
}
|
||||
select.append(option);
|
||||
});
|
||||
|
||||
select.addEventListener("change", () => {
|
||||
selected.set(() => fromKey(select.value));
|
||||
});
|
||||
|
||||
div.append(select);
|
||||
|
||||
if (remainingSmall) {
|
||||
const remaining = choices.length - 1;
|
||||
if (remaining > 0) {
|
||||
remainingSmall.textContent = ` +${remaining}`;
|
||||
remainingSmall.hidden = false;
|
||||
} else {
|
||||
remainingSmall.hidden = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
choices.forEach((choice) => {
|
||||
const choiceKey = toKey(choice);
|
||||
const choiceLabel = toLabel(choice);
|
||||
const { label } = createLabeledInput({
|
||||
inputId: `${id ?? key}-${choiceKey.toLowerCase()}`,
|
||||
inputName: id ?? key,
|
||||
inputValue: choiceKey,
|
||||
inputChecked: choiceKey === sKey,
|
||||
// title: choiceLabel,
|
||||
type: "radio",
|
||||
});
|
||||
|
||||
const text = window.document.createTextNode(choiceLabel);
|
||||
label.append(text);
|
||||
div.append(label);
|
||||
});
|
||||
|
||||
field.addEventListener("change", (event) => {
|
||||
// @ts-ignore
|
||||
const value = event.target.value;
|
||||
selected.set(() => fromKey(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;
|
||||
}
|
||||
24
website/scripts/utils/elements.js
Normal file
24
website/scripts/utils/elements.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { getElementById } from "./dom.js";
|
||||
|
||||
export const style = getComputedStyle(window.document.documentElement);
|
||||
|
||||
export const headElement = window.document.getElementsByTagName("head")[0];
|
||||
export const bodyElement = window.document.body;
|
||||
|
||||
export const mainElement = getElementById("main");
|
||||
export const asideElement = getElementById("aside");
|
||||
export const searchElement = getElementById("search");
|
||||
export const navElement = getElementById("nav");
|
||||
export const chartElement = getElementById("chart");
|
||||
export const tableElement = getElementById("table");
|
||||
export const explorerElement = getElementById("explorer");
|
||||
export const simulationElement = getElementById("simulation");
|
||||
|
||||
export const asideLabelElement = getElementById("aside-selector-label");
|
||||
export const navLabelElement = getElementById(`nav-selector-label`);
|
||||
export const searchLabelElement = getElementById(`search-selector-label`);
|
||||
export const searchInput = /** @type {HTMLInputElement} */ (
|
||||
getElementById("search-input")
|
||||
);
|
||||
export const searchResultsElement = getElementById("search-results");
|
||||
export const frameSelectorsElement = getElementById("frame-selectors");
|
||||
12
website/scripts/utils/env.js
Normal file
12
website/scripts/utils/env.js
Normal file
@@ -0,0 +1,12 @@
|
||||
export const localhost = window.location.hostname === "localhost";
|
||||
export const standalone =
|
||||
"standalone" in window.navigator && !!window.navigator.standalone;
|
||||
export const userAgent = navigator.userAgent.toLowerCase();
|
||||
export const isChrome = userAgent.includes("chrome");
|
||||
export const safari = userAgent.includes("safari");
|
||||
export const safariOnly = safari && !isChrome;
|
||||
export const macOS = userAgent.includes("mac os");
|
||||
export const iphone = userAgent.includes("iphone");
|
||||
export const ipad = userAgent.includes("ipad");
|
||||
export const ios = iphone || ipad;
|
||||
export const canShare = "canShare" in navigator;
|
||||
38
website/scripts/utils/format.js
Normal file
38
website/scripts/utils/format.js
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* @param {number} value
|
||||
* @param {number} [digits]
|
||||
* @param {Intl.NumberFormatOptions} [options]
|
||||
*/
|
||||
export function numberToUSNumber(value, digits, options) {
|
||||
return value.toLocaleString("en-us", {
|
||||
...options,
|
||||
minimumFractionDigits: digits,
|
||||
maximumFractionDigits: digits,
|
||||
});
|
||||
}
|
||||
|
||||
export const numberToDollars = new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
});
|
||||
|
||||
export const numberToPercentage = new Intl.NumberFormat("en-US", {
|
||||
style: "percent",
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {string} s
|
||||
*/
|
||||
export function stringToId(s) {
|
||||
return (
|
||||
s
|
||||
// .replace(/\W/g, " ")
|
||||
.trim()
|
||||
.replace(/ +/g, "-")
|
||||
.toLowerCase()
|
||||
);
|
||||
}
|
||||
225
website/scripts/utils/serde.js
Normal file
225
website/scripts/utils/serde.js
Normal file
@@ -0,0 +1,225 @@
|
||||
const localhost = window.location.hostname === "localhost";
|
||||
console.log({ localhost });
|
||||
|
||||
export const serdeString = {
|
||||
/**
|
||||
* @param {string} v
|
||||
*/
|
||||
serialize(v) {
|
||||
return v;
|
||||
},
|
||||
/**
|
||||
* @param {string} v
|
||||
*/
|
||||
deserialize(v) {
|
||||
return v;
|
||||
},
|
||||
};
|
||||
|
||||
export const serdeMetrics = {
|
||||
/**
|
||||
* @param {Metric[]} v
|
||||
*/
|
||||
serialize(v) {
|
||||
return v.join(",");
|
||||
},
|
||||
/**
|
||||
* @param {string} v
|
||||
*/
|
||||
deserialize(v) {
|
||||
return /** @type {Metric[]} */ (v.split(","));
|
||||
},
|
||||
};
|
||||
|
||||
export const serdeNumber = {
|
||||
/**
|
||||
* @param {number} v
|
||||
*/
|
||||
serialize(v) {
|
||||
return String(v);
|
||||
},
|
||||
/**
|
||||
* @param {string} v
|
||||
*/
|
||||
deserialize(v) {
|
||||
return Number(v);
|
||||
},
|
||||
};
|
||||
|
||||
export const serdeOptNumber = {
|
||||
/**
|
||||
* @param {number | null} v
|
||||
*/
|
||||
serialize(v) {
|
||||
return v !== null ? String(v) : "";
|
||||
},
|
||||
/**
|
||||
* @param {string} v
|
||||
*/
|
||||
deserialize(v) {
|
||||
return v ? Number(v) : null;
|
||||
},
|
||||
};
|
||||
|
||||
export const serdeDate = {
|
||||
/**
|
||||
* @param {Date} date
|
||||
*/
|
||||
serialize(date) {
|
||||
return date.toString();
|
||||
},
|
||||
/**
|
||||
* @param {string} v
|
||||
*/
|
||||
deserialize(v) {
|
||||
return new Date(v);
|
||||
},
|
||||
};
|
||||
|
||||
export const serdeOptDate = {
|
||||
/**
|
||||
* @param {Date | null} date
|
||||
*/
|
||||
serialize(date) {
|
||||
return date !== null ? date.toString() : "";
|
||||
},
|
||||
/**
|
||||
* @param {string} v
|
||||
*/
|
||||
deserialize(v) {
|
||||
return new Date(v);
|
||||
},
|
||||
};
|
||||
|
||||
export const serdeBool = {
|
||||
/**
|
||||
* @param {boolean} v
|
||||
*/
|
||||
serialize(v) {
|
||||
return String(v);
|
||||
},
|
||||
/**
|
||||
* @param {string} v
|
||||
*/
|
||||
deserialize(v) {
|
||||
if (v === "true" || v === "1") {
|
||||
return true;
|
||||
} else if (v === "false" || v === "0") {
|
||||
return false;
|
||||
} else {
|
||||
throw "deser bool err";
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const serdeChartableIndex = {
|
||||
/**
|
||||
* @param {IndexName} v
|
||||
* @returns {ChartableIndexName | null}
|
||||
*/
|
||||
serialize(v) {
|
||||
switch (v) {
|
||||
case "dateindex":
|
||||
return "date";
|
||||
case "decadeindex":
|
||||
return "decade";
|
||||
case "height":
|
||||
return "timestamp";
|
||||
case "monthindex":
|
||||
return "month";
|
||||
case "quarterindex":
|
||||
return "quarter";
|
||||
case "semesterindex":
|
||||
return "semester";
|
||||
case "weekindex":
|
||||
return "week";
|
||||
case "yearindex":
|
||||
return "year";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @param {ChartableIndexName} v
|
||||
* @returns {ChartableIndex}
|
||||
*/
|
||||
deserialize(v) {
|
||||
switch (v) {
|
||||
case "timestamp":
|
||||
return "height";
|
||||
case "date":
|
||||
return "dateindex";
|
||||
case "week":
|
||||
return "weekindex";
|
||||
case "month":
|
||||
return "monthindex";
|
||||
case "quarter":
|
||||
return "quarterindex";
|
||||
case "semester":
|
||||
return "semesterindex";
|
||||
case "year":
|
||||
return "yearindex";
|
||||
case "decade":
|
||||
return "decadeindex";
|
||||
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
|
||||
*/
|
||||
51
website/scripts/utils/storage.js
Normal file
51
website/scripts/utils/storage.js
Normal file
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* @param {string} key
|
||||
*/
|
||||
export function readStoredNumber(key) {
|
||||
const saved = readStored(key);
|
||||
if (saved) {
|
||||
return Number(saved);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
*/
|
||||
export function readStoredBool(key) {
|
||||
const saved = readStored(key);
|
||||
if (saved) {
|
||||
return saved === "true" || saved === "1";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
*/
|
||||
export function readStored(key) {
|
||||
try {
|
||||
return localStorage.getItem(key);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
* @param {string | boolean | null | undefined} value
|
||||
*/
|
||||
export function writeToStorage(key, value) {
|
||||
try {
|
||||
value !== undefined && value !== null
|
||||
? localStorage.setItem(key, String(value))
|
||||
: localStorage.removeItem(key);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
*/
|
||||
export function removeStored(key) {
|
||||
writeToStorage(key, undefined);
|
||||
}
|
||||
38
website/scripts/utils/timing.js
Normal file
38
website/scripts/utils/timing.js
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* @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);
|
||||
}
|
||||
};
|
||||
}
|
||||
63
website/scripts/utils/units.js
Normal file
63
website/scripts/utils/units.js
Normal file
@@ -0,0 +1,63 @@
|
||||
/** Unit definitions for chart series */
|
||||
|
||||
/**
|
||||
* Unit enum with id (for serialization) and name (for display)
|
||||
*/
|
||||
export const Unit = /** @type {const} */ ({
|
||||
// Value units
|
||||
sats: { id: "sats", name: "Satoshis" },
|
||||
btc: { id: "btc", name: "Bitcoin" },
|
||||
usd: { id: "usd", name: "US Dollars" },
|
||||
|
||||
// Ratios & percentages
|
||||
percentage: { id: "percentage", name: "Percentage" },
|
||||
ratio: { id: "ratio", name: "Ratio" },
|
||||
index: { id: "index", name: "Index" },
|
||||
sd: { id: "sd", name: "Std Dev" },
|
||||
|
||||
// Relative percentages
|
||||
pctSupply: { id: "pct-supply", name: "% of Supply" },
|
||||
pctOwn: { id: "pct-own", name: "% of Own Supply" },
|
||||
pctMcap: { id: "pct-mcap", name: "% of Market Cap" },
|
||||
pctRcap: { id: "pct-rcap", name: "% of Realized Cap" },
|
||||
pctOwnMcap: { id: "pct-own-mcap", name: "% of Own Market Cap" },
|
||||
pctOwnPnl: { id: "pct-own-pnl", name: "% of Own P&L" },
|
||||
|
||||
// Time
|
||||
days: { id: "days", name: "Days" },
|
||||
years: { id: "years", name: "Years" },
|
||||
secs: { id: "secs", name: "Seconds" },
|
||||
|
||||
// Counts
|
||||
count: { id: "count", name: "Count" },
|
||||
blocks: { id: "blocks", name: "Blocks" },
|
||||
|
||||
// Size
|
||||
bytes: { id: "bytes", name: "Bytes" },
|
||||
vb: { id: "vb", name: "Virtual Bytes" },
|
||||
wu: { id: "wu", name: "Weight Units" },
|
||||
|
||||
// Mining
|
||||
hashRate: { id: "hashrate", name: "Hash Rate" },
|
||||
difficulty: { id: "difficulty", name: "Difficulty" },
|
||||
epoch: { id: "epoch", name: "Epoch" },
|
||||
|
||||
// Fees
|
||||
feeRate: { id: "feerate", name: "Sats/vByte" },
|
||||
|
||||
// Rates
|
||||
perSec: { id: "per-sec", name: "Per Second" },
|
||||
|
||||
// Cointime
|
||||
coinblocks: { id: "coinblocks", name: "Coinblocks" },
|
||||
coindays: { id: "coindays", name: "Coindays" },
|
||||
|
||||
// Hash price/value
|
||||
usdPerThsPerDay: { id: "usd-ths-day", name: "USD/TH/s/Day" },
|
||||
usdPerPhsPerDay: { id: "usd-phs-day", name: "USD/PH/s/Day" },
|
||||
satsPerThsPerDay: { id: "sats-ths-day", name: "Sats/TH/s/Day" },
|
||||
satsPerPhsPerDay: { id: "sats-phs-day", name: "Sats/PH/s/Day" },
|
||||
});
|
||||
|
||||
/** @typedef {keyof typeof Unit} UnitKey */
|
||||
/** @typedef {typeof Unit[UnitKey]} UnitObject */
|
||||
107
website/scripts/utils/url.js
Normal file
107
website/scripts/utils/url.js
Normal file
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* @param {string | string[]} [pathname]
|
||||
*/
|
||||
function processPathname(pathname) {
|
||||
pathname ||= window.location.pathname;
|
||||
return Array.isArray(pathname) ? pathname.join("/") : pathname;
|
||||
}
|
||||
|
||||
const chartParamsWhitelist = ["from", "to"];
|
||||
|
||||
/**
|
||||
* @param {string | string[]} pathname
|
||||
*/
|
||||
export function 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]
|
||||
*/
|
||||
export function 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
|
||||
*/
|
||||
export function resetParams(option) {
|
||||
const urlParams = new URLSearchParams();
|
||||
if (option.kind === "chart") {
|
||||
[...new URLSearchParams(window.location.search).entries()]
|
||||
.filter(([key, _]) => chartParamsWhitelist.includes(key))
|
||||
.forEach(([key, value]) => {
|
||||
urlParams.set(key, value);
|
||||
});
|
||||
}
|
||||
replaceHistory({ urlParams, pathname: option.path.join("/") });
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
* @param {string | boolean | null | undefined} value
|
||||
*/
|
||||
export function writeParam(key, value) {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
|
||||
if (value !== null && value !== undefined) {
|
||||
urlParams.set(key, String(value));
|
||||
} else {
|
||||
urlParams.delete(key);
|
||||
}
|
||||
|
||||
replaceHistory({ urlParams });
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
*/
|
||||
export function removeParam(key) {
|
||||
writeParam(key, undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
*/
|
||||
export function readBoolParam(key) {
|
||||
const param = readParam(key);
|
||||
if (param) {
|
||||
return param === "true" || param === "1";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
*/
|
||||
export function readNumberParam(key) {
|
||||
const param = readParam(key);
|
||||
if (param) {
|
||||
return Number(param);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} key
|
||||
* @returns {string | null}
|
||||
*/
|
||||
export function readParam(key) {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
return params.get(key);
|
||||
}
|
||||
114
website/scripts/utils/ws.js
Normal file
114
website/scripts/utils/ws.js
Normal file
@@ -0,0 +1,114 @@
|
||||
import signals from "../signals.js";
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {(callback: (value: T) => void) => WebSocket} creator
|
||||
*/
|
||||
function createWebsocket(creator) {
|
||||
let ws = /** @type {WebSocket | null} */ (null);
|
||||
|
||||
const live = signals.createSignal(false);
|
||||
const latest = signals.createSignal(/** @type {T | null} */ (null));
|
||||
|
||||
function reinitWebSocket() {
|
||||
if (!ws || ws.readyState === ws.CLOSED) {
|
||||
console.log("ws: reinit");
|
||||
resource.open();
|
||||
}
|
||||
}
|
||||
|
||||
function reinitWebSocketIfDocumentNotHidden() {
|
||||
!window.document.hidden && reinitWebSocket();
|
||||
}
|
||||
|
||||
const resource = {
|
||||
live,
|
||||
latest,
|
||||
open() {
|
||||
ws = creator((value) => latest.set(() => value));
|
||||
|
||||
ws.addEventListener("open", () => {
|
||||
console.log("ws: open");
|
||||
live.set(true);
|
||||
});
|
||||
|
||||
ws.addEventListener("close", () => {
|
||||
console.log("ws: close");
|
||||
live.set(false);
|
||||
});
|
||||
|
||||
window.document.addEventListener(
|
||||
"visibilitychange",
|
||||
reinitWebSocketIfDocumentNotHidden,
|
||||
);
|
||||
|
||||
window.document.addEventListener("online", reinitWebSocket);
|
||||
},
|
||||
close() {
|
||||
ws?.close();
|
||||
window.document.removeEventListener(
|
||||
"visibilitychange",
|
||||
reinitWebSocketIfDocumentNotHidden,
|
||||
);
|
||||
window.document.removeEventListener("online", reinitWebSocket);
|
||||
live.set(false);
|
||||
ws = null;
|
||||
},
|
||||
};
|
||||
|
||||
return resource;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {(candle: CandlestickData) => void} callback
|
||||
*/
|
||||
function krakenCandleWebSocketCreator(callback) {
|
||||
const ws = new WebSocket("wss://ws.kraken.com/v2");
|
||||
|
||||
ws.addEventListener("open", () => {
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
method: "subscribe",
|
||||
params: {
|
||||
channel: "ohlc",
|
||||
symbol: ["BTC/USD"],
|
||||
interval: 1440,
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
ws.addEventListener("message", (message) => {
|
||||
const result = JSON.parse(message.data);
|
||||
|
||||
if (result.channel !== "ohlc") return;
|
||||
|
||||
const { interval_begin, open, high, low, close } = result.data.at(-1);
|
||||
|
||||
/** @type {CandlestickData} */
|
||||
const candle = {
|
||||
// index: -1,
|
||||
time: /** @type {Time} */ (new Date(interval_begin).valueOf() / 1000),
|
||||
open: Number(open),
|
||||
high: Number(high),
|
||||
low: Number(low),
|
||||
close: Number(close),
|
||||
};
|
||||
|
||||
candle && callback({ ...candle });
|
||||
});
|
||||
|
||||
return ws;
|
||||
}
|
||||
|
||||
/** @type {ReturnType<typeof createWebsocket<CandlestickData>>} */
|
||||
const kraken1dCandle = createWebsocket((callback) =>
|
||||
krakenCandleWebSocketCreator(callback),
|
||||
);
|
||||
|
||||
kraken1dCandle.open();
|
||||
|
||||
export const webSockets = {
|
||||
kraken1dCandle,
|
||||
};
|
||||
/** @typedef {typeof webSockets} WebSockets */
|
||||
Reference in New Issue
Block a user