global: snapshot

This commit is contained in:
nym21
2026-03-18 21:04:08 +01:00
parent 24f344c0b1
commit 92e1a0ccaf
39 changed files with 819 additions and 1912 deletions

View File

@@ -1,7 +1,14 @@
import { colors } from "../utils/colors.js";
import { brk } from "../client.js";
import { Unit } from "../utils/units.js";
import { dots, line, baseline, price, rollingWindowsTree, percentRatioDots } from "./series.js";
import {
dots,
line,
price,
sumsTree,
multiSeriesTree,
percentRatioDots,
} from "./series.js";
import { satsBtcUsd, priceRatioPercentilesTree } from "./shared.js";
/**
@@ -36,50 +43,59 @@ export function createCointimeSection() {
pattern: cointimePrices.trueMarketMean,
name: "True Market Mean",
color: colors.trueMarketMean,
defaultActive: true,
},
{
pattern: cointimePrices.vaulted,
name: "Vaulted",
color: colors.vaulted,
defaultActive: true,
},
{
pattern: cointimePrices.active,
name: "Active",
color: colors.active,
defaultActive: true,
},
{
pattern: cointimePrices.cointime,
name: "Cointime",
color: colors.cointime,
},
{
pattern: cointimePrices.transfer,
name: "Transfer",
color: colors.transfer,
},
{
pattern: cointimePrices.balanced,
name: "Balanced",
color: colors.balanced,
},
{
pattern: cointimePrices.terminal,
name: "Terminal",
color: colors.terminal,
},
{
pattern: cointimePrices.delta,
name: "Delta",
color: colors.delta,
defaultActive: true,
},
]);
const caps = /** @type {const} */ ([
{ series: cap.vaulted.usd, name: "Vaulted", color: colors.vaulted },
{ series: cap.active.usd, name: "Active", color: colors.active },
{ series: cap.cointime.usd, name: "Cointime", color: colors.cointime },
{ series: cap.investor.usd, name: "Investor", color: colors.investor },
{ series: cap.thermo.usd, name: "Thermo", color: colors.thermo },
{
series: cap.vaulted.usd,
name: "Vaulted",
color: colors.vaulted,
defaultActive: true,
},
{
series: cap.active.usd,
name: "Active",
color: colors.active,
defaultActive: true,
},
{
series: cap.cointime.usd,
name: "Cointime",
color: colors.cointime,
defaultActive: true,
},
{
series: cap.investor.usd,
name: "Investor",
color: colors.investor,
defaultActive: false,
},
{
series: cap.thermo.usd,
name: "Thermo",
color: colors.thermo,
defaultActive: false,
},
]);
const supplyBreakdown = /** @type {const} */ ([
@@ -167,8 +183,8 @@ export function createCointimeSection() {
name: "Investor",
color: colors.investor,
}),
...prices.map(({ pattern, name, color }) =>
price({ series: pattern, name, color }),
...prices.map(({ pattern, name, color, defaultActive }) =>
price({ series: pattern, name, color, defaultActive }),
),
],
},
@@ -180,7 +196,12 @@ export function createCointimeSection() {
legend: name,
color,
priceReferences: [
price({ series: all.realized.price, name: "Realized", color: colors.realized, defaultActive: false }),
price({
series: all.realized.price,
name: "Realized",
color: colors.realized,
defaultActive: false,
}),
],
}),
})),
@@ -198,8 +219,8 @@ export function createCointimeSection() {
...capReferenceLines.map(({ series, name, color }) =>
line({ series, name, color, unit: Unit.usd }),
),
...caps.map(({ series, name, color }) =>
line({ series, name, color, unit: Unit.usd }),
...caps.map(({ series, name, color, defaultActive }) =>
line({ series, name, color, defaultActive, unit: Unit.usd }),
),
],
},
@@ -263,32 +284,17 @@ export function createCointimeSection() {
tree: [
{
name: "Compare",
tree: [
{
name: "Base",
title: "Coinblocks",
bottom: coinblocks.map(({ pattern, name, color }) =>
line({
series: pattern.base,
name,
color,
unit: Unit.coinblocks,
}),
),
},
{
name: "Cumulative",
title: "Coinblocks (Total)",
bottom: coinblocks.map(({ pattern, name, color }) =>
line({
series: pattern.cumulative,
name,
color,
unit: Unit.coinblocks,
}),
),
},
],
tree: multiSeriesTree({
entries: coinblocks.map(({ pattern, name, color }) => ({
name,
color,
base: pattern.base,
rolling: pattern.sum,
cumulative: pattern.cumulative,
})),
title: "Coinblocks",
unit: Unit.coinblocks,
}),
},
...coinblocks.map(({ pattern, name, title, color }) => ({
name,
@@ -305,7 +311,7 @@ export function createCointimeSection() {
}),
],
},
rollingWindowsTree({ windows: pattern.sum, title, unit: Unit.coinblocks }),
sumsTree({ windows: pattern.sum, title, unit: Unit.coinblocks }),
{
name: "Cumulative",
title: `${title} (Total)`,
@@ -329,43 +335,26 @@ export function createCointimeSection() {
tree: [
{
name: "Compare",
tree: [
{
name: "Base",
title: "Cointime Value",
bottom: [
...cointimeValues.map(({ pattern, name, color }) =>
line({ series: pattern.base, name, color, unit: Unit.usd }),
),
line({
series: vocdd.pattern.base,
name: vocdd.name,
color: vocdd.color,
unit: Unit.usd,
}),
],
},
{
name: "Cumulative",
title: "Cointime Value (Total)",
bottom: [
...cointimeValues.map(({ pattern, name, color }) =>
line({
series: pattern.cumulative,
name,
color,
unit: Unit.usd,
}),
),
line({
series: vocdd.pattern.cumulative,
name: vocdd.name,
color: vocdd.color,
unit: Unit.usd,
}),
],
},
],
tree: multiSeriesTree({
entries: [
...cointimeValues.map(({ pattern, name, color }) => ({
name,
color,
base: pattern.base,
rolling: pattern.sum,
cumulative: pattern.cumulative,
})),
{
name: vocdd.name,
color: vocdd.color,
base: vocdd.pattern.base,
rolling: vocdd.pattern.sum,
cumulative: vocdd.pattern.cumulative,
},
],
title: "Cointime Value",
unit: Unit.usd,
}),
},
...cointimeValues.map(({ pattern, name, title, color }) => ({
name,
@@ -377,7 +366,7 @@ export function createCointimeSection() {
line({ series: pattern.base, name, color, unit: Unit.usd }),
],
},
rollingWindowsTree({ windows: pattern.sum, title, unit: Unit.usd }),
sumsTree({ windows: pattern.sum, title, unit: Unit.usd }),
{
name: "Cumulative",
title: `${title} (Total)`,
@@ -413,7 +402,11 @@ export function createCointimeSection() {
}),
],
},
rollingWindowsTree({ windows: vocdd.pattern.sum, title: vocdd.title, unit: Unit.usd }),
sumsTree({
windows: vocdd.pattern.sum,
title: vocdd.title,
unit: Unit.usd,
}),
{
name: "Cumulative",
title: `${vocdd.title} (Total)`,
@@ -450,25 +443,11 @@ export function createCointimeSection() {
{
name: "AVIV",
title: "AVIV Ratio",
bottom: [
baseline({
series: cap.aviv.ratio,
name: "Ratio",
color: colors.reserveRisk,
unit: Unit.ratio,
base: 1,
}),
],
},
{
name: "HODL Bank",
title: "HODL Bank",
bottom: [
line({
series: reserveRisk.hodlBank,
name: "Value",
color: colors.hodlBank,
unit: Unit.usd,
series: cap.aviv.ratio,
name: "aviv",
unit: Unit.ratio,
}),
],
},

View File

@@ -10,7 +10,7 @@
*/
import { Unit } from "../../utils/units.js";
import { ROLLING_WINDOWS, line, baseline, rollingWindowsTree, rollingPercentRatioTree, percentRatio } from "../series.js";
import { ROLLING_WINDOWS, line, baseline, sumsTree, rollingPercentRatioTree, percentRatio } from "../series.js";
import {
satsBtcUsd,
mapCohorts,
@@ -174,7 +174,7 @@ function singleDeltaTree(delta, unit, title, name) {
return {
name,
tree: [
{ ...rollingWindowsTree({ windows: delta.absolute, title: title(`${name} Change`), unit, series: baseline }), name: "Absolute" },
{ ...sumsTree({ windows: delta.absolute, title: title(`${name} Change`), unit, series: baseline }), name: "Absolute" },
{ ...rollingPercentRatioTree({ windows: delta.rate, title: title(`${name} Rate`) }), name: "Rate" },
],
};

View File

@@ -11,7 +11,7 @@
*/
import { formatCohortTitle, satsBtcUsd, satsBtcUsdFullTree } from "../shared.js";
import { ROLLING_WINDOWS, line, baseline, percentRatio, rollingWindowsTree, rollingPercentRatioTree } from "../series.js";
import { ROLLING_WINDOWS, line, baseline, percentRatio, sumsTree, rollingPercentRatioTree } from "../series.js";
import { Unit } from "../../utils/units.js";
import { colors } from "../../utils/colors.js";
@@ -613,7 +613,7 @@ function singleBucketFolder({ name, color, pattern }) {
{
name: "Change",
tree: [
{ ...rollingWindowsTree({ windows: pattern.supply.all.delta.absolute, title: `${name}: Supply Change`, unit: Unit.sats, series: baseline }), name: "Absolute" },
{ ...sumsTree({ windows: pattern.supply.all.delta.absolute, title: `${name}: Supply Change`, unit: Unit.sats, series: baseline }), name: "Absolute" },
{ ...rollingPercentRatioTree({ windows: pattern.supply.all.delta.rate, title: `${name}: Supply Rate` }), name: "Rate" },
],
},

View File

@@ -35,8 +35,6 @@ export function createPricesSectionFull({ cohort, title }) {
top: [
price({ series: tree.realized.price, name: "Realized", color: colors.realized }),
price({ series: tree.realized.investor.price, name: "Investor", color: colors.investor }),
price({ series: tree.realized.investor.investorUpperBand, name: "I²/R", color: colors.stat.max, style: 2, defaultActive: false }),
price({ series: tree.realized.investor.investorLowerBand, name: "R²/I", color: colors.stat.min, style: 2, defaultActive: false }),
],
},
{

View File

@@ -11,7 +11,7 @@
*/
import { Unit } from "../../utils/units.js";
import { ROLLING_WINDOWS, line, baseline, mapWindows, rollingWindowsTree, rollingPercentRatioTree, percentRatio, percentRatioBaseline } from "../series.js";
import { ROLLING_WINDOWS, line, baseline, mapWindows, sumsTree, rollingPercentRatioTree, percentRatio, percentRatioBaseline } from "../series.js";
import { createRatioChart, mapCohortsWithAll, flatMapCohortsWithAll } from "../shared.js";
/**
@@ -59,7 +59,7 @@ export function createValuationSectionFull({ cohort, title }) {
{
name: "Change",
tree: [
{ ...rollingWindowsTree({ windows: mapWindows(tree.realized.cap.delta.absolute, (c) => c.usd), title: title("Realized Cap Change"), unit: Unit.usd, series: baseline }), name: "Absolute" },
{ ...sumsTree({ windows: mapWindows(tree.realized.cap.delta.absolute, (c) => c.usd), title: title("Realized Cap Change"), unit: Unit.usd, series: baseline }), name: "Absolute" },
{ ...rollingPercentRatioTree({ windows: tree.realized.cap.delta.rate, title: title("Realized Cap Rate") }), name: "Rate" },
],
},
@@ -93,7 +93,7 @@ export function createValuationSection({ cohort, title }) {
{
name: "Change",
tree: [
{ ...rollingWindowsTree({ windows: mapWindows(tree.realized.cap.delta.absolute, (c) => c.usd), title: title("Realized Cap Change"), unit: Unit.usd, series: baseline }), name: "Absolute" },
{ ...sumsTree({ windows: mapWindows(tree.realized.cap.delta.absolute, (c) => c.usd), title: title("Realized Cap Change"), unit: Unit.usd, series: baseline }), name: "Absolute" },
{ ...rollingPercentRatioTree({ windows: tree.realized.cap.delta.rate, title: title("Realized Cap Rate") }), name: "Rate" },
],
},

View File

@@ -145,6 +145,59 @@ function createCompareFolder(context, items) {
};
}
/**
* Create compare folder from long items (includes CAGR chart)
* @param {string} context
* @param {LongEntryItem[]} items
*/
function createLongCompareFolder(context, items) {
const topPane = items.map(({ name, color, costBasis }) =>
price({ series: costBasis, name, color }),
);
return {
name: "Compare",
tree: [
{
name: "Cost Basis",
title: `Cost Basis: ${context}`,
top: topPane,
},
{
name: "Returns",
title: `Returns: ${context}`,
top: topPane,
bottom: items.flatMap(({ name, color, returns }) =>
percentRatioBaseline({
pattern: returns,
name,
color: [color, color],
}),
),
},
{
name: "CAGR",
title: `CAGR: ${context}`,
top: topPane,
bottom: items.flatMap(({ name, color, cagr }) =>
percentRatioBaseline({
pattern: cagr,
name,
color: [color, color],
}),
),
},
{
name: "Accumulated",
title: `Accumulated Value: ${context}`,
top: topPane,
bottom: items.flatMap(({ name, color, stack }) =>
satsBtcUsd({ pattern: stack, name, color }),
),
},
],
};
}
/**
* Create single entry tree structure
* @param {BaseEntryItem & { titlePrefix?: string }} item
@@ -182,14 +235,36 @@ function createShortSingleEntry(item) {
}
/**
* Create a single entry from a long item (with CAGR)
* Create a single entry from a long item (with CAGR as its own chart)
* @param {LongEntryItem & { titlePrefix?: string }} item
*/
function createLongSingleEntry(item) {
return createSingleEntryTree(item, [
...percentRatioBaseline({ pattern: item.returns, name: "Current" }),
...percentRatioBaseline({ pattern: item.cagr, name: "CAGR" }),
]);
const { name, titlePrefix = name, color, costBasis, returns, cagr, stack } = item;
const top = [price({ series: costBasis, name: "Cost Basis", color })];
return {
name,
tree: [
{ name: "Cost Basis", title: `Cost Basis: ${titlePrefix}`, top },
{
name: "Returns",
title: `Returns: ${titlePrefix}`,
top,
bottom: percentRatioBaseline({ pattern: returns, name: "Current" }),
},
{
name: "CAGR",
title: `CAGR: ${titlePrefix}`,
top,
bottom: percentRatioBaseline({ pattern: cagr, name: "CAGR" }),
},
{
name: "Accumulated",
title: `Accumulated Value: ${titlePrefix}`,
top,
bottom: satsBtcUsd({ pattern: stack, name: "Value" }),
},
],
};
}
/**
@@ -250,13 +325,22 @@ export function createDcaVsLumpSumSection({ dca, lookback, returns }) {
name: "Lump Sum",
color: colors.bi.p2,
}),
],
});
/** @param {string} name @param {LongPeriodKey} key */
const longCagrChart = (name, key) => ({
name: "CAGR",
title: `CAGR: ${name} DCA vs Lump Sum`,
top: topPane(key),
bottom: [
...percentRatioBaseline({
pattern: dca.period.cagr[key],
name: "DCA CAGR",
name: "DCA",
}),
...percentRatioBaseline({
pattern: returns.cagr[key],
name: "Lump Sum CAGR",
name: "Lump Sum",
color: colors.bi.p2,
}),
],
@@ -302,6 +386,7 @@ export function createDcaVsLumpSumSection({ dca, lookback, returns }) {
tree: [
costBasisChart(name, key),
longReturnsChart(name, key),
longCagrChart(name, key),
stackChart(name, key),
],
};
@@ -381,10 +466,6 @@ function createPeriodSection({ dca, lookback, returns }) {
name: `${suffix} by Period`,
title: `${suffix} Performance by Investment Period`,
tree: [
createCompareFolder(`All Periods ${suffix}`, [
...shortEntries,
...longEntries,
]),
{
name: "Short Term",
title: "Up to 1 Year",
@@ -397,7 +478,7 @@ function createPeriodSection({ dca, lookback, returns }) {
name: "Long Term",
title: "2+ Years",
tree: [
createCompareFolder(`Long Term ${suffix}`, longEntries),
createLongCompareFolder(`Long Term ${suffix}`, longEntries),
...longEntries.map(createLongEntry),
],
},

View File

@@ -86,7 +86,12 @@ function createMaSubSection(label, averages) {
name: "Compare",
title: `Price ${label}s`,
top: averages.map((a) =>
price({ series: a.ratio, name: a.id, color: a.color }),
price({
series: a.ratio,
name: a.id,
color: a.color,
defaultActive: includes(commonMaIds, a.id),
}),
),
},
...common.map(toFolder),

View File

@@ -10,46 +10,44 @@ import {
dotted,
distributionBtcSatsUsd,
statsAtWindow,
rollingWindowsTree,
ROLLING_WINDOWS,
percentRatio,
percentRatioDots,
chartsFromCount,
} from "./series.js";
import {
satsBtcUsd,
satsBtcUsdFrom,
satsBtcUsdFullTree,
revenueBtcSatsUsd,
} from "./shared.js";
import { brk } from "../client.js";
/** Major pools to show in Compare section (by current hashrate dominance) */
const MAJOR_POOL_IDS = /** @type {const} */ ([
"foundryusa", // ~32% - largest pool
"antpool", // ~18% - Bitmain-owned
"viabtc", // ~14% - independent
"f2pool", // ~10% - one of the oldest pools
"marapool", // MARA Holdings
"braiinspool", // formerly Slush Pool
"spiderpool", // growing Asian pool
"ocean", // decentralization-focused
"foundryusa",
"antpool",
"viabtc",
"f2pool",
"marapool",
"braiinspool",
"spiderpool",
"ocean",
]);
/**
* AntPool & friends - pools sharing AntPool's block templates
* Based on b10c's research: https://b10c.me/blog/015-bitcoin-mining-centralization/
* Collectively ~35-40% of network hashrate
*/
const ANTPOOL_AND_FRIENDS_IDS = /** @type {const} */ ([
"antpool", // Bitmain-owned, template source
"poolin", // shares AntPool templates
"btccom", // CloverPool (formerly BTC.com)
"braiinspool", // shares AntPool templates
"ultimuspool", // shares AntPool templates
"binancepool", // shares AntPool templates
"secpool", // shares AntPool templates
"sigmapoolcom", // SigmaPool
"rawpool", // shares AntPool templates
"luxor", // shares AntPool templates
"antpool",
"poolin",
"btccom",
"braiinspool",
"ultimuspool",
"binancepool",
"secpool",
"sigmapoolcom",
"rawpool",
"luxor",
]);
/**
@@ -59,7 +57,6 @@ const ANTPOOL_AND_FRIENDS_IDS = /** @type {const} */ ([
export function createMiningSection() {
const { blocks, pools, mining } = brk.series;
// Pre-compute pool entries with resolved names
const majorPoolData = entries(pools.major).map(([id, pool]) => ({
id,
name: brk.POOL_ID_TO_POOL_NAME[id],
@@ -71,7 +68,6 @@ export function createMiningSection() {
pool,
}));
// Filtered pool groups for comparisons (major pools only have windowed dominance)
const featuredPools = majorPoolData.filter((p) =>
includes(MAJOR_POOL_IDS, p.id),
);
@@ -79,135 +75,126 @@ export function createMiningSection() {
includes(ANTPOOL_AND_FRIENDS_IDS, p.id),
);
// Build individual pool trees
const majorPoolsTree = majorPoolData.map(({ name, pool }) => ({
name,
/** @param {string} title @param {{ _24h: any, _1w: any, _1m: any, _1y: any, percent: any, ratio: any }} dominance */
const dominanceTree = (title, dominance) => ({
name: "Dominance",
tree: [
{
name: "Dominance",
title: `Dominance: ${name}`,
name: "Compare",
title,
bottom: [
...percentRatioDots({ pattern: pool.dominance._24h, name: "24h", color: colors.time._24h, defaultActive: false }),
...percentRatio({ pattern: pool.dominance._1w, name: "1w", color: colors.time._1w, defaultActive: false }),
...percentRatio({ pattern: pool.dominance._1m, name: "1m", color: colors.time._1m }),
...percentRatio({ pattern: pool.dominance._1y, name: "1y", color: colors.time._1y, defaultActive: false }),
...percentRatio({ pattern: pool.dominance, name: "All Time", color: colors.time.all, defaultActive: false }),
...ROLLING_WINDOWS.flatMap((w) =>
percentRatio({ pattern: dominance[w.key], name: w.name, color: w.color, defaultActive: w.key !== "_24h" }),
),
...percentRatio({ pattern: dominance, name: "All Time", color: colors.time.all }),
],
},
...ROLLING_WINDOWS.map((w) => ({
name: w.name,
title: `${title} ${w.title}`,
bottom: percentRatio({ pattern: dominance[w.key], name: w.name, color: w.color }),
})),
{
name: "Blocks Mined",
tree: [
{
name: "Base",
title: `Blocks Mined: ${name}`,
bottom: [
line({
series: pool.blocksMined.base,
name: "base",
unit: Unit.count,
}),
],
},
rollingWindowsTree({ windows: pool.blocksMined.sum, title: `Blocks Mined: ${name}`, unit: Unit.count }),
{
name: "Cumulative",
title: `Blocks Mined: ${name} (Total)`,
bottom: [
line({
series: pool.blocksMined.cumulative,
name: "all-time",
unit: Unit.count,
}),
],
},
],
},
{
name: "Rewards",
tree: [
{
name: "Sum",
title: `Rewards: ${name}`,
bottom: satsBtcUsdFrom({
source: pool.rewards,
key: "base",
name: "sum",
}),
},
{
name: "Rolling",
tree: [
{
name: "Compare",
title: `Rewards: ${name} Rolling`,
bottom: ROLLING_WINDOWS.flatMap((w) =>
satsBtcUsd({ pattern: pool.rewards.sum[w.key], name: w.name, color: w.color }),
),
},
...ROLLING_WINDOWS.map((w) => ({
name: w.name,
title: `Rewards: ${name} (${w.name})`,
bottom: satsBtcUsd({ pattern: pool.rewards.sum[w.key], name: w.name, color: w.color }),
})),
],
},
{
name: "Cumulative",
title: `Rewards: ${name} (Total)`,
bottom: satsBtcUsdFrom({
source: pool.rewards,
key: "cumulative",
name: "all-time",
}),
},
],
name: "All Time",
title: `${title} All Time`,
bottom: percentRatio({ pattern: dominance, name: "All Time", color: colors.time.all }),
},
],
}));
});
const minorPoolsTree = minorPoolData.map(({ name, pool }) => ({
name,
/**
* @param {typeof majorPoolData} poolList
*/
const createPoolTree = (poolList) =>
poolList.map(({ name, pool }) => ({
name,
tree: [
dominanceTree(`Dominance: ${name}`, pool.dominance),
{
name: "Blocks Mined",
tree: chartsFromCount({
pattern: pool.blocksMined,
title: `Blocks Mined: ${name}`,
unit: Unit.count,
}),
},
{
name: "Rewards",
tree: satsBtcUsdFullTree({
pattern: pool.rewards,
name: "Rewards",
title: `Rewards: ${name}`,
}),
},
],
}));
/**
* @param {typeof minorPoolData} poolList
*/
const createMinorPoolTree = (poolList) =>
poolList.map(({ name, pool }) => ({
name,
tree: [
{
name: "Dominance",
title: `Dominance: ${name}`,
bottom: percentRatio({ pattern: pool.dominance, name: "All Time", color: colors.time.all }),
},
{
name: "Blocks Mined",
tree: chartsFromCount({
pattern: pool.blocksMined,
title: `Blocks Mined: ${name}`,
unit: Unit.count,
}),
},
],
}));
/**
* @param {string} groupTitle
* @param {typeof majorPoolData} poolList
*/
const createPoolCompare = (groupTitle, poolList) => ({
name: "Compare",
tree: [
{
name: "Dominance",
title: `Dominance: ${name}`,
bottom: percentRatio({ pattern: pool.dominance, name: "All Time", color: colors.time.all }),
tree: ROLLING_WINDOWS.map((w) => ({
name: w.name,
title: `Dominance: ${groupTitle} ${w.title}`,
bottom: poolList.flatMap((p, i) =>
percentRatio({
pattern: p.pool.dominance[w.key],
name: p.name,
color: colors.at(i, poolList.length),
}),
),
})),
},
{
name: "Blocks Mined",
tree: [
{
name: "Base",
title: `Blocks Mined: ${name}`,
bottom: [
line({
series: pool.blocksMined.base,
name: "base",
unit: Unit.count,
}),
],
},
rollingWindowsTree({ windows: pool.blocksMined.sum, title: `Blocks Mined: ${name}`, unit: Unit.count }),
{
name: "Cumulative",
title: `Blocks Mined: ${name} (Total)`,
bottom: [
line({
series: pool.blocksMined.cumulative,
name: "all-time",
unit: Unit.count,
}),
],
},
],
tree: ROLLING_WINDOWS.map((w) => ({
name: w.name,
title: `Blocks Mined: ${groupTitle} ${w.title} Sum`,
bottom: poolList.map((p, i) =>
line({
series: p.pool.blocksMined.sum[w.key],
name: p.name,
color: colors.at(i, poolList.length),
unit: Unit.count,
}),
),
})),
},
],
}));
});
return {
name: "Mining",
tree: [
// Hashrate
{
name: "Hashrate",
tree: [
@@ -293,63 +280,6 @@ export function createMiningSection() {
],
},
// Difficulty
{
name: "Difficulty",
tree: [
{
name: "Current",
title: "Mining Difficulty",
bottom: [
line({
series: blocks.difficulty.value,
name: "Difficulty",
unit: Unit.difficulty,
}),
],
},
{
name: "Epoch",
title: "Difficulty Epoch",
bottom: [
line({
series: blocks.difficulty.epoch,
name: "Epoch",
unit: Unit.epoch,
}),
],
},
{
name: "Adjustment",
title: "Difficulty Adjustment",
bottom: [
baseline({
series: blocks.difficulty.adjustment.percent,
name: "Change",
unit: Unit.percentage,
}),
],
},
{
name: "Countdown",
title: "Next Difficulty Adjustment",
bottom: [
line({
series: blocks.difficulty.blocksToRetarget,
name: "Remaining",
unit: Unit.blocks,
}),
line({
series: blocks.difficulty.daysToRetarget,
name: "Remaining",
unit: Unit.days,
}),
],
},
],
},
// Revenue
{
name: "Revenue",
tree: [
@@ -357,7 +287,7 @@ export function createMiningSection() {
name: "Compare",
tree: [
{
name: "Sum",
name: "Per Block",
title: "Revenue Comparison",
bottom: revenueBtcSatsUsd({
coinbase: mining.rewards.coinbase,
@@ -380,105 +310,23 @@ export function createMiningSection() {
},
{
name: "Coinbase",
tree: [
{
name: "Sum",
title: "Coinbase Rewards",
bottom: satsBtcUsdFrom({
source: mining.rewards.coinbase,
key: "base",
name: "sum",
}),
},
{
name: "Rolling",
tree: [
{
name: "Compare",
title: "Coinbase Rolling Sum",
bottom: [
...satsBtcUsd({
pattern: mining.rewards.coinbase.sum._24h,
name: "24h",
color: colors.time._24h,
}),
...satsBtcUsd({
pattern: mining.rewards.coinbase.sum._1w,
name: "7d",
color: colors.time._1w,
}),
...satsBtcUsd({
pattern: mining.rewards.coinbase.sum._1m,
name: "30d",
color: colors.time._1m,
}),
...satsBtcUsd({
pattern: mining.rewards.coinbase.sum._1y,
name: "1y",
color: colors.time._1y,
}),
],
},
{
name: "24h",
title: "Coinbase 24h Rolling Sum",
bottom: satsBtcUsd({
pattern: mining.rewards.coinbase.sum._24h,
name: "24h",
color: colors.time._24h,
}),
},
{
name: "7d",
title: "Coinbase 7d Rolling Sum",
bottom: satsBtcUsd({
pattern: mining.rewards.coinbase.sum._1w,
name: "7d",
color: colors.time._1w,
}),
},
{
name: "30d",
title: "Coinbase 30d Rolling Sum",
bottom: satsBtcUsd({
pattern: mining.rewards.coinbase.sum._1m,
name: "30d",
color: colors.time._1m,
}),
},
{
name: "1y",
title: "Coinbase 1y Rolling Sum",
bottom: satsBtcUsd({
pattern: mining.rewards.coinbase.sum._1y,
name: "1y",
color: colors.time._1y,
}),
},
],
},
{
name: "Cumulative",
title: "Coinbase Rewards (Total)",
bottom: satsBtcUsdFrom({
source: mining.rewards.coinbase,
key: "cumulative",
name: "all-time",
}),
},
],
tree: satsBtcUsdFullTree({
pattern: mining.rewards.coinbase,
name: "Coinbase",
title: "Coinbase Rewards",
}),
},
{
name: "Subsidy",
tree: [
{
name: "Sum",
name: "Per Block",
title: "Block Subsidy",
bottom: [
...satsBtcUsdFrom({
source: mining.rewards.subsidy,
key: "base",
name: "sum",
name: "base",
}),
line({
series: mining.rewards.subsidy.sma1y.usd,
@@ -503,129 +351,32 @@ export function createMiningSection() {
{
name: "Fees",
tree: [
...satsBtcUsdFullTree({
pattern: mining.rewards.fees,
name: "Fees",
title: "Transaction Fee Revenue",
}),
{
name: "Sum",
title: "Transaction Fee Revenue per Block",
bottom: satsBtcUsdFrom({
source: mining.rewards.fees,
key: "base",
name: "sum",
}),
},
{
name: "Rolling",
tree: [
{
name: "Compare",
title: "Fee Rolling Sum",
bottom: [
...satsBtcUsd({
pattern: mining.rewards.fees.sum._24h,
name: "24h",
color: colors.time._24h,
}),
...satsBtcUsd({
pattern: mining.rewards.fees.sum._1w,
name: "7d",
color: colors.time._1w,
}),
...satsBtcUsd({
pattern: mining.rewards.fees.sum._1m,
name: "30d",
color: colors.time._1m,
}),
...satsBtcUsd({
pattern: mining.rewards.fees.sum._1y,
name: "1y",
color: colors.time._1y,
}),
],
},
{
name: "24h",
title: "Fee 24h Rolling Sum",
bottom: satsBtcUsd({
pattern: mining.rewards.fees.sum._24h,
name: "24h",
color: colors.time._24h,
}),
},
{
name: "7d",
title: "Fee 7d Rolling Sum",
bottom: satsBtcUsd({
pattern: mining.rewards.fees.sum._1w,
name: "7d",
color: colors.time._1w,
}),
},
{
name: "30d",
title: "Fee 30d Rolling Sum",
bottom: satsBtcUsd({
pattern: mining.rewards.fees.sum._1m,
name: "30d",
color: colors.time._1m,
}),
},
{
name: "1y",
title: "Fee 1y Rolling Sum",
bottom: satsBtcUsd({
pattern: mining.rewards.fees.sum._1y,
name: "1y",
color: colors.time._1y,
}),
},
],
},
{
name: "Distribution",
name: "Distributions",
tree: ROLLING_WINDOWS.map((w) => ({
name: w.name,
title: `Fee Revenue per Block Distribution (${w.name})`,
title: `Fee Revenue per Block ${w.title} Distribution`,
bottom: distributionBtcSatsUsd(statsAtWindow(mining.rewards.fees, w.key)),
})),
},
{
name: "Cumulative",
title: "Transaction Fee Revenue (Total)",
bottom: satsBtcUsdFrom({
source: mining.rewards.fees,
key: "cumulative",
name: "all-time",
}),
},
],
},
{
name: "Dominance",
tree: [
{
name: "Compare",
tree: [
{
name: "Subsidy",
title: "Subsidy Dominance",
bottom: [
...percentRatio({ pattern: mining.rewards.subsidy.dominance, name: "All-time", color: colors.time.all }),
...ROLLING_WINDOWS.flatMap((w) =>
percentRatio({ pattern: mining.rewards.subsidy.dominance[w.key], name: w.name, color: w.color }),
),
],
},
{
name: "Fees",
title: "Fee Dominance",
bottom: [
...percentRatio({ pattern: mining.rewards.fees.dominance, name: "All-time", color: colors.time.all }),
...ROLLING_WINDOWS.flatMap((w) =>
percentRatio({ pattern: mining.rewards.fees.dominance[w.key], name: w.name, color: w.color }),
),
],
},
...ROLLING_WINDOWS.map((w) => ({
name: w.name,
title: `Revenue Dominance ${w.title}`,
bottom: [
...percentRatio({ pattern: mining.rewards.subsidy.dominance[w.key], name: "Subsidy", color: colors.mining.subsidy }),
...percentRatio({ pattern: mining.rewards.fees.dominance[w.key], name: "Fees", color: colors.mining.fee }),
],
},
})),
{
name: "All-time",
title: "Revenue Dominance (All-time)",
@@ -634,77 +385,28 @@ export function createMiningSection() {
...percentRatio({ pattern: mining.rewards.fees.dominance, name: "Fees", color: colors.mining.fee }),
],
},
...ROLLING_WINDOWS.map((w) => ({
name: w.name,
title: `Revenue Dominance (${w.name})`,
bottom: [
...percentRatio({ pattern: mining.rewards.subsidy.dominance[w.key], name: "Subsidy", color: colors.mining.subsidy }),
...percentRatio({ pattern: mining.rewards.fees.dominance[w.key], name: "Fees", color: colors.mining.fee }),
],
})),
],
},
{
name: "Fee Multiple",
tree: [
{
name: "Compare",
title: "Fee-to-Subsidy Ratio",
bottom: ROLLING_WINDOWS.map((w) =>
line({ series: mining.rewards.fees.toSubsidyRatio[w.key].ratio, name: w.name, color: w.color, unit: Unit.ratio }),
),
},
...ROLLING_WINDOWS.map((w) => ({
name: w.name,
title: `Fee-to-Subsidy Ratio (${w.name})`,
bottom: [line({ series: mining.rewards.fees.toSubsidyRatio[w.key].ratio, name: w.name, color: w.color, unit: Unit.ratio })],
})),
],
tree: ROLLING_WINDOWS.map((w) => ({
name: w.name,
title: `Fee-to-Subsidy Ratio ${w.title}`,
bottom: [line({ series: mining.rewards.fees.toSubsidyRatio[w.key].ratio, name: "Ratio", color: colors.mining.fee, unit: Unit.ratio })],
})),
},
{
name: "Unclaimed",
tree: [
{
name: "Sum",
title: "Unclaimed Rewards",
bottom: satsBtcUsdFrom({
source: mining.rewards.unclaimed,
key: "base",
name: "sum",
}),
},
{
name: "Rolling",
tree: [
{
name: "Compare",
title: "Unclaimed Rewards Rolling",
bottom: ROLLING_WINDOWS.flatMap((w) =>
satsBtcUsd({ pattern: mining.rewards.unclaimed.sum[w.key], name: w.name, color: w.color }),
),
},
...ROLLING_WINDOWS.map((w) => ({
name: w.name,
title: `Unclaimed Rewards ${w.name}`,
bottom: satsBtcUsd({ pattern: mining.rewards.unclaimed.sum[w.key], name: w.name, color: w.color }),
})),
],
},
{
name: "Cumulative",
title: "Unclaimed Rewards (Total)",
bottom: satsBtcUsdFrom({
source: mining.rewards.unclaimed,
key: "cumulative",
name: "all-time",
}),
},
],
title: "Unclaimed Rewards (Total)",
bottom: satsBtcUsdFrom({
source: mining.rewards.unclaimed,
key: "cumulative",
name: "all-time",
}),
},
],
},
// Economics
{
name: "Economics",
tree: [
@@ -712,60 +414,20 @@ export function createMiningSection() {
name: "Hash Price",
title: "Hash Price",
bottom: [
line({
series: mining.hashrate.price.ths,
name: "TH/s",
color: colors.usd,
unit: Unit.usdPerThsPerDay,
}),
line({
series: mining.hashrate.price.phs,
name: "PH/s",
color: colors.usd,
unit: Unit.usdPerPhsPerDay,
}),
dotted({
series: mining.hashrate.price.thsMin,
name: "TH/s Min",
color: colors.stat.min,
unit: Unit.usdPerThsPerDay,
}),
dotted({
series: mining.hashrate.price.phsMin,
name: "PH/s Min",
color: colors.stat.min,
unit: Unit.usdPerPhsPerDay,
}),
line({ series: mining.hashrate.price.ths, name: "TH/s", color: colors.usd, unit: Unit.usdPerThsPerDay }),
line({ series: mining.hashrate.price.phs, name: "PH/s", color: colors.usd, unit: Unit.usdPerPhsPerDay }),
dotted({ series: mining.hashrate.price.thsMin, name: "TH/s Min", color: colors.stat.min, unit: Unit.usdPerThsPerDay }),
dotted({ series: mining.hashrate.price.phsMin, name: "PH/s Min", color: colors.stat.min, unit: Unit.usdPerPhsPerDay }),
],
},
{
name: "Hash Value",
title: "Hash Value",
bottom: [
line({
series: mining.hashrate.value.ths,
name: "TH/s",
color: colors.bitcoin,
unit: Unit.satsPerThsPerDay,
}),
line({
series: mining.hashrate.value.phs,
name: "PH/s",
color: colors.bitcoin,
unit: Unit.satsPerPhsPerDay,
}),
dotted({
series: mining.hashrate.value.thsMin,
name: "TH/s Min",
color: colors.stat.min,
unit: Unit.satsPerThsPerDay,
}),
dotted({
series: mining.hashrate.value.phsMin,
name: "PH/s Min",
color: colors.stat.min,
unit: Unit.satsPerPhsPerDay,
}),
line({ series: mining.hashrate.value.ths, name: "TH/s", color: colors.bitcoin, unit: Unit.satsPerThsPerDay }),
line({ series: mining.hashrate.value.phs, name: "PH/s", color: colors.bitcoin, unit: Unit.satsPerPhsPerDay }),
dotted({ series: mining.hashrate.value.thsMin, name: "TH/s Min", color: colors.stat.min, unit: Unit.satsPerThsPerDay }),
dotted({ series: mining.hashrate.value.phsMin, name: "PH/s Min", color: colors.stat.min, unit: Unit.satsPerPhsPerDay }),
],
},
{
@@ -779,7 +441,6 @@ export function createMiningSection() {
],
},
// Halving
{
name: "Halving",
tree: [
@@ -787,122 +448,64 @@ export function createMiningSection() {
name: "Countdown",
title: "Next Halving",
bottom: [
line({
series: blocks.halving.blocksToHalving,
name: "Remaining",
unit: Unit.blocks,
}),
line({
series: blocks.halving.daysToHalving,
name: "Remaining",
unit: Unit.days,
}),
line({ series: blocks.halving.blocksToHalving, name: "Remaining", unit: Unit.blocks }),
line({ series: blocks.halving.daysToHalving, name: "Remaining", unit: Unit.days }),
],
},
{
name: "Epoch",
title: "Halving Epoch",
bottom: [
line({
series: blocks.halving.epoch,
name: "Epoch",
unit: Unit.epoch,
}),
],
bottom: [line({ series: blocks.halving.epoch, name: "Epoch", unit: Unit.epoch })],
},
],
},
// Pools
{
name: "Difficulty",
tree: [
{
name: "Current",
title: "Mining Difficulty",
bottom: [line({ series: blocks.difficulty.value, name: "Difficulty", unit: Unit.difficulty })],
},
{
name: "Adjustment",
title: "Difficulty Adjustment",
bottom: [baseline({ series: blocks.difficulty.adjustment.percent, name: "Change", unit: Unit.percentage })],
},
{
name: "Countdown",
title: "Next Difficulty Adjustment",
bottom: [
line({ series: blocks.difficulty.blocksToRetarget, name: "Remaining", unit: Unit.blocks }),
line({ series: blocks.difficulty.daysToRetarget, name: "Remaining", unit: Unit.days }),
],
},
{
name: "Epoch",
title: "Difficulty Epoch",
bottom: [line({ series: blocks.difficulty.epoch, name: "Epoch", unit: Unit.epoch })],
},
],
},
{
name: "Pools",
tree: [
// Compare section (major pools only)
{
name: "Compare",
tree: [
{
name: "Dominance",
title: "Dominance: Major Pools (1m)",
bottom: featuredPools.flatMap((p, i) =>
percentRatio({
pattern: p.pool.dominance._1m,
name: p.name,
color: colors.at(i, featuredPools.length),
}),
),
},
{
name: "Blocks Mined",
title: "Blocks Mined: Major Pools (1m)",
bottom: featuredPools.map((p, i) =>
line({
series: p.pool.blocksMined.sum._1m,
name: p.name,
color: colors.at(i, featuredPools.length),
unit: Unit.count,
}),
),
},
{
name: "Total Rewards",
title: "Total Rewards: Major Pools",
bottom: featuredPools.flatMap((p, i) =>
satsBtcUsdFrom({
source: p.pool.rewards,
key: "base",
name: p.name,
color: colors.at(i, featuredPools.length),
}),
),
},
],
},
// AntPool & friends - pools sharing block templates
createPoolCompare("Major Pools", featuredPools),
{
name: "AntPool & Friends",
tree: [
{
name: "Dominance",
title: "Dominance: AntPool & Friends (1m)",
bottom: antpoolFriends.flatMap((p, i) =>
percentRatio({
pattern: p.pool.dominance._1m,
name: p.name,
color: colors.at(i, antpoolFriends.length),
}),
),
},
{
name: "Blocks Mined",
title: "Blocks Mined: AntPool & Friends (1m)",
bottom: antpoolFriends.map((p, i) =>
line({
series: p.pool.blocksMined.sum._1m,
name: p.name,
color: colors.at(i, antpoolFriends.length),
unit: Unit.count,
}),
),
},
{
name: "Total Rewards",
title: "Total Rewards: AntPool & Friends",
bottom: antpoolFriends.flatMap((p, i) =>
satsBtcUsdFrom({
source: p.pool.rewards,
key: "base",
name: p.name,
color: colors.at(i, antpoolFriends.length),
}),
),
},
createPoolCompare("AntPool & Friends", antpoolFriends),
...createPoolTree(antpoolFriends),
],
},
// All pools
{
name: "All Pools",
tree: [...majorPoolsTree, ...minorPoolsTree],
name: "Major",
tree: createPoolTree(majorPoolData),
},
{
name: "Minor",
tree: createMinorPoolTree(minorPoolData),
},
],
},

View File

@@ -8,21 +8,17 @@ import { priceLine } from "./constants.js";
import {
line,
dots,
baseline,
fromSupplyPattern,
chartsFromFullPerBlock,
chartsFromCount,
chartsFromCountEntries,
chartsFromAggregatedPerBlock,
rollingWindowsTree,
averagesTree,
simpleDeltaTree,
ROLLING_WINDOWS,
chartsFromBlockAnd6b,
multiSeriesTree,
simpleDeltaTree,
percentRatio,
percentRatioDots,
rollingPercentRatioTree,
} from "./series.js";
import { satsBtcUsd, satsBtcUsdFrom, satsBtcUsdFullTree } from "./shared.js";
@@ -59,12 +55,7 @@ export function createNetworkSection() {
// Non-addressable script types
const nonAddressableTypes = /** @type {const} */ ([
{ key: "p2ms", name: "P2MS", color: st.p2ms, defaultActive: false },
{
key: "opReturn",
name: "OP_RETURN",
color: st.opReturn,
defaultActive: false,
},
{ key: "opReturn", name: "OP_RETURN", color: st.opReturn, defaultActive: true },
{
key: "emptyOutput",
name: "Empty",
@@ -82,42 +73,13 @@ export function createNetworkSection() {
// All script types = addressable + non-addressable
const scriptTypes = [...addressTypes, ...nonAddressableTypes];
// Address type groups (by era)
const taprootAddresses = /** @type {const} */ ([
{ key: "p2a", name: "P2A", color: st.p2a },
{ key: "p2tr", name: "P2TR", color: st.p2tr },
]);
const segwitAddresses = /** @type {const} */ ([
{ key: "p2wsh", name: "P2WSH", color: st.p2wsh },
{ key: "p2wpkh", name: "P2WPKH", color: st.p2wpkh },
]);
const legacyAddresses = /** @type {const} */ ([
{ key: "p2sh", name: "P2SH", color: st.p2sh },
{ key: "p2pkh", name: "P2PKH", color: st.p2pkh },
{ key: "p2pk33", name: "P2PK33", color: st.p2pk33 },
{ key: "p2pk65", name: "P2PK65", color: st.p2pk65 },
]);
// Transacting types (transaction participation)
const transactingTypes = /** @type {const} */ ([
{
key: "sending",
name: "Sending",
title: "Unique Sending Addresses per Block",
compareTitle: "Unique Sending Addresses per Block by Type",
},
{
key: "receiving",
name: "Receiving",
title: "Unique Receiving Addresses per Block",
compareTitle: "Unique Receiving Addresses per Block by Type",
},
{
key: "both",
name: "Sending & Receiving",
title: "Unique Addresses Sending & Receiving per Block",
compareTitle: "Unique Addresses Sending & Receiving per Block by Type",
},
const activityTypes = /** @type {const} */ ([
{ key: "sending", name: "Sending" },
{ key: "receiving", name: "Receiving" },
{ key: "both", name: "Both" },
{ key: "reactivated", name: "Reactivated" },
]);
const countTypes = /** @type {const} */ ([
@@ -141,311 +103,140 @@ export function createNetworkSection() {
},
]);
const countMetrics = /** @type {const} */ ([
{ key: "funded", name: "Funded", color: undefined },
{ key: "empty", name: "Empty", color: colors.gray },
{ key: "total", name: "Total", color: colors.default },
]);
/**
* Create address series tree for a given type key
* @param {AddressableType | "all"} key
* @param {string} titlePrefix
*/
const createAddressSeriesTree = (key, titlePrefix) => [
{
name: "Count",
title: `${titlePrefix}Address Count`,
bottom: [
line({
series: addrs.funded[key],
name: "Funded",
unit: Unit.count,
}),
line({
series: addrs.empty[key],
name: "Empty",
color: colors.gray,
unit: Unit.count,
defaultActive: false,
}),
line({
series: addrs.total[key],
name: "Total",
color: colors.default,
unit: Unit.count,
defaultActive: false,
}),
],
},
{
name: "Trends",
tree: [
rollingWindowsTree({
windows: addrs.delta[key].absolute,
title: `${titlePrefix}Address Count Change`,
unit: Unit.count,
series: baseline,
}),
{
name: "New",
tree: (() => {
const p = addrs.new[key];
const t = `${titlePrefix}New Address Count`;
return [
{
name: "Sum",
title: t,
bottom: [
line({ series: p.base, name: "base", unit: Unit.count }),
],
},
rollingWindowsTree({
windows: p.sum,
title: t,
unit: Unit.count,
}),
{
name: "Cumulative",
title: `${t} (Total)`,
bottom: [
line({
series: p.cumulative,
name: "all-time",
unit: Unit.count,
}),
],
},
];
})(),
},
{
name: "Reactivated",
tree: [
{
name: "Base",
title: `${titlePrefix}Reactivated Addresses per Block`,
bottom: [
dots({
series: addrs.activity[key].reactivated.base,
name: "base",
unit: Unit.count,
}),
line({
series: addrs.activity[key].reactivated._24h,
name: "24h avg",
color: colors.stat.avg,
unit: Unit.count,
}),
],
},
rollingWindowsTree({
windows: addrs.activity[key].reactivated,
title: `${titlePrefix}Reactivated Addresses`,
unit: Unit.count,
}),
],
},
rollingPercentRatioTree({
windows: addrs.delta[key].rate,
title: `${titlePrefix}Address Growth Rate`,
}),
],
},
{
name: "Transacting",
tree: transactingTypes.map((t) => ({
name: t.name,
tree: [
{
name: "Base",
title: `${titlePrefix}${t.title}`,
bottom: [
dots({
series: addrs.activity[key][t.key].base,
name: "base",
unit: Unit.count,
}),
line({
series: addrs.activity[key][t.key]._24h,
name: "24h avg",
color: colors.stat.avg,
unit: Unit.count,
}),
],
},
rollingWindowsTree({
windows: addrs.activity[key][t.key],
title: `${titlePrefix}${t.title.replace(" per Block", "")}`,
unit: Unit.count,
}),
],
})),
},
];
/**
* Create Compare charts for an address group
* @template {AddressableType} K
* @param {string} groupName
* @param {ReadonlyArray<{key: K, name: string, color: Color}>} types
*/
const createAddressCompare = (groupName, types) => ({
name: "Compare",
tree: [
{
name: "Count",
tree: countTypes.map((c) => ({
name: c.name,
title: `${groupName} ${c.title}`,
bottom: types.map((t) =>
name: "Compare",
title: `${titlePrefix}Address Count`,
bottom: countMetrics.map((m) =>
line({
series: c.getSeries(t.key),
name: t.name,
color: t.color,
series: addrs[m.key][key],
name: m.name,
color: m.color,
unit: Unit.count,
}),
),
},
...countMetrics.map((m) => ({
name: m.name,
title: `${titlePrefix}${m.name} Addresses`,
bottom: [
line({ series: addrs[m.key][key], name: m.name, unit: Unit.count }),
],
})),
},
{
name: "New",
title: `${groupName} New Address Count`,
bottom: types.flatMap((t) => [
line({
series: addrs.new[t.key].base,
name: t.name,
color: t.color,
unit: Unit.count,
}),
line({
series: addrs.new[t.key].sum._24h,
name: t.name,
color: t.color,
unit: Unit.count,
}),
]),
},
{
name: "Reactivated",
tree: [
{
name: "Base",
title: `${groupName} Reactivated Addresses per Block`,
bottom: types.map((t) =>
],
},
...simpleDeltaTree({
delta: addrs.delta[key],
title: `${titlePrefix}Address Count`,
unit: Unit.count,
}),
{
name: "New",
tree: chartsFromCount({
pattern: addrs.new[key],
title: `${titlePrefix}New Addresses`,
unit: Unit.count,
}),
},
{
name: "Activity",
tree: [
{
name: "Compare",
tree: ROLLING_WINDOWS.map((w) => ({
name: w.name,
title: `${titlePrefix}Active Addresses ${w.title} Average`,
bottom: activityTypes.map((t, i) =>
line({
series: addrs.activity[t.key].reactivated.base,
series: addrs.activity[key][t.key][w.key],
name: t.name,
color: t.color,
color: colors.at(i, activityTypes.length),
unit: Unit.count,
}),
),
},
})),
},
...activityTypes.map((t) =>
averagesTree({
windows: addrs.activity[key][t.key],
title: `${titlePrefix}${t.name} Addresses`,
unit: Unit.count,
name: t.name,
}),
),
],
},
];
/** @type {Record<string, typeof scriptTypes[number]>} */
const byKey = Object.fromEntries(scriptTypes.map((t) => [t.key, t]));
const scriptGroups = [
{ name: "Legacy", types: [byKey.p2pkh, byKey.p2pk33, byKey.p2pk65] },
{ name: "Script Hash", types: [byKey.p2sh, byKey.p2ms] },
{ name: "SegWit", types: [byKey.p2wsh, byKey.p2wpkh] },
{ name: "Taproot", types: [byKey.p2a, byKey.p2tr] },
{ name: "Other", types: [byKey.opReturn, byKey.emptyOutput, byKey.unknownOutput] },
];
/**
* @template {keyof typeof scripts.count} K
* @param {string} groupName
* @param {ReadonlyArray<{key: K, name: string, color: Color}>} types
*/
const createScriptGroup = (groupName, types) => ({
name: groupName,
tree: [
{
name: "Compare",
tree: [
...ROLLING_WINDOWS.map((w) => ({
name: w.name,
title: `${groupName} Reactivated Addresses (${w.name})`,
title: `${groupName} Output Count ${w.title} Sum`,
bottom: types.map((t) =>
line({
series: addrs.activity[t.key].reactivated[w.key],
series: /** @type {CountPattern<number>} */ (scripts.count[t.key]).sum[w.key],
name: t.name,
color: t.color,
unit: Unit.count,
}),
),
})),
{
name: "Cumulative",
title: `${groupName} Output Count (Total)`,
bottom: types.map((t) =>
line({
series: scripts.count[t.key].cumulative,
name: t.name,
color: t.color,
unit: Unit.count,
}),
),
},
],
},
{
name: "Growth Rate",
tree: ROLLING_WINDOWS.map((w) => ({
name: w.name,
title: `${groupName} Address Growth Rate (${w.name})`,
bottom: types.flatMap((t) =>
percentRatio({
pattern: addrs.delta[t.key].rate[w.key],
name: t.name,
color: t.color,
}),
),
})),
},
{
name: "Transacting",
tree: transactingTypes.map((tr) => ({
name: tr.name,
tree: [
{
name: "Base",
title: `${groupName} ${tr.compareTitle}`,
bottom: types.map((t) =>
line({
series: addrs.activity[t.key][tr.key].base,
name: t.name,
color: t.color,
unit: Unit.count,
}),
),
},
...ROLLING_WINDOWS.map((w) => ({
name: w.name,
title: `${groupName} ${tr.compareTitle} (${w.name})`,
bottom: types.map((t) =>
line({
series: addrs.activity[t.key][tr.key][w.key],
name: t.name,
color: t.color,
unit: Unit.count,
}),
),
})),
],
})),
},
],
});
// Script type groups for Output Counts
const legacyScripts = legacyAddresses.slice(1); // p2pkh, p2pk33, p2pk65
const scriptHashScripts = [legacyAddresses[0], nonAddressableTypes[0]]; // p2sh, p2ms
const segwitScripts = [
/** @type {const} */ ({
key: "segwit",
name: "All SegWit",
color: colors.segwit,
}),
...segwitAddresses,
];
const otherScripts = nonAddressableTypes.slice(1); // opreturn, empty, unknown
/**
* Create Compare charts for a script group
* @template {keyof typeof scripts.count} K
* @param {string} groupName
* @param {ReadonlyArray<{key: K, name: string, color: Color}>} types
*/
const createScriptCompare = (groupName, types) => ({
name: "Compare",
tree: [
{
name: "Sum",
title: `${groupName} Output Count`,
bottom: types.map((t) =>
line({
series: /** @type {CountPattern<number>} */ (scripts.count[t.key])
.sum._24h,
name: t.name,
color: t.color,
unit: Unit.count,
}),
),
},
{
name: "Cumulative",
title: `${groupName} Output Count (Total)`,
bottom: types.map((t) =>
line({
series: /** @type {CountPattern<number>} */ (scripts.count[t.key])
.cumulative,
name: t.name,
color: t.color,
unit: Unit.count,
}),
),
},
...types.map((t) => ({
name: t.name,
tree: chartsFromCount({
pattern: /** @type {CountPattern<number>} */ (scripts.count[t.key]),
title: `${t.name} Output Count`,
unit: Unit.count,
}),
})),
],
});
@@ -563,47 +354,19 @@ export function createNetworkSection() {
},
{
name: "Velocity",
tree: [
{
name: "Compare",
title: "Transaction Velocity",
bottom: [
line({
series: supply.velocity.native,
name: "BTC",
unit: Unit.ratio,
}),
line({
series: supply.velocity.fiat,
name: "USD",
color: colors.usd,
unit: Unit.ratio,
}),
],
},
{
name: "Native",
title: "Transaction Velocity (BTC)",
bottom: [
line({
series: supply.velocity.native,
name: "BTC",
unit: Unit.ratio,
}),
],
},
{
name: "Fiat",
title: "Transaction Velocity (USD)",
bottom: [
line({
series: supply.velocity.fiat,
name: "USD",
color: colors.usd,
unit: Unit.ratio,
}),
],
},
title: "Transaction Velocity",
bottom: [
line({
series: supply.velocity.native,
name: "BTC",
unit: Unit.ratio,
}),
line({
series: supply.velocity.fiat,
name: "USD",
color: colors.usd,
unit: Unit.ratio,
}),
],
},
],
@@ -621,7 +384,7 @@ export function createNetworkSection() {
tree: [
{
name: "Compare",
title: "Block Count Rolling",
title: "Block Count",
bottom: ROLLING_WINDOWS.map((w) =>
line({
series: blocks.count.total.sum[w.key],
@@ -633,7 +396,7 @@ export function createNetworkSection() {
},
...ROLLING_WINDOWS.map((w) => ({
name: w.name,
title: `Block Count ${w.name}`,
title: `Block Count ${w.title} Sum`,
bottom: [
line({
series: blocks.count.total.sum[w.key],
@@ -655,7 +418,11 @@ export function createNetworkSection() {
name: "Cumulative",
title: "Block Count (Total)",
bottom: [
{ series: blocks.count.total.cumulative, title: "all-time", unit: Unit.count },
{
series: blocks.count.total.cumulative,
title: "all-time",
unit: Unit.count,
},
],
},
],
@@ -681,10 +448,9 @@ export function createNetworkSection() {
priceLine({ unit: Unit.secs, name: "Target", number: 600 }),
],
},
rollingWindowsTree({
averagesTree({
windows: blocks.interval,
title: "Block Interval",
name: "Averages",
unit: Unit.secs,
}),
],
@@ -778,10 +544,10 @@ export function createNetworkSection() {
}),
},
{
name: "Activity Rate",
name: "Throughput",
tree: ROLLING_WINDOWS.map((w) => ({
name: w.name,
title: `Activity Rate (${w.name})`,
title: `Throughput ${w.title} Average`,
bottom: [
line({
series: transactions.volume.txPerSec[w.key],
@@ -809,15 +575,12 @@ export function createNetworkSection() {
{
name: "Addresses",
tree: [
// Overview - global series for all addresses
{ name: "Overview", tree: createAddressSeriesTree("all", "") },
// Top-level Compare - all types
...createAddressSeriesTree("all", ""),
{
name: "Compare",
name: "By Type",
tree: [
{
name: "Count",
name: "Compare",
tree: countTypes.map((c) => ({
name: c.name,
title: c.title,
@@ -832,139 +595,7 @@ export function createNetworkSection() {
),
})),
},
{
name: "New",
title: "New Address Count by Type",
bottom: addressTypes.flatMap((t) => [
line({
series: addrs.new[t.key].base,
name: t.name,
color: t.color,
unit: Unit.count,
defaultActive: t.defaultActive,
}),
line({
series: addrs.new[t.key].sum._24h,
name: t.name,
color: t.color,
unit: Unit.count,
defaultActive: t.defaultActive,
}),
]),
},
{
name: "Reactivated",
tree: [
{
name: "Base",
title: "Reactivated Addresses per Block by Type",
bottom: addressTypes.map((t) =>
line({
series: addrs.activity[t.key].reactivated.base,
name: t.name,
color: t.color,
unit: Unit.count,
defaultActive: t.defaultActive,
}),
),
},
...ROLLING_WINDOWS.map((w) => ({
name: w.name,
title: `Reactivated Addresses by Type (${w.name})`,
bottom: addressTypes.map((t) =>
line({
series: addrs.activity[t.key].reactivated[w.key],
name: t.name,
color: t.color,
unit: Unit.count,
defaultActive: t.defaultActive,
}),
),
})),
],
},
{
name: "Growth Rate",
tree: ROLLING_WINDOWS.map((w) => ({
name: w.name,
title: `Address Growth Rate by Type (${w.name})`,
bottom: addressTypes.flatMap((t) =>
percentRatio({
pattern: addrs.delta[t.key].rate[w.key],
name: t.name,
color: t.color,
defaultActive: t.defaultActive,
}),
),
})),
},
{
name: "Transacting",
tree: transactingTypes.map((tr) => ({
name: tr.name,
tree: [
{
name: "Base",
title: tr.compareTitle,
bottom: addressTypes.map((t) =>
line({
series: addrs.activity[t.key][tr.key].base,
name: t.name,
color: t.color,
unit: Unit.count,
defaultActive: t.defaultActive,
}),
),
},
...ROLLING_WINDOWS.map((w) => ({
name: w.name,
title: `${tr.compareTitle} (${w.name})`,
bottom: addressTypes.map((t) =>
line({
series: addrs.activity[t.key][tr.key][w.key],
name: t.name,
color: t.color,
unit: Unit.count,
defaultActive: t.defaultActive,
}),
),
})),
],
})),
},
],
},
// Legacy (pre-SegWit)
{
name: "Legacy",
tree: [
createAddressCompare("Legacy", legacyAddresses),
...legacyAddresses.map((t) => ({
name: t.name,
tree: createAddressSeriesTree(t.key, `${t.name} `),
})),
],
},
// SegWit
{
name: "SegWit",
tree: [
createAddressCompare("SegWit", segwitAddresses),
...segwitAddresses.map((t) => ({
name: t.name,
tree: createAddressSeriesTree(t.key, `${t.name} `),
})),
],
},
// Taproot
{
name: "Taproot",
tree: [
createAddressCompare("Taproot", taprootAddresses),
...taprootAddresses.map((t) => ({
...addressTypes.map((t) => ({
name: t.name,
tree: createAddressSeriesTree(t.key, `${t.name} `),
})),
@@ -978,191 +609,37 @@ export function createNetworkSection() {
name: "Scripts",
tree: [
{
name: "By Type",
name: "Compare",
tree: [
// Compare section
{
name: "Compare",
tree: [
{
name: "Sum",
title: "Output Count by Script Type",
bottom: scriptTypes.map((t) =>
line({
series: /** @type {CountPattern<number>} */ (
scripts.count[t.key]
).sum._24h,
name: t.name,
color: t.color,
unit: Unit.count,
defaultActive: t.defaultActive,
}),
),
},
{
name: "Cumulative",
title: "Output Count by Script Type (Total)",
bottom: scriptTypes.map((t) =>
line({
series: scripts.count[t.key].cumulative,
name: t.name,
color: t.color,
unit: Unit.count,
defaultActive: t.defaultActive,
}),
),
},
],
},
{
name: "Legacy",
tree: [
createScriptCompare("Legacy", legacyScripts),
...legacyScripts.map((t) => ({
...ROLLING_WINDOWS.map((w) => ({
name: w.name,
title: `Output Count by Script Type ${w.title} Sum`,
bottom: scriptTypes.map((t) =>
line({
series: /** @type {CountPattern<number>} */ (scripts.count[t.key]).sum[w.key],
name: t.name,
tree: chartsFromCount({
pattern: /** @type {CountPattern<number>} */ (
scripts.count[t.key]
),
title: `${t.name} Output Count`,
unit: Unit.count,
}),
})),
],
},
color: t.color,
unit: Unit.count,
defaultActive: t.defaultActive,
}),
),
})),
{
name: "Script Hash",
tree: [
createScriptCompare("Script Hash", scriptHashScripts),
...scriptHashScripts.map((t) => ({
name: "Cumulative",
title: "Output Count by Script Type (Total)",
bottom: scriptTypes.map((t) =>
line({
series: scripts.count[t.key].cumulative,
name: t.name,
tree: chartsFromCount({
pattern: /** @type {CountPattern<number>} */ (
scripts.count[t.key]
),
title: `${t.name} Output Count`,
unit: Unit.count,
}),
})),
],
},
{
name: "SegWit",
tree: [
createScriptCompare("SegWit", segwitScripts),
...segwitScripts.map((t) => ({
name: t.name,
tree: chartsFromCount({
pattern: /** @type {CountPattern<number>} */ (
scripts.count[t.key]
),
title: `${t.name} Output Count`,
unit: Unit.count,
}),
})),
],
},
{
name: "Taproot",
tree: [
createScriptCompare("Taproot", taprootAddresses),
...taprootAddresses.map((t) => ({
name: t.name,
tree: chartsFromCount({
pattern: /** @type {CountPattern<number>} */ (
scripts.count[t.key]
),
title: `${t.name} Output Count`,
unit: Unit.count,
}),
})),
],
},
{
name: "Other",
tree: [
createScriptCompare("Other", otherScripts),
...otherScripts.map((t) => ({
name: t.name,
tree: chartsFromCount({
pattern: /** @type {CountPattern<number>} */ (
scripts.count[t.key]
),
title: `${t.name} Output Count`,
unit: Unit.count,
}),
})),
],
},
],
},
{
name: "Adoption",
tree: [
{
name: "Compare",
title: "Script Adoption",
bottom: [
line({
series: scripts.adoption.segwit.percent,
name: "SegWit",
color: colors.segwit,
unit: Unit.percentage,
}),
line({
series: scripts.adoption.segwit.ratio,
name: "SegWit",
color: colors.segwit,
unit: Unit.ratio,
}),
line({
series: scripts.adoption.taproot.percent,
name: "Taproot",
color: taprootAddresses[1].color,
unit: Unit.percentage,
}),
line({
series: scripts.adoption.taproot.ratio,
name: "Taproot",
color: taprootAddresses[1].color,
unit: Unit.ratio,
}),
],
},
{
name: "SegWit",
title: "SegWit Adoption",
bottom: [
line({
series: scripts.adoption.segwit.percent,
name: "Adoption",
unit: Unit.percentage,
}),
line({
series: scripts.adoption.segwit.ratio,
name: "Adoption",
unit: Unit.ratio,
}),
],
},
{
name: "Taproot",
title: "Taproot Adoption",
bottom: [
line({
series: scripts.adoption.taproot.percent,
name: "Adoption",
unit: Unit.percentage,
}),
line({
series: scripts.adoption.taproot.ratio,
name: "Adoption",
unit: Unit.ratio,
}),
],
color: t.color,
unit: Unit.count,
defaultActive: t.defaultActive,
}),
),
},
],
},
...scriptGroups.map((g) => createScriptGroup(g.name, g.types)),
],
},
],

View File

@@ -153,7 +153,9 @@ export function createPartialOptions() {
list: utxosUnderAmount,
all: cohortAll,
}),
...utxosUnderAmount.map(createCohortFolderBasicWithMarketCap),
...utxosUnderAmount.map(
createCohortFolderBasicWithMarketCap,
),
],
},
// More Than (≥ X sats)
@@ -166,7 +168,9 @@ export function createPartialOptions() {
list: utxosOverAmount,
all: cohortAll,
}),
...utxosOverAmount.map(createCohortFolderBasicWithMarketCap),
...utxosOverAmount.map(
createCohortFolderBasicWithMarketCap,
),
],
},
// Range
@@ -179,7 +183,9 @@ export function createPartialOptions() {
list: utxosAmountRange,
all: cohortAll,
}),
...utxosAmountRange.map(createCohortFolderBasicWithoutMarketCap),
...utxosAmountRange.map(
createCohortFolderBasicWithoutMarketCap,
),
],
},
],
@@ -283,14 +289,14 @@ export function createPartialOptions() {
],
},
// Investing section
createInvestingSection(),
// Frameworks section
{
name: "Frameworks",
tree: [createCointimeSection()],
},
// Investing section
createInvestingSection(),
],
},

View File

@@ -9,12 +9,12 @@ import { Unit } from "../utils/units.js";
/** @typedef {'_24h' | '_1w' | '_1m' | '_1y'} RollingWindowKey */
/** @type {ReadonlyArray<{key: RollingWindowKey, name: string, color: Color}>} */
/** @type {ReadonlyArray<{key: RollingWindowKey, name: string, title: string, color: Color}>} */
export const ROLLING_WINDOWS = [
{ key: "_24h", name: "24h", color: colors.time._24h },
{ key: "_1w", name: "1w", color: colors.time._1w },
{ key: "_1m", name: "1m", color: colors.time._1m },
{ key: "_1y", name: "1y", color: colors.time._1y },
{ key: "_24h", name: "24h", title: "Daily", color: colors.time._24h },
{ key: "_1w", name: "1w", title: "Weekly", color: colors.time._1w },
{ key: "_1m", name: "1m", title: "Monthly", color: colors.time._1m },
{ key: "_1y", name: "1y", title: "Yearly", color: colors.time._1y },
];
/** @type {ReadonlyArray<{key: '_24h' | '_1w' | '_1m', name: string, color: Color}>} */
@@ -474,19 +474,20 @@ export function statsAtWindow(pattern, window) {
* Create a Rolling folder tree from a _1m1w1y24hPattern (4 rolling windows)
* @param {Object} args
* @param {{ _24h: AnySeriesPattern, _1w: AnySeriesPattern, _1m: AnySeriesPattern, _1y: AnySeriesPattern }} args.windows
* @param {string} args.title
* @param {string} args.title - Compare chart title
* @param {(w: typeof ROLLING_WINDOWS[number]) => string} args.windowTitle - Individual window chart title
* @param {Unit} args.unit
* @param {string} [args.name]
* @param {string} args.name
* @param {(args: {series: AnySeriesPattern, name: string, color: Color, unit: Unit}) => AnyFetchedSeriesBlueprint} [args.series]
* @returns {PartialOptionsGroup}
*/
export function rollingWindowsTree({ windows, title, unit, name = "Sums", series = line }) {
function rollingWindowsTree({ windows, title, windowTitle, unit, name, series = line }) {
return {
name,
tree: [
{
name: "Compare",
title: `${title} Rolling`,
title,
bottom: ROLLING_WINDOWS.map((w) =>
series({
series: windows[w.key],
@@ -498,7 +499,7 @@ export function rollingWindowsTree({ windows, title, unit, name = "Sums", series
},
...ROLLING_WINDOWS.map((w) => ({
name: w.name,
title: `${title} ${w.name}`,
title: windowTitle(w),
bottom: [
series({
series: windows[w.key],
@@ -512,6 +513,32 @@ export function rollingWindowsTree({ windows, title, unit, name = "Sums", series
};
}
/**
* Rolling sums tree
* @param {Object} args
* @param {{ _24h: AnySeriesPattern, _1w: AnySeriesPattern, _1m: AnySeriesPattern, _1y: AnySeriesPattern }} args.windows
* @param {string} args.title
* @param {Unit} args.unit
* @param {(args: {series: AnySeriesPattern, name: string, color: Color, unit: Unit}) => AnyFetchedSeriesBlueprint} [args.series]
* @returns {PartialOptionsGroup}
*/
export function sumsTree({ windows, title, unit, series }) {
return rollingWindowsTree({ windows, title, windowTitle: (w) => `${title} ${w.title} Sum`, unit, name: "Sums", ...(series ? { series } : {}) });
}
/**
* Rolling averages tree
* @param {Object} args
* @param {{ _24h: AnySeriesPattern, _1w: AnySeriesPattern, _1m: AnySeriesPattern, _1y: AnySeriesPattern }} args.windows
* @param {string} args.title
* @param {Unit} args.unit
* @param {string} [args.name]
* @returns {PartialOptionsGroup}
*/
export function averagesTree({ windows, title, unit, name = "Averages" }) {
return rollingWindowsTree({ windows, title, windowTitle: (w) => `${title} ${w.title} Average`, unit, name });
}
/**
* Create a Distribution folder tree with stats at each rolling window (24h/7d/30d/1y)
* @param {Object} args
@@ -819,7 +846,7 @@ export function chartsFromFull({
title,
bottom: [{ series: pattern.base, title: "base", unit }],
},
rollingWindowsTree({ windows: pattern.sum, title, unit }),
sumsTree({ windows: pattern.sum, title, unit }),
distributionWindowsTree({ pattern, title: distTitle, unit }),
{
name: "Cumulative",
@@ -865,7 +892,7 @@ export function chartsFromAggregated({
title,
bottom: [{ series: pattern.sum, title: "base", color: stat.sum, unit }],
},
rollingWindowsTree({ windows: pattern.rolling.sum, title, unit }),
sumsTree({ windows: pattern.rolling.sum, title, unit }),
distributionWindowsTree({ pattern: pattern.rolling, title: distTitle, unit }),
{
name: "Cumulative",
@@ -920,7 +947,7 @@ export function chartsFromBlockAnd6b({ pattern, title, unit }) {
*/
export function chartsFromSumsCumulative({ pattern, title, unit, color }) {
return [
rollingWindowsTree({ windows: pattern.sum, title, unit }),
sumsTree({ windows: pattern.sum, title, unit }),
{
name: "Cumulative",
title: `${title} (Total)`,
@@ -988,16 +1015,13 @@ export function multiSeriesTree({ entries, title, unit }) {
line({ series: e.base, name: e.name, color: e.color, unit }),
),
},
{
name: "Sums",
tree: ROLLING_WINDOWS.map((w) => ({
name: w.name,
title: `${title} (${w.name})`,
bottom: entries.map((e) =>
line({ series: e.rolling[w.key], name: e.name, color: e.color, unit }),
),
})),
},
...ROLLING_WINDOWS.map((w) => ({
name: w.name,
title: `${title} ${w.title} Sum`,
bottom: entries.map((e) =>
line({ series: e.rolling[w.key], name: e.name, color: e.color, unit }),
),
})),
{
name: "Cumulative",
title: `${title} (Total)`,