website: snapshot

This commit is contained in:
nym21
2026-01-25 20:11:32 +01:00
parent 543cde525e
commit 35bf1afcff
38 changed files with 2221 additions and 3018 deletions

View File

@@ -189,6 +189,13 @@ export function createChainSection(ctx) {
unit: Unit.count,
options: { lineStyle: 4 },
}),
line({
metric: blocks.count._24hBlockCount,
name: "24h sum",
color: colors.pink,
unit: Unit.count,
defaultActive: false,
}),
line({
metric: blocks.count._1wBlockCount,
name: "1w sum",
@@ -199,7 +206,7 @@ export function createChainSection(ctx) {
line({
metric: blocks.count._1mBlockCount,
name: "1m sum",
color: colors.pink,
color: colors.orange,
unit: Unit.count,
defaultActive: false,
}),
@@ -225,10 +232,36 @@ export function createChainSection(ctx) {
title: "Block Size",
bottom: [
...fromSizePattern(blocks.size, Unit.bytes),
line({
metric: blocks.totalSize,
name: "total",
color: colors.purple,
unit: Unit.bytes,
defaultActive: false,
}),
...fromFullnessPattern(blocks.vbytes, Unit.vb),
...fromFullnessPattern(blocks.weight, Unit.wu),
line({
metric: blocks.weight.sum,
name: "sum",
color: colors.stat.sum,
unit: Unit.wu,
defaultActive: false,
}),
line({
metric: blocks.weight.cumulative,
name: "cumulative",
color: colors.stat.cumulative,
unit: Unit.wu,
defaultActive: false,
}),
],
},
{
name: "Fullness",
title: "Block Fullness",
bottom: fromFullnessPattern(blocks.fullness, Unit.percentage),
},
],
},
@@ -380,11 +413,6 @@ export function createChainSection(ctx) {
title: "Output Count",
bottom: [...fromSizePattern(outputs.count.totalCount, Unit.count)],
},
{
name: "OP_RETURN",
title: "OP_RETURN Outputs",
bottom: fromFullnessPattern(scripts.count.opreturn, Unit.count),
},
{
name: "Speed",
title: "Outputs Per Second",
@@ -416,6 +444,60 @@ export function createChainSection(ctx) {
],
},
// Scripts
{
name: "Scripts",
tree: [
{
name: "Count",
tree: [
{ name: "P2PKH", title: "P2PKH Output Count", bottom: fromDollarsPattern(scripts.count.p2pkh, Unit.count) },
{ name: "P2SH", title: "P2SH Output Count", bottom: fromDollarsPattern(scripts.count.p2sh, Unit.count) },
{ name: "P2WPKH", title: "P2WPKH Output Count", bottom: fromDollarsPattern(scripts.count.p2wpkh, Unit.count) },
{ name: "P2WSH", title: "P2WSH Output Count", bottom: fromDollarsPattern(scripts.count.p2wsh, Unit.count) },
{ name: "P2TR", title: "P2TR Output Count", bottom: fromDollarsPattern(scripts.count.p2tr, Unit.count) },
{ name: "P2PK33", title: "P2PK33 Output Count", bottom: fromDollarsPattern(scripts.count.p2pk33, Unit.count) },
{ name: "P2PK65", title: "P2PK65 Output Count", bottom: fromDollarsPattern(scripts.count.p2pk65, Unit.count) },
{ name: "P2MS", title: "P2MS Output Count", bottom: fromDollarsPattern(scripts.count.p2ms, Unit.count) },
{ name: "P2A", title: "P2A Output Count", bottom: fromDollarsPattern(scripts.count.p2a, Unit.count) },
{ name: "OP_RETURN", title: "OP_RETURN Output Count", bottom: fromDollarsPattern(scripts.count.opreturn, Unit.count) },
{ name: "SegWit", title: "SegWit Output Count", bottom: fromDollarsPattern(scripts.count.segwit, Unit.count) },
{ name: "Empty", title: "Empty Output Count", bottom: fromDollarsPattern(scripts.count.emptyoutput, Unit.count) },
{ name: "Unknown", title: "Unknown Output Count", bottom: fromDollarsPattern(scripts.count.unknownoutput, Unit.count) },
],
},
{
name: "Adoption",
tree: [
{
name: "SegWit",
title: "SegWit Adoption",
bottom: [
line({ metric: scripts.count.segwitAdoption.base, name: "base", unit: Unit.percentage }),
line({ metric: scripts.count.segwitAdoption.sum, name: "sum", color: colors.stat.sum, unit: Unit.percentage }),
line({ metric: scripts.count.segwitAdoption.cumulative, name: "cumulative", color: colors.stat.cumulative, unit: Unit.percentage, defaultActive: false }),
],
},
{
name: "Taproot",
title: "Taproot Adoption",
bottom: [
line({ metric: scripts.count.taprootAdoption.base, name: "base", unit: Unit.percentage }),
line({ metric: scripts.count.taprootAdoption.sum, name: "sum", color: colors.stat.sum, unit: Unit.percentage }),
line({ metric: scripts.count.taprootAdoption.cumulative, name: "cumulative", color: colors.stat.cumulative, unit: Unit.percentage, defaultActive: false }),
],
},
],
},
{
name: "Value",
tree: [
{ name: "OP_RETURN", title: "OP_RETURN Value", bottom: fromCoinbasePattern(scripts.value.opreturn) },
],
},
],
},
// Supply
{
name: "Supply",
@@ -456,7 +538,10 @@ export function createChainSection(ctx) {
{
name: "Coinbase",
title: "Coinbase Rewards",
bottom: fromCoinbasePattern(blocks.rewards.coinbase),
bottom: [
...fromCoinbasePattern(blocks.rewards.coinbase),
...satsBtcUsd(blocks.rewards._24hCoinbaseSum, "24h sum", colors.pink, { defaultActive: false }),
],
},
{
name: "Subsidy",
@@ -470,6 +555,13 @@ export function createChainSection(ctx) {
unit: Unit.percentage,
defaultActive: false,
}),
line({
metric: blocks.rewards.subsidyUsd1ySma,
name: "1y SMA",
color: colors.lime,
unit: Unit.usd,
defaultActive: false,
}),
],
},
{
@@ -557,6 +649,63 @@ export function createChainSection(ctx) {
}),
],
},
{
name: "Empty by Type",
title: "Empty Address Count by Type",
bottom: [
line({
metric: distribution.emptyAddrCount.p2pkh,
name: "P2PKH",
color: colors.orange,
unit: Unit.count,
}),
line({
metric: distribution.emptyAddrCount.p2sh,
name: "P2SH",
color: colors.yellow,
unit: Unit.count,
}),
line({
metric: distribution.emptyAddrCount.p2wpkh,
name: "P2WPKH",
color: colors.green,
unit: Unit.count,
}),
line({
metric: distribution.emptyAddrCount.p2wsh,
name: "P2WSH",
color: colors.teal,
unit: Unit.count,
}),
line({
metric: distribution.emptyAddrCount.p2tr,
name: "P2TR",
color: colors.purple,
unit: Unit.count,
}),
line({
metric: distribution.emptyAddrCount.p2pk65,
name: "P2PK65",
color: colors.pink,
unit: Unit.count,
defaultActive: false,
}),
line({
metric: distribution.emptyAddrCount.p2pk33,
name: "P2PK33",
color: colors.red,
unit: Unit.count,
defaultActive: false,
}),
line({
metric: distribution.emptyAddrCount.p2a,
name: "P2A",
color: colors.blue,
unit: Unit.count,
defaultActive: false,
}),
],
},
{
name: "By Type",
title: "Address Count by Type",
@@ -678,6 +827,12 @@ export function createChainSection(ctx) {
name: "Difficulty",
unit: Unit.difficulty,
}),
line({
metric: blocks.difficulty.epoch,
name: "Epoch",
color: colors.teal,
unit: Unit.epoch,
}),
line({
metric: blocks.difficulty.blocksBeforeNextAdjustment,
name: "before next",

View File

@@ -4,6 +4,7 @@ import {
satsBtcUsd,
createRatioChart,
createZScoresFolder,
formatCohortTitle,
} from "./shared.js";
/**
@@ -27,7 +28,7 @@ function createCointimePriceWithRatioOptions(
title,
top: [line({ metric: price, name: legend, color, unit: Unit.usd })],
},
createRatioChart(ctx, { title, price, ratio, color }),
createRatioChart(ctx, { title: formatCohortTitle(title), price, ratio, color }),
createZScoresFolder(ctx, { title, legend, price, ratio, color }),
];
}
@@ -40,7 +41,15 @@ function createCointimePriceWithRatioOptions(
export function createCointimeSection(ctx) {
const { colors, brk } = ctx;
const { cointime, distribution, supply } = brk.metrics;
const { pricing, cap, activity, supply: cointimeSupply, adjusted, reserveRisk, value } = cointime;
const {
pricing,
cap,
activity,
supply: cointimeSupply,
adjusted,
reserveRisk,
value,
} = cointime;
const { all } = distribution.utxoCohorts;
// Cointime prices data

View File

@@ -1,7 +1,4 @@
import {
fromBlockCount,
fromBitcoin,
fromBlockSize,
fromSizePattern,
fromFullnessPattern,
fromDollarsPattern,
@@ -32,15 +29,6 @@ export function createContext({ brk }) {
colors,
brk,
/** @type {OmitFirstArg<typeof fromBlockCount>} */
fromBlockCount: (pattern, title, color) =>
fromBlockCount(colors, pattern, title, color),
/** @type {OmitFirstArg<typeof fromBitcoin>} */
fromBitcoin: (pattern, title, color) =>
fromBitcoin(colors, pattern, title, color),
/** @type {OmitFirstArg<typeof fromBlockSize>} */
fromBlockSize: (pattern, title, color) =>
fromBlockSize(colors, pattern, title, color),
/** @type {OmitFirstArg<typeof fromSizePattern>} */
fromSizePattern: (pattern, unit, title) =>
fromSizePattern(colors, pattern, unit, title),

View File

@@ -7,11 +7,10 @@
import { Unit } from "../../utils/units.js";
import { priceLine } from "../constants.js";
import { line, baseline } from "../series.js";
import { formatCohortTitle } from "../shared.js";
import {
createSingleSupplySeries,
createGroupedSupplyTotalSeries,
createGroupedSupplyInProfitSeries,
createGroupedSupplyInLossSeries,
createGroupedSupplySection,
createUtxoCountSeries,
createAddressCountSeries,
createRealizedPriceSeries,
@@ -25,6 +24,8 @@ import {
createGroupedSentSatsSeries,
createGroupedSentBitcoinSeries,
createGroupedSentDollarsSeries,
groupedSupplyRelativeGenerators,
createSingleSupplyRelativeOptions,
} from "./shared.js";
/**
@@ -39,7 +40,7 @@ export function createAddressCohortFolder(ctx, args) {
const useGroupName = "list" in args;
const isSingle = !("list" in args);
const title = args.title ? `${useGroupName ? "by" : "of"} ${args.title}` : "";
const title = formatCohortTitle(args.title);
return {
name: args.name || "all",
@@ -48,44 +49,26 @@ export function createAddressCohortFolder(ctx, args) {
isSingle
? {
name: "supply",
title: `Supply ${title}`,
title: title("Supply"),
bottom: createSingleSupplySeries(
ctx,
/** @type {AddressCohortObject} */ (args),
createSingleSupplyRelativeOptions(ctx, /** @type {AddressCohortObject} */ (args)),
),
}
: {
name: "supply",
tree: [
{
name: "total",
title: `Supply ${title}`,
bottom: createGroupedSupplyTotalSeries(list),
},
{
name: "in profit",
title: `Supply In Profit ${title}`,
bottom: createGroupedSupplyInProfitSeries(list),
},
{
name: "in loss",
title: `Supply In Loss ${title}`,
bottom: createGroupedSupplyInLossSeries(list),
},
],
},
: createGroupedSupplySection(list, title, groupedSupplyRelativeGenerators),
// UTXO count
{
name: "utxo count",
title: `UTXO Count ${title}`,
title: title("UTXO Count"),
bottom: createUtxoCountSeries(list, useGroupName),
},
// Address count (ADDRESS COHORTS ONLY - fully type safe!)
{
name: "address count",
title: `Address Count ${title}`,
title: title("Address Count"),
bottom: createAddressCountSeries(ctx, list, useGroupName),
},
@@ -97,12 +80,12 @@ export function createAddressCohortFolder(ctx, args) {
? [
{
name: "Price",
title: `Realized Price ${title}`,
title: title("Realized Price"),
top: createRealizedPriceSeries(list),
},
{
name: "Ratio",
title: `Realized Price Ratio ${title}`,
title: title("Realized Price Ratio"),
bottom: createRealizedPriceRatioSeries(ctx, list),
},
]
@@ -112,9 +95,21 @@ export function createAddressCohortFolder(ctx, args) {
)),
{
name: "capitalization",
title: `Realized Cap ${title}`,
title: title("Realized Cap"),
bottom: createRealizedCapWithExtras(ctx, list, args, useGroupName),
},
{
name: "value",
title: title("Realized Value"),
bottom: list.map(({ color, name, tree }) =>
line({
metric: tree.realized.realizedValue,
name: useGroupName ? name : "Realized Value",
color,
unit: Unit.usd,
}),
),
},
...(!useGroupName
? createRealizedPnlSection(
ctx,
@@ -140,7 +135,7 @@ export function createAddressCohortFolder(ctx, args) {
/**
* Create realized price options for single cohort
* @param {AddressCohortObject} args
* @param {string} title
* @param {(metric: string) => string} title
* @returns {PartialOptionsTree}
*/
function createRealizedPriceOptions(args, title) {
@@ -149,7 +144,7 @@ function createRealizedPriceOptions(args, title) {
return [
{
name: "price",
title: `Realized Price ${title}`,
title: title("Realized Price"),
top: [
line({
metric: tree.realized.realizedPrice,
@@ -199,7 +194,7 @@ function createRealizedCapWithExtras(ctx, list, args, useGroupName) {
* Create realized PnL section for single cohort
* @param {PartialContext} ctx
* @param {AddressCohortObject} args
* @param {string} title
* @param {(metric: string) => string} title
* @returns {PartialOptionsTree}
*/
function createRealizedPnlSection(ctx, args, title) {
@@ -209,7 +204,7 @@ function createRealizedPnlSection(ctx, args, title) {
return [
{
name: "pnl",
title: `Realized P&L ${title}`,
title: title("Realized P&L"),
bottom: [
line({
metric: realized.realizedProfit.sum,
@@ -287,7 +282,7 @@ function createRealizedPnlSection(ctx, args, title) {
},
{
name: "Net pnl",
title: `Net Realized P&L ${title}`,
title: title("Net Realized P&L"),
bottom: [
baseline({
metric: realized.netRealizedPnl.sum,
@@ -345,7 +340,7 @@ function createRealizedPnlSection(ctx, args, title) {
},
{
name: "sopr",
title: `SOPR ${title}`,
title: title("SOPR"),
bottom: [
baseline({
metric: realized.sopr,
@@ -378,7 +373,7 @@ function createRealizedPnlSection(ctx, args, title) {
},
{
name: "Sell Side Risk",
title: `Sell Side Risk Ratio ${title}`,
title: title("Sell Side Risk Ratio"),
bottom: [
line({
metric: realized.sellSideRiskRatio,
@@ -404,7 +399,7 @@ function createRealizedPnlSection(ctx, args, title) {
},
{
name: "value",
title: `Value Created & Destroyed ${title}`,
title: title("Value Created & Destroyed"),
bottom: [
line({
metric: realized.valueCreated,
@@ -428,7 +423,7 @@ function createRealizedPnlSection(ctx, args, title) {
* @param {PartialContext} ctx
* @param {readonly AddressCohortObject[]} list
* @param {boolean} useGroupName
* @param {string} title
* @param {(metric: string) => string} title
* @returns {PartialOptionsTree}
*/
function createUnrealizedSection(ctx, list, useGroupName, title) {
@@ -440,7 +435,7 @@ function createUnrealizedSection(ctx, list, useGroupName, title) {
tree: [
{
name: "profit",
title: `Unrealized Profit ${title}`,
title: title("Unrealized Profit"),
bottom: list.flatMap(({ color, name, tree }) => [
line({
metric: tree.unrealized.unrealizedProfit,
@@ -452,7 +447,7 @@ function createUnrealizedSection(ctx, list, useGroupName, title) {
},
{
name: "loss",
title: `Unrealized Loss ${title}`,
title: title("Unrealized Loss"),
bottom: list.flatMap(({ color, name, tree }) => [
line({
metric: tree.unrealized.unrealizedLoss,
@@ -464,7 +459,7 @@ function createUnrealizedSection(ctx, list, useGroupName, title) {
},
{
name: "total pnl",
title: `Total Unrealized P&L ${title}`,
title: title("Total Unrealized P&L"),
bottom: list.flatMap(({ color, name, tree }) => [
baseline({
metric: tree.unrealized.totalUnrealizedPnl,
@@ -476,7 +471,7 @@ function createUnrealizedSection(ctx, list, useGroupName, title) {
},
{
name: "negative loss",
title: `Negative Unrealized Loss ${title}`,
title: title("Negative Unrealized Loss"),
bottom: list.flatMap(({ color, name, tree }) => [
line({
metric: tree.unrealized.negUnrealizedLoss,
@@ -491,7 +486,7 @@ function createUnrealizedSection(ctx, list, useGroupName, title) {
tree: [
{
name: "nupl",
title: `NUPL (Rel to Market Cap) ${title}`,
title: title("NUPL (Rel to Market Cap)"),
bottom: list.flatMap(({ color, name, tree }) => [
baseline({
metric: tree.relative.nupl,
@@ -504,7 +499,7 @@ function createUnrealizedSection(ctx, list, useGroupName, title) {
},
{
name: "profit",
title: `Unrealized Profit (% of Market Cap) ${title}`,
title: title("Unrealized Profit (% of Market Cap)"),
bottom: list.flatMap(({ color, name, tree }) => [
line({
metric: tree.relative.unrealizedProfitRelToMarketCap,
@@ -516,7 +511,7 @@ function createUnrealizedSection(ctx, list, useGroupName, title) {
},
{
name: "loss",
title: `Unrealized Loss (% of Market Cap) ${title}`,
title: title("Unrealized Loss (% of Market Cap)"),
bottom: list.flatMap(({ color, name, tree }) => [
line({
metric: tree.relative.unrealizedLossRelToMarketCap,
@@ -528,7 +523,7 @@ function createUnrealizedSection(ctx, list, useGroupName, title) {
},
{
name: "net pnl",
title: `Net Unrealized P&L (% of Market Cap) ${title}`,
title: title("Net Unrealized P&L (% of Market Cap)"),
bottom: list.flatMap(({ color, name, tree }) => [
baseline({
metric: tree.relative.netUnrealizedPnlRelToMarketCap,
@@ -540,7 +535,7 @@ function createUnrealizedSection(ctx, list, useGroupName, title) {
},
{
name: "negative loss",
title: `Negative Unrealized Loss (% of Market Cap) ${title}`,
title: title("Negative Unrealized Loss (% of Market Cap)"),
bottom: list.flatMap(({ color, name, tree }) => [
line({
metric: tree.relative.negUnrealizedLossRelToMarketCap,
@@ -554,7 +549,7 @@ function createUnrealizedSection(ctx, list, useGroupName, title) {
},
{
name: "nupl",
title: `Net Unrealized P&L ${title}`,
title: title("Net Unrealized P&L"),
bottom: list.flatMap(({ color, name, tree }) => [
baseline({
metric: tree.unrealized.netUnrealizedPnl,
@@ -577,7 +572,7 @@ function createUnrealizedSection(ctx, list, useGroupName, title) {
* Create cost basis section (no percentiles for address cohorts)
* @param {readonly AddressCohortObject[]} list
* @param {boolean} useGroupName
* @param {string} title
* @param {(metric: string) => string} title
* @returns {PartialOptionsTree}
*/
function createCostBasisSection(list, useGroupName, title) {
@@ -587,7 +582,7 @@ function createCostBasisSection(list, useGroupName, title) {
tree: [
{
name: "min",
title: `Min Cost Basis ${title}`,
title: title("Min Cost Basis"),
top: list.map(({ color, name, tree }) =>
line({
metric: tree.costBasis.min,
@@ -599,7 +594,7 @@ function createCostBasisSection(list, useGroupName, title) {
},
{
name: "max",
title: `Max Cost Basis ${title}`,
title: title("Max Cost Basis"),
top: list.map(({ color, name, tree }) =>
line({
metric: tree.costBasis.max,
@@ -617,7 +612,7 @@ function createCostBasisSection(list, useGroupName, title) {
/**
* Create activity section
* @param {AddressCohortObject | AddressCohortGroupObject} args
* @param {string} title
* @param {(metric: string) => string} title
* @returns {PartialOptionsTree}
*/
function createActivitySection(args, title) {
@@ -633,12 +628,12 @@ function createActivitySection(args, title) {
tree: [
{
name: "Coins Destroyed",
title: `Coins Destroyed ${title}`,
title: title("Coins Destroyed"),
bottom: createSingleCoinsDestroyedSeries(cohort),
},
{
name: "Sent",
title: `Sent ${title}`,
title: title("Sent"),
bottom: createSingleSentSeries(cohort),
},
],
@@ -653,22 +648,22 @@ function createActivitySection(args, title) {
tree: [
{
name: "coinblocks destroyed",
title: `Coinblocks Destroyed ${title}`,
title: title("Coinblocks Destroyed"),
bottom: createGroupedCoinblocksDestroyedSeries(list),
},
{
name: "coindays destroyed",
title: `Coindays Destroyed ${title}`,
title: title("Coindays Destroyed"),
bottom: createGroupedCoindaysDestroyedSeries(list),
},
{
name: "satblocks destroyed",
title: `Satblocks Destroyed ${title}`,
title: title("Satblocks Destroyed"),
bottom: createGroupedSatblocksDestroyedSeries(list),
},
{
name: "satdays destroyed",
title: `Satdays Destroyed ${title}`,
title: title("Satdays Destroyed"),
bottom: createGroupedSatdaysDestroyedSeries(list),
},
{
@@ -676,17 +671,17 @@ function createActivitySection(args, title) {
tree: [
{
name: "sats",
title: `Sent (Sats) ${title}`,
title: title("Sent (Sats)"),
bottom: createGroupedSentSatsSeries(list),
},
{
name: "bitcoin",
title: `Sent (BTC) ${title}`,
title: title("Sent (BTC)"),
bottom: createGroupedSentBitcoinSeries(list),
},
{
name: "dollars",
title: `Sent ($) ${title}`,
title: title("Sent ($)"),
bottom: createGroupedSentDollarsSeries(list),
},
],

View File

@@ -24,10 +24,20 @@ const entries = (obj) =>
);
/** @type {readonly AddressableType[]} */
const ADDRESSABLE_TYPES = ["p2pk65", "p2pk33", "p2pkh", "p2sh", "p2wpkh", "p2wsh", "p2tr", "p2a"];
const ADDRESSABLE_TYPES = [
"p2pk65",
"p2pk33",
"p2pkh",
"p2sh",
"p2wpkh",
"p2wsh",
"p2tr",
"p2a",
];
/** @type {(key: SpendableType) => key is AddressableType} */
const isAddressable = (key) => ADDRESSABLE_TYPES.includes(/** @type {any} */ (key));
const isAddressable = (key) =>
ADDRESSABLE_TYPES.includes(/** @type {any} */ (key));
/**
* Build all cohort data from brk tree
@@ -51,8 +61,7 @@ export function buildCohortData(colors, brk) {
YEAR_NAMES,
} = brk;
// Base cohort representing "all" - CohortAll (adjustedSopr + percentiles but no RelToMarketCap)
/** @type {CohortAll} */
// Base cohort representing "all"
const cohortAll = {
name: "",
title: "",
@@ -61,9 +70,8 @@ export function buildCohortData(colors, brk) {
addrCount: addrCount.all,
};
// Term cohorts - split because short is CohortFull, long is CohortWithPercentiles
// Term cohorts
const shortNames = TERM_NAMES.short;
/** @type {CohortFull} */
const termShort = {
name: shortNames.short,
title: shortNames.long,
@@ -79,43 +87,40 @@ export function buildCohortData(colors, brk) {
tree: utxoCohorts.term.long,
};
// Max age cohorts (up to X time) - CohortWithAdjusted (adjustedSopr only)
/** @type {readonly CohortWithAdjusted[]} */
// Max age cohorts (up to X time)
const upToDate = entries(utxoCohorts.maxAge).map(([key, tree]) => {
const names = MAX_AGE_NAMES[key];
return {
name: names.short,
title: names.long,
title: `UTXOs ${names.long}`,
color: colors[maxAgeColors[key]],
tree,
};
});
// Min age cohorts (from X time) - CohortBasicWithMarketCap (has RelToMarketCap)
/** @type {readonly CohortBasicWithMarketCap[]} */
// Min age cohorts (from X time)
const fromDate = entries(utxoCohorts.minAge).map(([key, tree]) => {
const names = MIN_AGE_NAMES[key];
return {
name: names.short,
title: names.long,
title: `UTXOs ${names.long}`,
color: colors[minAgeColors[key]],
tree,
};
});
// Age range cohorts - CohortAgeRange (no nupl)
// Age range cohorts
const dateRange = entries(utxoCohorts.ageRange).map(([key, tree]) => {
const names = AGE_RANGE_NAMES[key];
return {
name: names.short,
title: names.long,
title: `UTXOs ${names.long}`,
color: colors[ageRangeColors[key]],
tree,
};
});
// Epoch cohorts - CohortBasicWithoutMarketCap (no RelToMarketCap)
/** @type {readonly CohortBasicWithoutMarketCap[]} */
// Epoch cohorts
const epoch = entries(utxoCohorts.epoch).map(([key, tree]) => {
const names = EPOCH_NAMES[key];
return {
@@ -126,66 +131,61 @@ export function buildCohortData(colors, brk) {
};
});
// UTXOs above amount - CohortBasicWithMarketCap (has RelToMarketCap)
/** @type {readonly CohortBasicWithMarketCap[]} */
// UTXOs above amount
const utxosAboveAmount = entries(utxoCohorts.geAmount).map(([key, tree]) => {
const names = GE_AMOUNT_NAMES[key];
return {
name: names.short,
title: names.long,
title: `UTXOs ${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,
title: `Addresses ${names.long}`,
color: colors[geAmountColors[key]],
tree,
};
},
);
// UTXOs under amount - CohortBasicWithMarketCap (has RelToMarketCap)
/** @type {readonly CohortBasicWithMarketCap[]} */
// UTXOs under amount
const utxosUnderAmount = entries(utxoCohorts.ltAmount).map(([key, tree]) => {
const names = LT_AMOUNT_NAMES[key];
return {
name: names.short,
title: names.long,
title: `UTXOs ${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,
title: `Addresses ${names.long}`,
color: colors[ltAmountColors[key]],
tree,
};
},
);
// UTXOs amount ranges - CohortBasicWithoutMarketCap (no RelToMarketCap)
/** @type {readonly CohortBasicWithoutMarketCap[]} */
// UTXOs amount ranges
const utxosAmountRanges = entries(utxoCohorts.amountRange).map(
([key, tree]) => {
const names = AMOUNT_RANGE_NAMES[key];
return {
name: names.short,
title: names.long,
title: `UTXOs ${names.long}`,
color: colors[amountRangeColors[key]],
tree,
};
@@ -193,13 +193,12 @@ export function buildCohortData(colors, brk) {
);
// 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,
title: `Addresses ${names.long}`,
color: colors[amountRangeColors[key]],
tree,
};
@@ -207,33 +206,30 @@ export function buildCohortData(colors, brk) {
);
// Spendable type cohorts - split by addressability
/** @type {readonly CohortAddress[]} */
const typeAddressable = ADDRESSABLE_TYPES.map((key) => {
const names = SPENDABLE_TYPE_NAMES[key];
return {
name: names.short,
title: names.long,
title: names.short,
color: colors[spendableTypeColors[key]],
tree: utxoCohorts.type[key],
addrCount: addrCount[key],
};
});
/** @type {readonly CohortBasicWithoutMarketCap[]} */
const typeOther = entries(utxoCohorts.type)
.filter(([key]) => !isAddressable(key))
.map(([key, tree]) => {
const names = SPENDABLE_TYPE_NAMES[key];
return {
name: names.short,
title: names.long,
title: names.short,
color: colors[spendableTypeColors[key]],
tree,
};
});
// Year cohorts - CohortBasicWithoutMarketCap (no RelToMarketCap)
/** @type {readonly CohortBasicWithoutMarketCap[]} */
// Year cohorts
const year = entries(utxoCohorts.year).map(([key, tree]) => {
const names = YEAR_NAMES[key];
return {

View File

@@ -10,8 +10,7 @@ export {
createCohortFolderAll,
createCohortFolderFull,
createCohortFolderWithAdjusted,
createCohortFolderWithPercentiles,
createCohortFolderLongTerm,
createCohortFolderWithNupl,
createCohortFolderAgeRange,
createCohortFolderBasicWithMarketCap,
createCohortFolderBasicWithoutMarketCap,

View File

@@ -9,46 +9,25 @@ import { satsBtcUsd } from "../shared.js";
* Create supply section for a single cohort
* @param {PartialContext} ctx
* @param {CohortObject} cohort
* @param {Object} [options]
* @param {AnyFetchedSeriesBlueprint[]} [options.supplyRelative] - Supply relative to circulating supply metrics
* @param {AnyFetchedSeriesBlueprint[]} [options.pnlRelative] - Supply in profit/loss relative to circulating supply metrics
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createSingleSupplySeries(ctx, cohort) {
export function createSingleSupplySeries(ctx, cohort, { supplyRelative = [], pnlRelative = [] } = {}) {
const { colors } = ctx;
const { tree } = cohort;
return [
...satsBtcUsd(tree.supply.total, "Supply", colors.default),
...("supplyRelToCirculatingSupply" in tree.relative
? [
line({
metric: tree.relative.supplyRelToCirculatingSupply,
name: "Supply",
color: colors.default,
unit: Unit.pctSupply,
}),
]
: []),
...supplyRelative,
...satsBtcUsd(tree.unrealized.supplyInProfit, "In Profit", colors.green),
...satsBtcUsd(tree.unrealized.supplyInLoss, "In Loss", colors.red),
...satsBtcUsd(tree.supply.halved, "half", colors.gray).map((s) => ({
...s,
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,
}),
]
: []),
...pnlRelative,
line({
metric: tree.relative.supplyInProfitRelToOwnSupply,
name: "In Profit",
@@ -74,67 +53,198 @@ export function createSingleSupplySeries(ctx, cohort) {
/**
* Create supply total series for grouped cohorts
* @param {readonly CohortObject[]} list
* @template {readonly CohortObject[]} T
* @param {T} list
* @param {Object} [options]
* @param {(cohort: T[number]) => AnyFetchedSeriesBlueprint[]} [options.relativeMetrics] - Generator for relative supply metrics
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createGroupedSupplyTotalSeries(list) {
return list.flatMap(({ color, name, tree }) => [
...satsBtcUsd(tree.supply.total, name, color),
...("supplyRelToCirculatingSupply" in tree.relative
? [
line({
metric: tree.relative.supplyRelToCirculatingSupply,
name,
color,
unit: Unit.pctSupply,
}),
]
: []),
export function createGroupedSupplyTotalSeries(list, { relativeMetrics } = {}) {
return list.flatMap((cohort) => [
...satsBtcUsd(cohort.tree.supply.total, cohort.name, cohort.color),
...(relativeMetrics ? relativeMetrics(cohort) : []),
]);
}
/**
* Create supply in profit series for grouped cohorts
* @param {readonly CohortObject[]} list
* @template {readonly CohortObject[]} T
* @param {T} list
* @param {Object} [options]
* @param {(cohort: T[number]) => AnyFetchedSeriesBlueprint[]} [options.relativeMetrics] - Generator for relative supply metrics
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createGroupedSupplyInProfitSeries(list) {
return list.flatMap(({ color, name, tree }) => [
...satsBtcUsd(tree.unrealized.supplyInProfit, name, color),
...("supplyInProfitRelToCirculatingSupply" in tree.relative
? [
line({
metric: tree.relative.supplyInProfitRelToCirculatingSupply,
name,
color,
unit: Unit.pctSupply,
}),
]
: []),
export function createGroupedSupplyInProfitSeries(list, { relativeMetrics } = {}) {
return list.flatMap((cohort) => [
...satsBtcUsd(cohort.tree.unrealized.supplyInProfit, cohort.name, cohort.color),
...(relativeMetrics ? relativeMetrics(cohort) : []),
]);
}
/**
* Create supply in loss series for grouped cohorts
* @param {readonly CohortObject[]} list
* @template {readonly CohortObject[]} T
* @param {T} list
* @param {Object} [options]
* @param {(cohort: T[number]) => AnyFetchedSeriesBlueprint[]} [options.relativeMetrics] - Generator for relative supply metrics
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createGroupedSupplyInLossSeries(list) {
return list.flatMap(({ color, name, tree }) => [
...satsBtcUsd(tree.unrealized.supplyInLoss, name, color),
...("supplyInLossRelToCirculatingSupply" in tree.relative
? [
line({
metric: tree.relative.supplyInLossRelToCirculatingSupply,
name,
color,
unit: Unit.pctSupply,
}),
]
: []),
export function createGroupedSupplyInLossSeries(list, { relativeMetrics } = {}) {
return list.flatMap((cohort) => [
...satsBtcUsd(cohort.tree.unrealized.supplyInLoss, cohort.name, cohort.color),
...(relativeMetrics ? relativeMetrics(cohort) : []),
]);
}
/**
* Create supply section for grouped cohorts
* @template {readonly CohortObject[]} T
* @param {T} list
* @param {(metric: string) => string} title
* @param {Object} [options]
* @param {(cohort: T[number]) => AnyFetchedSeriesBlueprint[]} [options.supplyRelativeMetrics] - Generator for supply relative metrics
* @param {(cohort: T[number]) => AnyFetchedSeriesBlueprint[]} [options.profitRelativeMetrics] - Generator for supply in profit relative metrics
* @param {(cohort: T[number]) => AnyFetchedSeriesBlueprint[]} [options.lossRelativeMetrics] - Generator for supply in loss relative metrics
* @returns {PartialOptionsGroup}
*/
export function createGroupedSupplySection(list, title, { supplyRelativeMetrics, profitRelativeMetrics, lossRelativeMetrics } = {}) {
return {
name: "supply",
tree: [
{
name: "total",
title: title("Supply"),
bottom: createGroupedSupplyTotalSeries(list, { relativeMetrics: supplyRelativeMetrics }),
},
{
name: "in profit",
title: title("Supply In Profit"),
bottom: createGroupedSupplyInProfitSeries(list, { relativeMetrics: profitRelativeMetrics }),
},
{
name: "in loss",
title: title("Supply In Loss"),
bottom: createGroupedSupplyInLossSeries(list, { relativeMetrics: lossRelativeMetrics }),
},
],
};
}
// ============================================================================
// Circulating Supply Relative Metrics Generators
// ============================================================================
/**
* Create supply relative to circulating supply series for single cohort
* @param {PartialContext} ctx
* @param {CohortWithCirculatingSupplyRelative} cohort
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createSupplyRelativeToCirculatingSeries(ctx, cohort) {
return [
line({
metric: cohort.tree.relative.supplyRelToCirculatingSupply,
name: "Supply",
color: ctx.colors.default,
unit: Unit.pctSupply,
}),
];
}
/**
* Create supply in profit/loss relative to circulating supply series for single cohort
* @param {PartialContext} ctx
* @param {CohortWithCirculatingSupplyRelative} cohort
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createSupplyPnlRelativeToCirculatingSeries(ctx, cohort) {
return [
line({
metric: cohort.tree.relative.supplyInProfitRelToCirculatingSupply,
name: "In Profit",
color: ctx.colors.green,
unit: Unit.pctSupply,
}),
line({
metric: cohort.tree.relative.supplyInLossRelToCirculatingSupply,
name: "In Loss",
color: ctx.colors.red,
unit: Unit.pctSupply,
}),
];
}
/**
* Create supply relative to circulating supply metrics generator for grouped cohorts
* @param {CohortWithCirculatingSupplyRelative} cohort
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createGroupedSupplyRelativeMetrics(cohort) {
return [
line({
metric: cohort.tree.relative.supplyRelToCirculatingSupply,
name: cohort.name,
color: cohort.color,
unit: Unit.pctSupply,
}),
];
}
/**
* Create supply in profit relative to circulating supply metrics generator for grouped cohorts
* @param {CohortWithCirculatingSupplyRelative} cohort
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createGroupedSupplyInProfitRelativeMetrics(cohort) {
return [
line({
metric: cohort.tree.relative.supplyInProfitRelToCirculatingSupply,
name: cohort.name,
color: cohort.color,
unit: Unit.pctSupply,
}),
];
}
/**
* Create supply in loss relative to circulating supply metrics generator for grouped cohorts
* @param {CohortWithCirculatingSupplyRelative} cohort
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function createGroupedSupplyInLossRelativeMetrics(cohort) {
return [
line({
metric: cohort.tree.relative.supplyInLossRelToCirculatingSupply,
name: cohort.name,
color: cohort.color,
unit: Unit.pctSupply,
}),
];
}
/**
* Grouped supply relative generators object for cohorts with circulating supply relative
* @type {{ supplyRelativeMetrics: typeof createGroupedSupplyRelativeMetrics, profitRelativeMetrics: typeof createGroupedSupplyInProfitRelativeMetrics, lossRelativeMetrics: typeof createGroupedSupplyInLossRelativeMetrics }}
*/
export const groupedSupplyRelativeGenerators = {
supplyRelativeMetrics: createGroupedSupplyRelativeMetrics,
profitRelativeMetrics: createGroupedSupplyInProfitRelativeMetrics,
lossRelativeMetrics: createGroupedSupplyInLossRelativeMetrics,
};
/**
* Create single cohort supply relative options for cohorts with circulating supply relative
* @param {PartialContext} ctx
* @param {CohortWithCirculatingSupplyRelative} cohort
* @returns {{ supplyRelative: AnyFetchedSeriesBlueprint[], pnlRelative: AnyFetchedSeriesBlueprint[] }}
*/
export function createSingleSupplyRelativeOptions(ctx, cohort) {
return {
supplyRelative: createSupplyRelativeToCirculatingSeries(ctx, cohort),
pnlRelative: createSupplyPnlRelativeToCirculatingSeries(ctx, cohort),
};
}
/**
* Create UTXO count series
* @param {readonly CohortObject[]} list

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
import { Unit } from "../../utils/units.js";
import { line } from "../series.js";
import { createRatioChart, createZScoresFolder } from "../shared.js";
import { createRatioChart, createZScoresFolder, formatCohortTitle } from "../shared.js";
import { periodIdToName } from "./utils.js";
/**
@@ -89,7 +89,7 @@ export function createPriceWithRatioOptions(
title,
top: [line({ metric: priceMetric, name: legend, color, unit: Unit.usd })],
},
createRatioChart(ctx, { title, price: priceMetric, ratio, color }),
createRatioChart(ctx, { title: formatCohortTitle(title), price: priceMetric, ratio, color }),
createZScoresFolder(ctx, {
title,
legend,

View File

@@ -47,83 +47,104 @@ export function createInvestingSection(ctx, { dca, lookback, returns }) {
const { colors } = ctx;
const dcaClasses = buildDcaClasses(colors, dca);
/**
* @param {string} id
* @param {ShortPeriodKey} key
*/
const createPeriodTree = (id, key) => {
const name = periodIdToName(id, true);
return {
name,
tree: [
{
name: "Cost basis",
title: `${name} Cost Basis`,
top: [
line({ metric: dca.periodAveragePrice[key], name: "DCA", color: colors.green, unit: Unit.usd }),
line({ metric: lookback[key], name: "Lump sum", color: colors.orange, unit: Unit.usd }),
],
},
{
name: "Returns",
title: `${name} Returns`,
bottom: [
baseline({ metric: dca.periodReturns[key], name: "DCA", unit: Unit.percentage }),
baseline({ metric: returns.priceReturns[key], name: "Lump sum", color: [colors.cyan, colors.orange], unit: Unit.percentage }),
priceLine({ ctx, unit: Unit.percentage }),
],
},
{
name: "Stack",
title: `${name} Stack`,
bottom: [
...satsBtcUsd(dca.periodStack[key], "DCA", colors.green),
...satsBtcUsd(dca.periodLumpSumStack[key], "Lump sum", colors.orange),
],
},
],
};
};
/**
* @param {string} id
* @param {LongPeriodKey} key
*/
const createPeriodTreeWithCagr = (id, key) => {
const name = periodIdToName(id, true);
return {
name,
tree: [
{
name: "Cost basis",
title: `${name} Cost Basis`,
top: [
line({ metric: dca.periodAveragePrice[key], name: "DCA", color: colors.green, unit: Unit.usd }),
line({ metric: lookback[key], name: "Lump sum", color: colors.orange, unit: Unit.usd }),
],
},
{
name: "Returns",
title: `${name} Returns`,
bottom: [
baseline({ metric: dca.periodReturns[key], name: "DCA", unit: Unit.percentage }),
baseline({ metric: returns.priceReturns[key], name: "Lump sum", color: [colors.cyan, colors.orange], unit: Unit.percentage }),
line({ metric: dca.periodCagr[key], name: "DCA CAGR", color: colors.purple, unit: Unit.percentage, defaultActive: false }),
line({ metric: returns.cagr[key], name: "Lump sum CAGR", color: colors.indigo, unit: Unit.percentage, defaultActive: false }),
priceLine({ ctx, unit: Unit.percentage }),
],
},
{
name: "Stack",
title: `${name} Stack`,
bottom: [
...satsBtcUsd(dca.periodStack[key], "DCA", colors.green),
...satsBtcUsd(dca.periodLumpSumStack[key], "Lump sum", colors.orange),
],
},
],
};
};
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} 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} Returns`,
bottom: [
baseline({
metric: dcaReturns,
name: "DCA",
unit: Unit.percentage,
}),
baseline({
metric: priceReturns,
name: "Lump sum",
color: [colors.cyan, colors.orange],
unit: Unit.percentage,
}),
priceLine({ ctx, unit: Unit.percentage }),
],
},
{
name: "Stack",
title: `${name} Stack`,
bottom: [
...satsBtcUsd(dcaStack, "DCA", colors.green),
...satsBtcUsd(lumpSumStack, "Lump sum", colors.orange),
],
},
],
};
}),
tree: [
createPeriodTree("1w", "_1w"),
createPeriodTree("1m", "_1m"),
createPeriodTree("3m", "_3m"),
createPeriodTree("6m", "_6m"),
createPeriodTree("1y", "_1y"),
createPeriodTreeWithCagr("2y", "_2y"),
createPeriodTreeWithCagr("3y", "_3y"),
createPeriodTreeWithCagr("4y", "_4y"),
createPeriodTreeWithCagr("5y", "_5y"),
createPeriodTreeWithCagr("6y", "_6y"),
createPeriodTreeWithCagr("8y", "_8y"),
createPeriodTreeWithCagr("10y", "_10y"),
],
},
// DCA classes

View File

@@ -6,8 +6,7 @@ import {
createCohortFolderAll,
createCohortFolderFull,
createCohortFolderWithAdjusted,
createCohortFolderWithPercentiles,
createCohortFolderLongTerm,
createCohortFolderWithNupl,
createCohortFolderAgeRange,
createCohortFolderBasicWithMarketCap,
createCohortFolderBasicWithoutMarketCap,
@@ -99,19 +98,19 @@ export function createPartialOptions({ brk }) {
// All UTXOs - CohortAll (adjustedSopr + percentiles but no RelToMarketCap)
createCohortFolderAll(ctx, cohortAll),
// Terms (STH/LTH) - Short is Full, Long is LongTerm
// Terms (STH/LTH) - Short is Full, Long has nupl
{
name: "Terms",
tree: [
// Compare folder uses WithPercentiles (common capabilities)
createCohortFolderWithPercentiles(ctx, {
// Compare folder - both have nupl + percentiles
createCohortFolderWithNupl(ctx, {
name: "Compare",
title: "Term",
list: [termShort, termLong],
}),
// Individual cohorts with their specific capabilities
createCohortFolderFull(ctx, termShort),
createCohortFolderLongTerm(ctx, termLong),
createCohortFolderWithNupl(ctx, termLong),
],
},

View File

@@ -2,6 +2,34 @@
import { Unit } from "../utils/units.js";
// ============================================================================
// Shared percentile helper
// ============================================================================
/**
* Create percentile series (max/min/median/pct75/pct25/pct90/pct10) from any stats pattern
* Works with FullnessPattern, FeeRatePattern, AnyStatsPattern, DollarsPattern, etc.
* @param {Colors} colors
* @param {FullnessPattern<any> | FeeRatePattern<any> | AnyStatsPattern | DollarsPattern<any>} pattern
* @param {Unit} unit
* @param {string} title
* @param {{ type?: "Dots" }} [options]
* @returns {AnyFetchedSeriesBlueprint[]}
*/
function percentileSeries(colors, pattern, unit, title, { type } = {}) {
const { stat } = colors;
const base = { unit, defaultActive: false };
return [
{ type, metric: pattern.max, title: `${title} max`.trim(), color: stat.max, ...base },
{ type, metric: pattern.min, title: `${title} min`.trim(), color: stat.min, ...base },
{ type, metric: pattern.median, title: `${title} median`.trim(), color: stat.median, ...base },
{ type, metric: pattern.pct75, title: `${title} pct75`.trim(), color: stat.pct75, ...base },
{ type, metric: pattern.pct25, title: `${title} pct25`.trim(), color: stat.pct25, ...base },
{ type, metric: pattern.pct90, title: `${title} pct90`.trim(), color: stat.pct90, ...base },
{ type, metric: pattern.pct10, title: `${title} pct10`.trim(), color: stat.pct10, ...base },
];
}
/**
* Create a Line series
* @param {Object} args
@@ -180,158 +208,6 @@ export function histogram({
};
}
/**
* 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} cumulative`,
color: colors.stat.cumulative,
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) {
const { stat } = colors;
return [
{ metric: pattern.base, title, color: color ?? colors.default },
{
metric: pattern.average,
title: `${title} avg`,
color: stat.avg,
defaultActive: false,
},
{
metric: pattern.max,
title: `${title} max`,
color: stat.max,
defaultActive: false,
},
{
metric: pattern.min,
title: `${title} min`,
color: stat.min,
defaultActive: false,
},
{
metric: pattern.median,
title: `${title} median`,
color: stat.median,
defaultActive: false,
},
{
metric: pattern.pct75,
title: `${title} pct75`,
color: stat.pct75,
defaultActive: false,
},
{
metric: pattern.pct25,
title: `${title} pct25`,
color: stat.pct25,
defaultActive: false,
},
{
metric: pattern.pct90,
title: `${title} pct90`,
color: stat.pct90,
defaultActive: false,
},
{
metric: pattern.pct10,
title: `${title} pct10`,
color: stat.pct10,
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) {
const { stat } = colors;
return [
{ metric: pattern.sum, title, color: color ?? colors.default },
{
metric: pattern.average,
title: `${title} avg`,
color: stat.avg,
defaultActive: false,
},
{
metric: pattern.cumulative,
title: `${title} cumulative`,
color: stat.cumulative,
defaultActive: false,
},
{
metric: pattern.max,
title: `${title} max`,
color: stat.max,
defaultActive: false,
},
{
metric: pattern.min,
title: `${title} min`,
color: stat.min,
defaultActive: false,
},
{
metric: pattern.median,
title: `${title} median`,
color: stat.median,
defaultActive: false,
},
{
metric: pattern.pct75,
title: `${title} pct75`,
color: stat.pct75,
defaultActive: false,
},
{
metric: pattern.pct25,
title: `${title} pct25`,
color: stat.pct25,
defaultActive: false,
},
{
metric: pattern.pct90,
title: `${title} pct90`,
color: stat.pct90,
defaultActive: false,
},
{
metric: pattern.pct10,
title: `${title} pct10`,
color: stat.pct10,
defaultActive: false,
},
];
}
/**
* Create series from a SizePattern ({ average, sum, cumulative, min, max, percentiles })
* @param {Colors} colors
@@ -344,69 +220,9 @@ export function fromSizePattern(colors, pattern, unit, title = "") {
const { stat } = colors;
return [
{ metric: pattern.average, title: `${title} avg`.trim(), unit },
{
metric: pattern.sum,
title: `${title} sum`.trim(),
color: stat.sum,
unit,
defaultActive: false,
},
{
metric: pattern.cumulative,
title: `${title} cumulative`.trim(),
color: stat.cumulative,
unit,
defaultActive: false,
},
{
metric: pattern.max,
title: `${title} max`.trim(),
color: stat.max,
unit,
defaultActive: false,
},
{
metric: pattern.min,
title: `${title} min`.trim(),
color: stat.min,
unit,
defaultActive: false,
},
{
metric: pattern.median,
title: `${title} median`.trim(),
color: stat.median,
unit,
defaultActive: false,
},
{
metric: pattern.pct75,
title: `${title} pct75`.trim(),
color: stat.pct75,
unit,
defaultActive: false,
},
{
metric: pattern.pct25,
title: `${title} pct25`.trim(),
color: stat.pct25,
unit,
defaultActive: false,
},
{
metric: pattern.pct90,
title: `${title} pct90`.trim(),
color: stat.pct90,
unit,
defaultActive: false,
},
{
metric: pattern.pct10,
title: `${title} pct10`.trim(),
color: stat.pct10,
unit,
defaultActive: false,
},
{ metric: pattern.sum, title: `${title} sum`.trim(), color: stat.sum, unit, defaultActive: false },
{ metric: pattern.cumulative, title: `${title} cumulative`.trim(), color: stat.cumulative, unit, defaultActive: false },
...percentileSeries(colors, pattern, unit, title),
];
}
@@ -422,61 +238,8 @@ export function fromFullnessPattern(colors, pattern, unit, title = "") {
const { stat } = colors;
return [
{ metric: pattern.base, title: title || "base", unit },
{
metric: pattern.average,
title: `${title} avg`.trim(),
color: stat.avg,
unit,
},
{
metric: pattern.max,
title: `${title} max`.trim(),
color: stat.max,
unit,
defaultActive: false,
},
{
metric: pattern.min,
title: `${title} min`.trim(),
color: stat.min,
unit,
defaultActive: false,
},
{
metric: pattern.median,
title: `${title} median`.trim(),
color: stat.median,
unit,
defaultActive: false,
},
{
metric: pattern.pct75,
title: `${title} pct75`.trim(),
color: stat.pct75,
unit,
defaultActive: false,
},
{
metric: pattern.pct25,
title: `${title} pct25`.trim(),
color: stat.pct25,
unit,
defaultActive: false,
},
{
metric: pattern.pct90,
title: `${title} pct90`.trim(),
color: stat.pct90,
unit,
defaultActive: false,
},
{
metric: pattern.pct10,
title: `${title} pct10`.trim(),
color: stat.pct10,
unit,
defaultActive: false,
},
{ metric: pattern.average, title: `${title} avg`.trim(), color: stat.avg, unit },
...percentileSeries(colors, pattern, unit, title),
];
}
@@ -492,75 +255,10 @@ export function fromDollarsPattern(colors, pattern, unit, title = "") {
const { stat } = colors;
return [
{ metric: pattern.base, title: title || "base", unit },
{
metric: pattern.sum,
title: `${title} sum`.trim(),
color: stat.sum,
unit,
},
{
metric: pattern.cumulative,
title: `${title} cumulative`.trim(),
color: stat.cumulative,
unit,
defaultActive: false,
},
{
metric: pattern.average,
title: `${title} avg`.trim(),
color: stat.avg,
unit,
defaultActive: false,
},
{
metric: pattern.max,
title: `${title} max`.trim(),
color: stat.max,
unit,
defaultActive: false,
},
{
metric: pattern.min,
title: `${title} min`.trim(),
color: stat.min,
unit,
defaultActive: false,
},
{
metric: pattern.median,
title: `${title} median`.trim(),
color: stat.median,
unit,
defaultActive: false,
},
{
metric: pattern.pct75,
title: `${title} pct75`.trim(),
color: stat.pct75,
unit,
defaultActive: false,
},
{
metric: pattern.pct25,
title: `${title} pct25`.trim(),
color: stat.pct25,
unit,
defaultActive: false,
},
{
metric: pattern.pct90,
title: `${title} pct90`.trim(),
color: stat.pct90,
unit,
defaultActive: false,
},
{
metric: pattern.pct10,
title: `${title} pct10`.trim(),
color: stat.pct10,
unit,
defaultActive: false,
},
{ metric: pattern.sum, title: `${title} sum`.trim(), color: stat.sum, unit },
{ metric: pattern.cumulative, title: `${title} cumulative`.trim(), color: stat.cumulative, unit, defaultActive: false },
{ metric: pattern.average, title: `${title} avg`.trim(), color: stat.avg, unit, defaultActive: false },
...percentileSeries(colors, pattern, unit, title),
];
}
@@ -573,85 +271,41 @@ export function fromDollarsPattern(colors, pattern, unit, title = "") {
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function fromFeeRatePattern(colors, pattern, unit, title = "") {
const { stat } = colors;
return [
{
type: "Dots",
metric: pattern.average,
title: `${title} avg`.trim(),
unit,
},
{
type: "Dots",
metric: pattern.max,
title: `${title} max`.trim(),
color: stat.max,
unit,
defaultActive: false,
},
{
type: "Dots",
metric: pattern.min,
title: `${title} min`.trim(),
color: stat.min,
unit,
defaultActive: false,
},
{
type: "Dots",
metric: pattern.median,
title: `${title} median`.trim(),
color: stat.median,
unit,
defaultActive: false,
},
{
type: "Dots",
metric: pattern.pct75,
title: `${title} pct75`.trim(),
color: stat.pct75,
unit,
defaultActive: false,
},
{
type: "Dots",
metric: pattern.pct25,
title: `${title} pct25`.trim(),
color: stat.pct25,
unit,
defaultActive: false,
},
{
type: "Dots",
metric: pattern.pct90,
title: `${title} pct90`.trim(),
color: stat.pct90,
unit,
defaultActive: false,
},
{
type: "Dots",
metric: pattern.pct10,
title: `${title} pct10`.trim(),
color: stat.pct10,
unit,
defaultActive: false,
},
{ type: "Dots", metric: pattern.average, title: `${title} avg`.trim(), unit },
...percentileSeries(colors, pattern, unit, title, { type: "Dots" }),
];
}
/**
* Create series from a CoinbasePattern ({ sats, bitcoin, dollars } each as FullnessPattern)
* Create series from a pattern with sum and cumulative (fullness stats + sum + cumulative)
* @param {Colors} colors
* @param {FullnessPatternWithSumCumulative} pattern
* @param {Unit} unit
* @param {string} [title]
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function fromFullnessPatternWithSumCumulative(colors, pattern, unit, title = "") {
const { stat } = colors;
return [
...fromFullnessPattern(colors, pattern, unit, title),
{ metric: pattern.sum, title: `${title} sum`.trim(), color: stat.sum, unit },
{ metric: pattern.cumulative, title: `${title} cumulative`.trim(), color: stat.cumulative, unit, defaultActive: false },
];
}
/**
* Create series from a CoinbasePattern ({ sats, bitcoin, dollars } each with stats + sum + cumulative)
* @param {Colors} colors
* @param {CoinbasePattern} pattern
* @param {string} [title]
* @returns {AnyFetchedSeriesBlueprint[]}
*/
export function fromCoinbasePattern(colors, pattern, title) {
export function fromCoinbasePattern(colors, pattern, title = "") {
return [
...fromFullnessPattern(colors, pattern.bitcoin, Unit.btc, title),
...fromFullnessPattern(colors, pattern.sats, Unit.sats, title),
...fromFullnessPattern(colors, pattern.dollars, Unit.usd, title),
...fromFullnessPatternWithSumCumulative(colors, pattern.bitcoin, Unit.btc, title),
...fromFullnessPatternWithSumCumulative(colors, pattern.sats, Unit.sats, title),
...fromFullnessPatternWithSumCumulative(colors, pattern.dollars, Unit.usd, title),
];
}
@@ -797,62 +451,8 @@ export function fromIntervalPattern(colors, pattern, unit, title = "", color) {
const { stat } = colors;
return [
{ metric: pattern.base, title: title ?? "base", color, unit },
{
metric: pattern.average,
title: `${title} avg`.trim(),
color: stat.avg,
unit,
defaultActive: false,
},
{
metric: pattern.max,
title: `${title} max`.trim(),
color: stat.max,
unit,
defaultActive: false,
},
{
metric: pattern.min,
title: `${title} min`.trim(),
color: stat.min,
unit,
defaultActive: false,
},
{
metric: pattern.median,
title: `${title} median`.trim(),
color: stat.median,
unit,
defaultActive: false,
},
{
metric: pattern.pct75,
title: `${title} pct75`.trim(),
color: stat.pct75,
unit,
defaultActive: false,
},
{
metric: pattern.pct25,
title: `${title} pct25`.trim(),
color: stat.pct25,
unit,
defaultActive: false,
},
{
metric: pattern.pct90,
title: `${title} pct90`.trim(),
color: stat.pct90,
unit,
defaultActive: false,
},
{
metric: pattern.pct10,
title: `${title} pct10`.trim(),
color: stat.pct10,
unit,
defaultActive: false,
},
{ metric: pattern.average, title: `${title} avg`.trim(), color: stat.avg, unit, defaultActive: false },
...percentileSeries(colors, pattern, unit, title),
];
}

View File

@@ -4,6 +4,14 @@ import { Unit } from "../utils/units.js";
import { line, baseline } from "./series.js";
import { priceLine, priceLines } from "./constants.js";
/**
* Create a title formatter for chart titles
* @param {string} [cohortTitle]
* @returns {(metric: string) => string}
*/
export const formatCohortTitle = (cohortTitle) =>
(metric) => cohortTitle ? `${metric}: ${cohortTitle}` : metric;
/**
* Create sats/btc/usd line series from a pattern with .sats/.bitcoin/.dollars
* @param {{ sats: AnyMetricPattern, bitcoin: AnyMetricPattern, dollars: AnyMetricPattern }} pattern
@@ -144,7 +152,7 @@ export function ratioSmas(colors, ratio) {
* Create ratio chart from ActivePriceRatioPattern
* @param {PartialContext} ctx
* @param {Object} args
* @param {string} args.title
* @param {(metric: string) => string} args.title
* @param {AnyMetricPattern} args.price - The price metric to show in top pane
* @param {ActivePriceRatioPattern} args.ratio - The ratio pattern
* @param {Color} args.color
@@ -156,7 +164,7 @@ export function createRatioChart(ctx, { title, price, ratio, color, name }) {
return {
name: name ?? "ratio",
title: name ? (title ? `${name} - ${title}` : name) : `${title} Ratio`,
title: title(name ?? "Ratio"),
top: [
line({ metric: price, name: "price", color, unit: Unit.usd }),
...percentileUsdMap(colors, ratio).map(({ name, prop, color }) =>

View File

@@ -289,6 +289,11 @@
* @property {readonly AddressCohortObject[]} list
*
* @typedef {UtxoCohortGroupObject | AddressCohortGroupObject} CohortGroupObject
*
* @typedef {Object} CohortGroupAddress
* @property {string} name
* @property {string} title
* @property {readonly CohortAddress[]} list
*/
// Re-export for type consumers

View File

@@ -1,8 +1,19 @@
import { localhost } from "../utils/env.js";
import { serdeChartableIndex } from "../utils/serde.js";
/** @type {Map<AnyMetricPattern, string[]> | null} */
export const unused = localhost ? new Map() : null;
/**
* Check if a metric pattern has at least one chartable index
* @param {AnyMetricPattern} node
* @returns {boolean}
*/
function hasChartableIndex(node) {
const indexes = node.indexes();
return indexes.some((idx) => serdeChartableIndex.serialize(idx) !== null);
}
/**
* @param {TreeNode | null | undefined} node
* @param {Map<AnyMetricPattern, string[]>} map
@@ -10,7 +21,9 @@ export const unused = localhost ? new Map() : null;
*/
function walk(node, map, path) {
if (node && "by" in node) {
map.set(/** @type {AnyMetricPattern} */ (node), path);
const metricNode = /** @type {AnyMetricPattern} */ (node);
if (!hasChartableIndex(metricNode)) return;
map.set(metricNode, path);
} else if (node && typeof node === "object") {
for (const [key, value] of Object.entries(node)) {
const kn = key.toLowerCase();
@@ -19,12 +32,16 @@ function walk(node, map, path) {
kn === "time" ||
kn === "height" ||
kn === "constants" ||
kn === "blockhash" ||
kn === "oracle" ||
kn === "split" ||
kn === "ohlc" ||
kn === "outpoint" ||
kn === "positions" ||
kn === "outputtype" ||
kn === "heighttopool" ||
kn === "txid" ||
kn.endsWith("state") ||
kn.endsWith("index") ||
kn.endsWith("indexes") ||
kn.endsWith("bytes") ||