Files
brk/website/scripts/options/distribution/holdings.js
2026-03-15 00:57:53 +01:00

890 lines
27 KiB
JavaScript

/**
* Holdings section builders
*
* Supply pattern capabilities by cohort type:
* - DeltaHalfInRelTotalPattern2 (STH/LTH): inProfit + inLoss + relToCirculating + relToOwn
* - MetricsTree_Cohorts_Utxo_All_Supply (All): inProfit + inLoss + relToOwn (no relToCirculating)
* - DeltaHalfInRelTotalPattern (AgeRange/MaxAge/Epoch): inProfit + inLoss + relToCirculating (no relToOwn)
* - DeltaHalfInTotalPattern2 (Type.*): inProfit + inLoss (no rel)
* - DeltaHalfTotalPattern (Empty/UtxoAmount/AddrAmount): total + half only
*/
import { Unit } from "../../utils/units.js";
import { ROLLING_WINDOWS, line, baseline, rollingWindowsTree, rollingPercentRatioTree, percentRatio } from "../series.js";
import {
satsBtcUsd,
mapCohorts,
mapCohortsWithAll,
flatMapCohortsWithAll,
} from "../shared.js";
import { colors } from "../../utils/colors.js";
import { priceLines } from "../constants.js";
/**
* Simple supply series (total + half only, no profit/loss)
* @param {{ total: AnyValuePattern, half: AnyValuePattern }} supply
* @returns {AnyFetchedSeriesBlueprint[]}
*/
function simpleSupplySeries(supply) {
return [
...satsBtcUsd({
pattern: supply.total,
name: "Total",
color: colors.default,
}),
...satsBtcUsd({
pattern: supply.half,
name: "Halved",
color: colors.gray,
style: 4,
}),
];
}
/**
* Full supply series (total, profit, loss, halved)
* @param {{ total: AnyValuePattern, half: AnyValuePattern, inProfit: AnyValuePattern, inLoss: AnyValuePattern }} supply
* @returns {AnyFetchedSeriesBlueprint[]}
*/
function fullSupplySeries(supply) {
return [
...satsBtcUsd({
pattern: supply.total,
name: "Total",
color: colors.default,
}),
...satsBtcUsd({
pattern: supply.inProfit,
name: "In Profit",
color: colors.profit,
}),
...satsBtcUsd({
pattern: supply.inLoss,
name: "In Loss",
color: colors.loss,
}),
...satsBtcUsd({
pattern: supply.half,
name: "Halved",
color: colors.gray,
style: 4,
}),
];
}
/**
* % of Own Supply series (profit/loss relative to own supply)
* @param {{ inProfit: { relToOwn: { percent: AnyMetricPattern, ratio: AnyMetricPattern } }, inLoss: { relToOwn: { percent: AnyMetricPattern, ratio: AnyMetricPattern } } }} supply
* @returns {AnyFetchedSeriesBlueprint[]}
*/
function ownSupplyPctSeries(supply) {
return [
line({ metric: supply.inProfit.relToOwn.percent, name: "In Profit", color: colors.profit, unit: Unit.pctOwn }),
line({ metric: supply.inLoss.relToOwn.percent, name: "In Loss", color: colors.loss, unit: Unit.pctOwn }),
line({ metric: supply.inProfit.relToOwn.ratio, name: "In Profit", color: colors.profit, unit: Unit.ratio }),
line({ metric: supply.inLoss.relToOwn.ratio, name: "In Loss", color: colors.loss, unit: Unit.ratio }),
...priceLines({ numbers: [100, 50, 0], unit: Unit.pctOwn }),
];
}
/**
* % of Circulating Supply series (total, profit, loss)
* @param {{ relToCirculating: { percent: AnyMetricPattern }, inProfit: { relToCirculating: { percent: AnyMetricPattern } }, inLoss: { relToCirculating: { percent: AnyMetricPattern } } }} supply
* @returns {AnyFetchedSeriesBlueprint[]}
*/
function circulatingSupplyPctSeries(supply) {
return [
line({
metric: supply.relToCirculating.percent,
name: "Total",
color: colors.default,
unit: Unit.pctSupply,
}),
line({
metric: supply.inProfit.relToCirculating.percent,
name: "In Profit",
color: colors.profit,
unit: Unit.pctSupply,
}),
line({
metric: supply.inLoss.relToCirculating.percent,
name: "In Loss",
color: colors.loss,
unit: Unit.pctSupply,
}),
];
}
/**
* Ratio of Circulating Supply series (total, profit, loss)
* @param {{ relToCirculating: { ratio: AnyMetricPattern }, inProfit: { relToCirculating: { ratio: AnyMetricPattern } }, inLoss: { relToCirculating: { ratio: AnyMetricPattern } } }} supply
* @returns {AnyFetchedSeriesBlueprint[]}
*/
function circulatingSupplyRatioSeries(supply) {
return [
line({
metric: supply.relToCirculating.ratio,
name: "Total",
color: colors.default,
unit: Unit.ratio,
}),
line({
metric: supply.inProfit.relToCirculating.ratio,
name: "In Profit",
color: colors.profit,
unit: Unit.ratio,
}),
line({
metric: supply.inLoss.relToCirculating.ratio,
name: "In Loss",
color: colors.loss,
unit: Unit.ratio,
}),
];
}
/**
* @param {readonly (UtxoCohortObject | CohortWithoutRelative)[]} list
* @param {CohortAll} all
* @param {(metric: string) => string} title
*/
function groupedUtxoCountChart(list, all, title) {
return {
name: "UTXO Count",
title: title("UTXO Count"),
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
line({
metric: tree.outputs.unspentCount.inner,
name,
color,
unit: Unit.count,
}),
),
};
}
/**
* @param {{ absolute: { _24h: AnyMetricPattern, _1w: AnyMetricPattern, _1m: AnyMetricPattern, _1y: AnyMetricPattern }, rate: { _24h: { percent: AnyMetricPattern, ratio: AnyMetricPattern }, _1w: { percent: AnyMetricPattern, ratio: AnyMetricPattern }, _1m: { percent: AnyMetricPattern, ratio: AnyMetricPattern }, _1y: { percent: AnyMetricPattern, ratio: AnyMetricPattern } } }} delta
* @param {Unit} unit
* @param {(metric: string) => string} title
* @param {string} name
* @returns {PartialOptionsGroup}
*/
function singleDeltaTree(delta, unit, title, name) {
return {
name,
tree: [
{ ...rollingWindowsTree({ windows: delta.absolute, title: title(`${name} Change`), unit, series: baseline }), name: "Absolute" },
{ ...rollingPercentRatioTree({ windows: delta.rate, title: title(`${name} Rate`) }), name: "Rate" },
],
};
}
/**
* @template {{ name: string, color: Color }} T
* @template {{ name: string, color: Color }} A
* @param {readonly T[]} list
* @param {A} all
* @param {(c: T | A) => DeltaPattern} getDelta
* @param {Unit} unit
* @param {(metric: string) => string} title
* @param {string} name
* @returns {PartialOptionsGroup}
*/
function groupedDeltaTree(list, all, getDelta, unit, title, name) {
return {
name,
tree: [
{
name: "Absolute",
tree: ROLLING_WINDOWS.map((w) => ({
name: w.name,
title: title(`${name} Change (${w.name})`),
bottom: mapCohortsWithAll(list, all, (c) =>
baseline({ metric: getDelta(c).absolute[w.key], name: c.name, color: c.color, unit }),
),
})),
},
{
name: "Rate",
tree: ROLLING_WINDOWS.map((w) => ({
name: w.name,
title: title(`${name} Rate (${w.name})`),
bottom: flatMapCohortsWithAll(list, all, (c) =>
percentRatio({ pattern: getDelta(c).rate[w.key], name: c.name, color: c.color }),
),
})),
},
],
};
}
/**
* @param {UtxoCohortObject | CohortWithoutRelative} cohort
* @param {(metric: string) => string} title
* @returns {PartialChartOption}
*/
function singleUtxoCountChart(cohort, title) {
return {
name: "UTXO Count",
title: title("UTXO Count"),
bottom: [
line({
metric: cohort.tree.outputs.unspentCount.inner,
name: "UTXO Count",
color: cohort.color,
unit: Unit.count,
}),
],
};
}
/**
* @param {CohortAll | CohortAddress | AddressCohortObject} cohort
* @param {(metric: string) => string} title
* @returns {PartialChartOption}
*/
function singleAddressCountChart(cohort, title) {
return {
name: "Address Count",
title: title("Address Count"),
bottom: [
line({
metric: cohort.addressCount.inner,
name: "Address Count",
color: cohort.color,
unit: Unit.count,
}),
],
};
}
// ============================================================================
// Single Cohort Holdings Sections
// ============================================================================
/**
* Basic holdings (total + half only, no supply breakdown)
* For: CohortWithoutRelative, CohortBasicWithMarketCap, CohortBasicWithoutMarketCap
* @param {{ cohort: UtxoCohortObject | CohortWithoutRelative, title: (metric: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createHoldingsSection({ cohort, title }) {
return {
name: "Holdings",
tree: [
{
name: "Supply",
title: title("Supply"),
bottom: simpleSupplySeries(cohort.tree.supply),
},
singleUtxoCountChart(cohort, title),
{
name: "Change",
tree: [
singleDeltaTree(cohort.tree.supply.delta, Unit.sats, title, "Supply"),
singleDeltaTree(cohort.tree.outputs.unspentCount.delta, Unit.count, title, "UTXO Count"),
],
},
],
};
}
/**
* Holdings for CohortAll (has inProfit/inLoss with relToOwn but no relToCirculating)
* @param {{ cohort: CohortAll, title: (metric: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createHoldingsSectionAll({ cohort, title }) {
const { supply } = cohort.tree;
return {
name: "Holdings",
tree: [
{
name: "Supply",
title: title("Supply"),
bottom: [
...fullSupplySeries(supply),
...ownSupplyPctSeries(supply),
],
},
singleUtxoCountChart(cohort, title),
singleAddressCountChart(cohort, title),
{
name: "Change",
tree: [
singleDeltaTree(cohort.tree.supply.delta, Unit.sats, title, "Supply"),
singleDeltaTree(cohort.tree.outputs.unspentCount.delta, Unit.count, title, "UTXO Count"),
singleDeltaTree(cohort.addressCount.delta, Unit.count, title, "Address Count"),
],
},
],
};
}
/**
* Holdings with full relative metrics (relToCirculating + relToOwn)
* For: CohortFull, CohortLongTerm (have DeltaHalfInRelTotalPattern2)
* @param {{ cohort: CohortFull | CohortLongTerm, title: (metric: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createHoldingsSectionWithRelative({ cohort, title }) {
const { supply } = cohort.tree;
return {
name: "Holdings",
tree: [
{
name: "Supply",
tree: [
{
name: "Overview",
title: title("Supply"),
bottom: [
...fullSupplySeries(supply),
...circulatingSupplyPctSeries(supply),
...ownSupplyPctSeries(supply),
],
},
{
name: "Ratio",
title: title("Supply (% of Circulating)"),
bottom: circulatingSupplyRatioSeries(supply),
},
],
},
singleUtxoCountChart(cohort, title),
{
name: "Change",
tree: [
singleDeltaTree(cohort.tree.supply.delta, Unit.sats, title, "Supply"),
singleDeltaTree(cohort.tree.outputs.unspentCount.delta, Unit.count, title, "UTXO Count"),
],
},
],
};
}
/**
* Holdings with inProfit/inLoss + relToCirculating (no relToOwn)
* For: CohortWithAdjusted, CohortAgeRange (have DeltaHalfInRelTotalPattern)
* @param {{ cohort: CohortWithAdjusted | CohortAgeRange, title: (metric: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createHoldingsSectionWithOwnSupply({ cohort, title }) {
const { supply } = cohort.tree;
return {
name: "Holdings",
tree: [
{
name: "Supply",
tree: [
{
name: "Overview",
title: title("Supply"),
bottom: [
...fullSupplySeries(supply),
...circulatingSupplyPctSeries(supply),
],
},
{
name: "Ratio",
title: title("Supply (% of Circulating)"),
bottom: circulatingSupplyRatioSeries(supply),
},
],
},
singleUtxoCountChart(cohort, title),
{
name: "Change",
tree: [
singleDeltaTree(cohort.tree.supply.delta, Unit.sats, title, "Supply"),
singleDeltaTree(cohort.tree.outputs.unspentCount.delta, Unit.count, title, "UTXO Count"),
],
},
],
};
}
/**
* Holdings with inProfit/inLoss (no rel, no address count)
* For: CohortWithoutRelative (p2ms, unknown, empty)
* @param {{ cohort: CohortWithoutRelative, title: (metric: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createHoldingsSectionWithProfitLoss({ cohort, title }) {
return {
name: "Holdings",
tree: [
{
name: "Supply",
title: title("Supply"),
bottom: fullSupplySeries(cohort.tree.supply),
},
singleUtxoCountChart(cohort, title),
{
name: "Change",
tree: [
singleDeltaTree(cohort.tree.supply.delta, Unit.sats, title, "Supply"),
singleDeltaTree(cohort.tree.outputs.unspentCount.delta, Unit.count, title, "UTXO Count"),
],
},
],
};
}
/**
* Holdings for CohortAddress (has inProfit/inLoss but no rel, plus address count)
* @param {{ cohort: CohortAddress, title: (metric: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createHoldingsSectionAddress({ cohort, title }) {
return {
name: "Holdings",
tree: [
{
name: "Supply",
title: title("Supply"),
bottom: fullSupplySeries(cohort.tree.supply),
},
singleUtxoCountChart(cohort, title),
singleAddressCountChart(cohort, title),
{
name: "Change",
tree: [
singleDeltaTree(cohort.tree.supply.delta, Unit.sats, title, "Supply"),
singleDeltaTree(cohort.tree.outputs.unspentCount.delta, Unit.count, title, "UTXO Count"),
singleDeltaTree(cohort.addressCount.delta, Unit.count, title, "Address Count"),
],
},
],
};
}
/**
* Holdings for address amount cohorts (no inProfit/inLoss, has address count)
* @param {{ cohort: AddressCohortObject, title: (metric: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createHoldingsSectionAddressAmount({ cohort, title }) {
return {
name: "Holdings",
tree: [
{
name: "Supply",
title: title("Supply"),
bottom: simpleSupplySeries(cohort.tree.supply),
},
singleUtxoCountChart(cohort, title),
singleAddressCountChart(cohort, title),
{
name: "Change",
tree: [
singleDeltaTree(cohort.tree.supply.delta, Unit.sats, title, "Supply"),
singleDeltaTree(cohort.tree.outputs.unspentCount.delta, Unit.count, title, "UTXO Count"),
singleDeltaTree(cohort.addressCount.delta, Unit.count, title, "Address Count"),
],
},
],
};
}
// ============================================================================
// Grouped Cohort Holdings Sections
// ============================================================================
/**
* @param {{ list: readonly CohortAddress[], all: CohortAll, title: (metric: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createGroupedHoldingsSectionAddress({ list, all, title }) {
return {
name: "Holdings",
tree: [
{
name: "Supply",
tree: [
{
name: "Total",
title: title("Supply"),
bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) =>
satsBtcUsd({ pattern: tree.supply.total, name, color }),
),
},
{
name: "In Profit",
title: title("Supply In Profit"),
bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) =>
satsBtcUsd({
pattern: tree.supply.inProfit,
name,
color,
}),
),
},
{
name: "In Loss",
title: title("Supply In Loss"),
bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) =>
satsBtcUsd({
pattern: tree.supply.inLoss,
name,
color,
}),
),
},
],
},
groupedUtxoCountChart(list, all, title),
{
name: "Address Count",
title: title("Address Count"),
bottom: mapCohortsWithAll(list, all, ({ name, color, addressCount }) =>
line({ metric: addressCount.inner, name, color, unit: Unit.count }),
),
},
{
name: "Change",
tree: [
groupedDeltaTree(list, all, (c) => c.tree.supply.delta, Unit.sats, title, "Supply"),
groupedDeltaTree(list, all, (c) => c.tree.outputs.unspentCount.delta, Unit.count, title, "UTXO Count"),
groupedDeltaTree(list, all, (c) => c.addressCount.delta, Unit.count, title, "Address Count"),
],
},
],
};
}
/**
* Grouped holdings for address amount cohorts (no inProfit/inLoss, has address count)
* @param {{ list: readonly AddressCohortObject[], all: CohortAll, title: (metric: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createGroupedHoldingsSectionAddressAmount({
list,
all,
title,
}) {
return {
name: "Holdings",
tree: [
{
name: "Supply",
tree: [
{
name: "Total",
title: title("Supply"),
bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) =>
satsBtcUsd({ pattern: tree.supply.total, name, color }),
),
},
],
},
groupedUtxoCountChart(list, all, title),
{
name: "Address Count",
title: title("Address Count"),
bottom: mapCohortsWithAll(list, all, ({ name, color, addressCount }) =>
line({ metric: addressCount.inner, name, color, unit: Unit.count }),
),
},
{
name: "Change",
tree: [
groupedDeltaTree(list, all, (c) => c.tree.supply.delta, Unit.sats, title, "Supply"),
groupedDeltaTree(list, all, (c) => c.tree.outputs.unspentCount.delta, Unit.count, title, "UTXO Count"),
groupedDeltaTree(list, all, (c) => c.addressCount.delta, Unit.count, title, "Address Count"),
],
},
],
};
}
/**
* Basic grouped holdings (total + half only)
* @param {{ list: readonly (UtxoCohortObject | CohortWithoutRelative)[], all: CohortAll, title: (metric: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createGroupedHoldingsSection({ list, all, title }) {
return {
name: "Holdings",
tree: [
{
name: "Supply",
tree: [
{
name: "Total",
title: title("Supply"),
bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) =>
satsBtcUsd({ pattern: tree.supply.total, name, color }),
),
},
],
},
groupedUtxoCountChart(list, all, title),
{
name: "Change",
tree: [
groupedDeltaTree(list, all, (c) => c.tree.supply.delta, Unit.sats, title, "Supply"),
groupedDeltaTree(list, all, (c) => c.tree.outputs.unspentCount.delta, Unit.count, title, "UTXO Count"),
],
},
],
};
}
/**
* Grouped holdings with inProfit/inLoss (no rel, no address count)
* For: CohortWithoutRelative (p2ms, unknown, empty)
* @param {{ list: readonly CohortWithoutRelative[], all: CohortAll, title: (metric: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createGroupedHoldingsSectionWithProfitLoss({
list,
all,
title,
}) {
return {
name: "Holdings",
tree: [
{
name: "Supply",
tree: [
{
name: "Total",
title: title("Supply"),
bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) =>
satsBtcUsd({ pattern: tree.supply.total, name, color }),
),
},
{
name: "In Profit",
title: title("Supply In Profit"),
bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) =>
satsBtcUsd({
pattern: tree.supply.inProfit,
name,
color,
}),
),
},
{
name: "In Loss",
title: title("Supply In Loss"),
bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) =>
satsBtcUsd({
pattern: tree.supply.inLoss,
name,
color,
}),
),
},
],
},
groupedUtxoCountChart(list, all, title),
{
name: "Change",
tree: [
groupedDeltaTree(list, all, (c) => c.tree.supply.delta, Unit.sats, title, "Supply"),
groupedDeltaTree(list, all, (c) => c.tree.outputs.unspentCount.delta, Unit.count, title, "UTXO Count"),
],
},
],
};
}
/**
* Grouped holdings with inProfit/inLoss + relToCirculating (no relToOwn)
* For: CohortWithAdjusted, CohortAgeRange
* @param {{ list: readonly (CohortWithAdjusted | CohortAgeRange)[], all: CohortAll, title: (metric: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createGroupedHoldingsSectionWithOwnSupply({
list,
all,
title,
}) {
return {
name: "Holdings",
tree: [
{
name: "Supply",
tree: [
{
name: "Total",
title: title("Supply"),
bottom: [
...flatMapCohortsWithAll(list, all, ({ name, color, tree }) =>
satsBtcUsd({ pattern: tree.supply.total, name, color }),
),
...mapCohorts(list, ({ name, color, tree }) =>
line({
metric: tree.supply.relToCirculating.percent,
name,
color,
unit: Unit.pctSupply,
}),
),
],
},
{
name: "In Profit",
title: title("Supply In Profit"),
bottom: [
...flatMapCohortsWithAll(list, all, ({ name, color, tree }) =>
satsBtcUsd({
pattern: tree.supply.inProfit,
name,
color,
}),
),
...mapCohorts(list, ({ name, color, tree }) =>
line({
metric: tree.supply.inProfit.relToCirculating.percent,
name,
color,
unit: Unit.pctSupply,
}),
),
],
},
{
name: "In Loss",
title: title("Supply In Loss"),
bottom: [
...flatMapCohortsWithAll(list, all, ({ name, color, tree }) =>
satsBtcUsd({
pattern: tree.supply.inLoss,
name,
color,
}),
),
...mapCohorts(list, ({ name, color, tree }) =>
line({
metric: tree.supply.inLoss.relToCirculating.percent,
name,
color,
unit: Unit.pctSupply,
}),
),
],
},
],
},
groupedUtxoCountChart(list, all, title),
{
name: "Change",
tree: [
groupedDeltaTree(list, all, (c) => c.tree.supply.delta, Unit.sats, title, "Supply"),
groupedDeltaTree(list, all, (c) => c.tree.outputs.unspentCount.delta, Unit.count, title, "UTXO Count"),
],
},
],
};
}
/**
* Grouped holdings with full relative metrics (relToCirculating + relToOwn)
* For: CohortFull, CohortLongTerm
* @param {{ list: readonly (CohortFull | CohortLongTerm)[], all: CohortAll, title: (metric: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createGroupedHoldingsSectionWithRelative({ list, all, title }) {
return {
name: "Holdings",
tree: [
{
name: "Supply",
tree: [
{
name: "Total",
title: title("Supply"),
bottom: [
...flatMapCohortsWithAll(list, all, ({ name, color, tree }) =>
satsBtcUsd({ pattern: tree.supply.total, name, color }),
),
...mapCohorts(list, ({ name, color, tree }) =>
line({
metric: tree.supply.relToCirculating.percent,
name,
color,
unit: Unit.pctSupply,
}),
),
],
},
{
name: "In Profit",
title: title("Supply In Profit"),
bottom: [
...flatMapCohortsWithAll(list, all, ({ name, color, tree }) =>
satsBtcUsd({
pattern: tree.supply.inProfit,
name,
color,
}),
),
...mapCohorts(list, ({ name, color, tree }) =>
line({
metric: tree.supply.inProfit.relToCirculating.percent,
name,
color,
unit: Unit.pctSupply,
}),
),
...mapCohortsWithAll(list, all, ({ name, color, tree }) =>
line({
metric: tree.supply.inProfit.relToOwn.percent,
name,
color,
unit: Unit.pctOwn,
}),
),
...priceLines({ numbers: [100, 50, 0], unit: Unit.pctOwn }),
],
},
{
name: "In Loss",
title: title("Supply In Loss"),
bottom: [
...flatMapCohortsWithAll(list, all, ({ name, color, tree }) =>
satsBtcUsd({
pattern: tree.supply.inLoss,
name,
color,
}),
),
...mapCohorts(list, ({ name, color, tree }) =>
line({
metric: tree.supply.inLoss.relToCirculating.percent,
name,
color,
unit: Unit.pctSupply,
}),
),
...mapCohortsWithAll(list, all, ({ name, color, tree }) =>
line({
metric: tree.supply.inLoss.relToOwn.percent,
name,
color,
unit: Unit.pctOwn,
}),
),
...priceLines({ numbers: [100, 50, 0], unit: Unit.pctOwn }),
],
},
],
},
groupedUtxoCountChart(list, all, title),
{
name: "Change",
tree: [
groupedDeltaTree(list, all, (c) => c.tree.supply.delta, Unit.sats, title, "Supply"),
groupedDeltaTree(list, all, (c) => c.tree.outputs.unspentCount.delta, Unit.count, title, "UTXO Count"),
],
},
],
};
}