diff --git a/website/scripts/chart/index.js b/website/scripts/chart/index.js index b2795c627..e3908b865 100644 --- a/website/scripts/chart/index.js +++ b/website/scripts/chart/index.js @@ -16,6 +16,7 @@ import { throttle, debounce } from "../utils/timing.js"; import { serdeBool, serdeChartableIndex } from "../utils/serde.js"; import { stringToId, numberToShortUSFormat } from "../utils/format.js"; import { style } from "../utils/elements.js"; +import { Unit } from "../utils/units.js"; /** * @typedef {_ISeriesApi} ISeries @@ -1275,9 +1276,8 @@ export function createChart({ parent, id: chartId, brk, fitContent }) { /** * @param {number} paneIndex - * @param {Unit} unit */ - function applyScaleForUnit(paneIndex, unit) { + function applyScaleForUnit(paneIndex) { const id = `${storageId}-scale`; const defaultValue = paneIndex === 0 ? "log" : "lin"; @@ -1351,7 +1351,7 @@ export function createChart({ parent, id: chartId, brk, fitContent }) { const options = blueprint.options; const indexes = Object.keys(blueprint.metric.by); - const defaultColor = unit.id === "usd" ? colors.green : colors.orange; + const defaultColor = unit === Unit.usd || unit === Unit.usdCumulative ? colors.green : colors.orange; if (indexes.includes(idx)) { switch (blueprint.type) { @@ -1443,7 +1443,7 @@ export function createChart({ parent, id: chartId, brk, fitContent }) { oldSeries.forEach((s) => s.remove()); // Store scale config - it will be applied when createForPane runs after updateVisibility - applyScaleForUnit(paneIndex, unit); + applyScaleForUnit(paneIndex); }, rebuild() { @@ -1480,10 +1480,6 @@ export function createChart({ parent, id: chartId, brk, fitContent }) { } const defaultUnit = units[0]; - const sortedUnitIds = units - .map((u) => u.id) - .sort() - .join(","); const persistedUnit = createPersistedValue({ defaultValue: /** @type {string} */ (defaultUnit.id), storageKey: `${storageId}-p${paneIndex}-unit`, diff --git a/website/scripts/options/chain.js b/website/scripts/options/chain.js index 813150597..b803514eb 100644 --- a/website/scripts/options/chain.js +++ b/website/scripts/options/chain.js @@ -191,6 +191,7 @@ export function createChainSection(ctx) { bottom: fromFullStatsPattern({ pattern: distribution.newAddrCount[key], unit: Unit.count, + cumulativeUnit: Unit.countCumulative, }), }, { @@ -316,20 +317,17 @@ export function createChainSection(ctx) { ...fromValuePattern({ pattern: pool.coinbase, title: "coinbase", - sumColor: colors.orange, - cumulativeColor: colors.red, + color: colors.orange, }), ...fromValuePattern({ pattern: pool.subsidy, title: "subsidy", - sumColor: colors.lime, - cumulativeColor: colors.emerald, + color: colors.lime, }), ...fromValuePattern({ pattern: pool.fee, title: "fee", - sumColor: colors.cyan, - cumulativeColor: colors.indigo, + color: colors.cyan, }), ], }, @@ -367,6 +365,7 @@ export function createChainSection(ctx) { ...fromCountPattern({ pattern: blocks.count.blockCount, unit: Unit.count, + cumulativeUnit: Unit.countCumulative, }), line({ metric: blocks.count.blockCountTarget, @@ -424,6 +423,7 @@ export function createChainSection(ctx) { ...fromSumStatsPattern({ pattern: blocks.size, unit: Unit.bytes, + cumulativeUnit: Unit.bytesCumulative, }), line({ metric: blocks.totalSize, @@ -440,20 +440,6 @@ export function createChainSection(ctx) { pattern: blocks.weight, unit: 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, - }), ], }, { @@ -477,6 +463,7 @@ export function createChainSection(ctx) { bottom: fromFullStatsPattern({ pattern: transactions.count.txCount, unit: Unit.count, + cumulativeUnit: Unit.countCumulative, }), }, { @@ -541,23 +528,23 @@ export function createChainSection(ctx) { ...fromCountPattern({ pattern: transactions.versions.v1, unit: Unit.count, + cumulativeUnit: Unit.countCumulative, title: "v1", - sumColor: colors.orange, - cumulativeColor: colors.red, + color: colors.orange, }), ...fromCountPattern({ pattern: transactions.versions.v2, unit: Unit.count, + cumulativeUnit: Unit.countCumulative, title: "v2", - sumColor: colors.cyan, - cumulativeColor: colors.blue, + color: colors.cyan, }), ...fromCountPattern({ pattern: transactions.versions.v3, unit: Unit.count, + cumulativeUnit: Unit.countCumulative, title: "v3", - sumColor: colors.lime, - cumulativeColor: colors.green, + color: colors.lime, }), ], }, @@ -592,6 +579,7 @@ export function createChainSection(ctx) { ...fromSumStatsPattern({ pattern: inputs.count, unit: Unit.count, + cumulativeUnit: Unit.countCumulative, }), ], }, @@ -602,6 +590,7 @@ export function createChainSection(ctx) { ...fromSumStatsPattern({ pattern: outputs.count.totalCount, unit: Unit.count, + cumulativeUnit: Unit.countCumulative, }), ], }, @@ -658,6 +647,7 @@ export function createChainSection(ctx) { bottom: fromFullStatsPattern({ pattern: scripts.count.p2pkh, unit: Unit.count, + cumulativeUnit: Unit.countCumulative, }), }, { @@ -666,6 +656,7 @@ export function createChainSection(ctx) { bottom: fromFullStatsPattern({ pattern: scripts.count.p2pk33, unit: Unit.count, + cumulativeUnit: Unit.countCumulative, }), }, { @@ -674,6 +665,7 @@ export function createChainSection(ctx) { bottom: fromFullStatsPattern({ pattern: scripts.count.p2pk65, unit: Unit.count, + cumulativeUnit: Unit.countCumulative, }), }, ], @@ -688,6 +680,7 @@ export function createChainSection(ctx) { bottom: fromFullStatsPattern({ pattern: scripts.count.p2sh, unit: Unit.count, + cumulativeUnit: Unit.countCumulative, }), }, { @@ -696,6 +689,7 @@ export function createChainSection(ctx) { bottom: fromFullStatsPattern({ pattern: scripts.count.p2ms, unit: Unit.count, + cumulativeUnit: Unit.countCumulative, }), }, ], @@ -710,6 +704,7 @@ export function createChainSection(ctx) { bottom: fromFullStatsPattern({ pattern: scripts.count.segwit, unit: Unit.count, + cumulativeUnit: Unit.countCumulative, }), }, { @@ -718,6 +713,7 @@ export function createChainSection(ctx) { bottom: fromFullStatsPattern({ pattern: scripts.count.p2wpkh, unit: Unit.count, + cumulativeUnit: Unit.countCumulative, }), }, { @@ -726,6 +722,7 @@ export function createChainSection(ctx) { bottom: fromFullStatsPattern({ pattern: scripts.count.p2wsh, unit: Unit.count, + cumulativeUnit: Unit.countCumulative, }), }, ], @@ -740,6 +737,7 @@ export function createChainSection(ctx) { bottom: fromFullStatsPattern({ pattern: scripts.count.p2tr, unit: Unit.count, + cumulativeUnit: Unit.countCumulative, }), }, { @@ -748,6 +746,7 @@ export function createChainSection(ctx) { bottom: fromFullStatsPattern({ pattern: scripts.count.p2a, unit: Unit.count, + cumulativeUnit: Unit.countCumulative, }), }, ], @@ -762,6 +761,7 @@ export function createChainSection(ctx) { bottom: fromFullStatsPattern({ pattern: scripts.count.opreturn, unit: Unit.count, + cumulativeUnit: Unit.countCumulative, }), }, { @@ -770,6 +770,7 @@ export function createChainSection(ctx) { bottom: fromFullStatsPattern({ pattern: scripts.count.emptyoutput, unit: Unit.count, + cumulativeUnit: Unit.countCumulative, }), }, { @@ -778,6 +779,7 @@ export function createChainSection(ctx) { bottom: fromFullStatsPattern({ pattern: scripts.count.unknownoutput, unit: Unit.count, + cumulativeUnit: Unit.countCumulative, }), }, ], @@ -799,15 +801,13 @@ export function createChainSection(ctx) { 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, + color: colors.red, unit: Unit.percentage, - defaultActive: false, }), ], }, @@ -823,15 +823,13 @@ export function createChainSection(ctx) { 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, + color: colors.red, unit: Unit.percentage, - defaultActive: false, }), ], }, @@ -926,14 +924,17 @@ export function createChainSection(ctx) { ...fromSumStatsPattern({ pattern: transactions.fees.fee.bitcoin, unit: Unit.btc, + cumulativeUnit: Unit.btcCumulative, }), ...fromSumStatsPattern({ pattern: transactions.fees.fee.sats, unit: Unit.sats, + cumulativeUnit: Unit.satsCumulative, }), ...fromSumStatsPattern({ pattern: transactions.fees.fee.dollars, unit: Unit.usd, + cumulativeUnit: Unit.usdCumulative, }), line({ metric: blocks.rewards.feeDominance, @@ -949,7 +950,6 @@ export function createChainSection(ctx) { title: "Unclaimed Rewards", bottom: fromValuePattern({ pattern: blocks.rewards.unclaimedRewards, - title: "Unclaimed", }), }, ], diff --git a/website/scripts/options/colors/cohorts.js b/website/scripts/options/colors/cohorts.js index 3c03cacf8..a15b6aad5 100644 --- a/website/scripts/options/colors/cohorts.js +++ b/website/scripts/options/colors/cohorts.js @@ -147,6 +147,7 @@ export const spendableTypeColors = { p2wsh: "blue", p2tr: "indigo", p2a: "purple", + opreturn: "pink", unknown: "violet", empty: "fuchsia", }; diff --git a/website/scripts/options/context.js b/website/scripts/options/context.js index 45b938000..bfc1ea281 100644 --- a/website/scripts/options/context.js +++ b/website/scripts/options/context.js @@ -43,9 +43,9 @@ export function createContext({ brk }) { fromFullStatsPattern: bind(fromFullStatsPattern), fromStatsPattern: bind(fromStatsPattern), fromCoinbasePattern: bind(fromCoinbasePattern), - fromValuePattern: bind(fromValuePattern), - fromBitcoinPatternWithUnit: bind(fromBitcoinPatternWithUnit), - fromCountPattern: bind(fromCountPattern), + fromValuePattern, + fromBitcoinPatternWithUnit, + fromCountPattern, fromSupplyPattern, }; } diff --git a/website/scripts/options/distribution/utxo.js b/website/scripts/options/distribution/utxo.js index a18cd2cda..eb761e251 100644 --- a/website/scripts/options/distribution/utxo.js +++ b/website/scripts/options/distribution/utxo.js @@ -994,8 +994,9 @@ function createSingleRealizedPnlSection( ...fromCountPattern({ pattern: tree.realized.realizedProfit, unit: Unit.usd, + cumulativeUnit: Unit.usdCumulative, title: "Profit", - sumColor: colors.green, + color: colors.green, }), line({ metric: tree.realized.realizedProfit7dEma, @@ -1006,8 +1007,9 @@ function createSingleRealizedPnlSection( ...fromCountPattern({ pattern: tree.realized.realizedLoss, unit: Unit.usd, + cumulativeUnit: Unit.usdCumulative, title: "Loss", - sumColor: colors.red, + color: colors.red, }), line({ metric: tree.realized.realizedLoss7dEma, @@ -1018,8 +1020,9 @@ function createSingleRealizedPnlSection( ...fromBitcoinPatternWithUnit({ pattern: tree.realized.negRealizedLoss, unit: Unit.usd, + cumulativeUnit: Unit.usdCumulative, title: "Negative Loss", - sumColor: colors.red, + color: colors.red, defaultActive: false, }), ...extra, @@ -1067,6 +1070,7 @@ function createSingleRealizedPnlSection( ...fromCountPattern({ pattern: tree.realized.netRealizedPnl, unit: Unit.usd, + cumulativeUnit: Unit.usdCumulative, title: "Net", }), baseline({ @@ -2904,17 +2908,20 @@ function createActivitySection({ ctx, cohort, title, valueMetrics = [] }) { ...fromCountPattern({ pattern: tree.activity.sent.sats, unit: Unit.sats, - sumColor: color, + cumulativeUnit: Unit.satsCumulative, + color: color, }), ...fromBitcoinPatternWithUnit({ pattern: tree.activity.sent.bitcoin, unit: Unit.btc, - sumColor: color, + cumulativeUnit: Unit.btcCumulative, + color: color, }), ...fromCountPattern({ pattern: tree.activity.sent.dollars, unit: Unit.usd, - sumColor: color, + cumulativeUnit: Unit.usdCumulative, + color: color, }), line({ metric: tree.activity.sent14dEma.sats, diff --git a/website/scripts/options/investing.js b/website/scripts/options/investing.js index 805a0013a..58cb63afc 100644 --- a/website/scripts/options/investing.js +++ b/website/scripts/options/investing.js @@ -1,11 +1,72 @@ /** Investing section - Investment strategy tools and analysis */ import { Unit } from "../utils/units.js"; -import { priceLine } from "./constants.js"; import { line, baseline, price, dotted } from "./series.js"; import { satsBtcUsd } from "./shared.js"; import { periodIdToName } from "./utils.js"; +const SHORT_PERIODS = /** @type {const} */ (["_1w", "_1m", "_3m", "_6m", "_1y"]); +const LONG_PERIODS = /** @type {const} */ (["_2y", "_3y", "_4y", "_5y", "_6y", "_8y", "_10y"]); + +/** @typedef {typeof SHORT_PERIODS[number]} ShortPeriodKey */ +/** @typedef {typeof LONG_PERIODS[number]} LongPeriodKey */ +/** @typedef {ShortPeriodKey | LongPeriodKey} AllPeriodKey */ + +/** + * Add CAGR to a base entry item + * @param {BaseEntryItem} entry + * @param {AnyMetricPattern} cagr + * @returns {LongEntryItem} + */ +const withCagr = (entry, cagr) => ({ ...entry, cagr }); + +const YEARS_2020S = /** @type {const} */ ([2026, 2025, 2024, 2023, 2022, 2021, 2020]); +const YEARS_2010S = /** @type {const} */ ([2019, 2018, 2017, 2016, 2015]); + +/** @param {AllPeriodKey} key */ +const periodName = (key) => periodIdToName(key.slice(1), true); + +/** + * Base entry item for compare and single-entry charts + * @typedef {Object} BaseEntryItem + * @property {string} name - Display name + * @property {Color} color - Item color + * @property {AnyPricePattern} costBasis - Cost basis metric + * @property {AnyMetricPattern} returns - Returns metric + * @property {AnyMetricPattern} minReturn - Min return metric + * @property {AnyMetricPattern} maxReturn - Max return metric + * @property {AnyMetricPattern} daysInProfit - Days in profit metric + * @property {AnyMetricPattern} daysInLoss - Days in loss metric + * @property {AnyValuePattern} stack - Stack pattern + */ + +/** + * Long-term entry item with CAGR + * @typedef {BaseEntryItem & { cagr: AnyMetricPattern }} LongEntryItem + */ + +/** + * Build DCA class entry from year + * @param {Colors} colors + * @param {MarketDca} dca + * @param {number} year + * @returns {BaseEntryItem} + */ +function buildYearEntry(colors, dca, year) { + const key = /** @type {keyof Colors["dcaYears"]} */ (`_${year}`); + return { + name: `${year}`, + color: colors.dcaYears[key], + costBasis: dca.classAveragePrice[key], + returns: dca.classReturns[key], + minReturn: dca.classMinReturn[key], + maxReturn: dca.classMaxReturn[key], + daysInProfit: dca.classDaysInProfit[key], + daysInLoss: dca.classDaysInLoss[key], + stack: dca.classStack[key], + }; +} + /** * Create Investing section * @param {PartialContext} ctx @@ -20,99 +81,17 @@ export function createInvestingSection(ctx) { name: "Investing", tree: [ createDcaVsLumpSumSection(ctx, { dca, lookback, returns }), - createDcaByPeriodSection(ctx, { dca }), - createLumpSumByPeriodSection(ctx, { dca, lookback }), + createDcaByPeriodSection(ctx, { dca, returns }), + createLumpSumByPeriodSection(ctx, { dca, lookback, returns }), createDcaByStartYearSection(ctx, { dca }), ], }; } -/** Period configuration by term group */ -const PERIODS = { - short: [ - { id: "1w", key: /** @type {const} */ ("_1w") }, - { id: "1m", key: /** @type {const} */ ("_1m") }, - { id: "3m", key: /** @type {const} */ ("_3m") }, - { id: "6m", key: /** @type {const} */ ("_6m") }, - ], - medium: [ - { id: "1y", key: /** @type {const} */ ("_1y") }, - { id: "2y", key: /** @type {const} */ ("_2y") }, - { id: "3y", key: /** @type {const} */ ("_3y") }, - ], - long: [ - { id: "4y", key: /** @type {const} */ ("_4y") }, - { id: "5y", key: /** @type {const} */ ("_5y") }, - { id: "6y", key: /** @type {const} */ ("_6y") }, - { id: "8y", key: /** @type {const} */ ("_8y") }, - { id: "10y", key: /** @type {const} */ ("_10y") }, - ], -}; - -const ALL_PERIODS = [...PERIODS.short, ...PERIODS.medium, ...PERIODS.long]; - -/** DCA year classes by decade */ -const YEAR_GROUPS = { - _2020s: /** @type {const} */ ([2026, 2025, 2024, 2023, 2022, 2021, 2020]), - _2010s: /** @type {const} */ ([2019, 2018, 2017, 2016, 2015]), -}; - -const ALL_YEARS = [...YEAR_GROUPS._2020s, ...YEAR_GROUPS._2010s]; - -/** @typedef {ReturnType} YearClass */ - -/** - * Build DCA class data from year - * @param {Colors} colors - * @param {MarketDca} dca - * @param {number} year - */ -function buildYearClass(colors, dca, year) { - const key = /** @type {keyof Colors["dcaYears"]} */ (`_${year}`); - return { - year, - color: colors.dcaYears[key], - costBasis: dca.classAveragePrice[key], - returns: dca.classReturns[key], - stack: dca.classStack[key], - daysInProfit: dca.classDaysInProfit[key], - daysInLoss: dca.classDaysInLoss[key], - minReturn: dca.classMinReturn[key], - maxReturn: dca.classMaxReturn[key], - }; -} - -/** - * Pattern for creating a single entry (period or year) - * @typedef {Object} SingleEntryPattern - * @property {string} name - Display name - * @property {string} [titlePrefix] - Prefix for chart titles (defaults to name) - * @property {Color} color - Primary color - * @property {AnyPricePattern} costBasis - Cost basis metric - * @property {AnyMetricPattern} returns - Returns metric - * @property {AnyMetricPattern} minReturn - Min return metric - * @property {AnyMetricPattern} maxReturn - Max return metric - * @property {AnyMetricPattern} daysInProfit - Days in profit metric - * @property {AnyMetricPattern} daysInLoss - Days in loss metric - * @property {AnyValuePattern} stack - Stack pattern - */ - -/** - * Item for compare charts - * @typedef {Object} CompareItem - * @property {string} name - Display name - * @property {Color} color - Item color - * @property {AnyPricePattern} costBasis - Cost basis metric - * @property {AnyMetricPattern} returns - Returns metric - * @property {AnyMetricPattern} daysInProfit - Days in profit metric - * @property {AnyMetricPattern} daysInLoss - Days in loss metric - * @property {AnyValuePattern} stack - Stack pattern - */ - /** * Create profitability folder for compare charts * @param {string} context - * @param {CompareItem[]} items + * @param {Pick[]} items */ function createProfitabilityFolder(context, items) { const top = items.map(({ name, color, costBasis }) => @@ -144,7 +123,7 @@ function createProfitabilityFolder(context, items) { /** * Create compare folder from items * @param {string} context - * @param {CompareItem[]} items + * @param {Pick[]} items */ function createCompareFolder(context, items) { const topPane = items.map(({ name, color, costBasis }) => @@ -163,12 +142,7 @@ function createCompareFolder(context, items) { title: `Returns: ${context}`, top: topPane, bottom: items.map(({ name, color, returns }) => - baseline({ - metric: returns, - name, - color: [color, color], - unit: Unit.percentage, - }), + baseline({ metric: returns, name, color: [color, color], unit: Unit.percentage }), ), }, createProfitabilityFolder(context, items), @@ -185,79 +159,62 @@ function createCompareFolder(context, items) { } /** - * Create a single entry from a pattern + * Create single entry tree structure * @param {Colors} colors - * @param {SingleEntryPattern} pattern + * @param {BaseEntryItem & { titlePrefix?: string }} item + * @param {object[]} returnsBottom - Bottom pane items for returns chart */ -function createSingleEntry(colors, pattern) { - const { - name, - titlePrefix = name, - color, - costBasis, - returns, - minReturn, - maxReturn, - daysInProfit, - daysInLoss, - stack, - } = pattern; +function createSingleEntryTree(colors, item, returnsBottom) { + const { name, titlePrefix = name, color, costBasis, daysInProfit, daysInLoss, stack } = item; const top = [price({ metric: costBasis, name: "Cost Basis", color })]; return { name, tree: [ { name: "Cost Basis", title: `Cost Basis: ${titlePrefix}`, top }, - { - name: "Returns", - title: `Returns: ${titlePrefix}`, - top, - bottom: [ - baseline({ metric: returns, name: "Current", unit: Unit.percentage }), - dotted({ - metric: maxReturn, - name: "Max", - color: colors.green, - unit: Unit.percentage, - defaultActive: false, - }), - dotted({ - metric: minReturn, - name: "Min", - color: colors.red, - unit: Unit.percentage, - defaultActive: false, - }), - ], - }, + { name: "Returns", title: `Returns: ${titlePrefix}`, top, bottom: returnsBottom }, { name: "Profitability", title: `Profitability: ${titlePrefix}`, top, bottom: [ - line({ - metric: daysInProfit, - name: "Days in Profit", - color: colors.green, - unit: Unit.days, - }), - line({ - metric: daysInLoss, - name: "Days in Loss", - color: colors.red, - unit: Unit.days, - }), + line({ metric: daysInProfit, name: "Days in Profit", color: colors.green, unit: Unit.days }), + line({ metric: daysInLoss, name: "Days in Loss", color: colors.red, unit: Unit.days }), ], }, - { - name: "Accumulated", - title: `Accumulated Value: ${titlePrefix}`, - top, - bottom: satsBtcUsd({ pattern: stack, name: "Value" }), - }, + { name: "Accumulated", title: `Accumulated Value: ${titlePrefix}`, top, bottom: satsBtcUsd({ pattern: stack, name: "Value" }) }, ], }; } +/** + * Create a single entry from a base item (no CAGR) + * @param {Colors} colors + * @param {BaseEntryItem & { titlePrefix?: string }} item + */ +function createShortSingleEntry(colors, item) { + const { returns, minReturn, maxReturn } = item; + return createSingleEntryTree(colors, item, [ + baseline({ metric: returns, name: "Current", unit: Unit.percentage }), + dotted({ metric: maxReturn, name: "Max", color: colors.green, unit: Unit.percentage, defaultActive: false }), + dotted({ metric: minReturn, name: "Min", color: colors.red, unit: Unit.percentage, defaultActive: false }), + ]); +} + +/** + * Create a single entry from a long item (with CAGR) + * @param {Colors} colors + * @param {LongEntryItem & { titlePrefix?: string }} item + */ +function createLongSingleEntry(colors, item) { + const { returns, minReturn, maxReturn, cagr } = item; + return createSingleEntryTree(colors, item, [ + baseline({ metric: returns, name: "Current", unit: Unit.percentage }), + baseline({ metric: cagr, name: "CAGR", unit: Unit.cagr }), + dotted({ metric: maxReturn, name: "Max", color: colors.green, unit: Unit.percentage, defaultActive: false }), + dotted({ metric: minReturn, name: "Min", color: colors.red, unit: Unit.percentage, defaultActive: false }), + ]); +} + /** * Create DCA vs Lump Sum section * @param {PartialContext} ctx @@ -269,14 +226,9 @@ function createSingleEntry(colors, pattern) { export function createDcaVsLumpSumSection(ctx, { dca, lookback, returns }) { const { colors } = ctx; - // Chart builders /** @param {AllPeriodKey} key */ const topPane = (key) => [ - price({ - metric: dca.periodAveragePrice[key], - name: "DCA", - color: colors.green, - }), + price({ metric: dca.periodAveragePrice[key], name: "DCA", color: colors.green }), price({ metric: lookback[key], name: "Lump Sum", color: colors.orange }), ]; @@ -288,7 +240,29 @@ export function createDcaVsLumpSumSection(ctx, { dca, lookback, returns }) { }); /** @param {string} name @param {AllPeriodKey} key */ - const returnsFolder = (name, key) => ({ + const returnsMinMax = (name, key) => [ + { + name: "Max", + title: `Max Return: ${name} DCA vs Lump Sum`, + top: topPane(key), + bottom: [ + baseline({ metric: dca.periodMaxReturn[key], name: "DCA", unit: Unit.percentage }), + baseline({ metric: dca.periodLumpSumMaxReturn[key], name: "Lump Sum", color: [colors.cyan, colors.orange], unit: Unit.percentage }), + ], + }, + { + name: "Min", + title: `Min Return: ${name} DCA vs Lump Sum`, + top: topPane(key), + bottom: [ + baseline({ metric: dca.periodMinReturn[key], name: "DCA", unit: Unit.percentage }), + baseline({ metric: dca.periodLumpSumMinReturn[key], name: "Lump Sum", color: [colors.cyan, colors.orange], unit: Unit.percentage }), + ], + }, + ]; + + /** @param {string} name @param {ShortPeriodKey} key */ + const shortReturnsFolder = (name, key) => ({ name: "Returns", tree: [ { @@ -296,60 +270,16 @@ export function createDcaVsLumpSumSection(ctx, { dca, lookback, returns }) { title: `Returns: ${name} DCA vs Lump Sum`, top: topPane(key), bottom: [ - baseline({ - metric: dca.periodReturns[key], - name: "DCA", - unit: Unit.percentage, - }), - baseline({ - metric: dca.periodLumpSumReturns[key], - name: "Lump Sum", - color: [colors.cyan, colors.orange], - unit: Unit.percentage, - }), - ], - }, - { - name: "Max", - title: `Max Return: ${name} DCA vs Lump Sum`, - top: topPane(key), - bottom: [ - baseline({ - metric: dca.periodMaxReturn[key], - name: "DCA", - unit: Unit.percentage, - }), - baseline({ - metric: dca.periodLumpSumMaxReturn[key], - name: "Lump Sum", - color: [colors.cyan, colors.orange], - unit: Unit.percentage, - }), - ], - }, - { - name: "Min", - title: `Min Return: ${name} DCA vs Lump Sum`, - top: topPane(key), - bottom: [ - baseline({ - metric: dca.periodMinReturn[key], - name: "DCA", - unit: Unit.percentage, - }), - baseline({ - metric: dca.periodLumpSumMinReturn[key], - name: "Lump Sum", - color: [colors.cyan, colors.orange], - unit: Unit.percentage, - }), + baseline({ metric: dca.periodReturns[key], name: "DCA", unit: Unit.percentage }), + baseline({ metric: dca.periodLumpSumReturns[key], name: "Lump Sum", color: [colors.cyan, colors.orange], unit: Unit.percentage }), ], }, + ...returnsMinMax(name, key), ], }); /** @param {string} name @param {LongPeriodKey} key */ - const returnsFolderWithCagr = (name, key) => ({ + const longReturnsFolder = (name, key) => ({ name: "Returns", tree: [ { @@ -357,72 +287,13 @@ export function createDcaVsLumpSumSection(ctx, { dca, lookback, returns }) { title: `Returns: ${name} DCA vs Lump Sum`, top: topPane(key), bottom: [ - baseline({ - metric: dca.periodReturns[key], - name: "DCA", - unit: Unit.percentage, - }), - baseline({ - metric: dca.periodLumpSumReturns[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: "Max", - title: `Max Return: ${name} DCA vs Lump Sum`, - top: topPane(key), - bottom: [ - line({ - metric: dca.periodMaxReturn[key], - name: "DCA", - color: colors.green, - unit: Unit.percentage, - }), - line({ - metric: dca.periodLumpSumMaxReturn[key], - name: "Lump Sum", - color: colors.orange, - unit: Unit.percentage, - }), - ], - }, - { - name: "Min", - title: `Min Return: ${name} DCA vs Lump Sum`, - top: topPane(key), - bottom: [ - line({ - metric: dca.periodMinReturn[key], - name: "DCA", - color: colors.green, - unit: Unit.percentage, - }), - line({ - metric: dca.periodLumpSumMinReturn[key], - name: "Lump Sum", - color: colors.orange, - unit: Unit.percentage, - }), + baseline({ metric: dca.periodReturns[key], name: "DCA", unit: Unit.percentage }), + baseline({ metric: dca.periodLumpSumReturns[key], name: "Lump Sum", color: [colors.cyan, colors.orange], unit: Unit.percentage }), + baseline({ metric: dca.periodCagr[key], name: "DCA CAGR", unit: Unit.cagr }), + baseline({ metric: returns.cagr[key], name: "Lump Sum CAGR", color: [colors.cyan, colors.orange], unit: Unit.cagr }), ], }, + ...returnsMinMax(name, key), ], }); @@ -435,18 +306,8 @@ export function createDcaVsLumpSumSection(ctx, { dca, lookback, returns }) { title: `Days in Profit: ${name} DCA vs Lump Sum`, top: topPane(key), bottom: [ - line({ - metric: dca.periodDaysInProfit[key], - name: "DCA", - color: colors.green, - unit: Unit.days, - }), - line({ - metric: dca.periodLumpSumDaysInProfit[key], - name: "Lump Sum", - color: colors.orange, - unit: Unit.days, - }), + line({ metric: dca.periodDaysInProfit[key], name: "DCA", color: colors.green, unit: Unit.days }), + line({ metric: dca.periodLumpSumDaysInProfit[key], name: "Lump Sum", color: colors.orange, unit: Unit.days }), ], }, { @@ -454,18 +315,8 @@ export function createDcaVsLumpSumSection(ctx, { dca, lookback, returns }) { title: `Days in Loss: ${name} DCA vs Lump Sum`, top: topPane(key), bottom: [ - line({ - metric: dca.periodDaysInLoss[key], - name: "DCA", - color: colors.green, - unit: Unit.days, - }), - line({ - metric: dca.periodLumpSumDaysInLoss[key], - name: "Lump Sum", - color: colors.orange, - unit: Unit.days, - }), + line({ metric: dca.periodDaysInLoss[key], name: "DCA", color: colors.green, unit: Unit.days }), + line({ metric: dca.periodLumpSumDaysInLoss[key], name: "Lump Sum", color: colors.orange, unit: Unit.days }), ], }, ], @@ -474,210 +325,125 @@ export function createDcaVsLumpSumSection(ctx, { dca, lookback, returns }) { /** @param {string} name @param {AllPeriodKey} key */ const stackChart = (name, key) => ({ name: "Accumulated", - title: `Accumulated Value: ${name} DCA vs Lump Sum`, + title: `Accumulated Value ($100/day): ${name} DCA vs Lump Sum`, top: topPane(key), bottom: [ - ...satsBtcUsd({ - pattern: dca.periodStack[key], - name: "DCA", - color: colors.green, - }), - ...satsBtcUsd({ - pattern: dca.periodLumpSumStack[key], - name: "Lump Sum", - color: colors.orange, - }), + ...satsBtcUsd({ pattern: dca.periodStack[key], name: "DCA", color: colors.green }), + ...satsBtcUsd({ pattern: dca.periodLumpSumStack[key], name: "Lump Sum", color: colors.orange }), ], }); - /** - * Check if a period key has CAGR data - * @param {AllPeriodKey} key - * @returns {key is LongPeriodKey} - */ - const hasCagr = (key) => key in dca.periodCagr; - - /** - * Create individual period entry - * @param {{ id: string, key: AllPeriodKey }} period - */ - const createPeriodEntry = ({ id, key }) => { - const name = periodIdToName(id, true); + /** @param {ShortPeriodKey} key */ + const createShortPeriodEntry = (key) => { + const name = periodName(key); return { name, - tree: [ - costBasisChart(name, key), - hasCagr(key) - ? returnsFolderWithCagr(name, key) - : returnsFolder(name, key), - profitabilityFolder(name, key), - stackChart(name, key), - ], + tree: [costBasisChart(name, key), shortReturnsFolder(name, key), profitabilityFolder(name, key), stackChart(name, key)], }; }; - /** - * Create term group - * @param {string} name - * @param {string} title - * @param {{ id: string, key: AllPeriodKey }[]} periods - */ - const createTermGroup = (name, title, periods) => ({ - name, - title, - tree: periods.map(createPeriodEntry), - }); + /** @param {LongPeriodKey} key */ + const createLongPeriodEntry = (key) => { + const name = periodName(key); + return { + name, + tree: [costBasisChart(name, key), longReturnsFolder(name, key), profitabilityFolder(name, key), stackChart(name, key)], + }; + }; return { name: "DCA vs Lump Sum", title: "Compare Investment Strategies", tree: [ - createTermGroup("Short Term", "Under 1 Year", PERIODS.short), - createTermGroup("Medium Term", "1-3 Years", PERIODS.medium), - createTermGroup("Long Term", "4+ Years", PERIODS.long), + { name: "Short Term", title: "Up to 1 Year", tree: SHORT_PERIODS.map(createShortPeriodEntry) }, + { name: "Long Term", title: "2+ Years", tree: LONG_PERIODS.map(createLongPeriodEntry) }, ], }; } /** - * Create DCA by Period section (DCA only, no Lump Sum comparison) + * Create period-based section (DCA or Lump Sum) * @param {PartialContext} ctx * @param {Object} args * @param {Market["dca"]} args.dca + * @param {Market["lookback"]} [args.lookback] + * @param {Market["returns"]} args.returns */ -export function createDcaByPeriodSection(ctx, { dca }) { +function createPeriodSection(ctx, { dca, lookback, returns }) { const { colors } = ctx; + const isLumpSum = !!lookback; + const suffix = isLumpSum ? "Lump Sum" : "DCA"; - /** - * Create compare charts for a set of periods - * @param {string} context - * @param {{ id: string, key: AllPeriodKey }[]} periods - */ - const createCompare = (context, periods) => - createCompareFolder( - context, - periods.map(({ id, key }) => ({ - name: id, - color: colors.dcaPeriods[key], - costBasis: dca.periodAveragePrice[key], - returns: dca.periodReturns[key], - daysInProfit: dca.periodDaysInProfit[key], - daysInLoss: dca.periodDaysInLoss[key], - stack: dca.periodStack[key], - })), - ); - - /** - * Create individual period entry (DCA only) - * @param {{ id: string, key: AllPeriodKey }} period - */ - const createPeriodEntry = ({ id, key }) => { - const name = periodIdToName(id, true); - return createSingleEntry(colors, { - name, - titlePrefix: `${name} DCA`, - color: colors.dcaPeriods[key], - costBasis: dca.periodAveragePrice[key], - returns: dca.periodReturns[key], - maxReturn: dca.periodMaxReturn[key], - minReturn: dca.periodMinReturn[key], - daysInProfit: dca.periodDaysInProfit[key], - daysInLoss: dca.periodDaysInLoss[key], - stack: dca.periodStack[key], - }); - }; - - /** @param {string} name @param {string} title @param {{ id: string, key: AllPeriodKey }[]} periods */ - const createTermGroup = (name, title, periods) => ({ - name, - title, - tree: [ - createCompare(`${name} DCA`, periods), - ...periods.map(createPeriodEntry), - ], + /** @param {AllPeriodKey} key @returns {BaseEntryItem} */ + const buildBaseEntry = (key) => ({ + name: periodName(key), + color: colors.dcaPeriods[key], + costBasis: isLumpSum ? lookback[key] : dca.periodAveragePrice[key], + returns: isLumpSum ? dca.periodLumpSumReturns[key] : dca.periodReturns[key], + minReturn: isLumpSum ? dca.periodLumpSumMinReturn[key] : dca.periodMinReturn[key], + maxReturn: isLumpSum ? dca.periodLumpSumMaxReturn[key] : dca.periodMaxReturn[key], + daysInProfit: isLumpSum ? dca.periodLumpSumDaysInProfit[key] : dca.periodDaysInProfit[key], + daysInLoss: isLumpSum ? dca.periodLumpSumDaysInLoss[key] : dca.periodDaysInLoss[key], + stack: isLumpSum ? dca.periodLumpSumStack[key] : dca.periodStack[key], }); + /** @param {LongPeriodKey} key @returns {LongEntryItem} */ + const buildLongEntry = (key) => withCagr( + buildBaseEntry(key), + isLumpSum ? returns.cagr[key] : dca.periodCagr[key], + ); + + /** @param {BaseEntryItem} entry */ + const createShortEntry = (entry) => + createShortSingleEntry(colors, { ...entry, titlePrefix: `${entry.name} ${suffix}` }); + + /** @param {LongEntryItem} entry */ + const createLongEntry = (entry) => + createLongSingleEntry(colors, { ...entry, titlePrefix: `${entry.name} ${suffix}` }); + + const shortEntries = SHORT_PERIODS.map(buildBaseEntry); + const longEntries = LONG_PERIODS.map(buildLongEntry); + return { - name: "DCA by Period", - title: "DCA Performance by Investment Period", + name: `${suffix} by Period`, + title: `${suffix} Performance by Investment Period`, tree: [ - createCompare("All Periods DCA", ALL_PERIODS), - createTermGroup("Short Term", "Under 1 Year", PERIODS.short), - createTermGroup("Medium Term", "1-3 Years", PERIODS.medium), - createTermGroup("Long Term", "4+ Years", PERIODS.long), + createCompareFolder(`All Periods ${suffix}`, [...shortEntries, ...longEntries]), + { + name: "Short Term", + title: "Up to 1 Year", + tree: [createCompareFolder(`Short Term ${suffix}`, shortEntries), ...shortEntries.map(createShortEntry)], + }, + { + name: "Long Term", + title: "2+ Years", + tree: [createCompareFolder(`Long Term ${suffix}`, longEntries), ...longEntries.map(createLongEntry)], + }, ], }; } +/** + * Create DCA by Period section + * @param {PartialContext} ctx + * @param {Object} args + * @param {Market["dca"]} args.dca + * @param {Market["returns"]} args.returns + */ +export function createDcaByPeriodSection(ctx, { dca, returns }) { + return createPeriodSection(ctx, { dca, returns }); +} + /** * Create Lump Sum by Period section * @param {PartialContext} ctx * @param {Object} args * @param {Market["dca"]} args.dca * @param {Market["lookback"]} args.lookback + * @param {Market["returns"]} args.returns */ -export function createLumpSumByPeriodSection(ctx, { dca, lookback }) { - const { colors } = ctx; - - /** - * Create compare charts for a set of periods - * @param {string} context - * @param {{ id: string, key: AllPeriodKey }[]} periods - */ - const createCompare = (context, periods) => - createCompareFolder( - context, - periods.map(({ id, key }) => ({ - name: id, - color: colors.dcaPeriods[key], - costBasis: lookback[key], - returns: dca.periodLumpSumReturns[key], - daysInProfit: dca.periodLumpSumDaysInProfit[key], - daysInLoss: dca.periodLumpSumDaysInLoss[key], - stack: dca.periodLumpSumStack[key], - })), - ); - - /** - * Create individual period entry (Lump Sum only) - * @param {{ id: string, key: AllPeriodKey }} period - */ - const createPeriodEntry = ({ id, key }) => { - const name = periodIdToName(id, true); - return createSingleEntry(colors, { - name, - titlePrefix: `${name} Lump Sum`, - color: colors.dcaPeriods[key], - costBasis: lookback[key], - returns: dca.periodLumpSumReturns[key], - maxReturn: dca.periodLumpSumMaxReturn[key], - minReturn: dca.periodLumpSumMinReturn[key], - daysInProfit: dca.periodLumpSumDaysInProfit[key], - daysInLoss: dca.periodLumpSumDaysInLoss[key], - stack: dca.periodLumpSumStack[key], - }); - }; - - /** @param {string} name @param {string} title @param {{ id: string, key: AllPeriodKey }[]} periods */ - const createTermGroup = (name, title, periods) => ({ - name, - title, - tree: [ - createCompare(`${name} Lump Sum`, periods), - ...periods.map(createPeriodEntry), - ], - }); - - return { - name: "Lump Sum by Period", - title: "Lump Sum Performance by Investment Period", - tree: [ - createCompare("All Periods Lump Sum", ALL_PERIODS), - createTermGroup("Short Term", "Under 1 Year", PERIODS.short), - createTermGroup("Medium Term", "1-3 Years", PERIODS.medium), - createTermGroup("Long Term", "4+ Years", PERIODS.long), - ], - }; +export function createLumpSumByPeriodSection(ctx, { dca, lookback, returns }) { + return createPeriodSection(ctx, { dca, lookback, returns }); } /** @@ -689,61 +455,26 @@ export function createLumpSumByPeriodSection(ctx, { dca, lookback }) { export function createDcaByStartYearSection(ctx, { dca }) { const { colors } = ctx; - /** - * Convert YearClass to CompareItem - * @param {YearClass} c - * @returns {CompareItem} - */ - const toCompareItem = (c) => ({ - name: `${c.year}`, - color: c.color, - costBasis: c.costBasis, - returns: c.returns, - daysInProfit: c.daysInProfit, - daysInLoss: c.daysInLoss, - stack: c.stack, - }); - - /** - * Create individual year entry - * @param {YearClass} yearClass - */ - const createYearEntry = (yearClass) => - createSingleEntry(colors, { - name: `${yearClass.year}`, - titlePrefix: `${yearClass.year} DCA`, - color: yearClass.color, - costBasis: yearClass.costBasis, - returns: yearClass.returns, - maxReturn: yearClass.maxReturn, - minReturn: yearClass.minReturn, - daysInProfit: yearClass.daysInProfit, - daysInLoss: yearClass.daysInLoss, - stack: yearClass.stack, - }); - - /** @param {string} name @param {string} title @param {YearClass[]} classes */ - const createDecadeGroup = (name, title, classes) => ({ + /** @param {string} name @param {string} title @param {BaseEntryItem[]} entries */ + const createDecadeGroup = (name, title, entries) => ({ name, title, tree: [ - createCompareFolder(`${name} DCA`, classes.map(toCompareItem)), - ...classes.map(createYearEntry), + createCompareFolder(`${name} DCA`, entries), + ...entries.map((entry) => createShortSingleEntry(colors, { ...entry, titlePrefix: `${entry.name} DCA` })), ], }); - // Build all classes once, then filter by decade - const allClasses = ALL_YEARS.map((year) => buildYearClass(colors, dca, year)); - const classes2020s = allClasses.filter((c) => c.year >= 2020); - const classes2010s = allClasses.filter((c) => c.year < 2020); + const entries2020s = YEARS_2020S.map((year) => buildYearEntry(colors, dca, year)); + const entries2010s = YEARS_2010S.map((year) => buildYearEntry(colors, dca, year)); return { name: "DCA by Start Year", title: "DCA Performance by When You Started", tree: [ - createCompareFolder("All Years DCA", allClasses.map(toCompareItem)), - createDecadeGroup("2020s", "2020-2026", classes2020s), - createDecadeGroup("2010s", "2015-2019", classes2010s), + createCompareFolder("All Years DCA", [...entries2020s, ...entries2010s]), + createDecadeGroup("2020s", "2020-2026", entries2020s), + createDecadeGroup("2010s", "2015-2019", entries2010s), ], }; } diff --git a/website/scripts/options/mining.js b/website/scripts/options/mining.js index 655baaa4b..e1034c992 100644 --- a/website/scripts/options/mining.js +++ b/website/scripts/options/mining.js @@ -4,6 +4,7 @@ import { Unit } from "../utils/units.js"; import { priceLine } from "./constants.js"; import { line, baseline, dots, dotted } from "./series.js"; import { satsBtcUsd } from "./shared.js"; +import { fromCountPattern } from "./series.js"; /** Major pools to show in Compare section (by current hashrate dominance) */ const MAJOR_POOL_IDS = [ @@ -103,42 +104,35 @@ export function createMiningSection(ctx) { name: "Blocks Mined", title: `Blocks Mined: ${poolName}`, bottom: [ - dots({ - metric: pool.blocksMined.sum, - name: "Sum", + ...fromCountPattern({ + pattern: pool.blocksMined, unit: Unit.count, - }), - line({ - metric: pool.blocksMined.cumulative, - name: "Cumulative", - color: colors.blue, - unit: Unit.count, - defaultActive: false, + cumulativeUnit: Unit.countCumulative, }), line({ metric: pool._24hBlocksMined, - name: "24h sum", + name: "24h", color: colors.pink, unit: Unit.count, defaultActive: false, }), line({ metric: pool._1wBlocksMined, - name: "1w sum", + name: "1w", color: colors.red, unit: Unit.count, defaultActive: false, }), line({ metric: pool._1mBlocksMined, - name: "1m sum", - color: colors.pink, + name: "1m", + color: colors.orange, unit: Unit.count, defaultActive: false, }), line({ metric: pool._1yBlocksMined, - name: "1y sum", + name: "1y", color: colors.purple, unit: Unit.count, defaultActive: false, @@ -152,20 +146,17 @@ export function createMiningSection(ctx) { ...fromValuePattern({ pattern: pool.coinbase, title: "coinbase", - sumColor: colors.orange, - cumulativeColor: colors.red, + color: colors.orange, }), ...fromValuePattern({ pattern: pool.subsidy, title: "subsidy", - sumColor: colors.lime, - cumulativeColor: colors.emerald, + color: colors.lime, }), ...fromValuePattern({ pattern: pool.fee, title: "fee", - sumColor: colors.cyan, - cumulativeColor: colors.indigo, + color: colors.cyan, }), ], }, @@ -342,14 +333,17 @@ export function createMiningSection(ctx) { ...fromSumStatsPattern({ pattern: transactions.fees.fee.bitcoin, unit: Unit.btc, + cumulativeUnit: Unit.btcCumulative, }), ...fromSumStatsPattern({ pattern: transactions.fees.fee.sats, unit: Unit.sats, + cumulativeUnit: Unit.satsCumulative, }), ...fromSumStatsPattern({ pattern: transactions.fees.fee.dollars, unit: Unit.usd, + cumulativeUnit: Unit.usdCumulative, }), line({ metric: blocks.rewards.feeDominance, @@ -364,7 +358,6 @@ export function createMiningSection(ctx) { title: "Unclaimed Rewards", bottom: fromValuePattern({ pattern: blocks.rewards.unclaimedRewards, - title: "Unclaimed", }), }, ], diff --git a/website/scripts/options/network.js b/website/scripts/options/network.js index ec722266b..26a50394b 100644 --- a/website/scripts/options/network.js +++ b/website/scripts/options/network.js @@ -47,6 +47,23 @@ export function createNetworkSection(ctx) { { key: "p2a", name: "P2A", color: colors[spendableTypeColors.p2a], defaultActive: false }, ]; + // Script types for output count comparisons (address types + non-addressable scripts) + /** @type {ReadonlyArray<{key: AddressableType | "p2ms" | "opreturn" | "emptyoutput" | "unknownoutput", name: string, color: Color, defaultActive?: boolean}>} */ + const scriptTypes = [ + { key: "p2pkh", name: "P2PKH", color: colors[spendableTypeColors.p2pkh] }, + { key: "p2sh", name: "P2SH", color: colors[spendableTypeColors.p2sh] }, + { key: "p2wpkh", name: "P2WPKH", color: colors[spendableTypeColors.p2wpkh] }, + { key: "p2wsh", name: "P2WSH", color: colors[spendableTypeColors.p2wsh] }, + { key: "p2tr", name: "P2TR", color: colors[spendableTypeColors.p2tr] }, + { key: "p2pk65", name: "P2PK65", color: colors[spendableTypeColors.p2pk65], defaultActive: false }, + { key: "p2pk33", name: "P2PK33", color: colors[spendableTypeColors.p2pk33], defaultActive: false }, + { key: "p2a", name: "P2A", color: colors[spendableTypeColors.p2a], defaultActive: false }, + { key: "p2ms", name: "P2MS", color: colors[spendableTypeColors.p2ms], defaultActive: false }, + { key: "opreturn", name: "OP_RETURN", color: colors[spendableTypeColors.opreturn], defaultActive: false }, + { key: "emptyoutput", name: "Empty", color: colors[spendableTypeColors.empty], defaultActive: false }, + { key: "unknownoutput", name: "Unknown", color: colors[spendableTypeColors.unknown], defaultActive: false }, + ]; + // Activity types for mapping /** @type {ReadonlyArray<{key: "sending" | "receiving" | "both" | "reactivated" | "balanceIncreased" | "balanceDecreased", name: string, title: string, compareTitle: string}>} */ const activityTypes = [ @@ -100,7 +117,7 @@ export function createNetworkSection(ctx) { { name: "New", title: `${titlePrefix}New Address Count`, - bottom: fromFullStatsPattern({ pattern: distribution.newAddrCount[key], unit: Unit.count }), + bottom: fromFullStatsPattern({ pattern: distribution.newAddrCount[key], unit: Unit.count, cumulativeUnit: Unit.countCumulative }), }, { name: "Growth Rate", @@ -123,6 +140,39 @@ export function createNetworkSection(ctx) { return { name: "Network", tree: [ + // Supply + { + name: "Supply", + tree: [ + { + name: "Circulating", + title: "Circulating Supply", + bottom: fromSupplyPattern({ pattern: supply.circulating, title: "Supply" }), + }, + { + name: "Inflation", + title: "Inflation Rate", + bottom: [ + dots({ + metric: supply.inflation, + name: "Rate", + unit: Unit.percentage, + }), + ], + }, + { + name: "Unspendable", + title: "Unspendable Supply", + bottom: fromValuePattern({ pattern: supply.burned.unspendable }), + }, + { + name: "OP_RETURN", + title: "OP_RETURN Burned", + bottom: fromCoinbasePattern({ pattern: scripts.value.opreturn }), + }, + ], + }, + // Transactions { name: "Transactions", @@ -130,7 +180,7 @@ export function createNetworkSection(ctx) { { name: "Count", title: "Transaction Count", - bottom: fromFullStatsPattern({ pattern: transactions.count.txCount, unit: Unit.count }), + bottom: fromFullStatsPattern({ pattern: transactions.count.txCount, unit: Unit.count, cumulativeUnit: Unit.countCumulative }), }, { name: "Per Second", @@ -143,6 +193,11 @@ export function createNetworkSection(ctx) { }), ], }, + { + name: "Fee Rate", + title: "Fee Rate", + bottom: fromStatsPattern({ pattern: transactions.fees.feeRate, unit: Unit.feeRate }), + }, { name: "Volume", title: "Transaction Volume", @@ -177,23 +232,23 @@ export function createNetworkSection(ctx) { ...fromCountPattern({ pattern: transactions.versions.v1, unit: Unit.count, + cumulativeUnit: Unit.countCumulative, title: "v1", - sumColor: colors.orange, - cumulativeColor: colors.red, + color: colors.orange, }), ...fromCountPattern({ pattern: transactions.versions.v2, unit: Unit.count, + cumulativeUnit: Unit.countCumulative, title: "v2", - sumColor: colors.cyan, - cumulativeColor: colors.blue, + color: colors.cyan, }), ...fromCountPattern({ pattern: transactions.versions.v3, unit: Unit.count, + cumulativeUnit: Unit.countCumulative, title: "v3", - sumColor: colors.lime, - cumulativeColor: colors.green, + color: colors.lime, }), ], }, @@ -217,27 +272,6 @@ export function createNetworkSection(ctx) { ], }, - // Fees - { - name: "Fees", - tree: [ - { - name: "Fee Rate", - title: "Fee Rate", - bottom: fromStatsPattern({ pattern: transactions.fees.feeRate, unit: Unit.feeRate }), - }, - { - name: "Total", - title: "Total Fees", - bottom: [ - ...fromSumStatsPattern({ pattern: transactions.fees.fee.bitcoin, unit: Unit.btc }), - ...fromSumStatsPattern({ pattern: transactions.fees.fee.sats, unit: Unit.sats }), - ...fromSumStatsPattern({ pattern: transactions.fees.fee.dollars, unit: Unit.usd }), - ], - }, - ], - }, - // Blocks { name: "Blocks", @@ -246,7 +280,7 @@ export function createNetworkSection(ctx) { name: "Count", title: "Block Count", bottom: [ - ...fromCountPattern({ pattern: blocks.count.blockCount, unit: Unit.count }), + ...fromCountPattern({ pattern: blocks.count.blockCount, unit: Unit.count, cumulativeUnit: Unit.countCumulative }), line({ metric: blocks.count.blockCountTarget, name: "Target", @@ -296,7 +330,7 @@ export function createNetworkSection(ctx) { name: "Size", title: "Block Size", bottom: [ - ...fromSumStatsPattern({ pattern: blocks.size, unit: Unit.bytes }), + ...fromSumStatsPattern({ pattern: blocks.size, unit: Unit.bytes, cumulativeUnit: Unit.bytesCumulative }), line({ metric: blocks.totalSize, name: "Total", @@ -306,20 +340,6 @@ export function createNetworkSection(ctx) { }), ...fromBaseStatsPattern({ pattern: blocks.vbytes, unit: Unit.vb }), ...fromBaseStatsPattern({ pattern: blocks.weight, unit: 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, - }), ], }, { @@ -330,9 +350,9 @@ export function createNetworkSection(ctx) { ], }, - // UTXO Set + // UTXOs { - name: "UTXO Set", + name: "UTXOs", tree: [ { name: "UTXO Count", @@ -351,7 +371,7 @@ export function createNetworkSection(ctx) { { name: "Count", title: "Input Count", - bottom: [...fromSumStatsPattern({ pattern: inputs.count, unit: Unit.count })], + bottom: [...fromSumStatsPattern({ pattern: inputs.count, unit: Unit.count, cumulativeUnit: Unit.countCumulative })], }, { name: "Rate", @@ -372,7 +392,7 @@ export function createNetworkSection(ctx) { { name: "Count", title: "Output Count", - bottom: [...fromSumStatsPattern({ pattern: outputs.count.totalCount, unit: Unit.count })], + bottom: [...fromSumStatsPattern({ pattern: outputs.count.totalCount, unit: Unit.count, cumulativeUnit: Unit.countCumulative })], }, { name: "Rate", @@ -498,6 +518,20 @@ export function createNetworkSection(ctx) { { name: "Output Counts", tree: [ + // Compare section + { + name: "Compare", + title: "Output Count by Script Type", + bottom: scriptTypes.map((t) => + line({ + metric: scripts.count[t.key].cumulative, + name: t.name, + color: t.color, + unit: Unit.countCumulative, + defaultActive: t.defaultActive, + }), + ), + }, // Legacy scripts { name: "Legacy", @@ -505,17 +539,17 @@ export function createNetworkSection(ctx) { { name: "P2PKH", title: "P2PKH Output Count", - bottom: fromFullStatsPattern({ pattern: scripts.count.p2pkh, unit: Unit.count }), + bottom: fromFullStatsPattern({ pattern: scripts.count.p2pkh, unit: Unit.count, cumulativeUnit: Unit.countCumulative }), }, { name: "P2PK33", title: "P2PK33 Output Count", - bottom: fromFullStatsPattern({ pattern: scripts.count.p2pk33, unit: Unit.count }), + bottom: fromFullStatsPattern({ pattern: scripts.count.p2pk33, unit: Unit.count, cumulativeUnit: Unit.countCumulative }), }, { name: "P2PK65", title: "P2PK65 Output Count", - bottom: fromFullStatsPattern({ pattern: scripts.count.p2pk65, unit: Unit.count }), + bottom: fromFullStatsPattern({ pattern: scripts.count.p2pk65, unit: Unit.count, cumulativeUnit: Unit.countCumulative }), }, ], }, @@ -526,12 +560,12 @@ export function createNetworkSection(ctx) { { name: "P2SH", title: "P2SH Output Count", - bottom: fromFullStatsPattern({ pattern: scripts.count.p2sh, unit: Unit.count }), + bottom: fromFullStatsPattern({ pattern: scripts.count.p2sh, unit: Unit.count, cumulativeUnit: Unit.countCumulative }), }, { name: "P2MS", title: "P2MS Output Count", - bottom: fromFullStatsPattern({ pattern: scripts.count.p2ms, unit: Unit.count }), + bottom: fromFullStatsPattern({ pattern: scripts.count.p2ms, unit: Unit.count, cumulativeUnit: Unit.countCumulative }), }, ], }, @@ -542,17 +576,17 @@ export function createNetworkSection(ctx) { { name: "All SegWit", title: "SegWit Output Count", - bottom: fromFullStatsPattern({ pattern: scripts.count.segwit, unit: Unit.count }), + bottom: fromFullStatsPattern({ pattern: scripts.count.segwit, unit: Unit.count, cumulativeUnit: Unit.countCumulative }), }, { name: "P2WPKH", title: "P2WPKH Output Count", - bottom: fromFullStatsPattern({ pattern: scripts.count.p2wpkh, unit: Unit.count }), + bottom: fromFullStatsPattern({ pattern: scripts.count.p2wpkh, unit: Unit.count, cumulativeUnit: Unit.countCumulative }), }, { name: "P2WSH", title: "P2WSH Output Count", - bottom: fromFullStatsPattern({ pattern: scripts.count.p2wsh, unit: Unit.count }), + bottom: fromFullStatsPattern({ pattern: scripts.count.p2wsh, unit: Unit.count, cumulativeUnit: Unit.countCumulative }), }, ], }, @@ -563,12 +597,12 @@ export function createNetworkSection(ctx) { { name: "P2TR", title: "P2TR Output Count", - bottom: fromFullStatsPattern({ pattern: scripts.count.p2tr, unit: Unit.count }), + bottom: fromFullStatsPattern({ pattern: scripts.count.p2tr, unit: Unit.count, cumulativeUnit: Unit.countCumulative }), }, { name: "P2A", title: "P2A Output Count", - bottom: fromFullStatsPattern({ pattern: scripts.count.p2a, unit: Unit.count }), + bottom: fromFullStatsPattern({ pattern: scripts.count.p2a, unit: Unit.count, cumulativeUnit: Unit.countCumulative }), }, ], }, @@ -579,17 +613,17 @@ export function createNetworkSection(ctx) { { name: "OP_RETURN", title: "OP_RETURN Output Count", - bottom: fromFullStatsPattern({ pattern: scripts.count.opreturn, unit: Unit.count }), + bottom: fromFullStatsPattern({ pattern: scripts.count.opreturn, unit: Unit.count, cumulativeUnit: Unit.countCumulative }), }, { name: "Empty", title: "Empty Output Count", - bottom: fromFullStatsPattern({ pattern: scripts.count.emptyoutput, unit: Unit.count }), + bottom: fromFullStatsPattern({ pattern: scripts.count.emptyoutput, unit: Unit.count, cumulativeUnit: Unit.countCumulative }), }, { name: "Unknown", title: "Unknown Output Count", - bottom: fromFullStatsPattern({ pattern: scripts.count.unknownoutput, unit: Unit.count }), + bottom: fromFullStatsPattern({ pattern: scripts.count.unknownoutput, unit: Unit.count, cumulativeUnit: Unit.countCumulative }), }, ], }, @@ -610,15 +644,13 @@ export function createNetworkSection(ctx) { 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, + color: colors.red, unit: Unit.percentage, - defaultActive: false, }), ], }, @@ -634,15 +666,13 @@ export function createNetworkSection(ctx) { 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, + color: colors.red, unit: Unit.percentage, - defaultActive: false, }), ], }, @@ -651,46 +681,6 @@ export function createNetworkSection(ctx) { ], }, - // Supply - { - name: "Supply", - tree: [ - { - name: "Circulating", - title: "Circulating Supply", - bottom: fromSupplyPattern({ pattern: supply.circulating, title: "Supply" }), - }, - { - name: "Inflation", - title: "Inflation Rate", - bottom: [ - dots({ - metric: supply.inflation, - name: "Rate", - unit: Unit.percentage, - }), - ], - }, - { - name: "Burned", - tree: [ - { - name: "Unspendable", - title: "Unspendable Supply", - bottom: fromValuePattern({ pattern: supply.burned.unspendable }), - }, - { - name: "OP_RETURN", - title: "OP_RETURN Burned", - bottom: [ - ...fromValuePattern({ pattern: supply.burned.opreturn }), - ...fromCoinbasePattern({ pattern: scripts.value.opreturn }), - ], - }, - ], - }, - ], - }, ], }; } diff --git a/website/scripts/options/series.js b/website/scripts/options/series.js index 9c2512598..546466146 100644 --- a/website/scripts/options/series.js +++ b/website/scripts/options/series.js @@ -311,26 +311,25 @@ export function histogram({ * @param {Object} args * @param {AnyStatsPattern} args.pattern * @param {Unit} args.unit + * @param {Unit} args.cumulativeUnit * @param {string} [args.title] * @returns {AnyFetchedSeriesBlueprint[]} */ -export function fromSumStatsPattern(colors, { pattern, unit, title = "" }) { +export function fromSumStatsPattern(colors, { pattern, unit, cumulativeUnit, title = "" }) { const { stat } = colors; return [ { metric: pattern.average, title: `${title} avg`.trim(), unit }, { metric: pattern.sum, - title: `${title} sum`.trim(), + title: title || "sum", color: stat.sum, unit, defaultActive: false, }, { metric: pattern.cumulative, - title: `${title} cumulative`.trim(), - color: stat.cumulative, - unit, - defaultActive: false, + title: title || "cumulative", + unit: cumulativeUnit, }, ...percentileSeries(colors, pattern, unit, title), ]; @@ -371,25 +370,24 @@ export function fromBaseStatsPattern( * @param {Object} args * @param {FullStatsPattern} args.pattern * @param {Unit} args.unit + * @param {Unit} args.cumulativeUnit * @param {string} [args.title] * @returns {AnyFetchedSeriesBlueprint[]} */ -export function fromFullStatsPattern(colors, { pattern, unit, title = "" }) { +export function fromFullStatsPattern(colors, { pattern, unit, cumulativeUnit, title = "" }) { const { stat } = colors; return [ { metric: pattern.base, title: title || "base", unit }, { metric: pattern.sum, - title: `${title} sum`.trim(), + title: title || "sum", color: stat.sum, unit, }, { metric: pattern.cumulative, - title: `${title} cumulative`.trim(), - color: stat.cumulative, - unit, - defaultActive: false, + title: title || "cumulative", + unit: cumulativeUnit, }, { metric: pattern.average, @@ -429,25 +427,24 @@ export function fromStatsPattern(colors, { pattern, unit, title = "" }) { * @param {Object} args * @param {AnyFullStatsPattern} args.pattern * @param {Unit} args.unit + * @param {Unit} args.cumulativeUnit * @param {string} [args.title] * @returns {AnyFetchedSeriesBlueprint[]} */ -export function fromAnyFullStatsPattern(colors, { pattern, unit, title = "" }) { +export function fromAnyFullStatsPattern(colors, { pattern, unit, cumulativeUnit, title = "" }) { const { stat } = colors; return [ ...fromBaseStatsPattern(colors, { pattern, unit, title }), { metric: pattern.sum, - title: `${title} sum`.trim(), + title: title || "sum", color: stat.sum, unit, }, { metric: pattern.cumulative, - title: `${title} cumulative`.trim(), - color: stat.cumulative, - unit, - defaultActive: false, + title: title || "cumulative", + unit: cumulativeUnit, }, ]; } @@ -465,16 +462,19 @@ export function fromCoinbasePattern(colors, { pattern, title = "" }) { ...fromAnyFullStatsPattern(colors, { pattern: pattern.bitcoin, unit: Unit.btc, + cumulativeUnit: Unit.btcCumulative, title, }), ...fromAnyFullStatsPattern(colors, { pattern: pattern.sats, unit: Unit.sats, + cumulativeUnit: Unit.satsCumulative, title, }), ...fromAnyFullStatsPattern(colors, { pattern: pattern.dollars, unit: Unit.usd, + cumulativeUnit: Unit.usdCumulative, title, }), ]; @@ -482,123 +482,112 @@ export function fromCoinbasePattern(colors, { pattern, title = "" }) { /** * Create series from a ValuePattern ({ sats, bitcoin, dollars } each as CountPattern with sum + cumulative) - * @param {Colors} colors * @param {Object} args * @param {ValuePattern} args.pattern * @param {string} [args.title] - * @param {Color} [args.sumColor] - * @param {Color} [args.cumulativeColor] + * @param {Color} [args.color] * @returns {AnyFetchedSeriesBlueprint[]} */ -export function fromValuePattern( - colors, - { pattern, title = "", sumColor, cumulativeColor }, -) { +export function fromValuePattern({ pattern, title = "", color }) { return [ { metric: pattern.bitcoin.sum, title: title || "sum", - color: sumColor, + color, unit: Unit.btc, }, { metric: pattern.bitcoin.cumulative, - title: `${title} cumulative`.trim(), - color: cumulativeColor ?? colors.stat.cumulative, + title: title || "cumulative", + color, unit: Unit.btcCumulative, - defaultActive: false, }, { metric: pattern.sats.sum, title: title || "sum", - color: sumColor, + color, unit: Unit.sats, }, { metric: pattern.sats.cumulative, - title: `${title} cumulative`.trim(), - color: cumulativeColor ?? colors.stat.cumulative, + title: title || "cumulative", + color, unit: Unit.satsCumulative, - defaultActive: false, }, { metric: pattern.dollars.sum, title: title || "sum", - color: sumColor, + color, unit: Unit.usd, }, { metric: pattern.dollars.cumulative, - title: `${title} cumulative`.trim(), - color: cumulativeColor ?? colors.stat.cumulative, + title: title || "cumulative", + color, unit: Unit.usdCumulative, - defaultActive: false, }, ]; } /** * Create sum/cumulative series from a BitcoinPattern ({ sum, cumulative }) with explicit unit and colors - * @param {Colors} colors * @param {Object} args * @param {{ sum: AnyMetricPattern, cumulative: AnyMetricPattern }} args.pattern * @param {Unit} args.unit + * @param {Unit} args.cumulativeUnit * @param {string} [args.title] - * @param {Color} [args.sumColor] - * @param {Color} [args.cumulativeColor] + * @param {Color} [args.color] * @param {boolean} [args.defaultActive] * @returns {AnyFetchedSeriesBlueprint[]} */ -export function fromBitcoinPatternWithUnit( - colors, - { pattern, unit, title = "", sumColor, cumulativeColor, defaultActive }, -) { +export function fromBitcoinPatternWithUnit({ + pattern, + unit, + cumulativeUnit, + title = "", + color, + defaultActive, +}) { return [ { metric: pattern.sum, - title: `${title} sum`.trim(), - color: sumColor, + title: title || "sum", + color, unit, defaultActive, }, { metric: pattern.cumulative, - title: `${title} cumulative`.trim(), - color: cumulativeColor ?? colors.stat.cumulative, - unit, - defaultActive: false, + title: title || "cumulative", + color, + unit: cumulativeUnit, }, ]; } /** * Create sum/cumulative series from a CountPattern with explicit unit and colors - * @param {Colors} colors * @param {Object} args * @param {CountPattern} args.pattern * @param {Unit} args.unit + * @param {Unit} args.cumulativeUnit * @param {string} [args.title] - * @param {Color} [args.sumColor] - * @param {Color} [args.cumulativeColor] + * @param {Color} [args.color] * @returns {AnyFetchedSeriesBlueprint[]} */ -export function fromCountPattern( - colors, - { pattern, unit, title = "", sumColor, cumulativeColor }, -) { +export function fromCountPattern({ pattern, unit, cumulativeUnit, title = "", color }) { return [ { metric: pattern.sum, - title: `${title} sum`.trim(), - color: sumColor, + title: title || "sum", + color, unit, }, { metric: pattern.cumulative, - title: `${title} cumulative`.trim(), - color: cumulativeColor ?? colors.stat.cumulative, - unit, - defaultActive: false, + title: title || "cumulative", + color, + unit: cumulativeUnit, }, ]; } diff --git a/website/scripts/utils/units.js b/website/scripts/utils/units.js index e455695a8..87833a048 100644 --- a/website/scripts/utils/units.js +++ b/website/scripts/utils/units.js @@ -16,6 +16,7 @@ export const Unit = /** @type {const} */ ({ // Ratios & percentages percentage: { id: "percentage", name: "Percentage" }, + cagr: { id: "cagr", name: "CAGR (%/year)" }, ratio: { id: "ratio", name: "Ratio" }, index: { id: "index", name: "Index" }, sd: { id: "sd", name: "Std Dev" }, @@ -40,8 +41,10 @@ export const Unit = /** @type {const} */ ({ // Size bytes: { id: "bytes", name: "Bytes" }, + bytesCumulative: { id: "bytes-total", name: "Bytes (Total)" }, vb: { id: "vb", name: "Virtual Bytes" }, wu: { id: "wu", name: "Weight Units" }, + wuCumulative: { id: "wu-total", name: "Weight Units (Total)" }, // Mining hashRate: { id: "hashrate", name: "Hash Rate" },