/** Network section - On-chain activity and health */ import { colors } from "../utils/colors.js"; import { brk } from "../utils/client.js"; import { Unit } from "../utils/units.js"; import { entries } from "../utils/array.js"; import { line, baseline, fromSupplyPattern, chartsFromFull, chartsFromFullPerBlock, chartsFromCount, chartsFromCountEntries, chartsFromPercentCumulative, chartsFromPercentCumulativeEntries, chartsFromAggregatedPerBlock, distributionWindowsTree, averagesArray, simpleDeltaTree, ROLLING_WINDOWS, chartsFromBlockAnd6b, multiSeriesTree, percentRatioDots, } from "./series.js"; import { satsBtcUsd, satsBtcUsdFrom, satsBtcUsdFullTree, formatCohortTitle, groupedWindowsCumulative, } from "./shared.js"; /** * Create Network section * @returns {PartialOptionsGroup} */ export function createNetworkSection() { const { blocks, transactions, inputs, outputs, supply, addrs, cohorts } = brk.series; const st = colors.scriptType; // Addressable types - newest to oldest (for addresses/counts that only support addressable types) const addressTypes = /** @type {const} */ ([ { key: "p2a", name: "P2A", color: st.p2a, defaultActive: false }, { key: "p2tr", name: "P2TR", color: st.p2tr, defaultActive: true }, { key: "p2wsh", name: "P2WSH", color: st.p2wsh, defaultActive: true }, { key: "p2wpkh", name: "P2WPKH", color: st.p2wpkh, defaultActive: true }, { key: "p2sh", name: "P2SH", color: st.p2sh, defaultActive: true }, { key: "p2pkh", name: "P2PKH", color: st.p2pkh, defaultActive: true }, { key: "p2pk33", name: "P2PK33", color: st.p2pk33, defaultActive: false }, { key: "p2pk65", name: "P2PK65", color: st.p2pk65, defaultActive: false }, ]); // Non-addressable script types const nonAddressableTypes = /** @type {const} */ ([ { key: "p2ms", name: "P2MS", color: st.p2ms, defaultActive: false }, { key: "opReturn", name: "OP_RETURN", color: st.opReturn, defaultActive: true, }, { key: "empty", name: "Empty", color: st.empty, defaultActive: false, }, { key: "unknown", name: "Unknown", color: st.unknown, defaultActive: false, }, ]); // All output types = addressable + non-addressable (12 total) const outputTypes = [...addressTypes, ...nonAddressableTypes]; // Spendable input types: every output type can fund an input *except* OP_RETURN const inputTypes = [ ...addressTypes, ...nonAddressableTypes.filter((t) => t.key !== "opReturn"), ]; // Transacting types (transaction participation) const activityTypes = /** @type {const} */ ([ { key: "active", name: "Active" }, { key: "sending", name: "Sending" }, { key: "receiving", name: "Receiving" }, { key: "bidirectional", name: "Bidirectional" }, { key: "reactivated", name: "Reactivated" }, ]); const countMetrics = /** @type {const} */ ([ { key: "funded", name: "Funded", color: undefined }, { key: "empty", name: "Empty", color: colors.gray }, { key: "total", name: "Total", color: colors.default }, ]); const reusedSetEntries = /** * @param {AddressableType | "all"} key * @param {(name: string) => string} title */ (key, title) => [ { name: "Compare", title: title("Reused Address Count"), bottom: [ line({ series: addrs.reused.count.funded[key], name: "Funded", unit: Unit.count, }), line({ series: addrs.reused.count.total[key], name: "Total", color: colors.gray, unit: Unit.count, }), ], }, { name: "Funded", title: title("Funded Reused Addresses"), bottom: [ line({ series: addrs.reused.count.funded[key], name: "Funded Reused", unit: Unit.count, }), ], }, { name: "Total", title: title("Total Reused Addresses"), bottom: [ line({ series: addrs.reused.count.total[key], name: "Total Reused", color: colors.gray, unit: Unit.count, }), ], }, ]; const reusedInputsSubtree = /** * @param {AddressableType | "all"} key * @param {(name: string) => string} title */ (key, title) => ({ name: "Inputs", tree: [ { name: "Count", tree: chartsFromCount({ pattern: addrs.reused.events.inputFromReusedAddrCount[key], title, metric: "Transaction Inputs from Reused Addresses", unit: Unit.count, }), }, { name: "Share", tree: chartsFromPercentCumulative({ pattern: addrs.reused.events.inputFromReusedAddrShare[key], title, metric: "Share of Transaction Inputs from Reused Addresses", }), }, ], }); const reusedOutputsSubtreeForAll = /** @param {(name: string) => string} title */ (title) => ({ name: "Outputs", tree: [ { name: "Count", tree: chartsFromCount({ pattern: addrs.reused.events.outputToReusedAddrCount.all, title, metric: "Transaction Outputs to Reused Addresses", unit: Unit.count, }), }, { name: "Share", tree: chartsFromPercentCumulativeEntries({ entries: [ { name: "All", pattern: addrs.reused.events.outputToReusedAddrShare.all, }, { name: "Spendable", pattern: addrs.reused.events.spendableOutputToReusedAddrShare, color: colors.gray, }, ], title, metric: "Share of Transaction Outputs to Reused Addresses", }), }, ], }); const reusedOutputsSubtreeForType = /** * @param {AddressableType} key * @param {(name: string) => string} title */ (key, title) => ({ name: "Outputs", tree: [ { name: "Count", tree: chartsFromCount({ pattern: addrs.reused.events.outputToReusedAddrCount[key], title, metric: "Transaction Outputs to Reused Addresses", unit: Unit.count, }), }, { name: "Share", tree: chartsFromPercentCumulative({ pattern: addrs.reused.events.outputToReusedAddrShare[key], title, metric: "Share of Transaction Outputs to Reused Addresses", }), }, ], }); const reusedActiveSubtreeForAll = /** @param {(name: string) => string} title */ (title) => ({ name: "Active", tree: [ { name: "Count", tree: averagesArray({ windows: addrs.reused.events.activeReusedAddrCount, title, metric: "Active Reused Addresses", unit: Unit.count, }), }, { name: "Share", tree: averagesArray({ windows: addrs.reused.events.activeReusedAddrShare, title, metric: "Active Reused Address Share", unit: Unit.percentage, }), }, ], }); const reusedSubtreeForAll = /** @param {(name: string) => string} title */ (title) => ({ name: "Reused", tree: [ ...reusedSetEntries("all", title), reusedActiveSubtreeForAll(title), reusedOutputsSubtreeForAll(title), reusedInputsSubtree("all", title), ], }); const reusedSubtreeForType = /** * @param {AddressableType} key * @param {(name: string) => string} title */ (key, title) => ({ name: "Reused", tree: [ ...reusedSetEntries(key, title), reusedOutputsSubtreeForType(key, title), reusedInputsSubtree(key, title), ], }); const countSubtree = /** * @param {AddressableType | "all"} key * @param {(name: string) => string} title */ (key, title) => ({ name: "Count", tree: [ { name: "Compare", title: title("Address Count"), bottom: countMetrics.map((m) => line({ series: addrs[m.key][key], name: m.name, color: m.color, unit: Unit.count, }), ), }, ...countMetrics.map((m) => ({ name: m.name, title: title(`${m.name} Addresses`), bottom: [ line({ series: addrs[m.key][key], name: m.name, unit: Unit.count, }), ], })), ], }); const exposedSubtree = /** * @param {AddressableType | "all"} key * @param {(name: string) => string} title */ (key, title) => ({ name: "Exposed", tree: [ { name: "Compare", title: title("Exposed Address Count"), bottom: [ line({ series: addrs.exposed.count.funded[key], name: "Funded", unit: Unit.count, }), line({ series: addrs.exposed.count.total[key], name: "Total", color: colors.gray, unit: Unit.count, }), ], }, { name: "Funded", title: title("Funded Exposed Address Count"), bottom: [ line({ series: addrs.exposed.count.funded[key], name: "Funded Exposed", unit: Unit.count, }), ], }, { name: "Total", title: title("Total Exposed Address Count"), bottom: [ line({ series: addrs.exposed.count.total[key], name: "Total Exposed", color: colors.gray, unit: Unit.count, }), ], }, { name: "Supply", title: title("Supply in Exposed Addresses"), bottom: satsBtcUsd({ pattern: addrs.exposed.supply[key], name: "Supply", }), }, ], }); const activityPerTypeEntries = /** * @param {AddressableType | "all"} key * @param {(name: string) => string} title */ (key, title) => activityTypes.map((t) => ({ name: t.name, tree: averagesArray({ windows: addrs.activity[key][t.key], title, metric: `${t.name} Addresses`, unit: Unit.count, }), })); const activitySubtreeForAll = /** @param {(name: string) => string} title */ (title) => ({ name: "Activity", tree: [ { name: "Compare", tree: ROLLING_WINDOWS.map((w) => ({ name: w.name, title: title(`${w.title} Active Addresses`), bottom: [ ...activityTypes.map((t, i) => line({ series: addrs.activity.all[t.key][w.key], name: t.name, color: colors.at(i, activityTypes.length), unit: Unit.count, }), ), line({ series: addrs.reused.events.activeReusedAddrShare[w.key], name: "Reused Share", unit: Unit.percentage, }), ], })), }, ...activityPerTypeEntries("all", title), ], }); const activitySubtreeForType = /** * @param {AddressableType} key * @param {(name: string) => string} title */ (key, title) => ({ name: "Activity", tree: [ { name: "Compare", tree: ROLLING_WINDOWS.map((w) => ({ name: w.name, title: title(`${w.title} Active Addresses`), bottom: activityTypes.map((t, i) => line({ series: addrs.activity[key][t.key][w.key], name: t.name, color: colors.at(i, activityTypes.length), unit: Unit.count, }), ), })), }, ...activityPerTypeEntries(key, title), ], }); const createAddressSeriesTreeForAll = () => { const title = formatCohortTitle(); return [ countSubtree("all", title), { name: "New", tree: chartsFromCount({ pattern: addrs.new.all, title, metric: "New Addresses", unit: Unit.count, }), }, ...simpleDeltaTree({ delta: addrs.delta.all, title, metric: "Address Count", unit: Unit.count, }), activitySubtreeForAll(title), reusedSubtreeForAll(title), exposedSubtree("all", title), ]; }; const createAddressSeriesTreeForType = /** * @param {AddressableType} addrType * @param {string} typeName */ (addrType, typeName) => { const title = formatCohortTitle(typeName); return [ countSubtree(addrType, title), { name: "New", tree: chartsFromCount({ pattern: addrs.new[addrType], title, metric: "New Addresses", unit: Unit.count, }), }, ...simpleDeltaTree({ delta: addrs.delta[addrType], title, metric: "Address Count", unit: Unit.count, }), activitySubtreeForType(addrType, title), reusedSubtreeForType(addrType, title), exposedSubtree(addrType, title), ]; }; /** * Mirror of the per-type singles tree, but every leaf is a cross-type * comparison chart (same metric, same unit, one line per addr type). * Structure parallels `createAddressSeriesTreeForType` section-by-section * so users can compare anything they can view on a single type. */ const createAddressByTypeCompare = () => { const typeLines = /** * @param {(t: (typeof addressTypes)[number]) => AnySeriesPattern} getSeries * @param {Unit} [unit] */ (getSeries, unit = Unit.count) => addressTypes.map((t) => line({ series: getSeries(t), name: t.name, color: t.color, unit, defaultActive: t.defaultActive, }), ); const typeBaselines = /** * @param {(t: (typeof addressTypes)[number]) => AnySeriesPattern} getSeries * @param {Unit} [unit] */ (getSeries, unit = Unit.count) => addressTypes.map((t) => baseline({ series: getSeries(t), name: t.name, color: t.color, unit, defaultActive: t.defaultActive, }), ); return { name: "Compare", tree: [ // Count (lifetime Funded/Empty/Total) { name: "Count", tree: countMetrics.map((m) => ({ name: m.name, title: `${m.name} Address Count by Type`, bottom: typeLines((t) => addrs[m.key][t.key]), })), }, // New (rolling sums + cumulative) { name: "New", tree: groupedWindowsCumulative({ list: addressTypes, title: (s) => s, metricTitle: "New Addresses by Type", getWindowSeries: (t, key) => addrs.new[t.key].sum[key], getCumulativeSeries: (t) => addrs.new[t.key].cumulative, seriesFn: line, unit: Unit.count, }), }, // Change (rolling deltas, signed, baseline) { name: "Change", tree: ROLLING_WINDOWS.map((w) => ({ name: w.name, title: `${w.title} Address Count Change by Type`, bottom: typeBaselines( (t) => addrs.delta[t.key].absolute[w.key], ), })), }, // Growth Rate (rolling percent rates) { name: "Growth Rate", tree: ROLLING_WINDOWS.map((w) => ({ name: w.name, title: `${w.title} Address Growth Rate by Type`, bottom: typeLines( (t) => addrs.delta[t.key].rate[w.key].percent, Unit.percentage, ), })), }, // Activity (per activity type, per window) { name: "Activity", tree: activityTypes.map((a) => ({ name: a.name, tree: ROLLING_WINDOWS.map((w) => ({ name: w.name, title: `${w.title} ${a.name} Addresses by Type`, bottom: typeLines( (t) => addrs.activity[t.key][a.key][w.key], ), })), })), }, // Reused { name: "Reused", tree: [ { name: "Funded", title: "Funded Reused Address Count by Type", bottom: typeLines((t) => addrs.reused.count.funded[t.key]), }, { name: "Total", title: "Total Reused Address Count by Type", bottom: typeLines((t) => addrs.reused.count.total[t.key]), }, { name: "Outputs", tree: [ { name: "Count", tree: groupedWindowsCumulative({ list: addressTypes, title: (s) => s, metricTitle: "Transaction Outputs to Reused Addresses by Type", getWindowSeries: (t, key) => addrs.reused.events.outputToReusedAddrCount[t.key].sum[ key ], getCumulativeSeries: (t) => addrs.reused.events.outputToReusedAddrCount[t.key] .cumulative, seriesFn: line, unit: Unit.count, }), }, { name: "Share", tree: [ ...ROLLING_WINDOWS.map((w) => ({ name: w.name, title: `${w.title} Share of Transaction Outputs to Reused Addresses by Type`, bottom: typeLines( (t) => addrs.reused.events.outputToReusedAddrShare[t.key][ w.key ].percent, Unit.percentage, ), })), { name: "Cumulative", title: "Cumulative Share of Transaction Outputs to Reused Addresses by Type", bottom: typeLines( (t) => addrs.reused.events.outputToReusedAddrShare[t.key] .percent, Unit.percentage, ), }, ], }, ], }, { name: "Inputs", tree: [ { name: "Count", tree: groupedWindowsCumulative({ list: addressTypes, title: (s) => s, metricTitle: "Transaction Inputs from Reused Addresses by Type", getWindowSeries: (t, key) => addrs.reused.events.inputFromReusedAddrCount[t.key].sum[ key ], getCumulativeSeries: (t) => addrs.reused.events.inputFromReusedAddrCount[t.key] .cumulative, seriesFn: line, unit: Unit.count, }), }, { name: "Share", tree: [ ...ROLLING_WINDOWS.map((w) => ({ name: w.name, title: `${w.title} Share of Transaction Inputs from Reused Addresses by Type`, bottom: typeLines( (t) => addrs.reused.events.inputFromReusedAddrShare[t.key][ w.key ].percent, Unit.percentage, ), })), { name: "Cumulative", title: "Cumulative Share of Transaction Inputs from Reused Addresses by Type", bottom: typeLines( (t) => addrs.reused.events.inputFromReusedAddrShare[t.key] .percent, Unit.percentage, ), }, ], }, ], }, ], }, // Exposed { name: "Exposed", tree: [ { name: "Funded", title: "Funded Exposed Address Count by Type", bottom: typeLines((t) => addrs.exposed.count.funded[t.key]), }, { name: "Total", title: "Total Exposed Address Count by Type", bottom: typeLines((t) => addrs.exposed.count.total[t.key]), }, { name: "Supply", title: "Supply in Exposed Addresses by Type", bottom: addressTypes.flatMap((t) => satsBtcUsd({ pattern: addrs.exposed.supply[t.key], name: t.name, color: t.color, defaultActive: t.defaultActive, }), ), }, ], }, ], }; }; /** * Build a "By Type" subtree: Compare (count / tx count / tx %) plus a * per-type drill-down with the same three metrics. * * @template {string} K * @param {Object} args * @param {string} args.label - Singular noun for count/tree labels ("Output" / "Prev-Out") * @param {Readonly>>} args.count * @param {Readonly>>} args.txCount * @param {Readonly>} args.share * @param {Readonly>} args.txShare * @param {ReadonlyArray<{key: K, name: string, color: Color, defaultActive: boolean}>} args.types * @returns {PartialOptionsTree} */ const createByTypeTree = ({ label, count, share, txCount, txShare, types, }) => { const lowerLabel = label.toLowerCase(); return [ { name: "Compare", tree: [ { name: "Count", tree: [ ...ROLLING_WINDOWS.map((w) => ({ name: w.name, title: `${w.title} ${label} Count by Type`, bottom: types.map((t) => line({ series: count[t.key].sum[w.key], name: t.name, color: t.color, unit: Unit.count, defaultActive: t.defaultActive, }), ), })), { name: "Cumulative", title: `Cumulative ${label} Count by Type`, bottom: types.map((t) => line({ series: count[t.key].cumulative, name: t.name, color: t.color, unit: Unit.count, defaultActive: t.defaultActive, }), ), }, ], }, { name: "Share", tree: groupedWindowsCumulative({ list: types, title: (n) => n, metricTitle: `Share of ${label}s by Type`, getWindowSeries: (t, key) => share[t.key][key].percent, getCumulativeSeries: (t) => share[t.key].percent, seriesFn: line, unit: Unit.percentage, }), }, { name: "Transaction Count", tree: [ ...ROLLING_WINDOWS.map((w) => ({ name: w.name, title: `${w.title} Transactions by ${label} Type`, bottom: types.map((t) => line({ series: txCount[t.key].sum[w.key], name: t.name, color: t.color, unit: Unit.count, defaultActive: t.defaultActive, }), ), })), { name: "Cumulative", title: `Cumulative Transactions by ${label} Type`, bottom: types.map((t) => line({ series: txCount[t.key].cumulative, name: t.name, color: t.color, unit: Unit.count, defaultActive: t.defaultActive, }), ), }, ], }, { name: "Transaction Share", tree: [ ...ROLLING_WINDOWS.map((w) => ({ name: w.name, title: `${w.title} Share of Transactions by ${label} Type`, bottom: types.map((t) => line({ series: txShare[t.key][w.key].percent, name: t.name, color: t.color, unit: Unit.percentage, defaultActive: t.defaultActive, }), ), })), { name: "Cumulative", title: `Cumulative Share of Transactions by ${label} Type`, bottom: types.map((t) => line({ series: txShare[t.key].percent, name: t.name, color: t.color, unit: Unit.percentage, defaultActive: t.defaultActive, }), ), }, ], }, ], }, ...types.map((t) => ({ name: t.name, tree: [ { name: "Count", tree: chartsFromCount({ pattern: count[t.key], metric: `${t.name} ${label} Count`, unit: Unit.count, color: t.color, }), }, { name: "Share", tree: chartsFromPercentCumulative({ pattern: share[t.key], metric: `Share of ${label}s that are ${t.name}`, color: t.color, }), }, { name: "Transaction Count", tree: chartsFromCount({ pattern: txCount[t.key], metric: `Transactions with ${t.name} ${lowerLabel}`, unit: Unit.count, color: t.color, }), }, { name: "Transaction Share", tree: chartsFromPercentCumulative({ pattern: txShare[t.key], metric: `Share of Transactions with ${t.name} ${lowerLabel}`, color: t.color, }), }, ], })), ]; }; 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: percentRatioDots({ pattern: supply.inflationRate, name: "Rate", }), }, { name: "Hodled or Lost", title: "Hodled or Lost Supply", bottom: satsBtcUsd({ pattern: supply.hodledOrLost, name: "Supply", }), }, { name: "Unspendable", tree: [ { name: "All", title: "Unspendable Supply", bottom: satsBtcUsdFrom({ source: supply.burned, key: "cumulative", name: "All Time", }), }, { name: "OP_RETURN", title: "OP_RETURN Burned", bottom: satsBtcUsd({ pattern: outputs.value.opReturn.cumulative, name: "All Time", }), }, ], }, ], }, // Blocks { name: "Blocks", tree: [ { name: "Count", tree: [ { name: "Compare", title: "Block Count", bottom: ROLLING_WINDOWS.map((w) => line({ series: blocks.count.total.sum[w.key], name: w.name, color: w.color, unit: Unit.count, }), ), }, ...ROLLING_WINDOWS.map((w) => ({ name: w.name, title: `${w.title} Block Count`, bottom: [ line({ series: blocks.count.total.sum[w.key], name: "Actual", unit: Unit.count, }), line({ series: blocks.count.target[w.key], name: "Target", color: colors.gray, unit: Unit.count, options: { lineStyle: 4 }, }), ], })), { name: "Cumulative", title: "Cumulative Block Count", bottom: [ { series: blocks.count.total.cumulative, title: "All Time", unit: Unit.count, }, ], }, ], }, { name: "Interval", tree: averagesArray({ windows: blocks.interval, metric: "Block Interval", unit: Unit.secs, }), }, { name: "Size", tree: chartsFromFull({ pattern: blocks.size, metric: "Block Size", unit: Unit.bytes, }), }, { name: "Weight", tree: chartsFromFull({ pattern: blocks.weight, metric: "Block Weight", unit: Unit.wu, }), }, { name: "vBytes", tree: chartsFromFull({ pattern: blocks.vbytes, metric: "Block vBytes", unit: Unit.vb, }), }, ], }, // Transactions { name: "Transactions", tree: [ { name: "Count", tree: chartsFromFullPerBlock({ pattern: transactions.count.total, metric: "Transaction Count", unit: Unit.count, }), }, { name: "Per Second", tree: averagesArray({ windows: transactions.volume.txPerSec, metric: "Transactions per Second", unit: Unit.perSec, }), }, { name: "Volume", tree: satsBtcUsdFullTree({ pattern: transactions.volume.transferVolume, metric: "Transaction Volume", }), }, { name: "Effective Fee Rate", tree: chartsFromBlockAnd6b({ pattern: transactions.fees.effectiveFeeRate, metric: "Effective Transaction Fee Rate", unit: Unit.feeRate, }), }, { name: "Fee", tree: chartsFromBlockAnd6b({ pattern: transactions.fees.fee, metric: "Transaction Fee", unit: Unit.sats, }), }, { name: "Size", tree: [ { name: "Weight", tree: chartsFromBlockAnd6b({ pattern: transactions.size.weight, metric: "Transaction Weight", unit: Unit.wu, }), }, { name: "Virtual", tree: chartsFromBlockAnd6b({ pattern: transactions.size.vsize, metric: "Transaction vSize", unit: Unit.vb, }), }, ], }, { name: "Versions", tree: chartsFromCountEntries({ entries: entries(transactions.versions), metric: "Transaction Versions", unit: Unit.count, }), }, { name: "Velocity", title: "Transaction Velocity", bottom: [ line({ series: supply.velocity.native, name: "BTC", unit: Unit.ratio, }), line({ series: supply.velocity.fiat, name: "USD", color: colors.usd, unit: Unit.ratio, }), ], }, ], }, // UTXOs { name: "UTXOs", tree: [ { name: "Count", title: "UTXO Count", bottom: [ line({ series: outputs.unspent.count, name: "Count", unit: Unit.count, }), ], }, ...simpleDeltaTree({ delta: cohorts.utxo.all.outputs.unspentCount.delta, metric: "UTXO Count", unit: Unit.count, }), { name: "Flow", tree: multiSeriesTree({ entries: [ { name: "Created", color: colors.entity.output, average: outputs.count.total.rolling.average, sum: outputs.count.total.rolling.sum, cumulative: outputs.count.total.cumulative, }, { name: "Spent", color: colors.entity.input, average: inputs.count.rolling.average, sum: inputs.count.rolling.sum, cumulative: inputs.count.cumulative, }, ], metric: "UTXO Flow", unit: Unit.count, }), }, ], }, { name: "Outputs", tree: [ { name: "Count", tree: [ { name: "Compare", title: "Output Count", bottom: ROLLING_WINDOWS.map((w) => line({ series: outputs.count.total.rolling.average[w.key], name: w.name, color: w.color, unit: Unit.count, }), ), }, ...ROLLING_WINDOWS.map((w) => ({ name: w.name, title: `${w.title} Output Count`, bottom: [ line({ series: outputs.count.total.rolling.sum[w.key], name: "Total (Sum)", color: w.color, unit: Unit.count, }), line({ series: outputs.byType.spendableOutputCount.sum[w.key], name: "Spendable (Sum)", color: colors.gray, unit: Unit.count, }), line({ series: outputs.count.total.rolling.average[w.key], name: "Total (Avg)", color: w.color, unit: Unit.count, defaultActive: false, }), line({ series: outputs.byType.spendableOutputCount.average[w.key], name: "Spendable (Avg)", color: colors.gray, unit: Unit.count, defaultActive: false, }), ], })), { name: "Cumulative", title: "Cumulative Output Count", bottom: [ line({ series: outputs.count.total.cumulative, name: "Total", unit: Unit.count, }), line({ series: outputs.byType.spendableOutputCount.cumulative, name: "Spendable", color: colors.gray, unit: Unit.count, }), ], }, distributionWindowsTree({ pattern: outputs.count.total.rolling, metric: "Output Count per Block", unit: Unit.count, }), ], }, { name: "Per Second", tree: averagesArray({ windows: outputs.perSec, metric: "Outputs per Second", unit: Unit.perSec, }), }, { name: "By Type", tree: createByTypeTree({ label: "Output", count: outputs.byType.outputCount, share: outputs.byType.outputShare, txCount: outputs.byType.txCount, txShare: outputs.byType.txShare, types: outputTypes, }), }, ], }, { name: "Inputs", tree: [ { name: "Count", tree: chartsFromAggregatedPerBlock({ pattern: inputs.count, metric: "Input Count", unit: Unit.count, }), }, { name: "Per Second", tree: averagesArray({ windows: inputs.perSec, metric: "Inputs per Second", unit: Unit.perSec, }), }, { name: "By Type", tree: createByTypeTree({ label: "Prev-Out", count: inputs.byType.inputCount, share: inputs.byType.inputShare, txCount: inputs.byType.txCount, txShare: inputs.byType.txShare, types: inputTypes, }), }, ], }, { name: "Throughput", tree: ROLLING_WINDOWS.map((w) => ({ name: w.name, title: `${w.title} Throughput`, bottom: [ line({ series: transactions.volume.txPerSec[w.key], name: "TX/sec", color: colors.entity.tx, unit: Unit.perSec, }), line({ series: inputs.perSec[w.key], name: "Inputs/sec", color: colors.entity.input, unit: Unit.perSec, }), line({ series: outputs.perSec[w.key], name: "Outputs/sec", color: colors.entity.output, unit: Unit.perSec, }), ], })), }, // Addresses { name: "Addresses", tree: [ ...createAddressSeriesTreeForAll(), { name: "By Type", tree: [ createAddressByTypeCompare(), ...addressTypes.map((t) => ({ name: t.name, tree: createAddressSeriesTreeForType(t.key, t.name), })), ], }, ], }, ], }; }