From 514fdc40ee89a243ce8038a165d7e64a98e02874 Mon Sep 17 00:00:00 2001 From: nym21 Date: Sun, 22 Mar 2026 12:19:06 +0100 Subject: [PATCH] global: snapshot --- Cargo.lock | 4 +- crates/brk_client/src/lib.rs | 8 +- crates/brk_cohort/src/loss.rs | 14 + crates/brk_cohort/src/profit.rs | 13 + crates/brk_cohort/src/profitability_range.rs | 55 ++- .../src/cointime/adjusted/import.rs | 4 +- .../distribution/cohorts/utxo/percentiles.rs | 75 +-- .../src/distribution/metrics/profitability.rs | 95 +++- .../src/supply/velocity/import.rs | 4 +- crates/brk_query/Cargo.toml | 2 +- modules/brk-client/index.js | 8 +- modules/quickmatch-js/0.3.1/src/index.js | 441 ------------------ modules/quickmatch-js/0.4.0/src/index.js | 417 +++++++++++++++++ packages/brk_client/brk_client/__init__.py | 8 +- website/scripts/chart/index.js | 46 +- .../scripts/options/distribution/activity.js | 20 +- .../scripts/options/distribution/holdings.js | 1 + website/scripts/options/distribution/index.js | 78 +--- .../scripts/options/distribution/prices.js | 2 +- .../options/distribution/profitability.js | 1 + .../scripts/options/distribution/valuation.js | 2 +- website/scripts/options/investing.js | 8 +- website/scripts/options/market.js | 30 +- website/scripts/options/mining.js | 28 +- website/scripts/options/network.js | 25 +- website/scripts/options/series.js | 34 +- website/scripts/options/shared.js | 8 +- website/scripts/options/unused.js | 5 +- website/scripts/panes/search.js | 8 +- website/scripts/panes/share.js | 7 +- website/scripts/types.js | 9 + website/scripts/utils/array.js | 2 +- website/scripts/utils/dom.js | 8 +- website/scripts/utils/timing.js | 4 +- website/service-worker.js | 3 +- 35 files changed, 732 insertions(+), 745 deletions(-) delete mode 100644 modules/quickmatch-js/0.3.1/src/index.js create mode 100644 modules/quickmatch-js/0.4.0/src/index.js diff --git a/Cargo.lock b/Cargo.lock index cbcbb9595..b6090729d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2460,9 +2460,9 @@ dependencies = [ [[package]] name = "quickmatch" -version = "0.3.2" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28628ced26de8cc0e8ce04e4c2e718bc8d84e9b99616a2a6abd2096e9a55f728" +checksum = "848244615004bddb7273545dfe909ead495ed734f9faf130c43a7daccca2bf99" dependencies = [ "rustc-hash", ] diff --git a/crates/brk_client/src/lib.rs b/crates/brk_client/src/lib.rs index 429028a04..413df287a 100644 --- a/crates/brk_client/src/lib.rs +++ b/crates/brk_client/src/lib.rs @@ -4470,8 +4470,8 @@ impl SeriesTree_Cointime_Adjusted { pub fn new(client: Arc, base_path: String) -> Self { Self { inflation_rate: BpsPercentRatioPattern::new(client.clone(), "cointime_adj_inflation_rate".to_string()), - tx_velocity_native: SeriesPattern1::new(client.clone(), "cointime_adj_tx_velocity".to_string()), - tx_velocity_fiat: SeriesPattern1::new(client.clone(), "cointime_adj_tx_velocity_fiat".to_string()), + tx_velocity_native: SeriesPattern1::new(client.clone(), "cointime_adj_tx_velocity_btc".to_string()), + tx_velocity_fiat: SeriesPattern1::new(client.clone(), "cointime_adj_tx_velocity_usd".to_string()), } } } @@ -6345,8 +6345,8 @@ pub struct SeriesTree_Supply_Velocity { impl SeriesTree_Supply_Velocity { pub fn new(client: Arc, base_path: String) -> Self { Self { - native: SeriesPattern1::new(client.clone(), "velocity".to_string()), - fiat: SeriesPattern1::new(client.clone(), "velocity_fiat".to_string()), + native: SeriesPattern1::new(client.clone(), "velocity_btc".to_string()), + fiat: SeriesPattern1::new(client.clone(), "velocity_usd".to_string()), } } } diff --git a/crates/brk_cohort/src/loss.rs b/crates/brk_cohort/src/loss.rs index cb065cec6..f9c39cd6e 100644 --- a/crates/brk_cohort/src/loss.rs +++ b/crates/brk_cohort/src/loss.rs @@ -141,4 +141,18 @@ impl Loss { &mut self._80pct, ] } + + /// Iterate from narrowest (_80pct) to broadest (all), yielding each threshold + /// with a growing suffix slice of `ranges` (1 range, 2 ranges, ..., LOSS_COUNT). + pub fn iter_mut_with_growing_suffix<'a, R>( + &'a mut self, + ranges: &'a [R], + ) -> impl Iterator { + let len = ranges.len(); + self.as_array_mut() + .into_iter() + .rev() + .enumerate() + .map(move |(n, threshold)| (threshold, &ranges[len - 1 - n..])) + } } diff --git a/crates/brk_cohort/src/profit.rs b/crates/brk_cohort/src/profit.rs index e0372e869..4b200fe20 100644 --- a/crates/brk_cohort/src/profit.rs +++ b/crates/brk_cohort/src/profit.rs @@ -181,4 +181,17 @@ impl Profit { &mut self._500pct, ] } + + /// Iterate from narrowest (_500pct) to broadest (all), yielding each threshold + /// with a growing prefix slice of `ranges` (1 range, 2 ranges, ..., PROFIT_COUNT). + pub fn iter_mut_with_growing_prefix<'a, R>( + &'a mut self, + ranges: &'a [R], + ) -> impl Iterator { + self.as_array_mut() + .into_iter() + .rev() + .enumerate() + .map(move |(n, threshold)| (threshold, &ranges[..n + 1])) + } } diff --git a/crates/brk_cohort/src/profitability_range.rs b/crates/brk_cohort/src/profitability_range.rs index eec59b50e..cad44f299 100644 --- a/crates/brk_cohort/src/profitability_range.rs +++ b/crates/brk_cohort/src/profitability_range.rs @@ -253,32 +253,37 @@ impl ProfitabilityRange { } pub fn iter_mut(&mut self) -> impl Iterator { + self.iter_mut_with_is_profit().map(|(_, v)| v) + } + + /// Iterate mutably, yielding `(is_profit, &mut T)` for each range. + pub fn iter_mut_with_is_profit(&mut self) -> impl Iterator { [ - &mut self.over_1000pct_in_profit, - &mut self._500pct_to_1000pct_in_profit, - &mut self._300pct_to_500pct_in_profit, - &mut self._200pct_to_300pct_in_profit, - &mut self._100pct_to_200pct_in_profit, - &mut self._90pct_to_100pct_in_profit, - &mut self._80pct_to_90pct_in_profit, - &mut self._70pct_to_80pct_in_profit, - &mut self._60pct_to_70pct_in_profit, - &mut self._50pct_to_60pct_in_profit, - &mut self._40pct_to_50pct_in_profit, - &mut self._30pct_to_40pct_in_profit, - &mut self._20pct_to_30pct_in_profit, - &mut self._10pct_to_20pct_in_profit, - &mut self._0pct_to_10pct_in_profit, - &mut self._0pct_to_10pct_in_loss, - &mut self._10pct_to_20pct_in_loss, - &mut self._20pct_to_30pct_in_loss, - &mut self._30pct_to_40pct_in_loss, - &mut self._40pct_to_50pct_in_loss, - &mut self._50pct_to_60pct_in_loss, - &mut self._60pct_to_70pct_in_loss, - &mut self._70pct_to_80pct_in_loss, - &mut self._80pct_to_90pct_in_loss, - &mut self._90pct_to_100pct_in_loss, + (true, &mut self.over_1000pct_in_profit), + (true, &mut self._500pct_to_1000pct_in_profit), + (true, &mut self._300pct_to_500pct_in_profit), + (true, &mut self._200pct_to_300pct_in_profit), + (true, &mut self._100pct_to_200pct_in_profit), + (true, &mut self._90pct_to_100pct_in_profit), + (true, &mut self._80pct_to_90pct_in_profit), + (true, &mut self._70pct_to_80pct_in_profit), + (true, &mut self._60pct_to_70pct_in_profit), + (true, &mut self._50pct_to_60pct_in_profit), + (true, &mut self._40pct_to_50pct_in_profit), + (true, &mut self._30pct_to_40pct_in_profit), + (true, &mut self._20pct_to_30pct_in_profit), + (true, &mut self._10pct_to_20pct_in_profit), + (true, &mut self._0pct_to_10pct_in_profit), + (false, &mut self._0pct_to_10pct_in_loss), + (false, &mut self._10pct_to_20pct_in_loss), + (false, &mut self._20pct_to_30pct_in_loss), + (false, &mut self._30pct_to_40pct_in_loss), + (false, &mut self._40pct_to_50pct_in_loss), + (false, &mut self._50pct_to_60pct_in_loss), + (false, &mut self._60pct_to_70pct_in_loss), + (false, &mut self._70pct_to_80pct_in_loss), + (false, &mut self._80pct_to_90pct_in_loss), + (false, &mut self._90pct_to_100pct_in_loss), ] .into_iter() } diff --git a/crates/brk_computer/src/cointime/adjusted/import.rs b/crates/brk_computer/src/cointime/adjusted/import.rs index 90c6e1e62..c911b6382 100644 --- a/crates/brk_computer/src/cointime/adjusted/import.rs +++ b/crates/brk_computer/src/cointime/adjusted/import.rs @@ -23,13 +23,13 @@ impl Vecs { )?, tx_velocity_native: PerBlock::forced_import( db, - "cointime_adj_tx_velocity", + "cointime_adj_tx_velocity_btc", version, indexes, )?, tx_velocity_fiat: PerBlock::forced_import( db, - "cointime_adj_tx_velocity_fiat", + "cointime_adj_tx_velocity_usd", version, indexes, )?, diff --git a/crates/brk_computer/src/distribution/cohorts/utxo/percentiles.rs b/crates/brk_computer/src/distribution/cohorts/utxo/percentiles.rs index d5645a9b5..d71484a7f 100644 --- a/crates/brk_computer/src/distribution/cohorts/utxo/percentiles.rs +++ b/crates/brk_computer/src/distribution/cohorts/utxo/percentiles.rs @@ -1,6 +1,6 @@ use std::{cmp::Reverse, collections::BinaryHeap, fs, path::Path}; -use brk_cohort::{Filtered, PROFIT_COUNT, PROFITABILITY_RANGE_COUNT, TERM_NAMES}; +use brk_cohort::{Filtered, PROFITABILITY_RANGE_COUNT, TERM_NAMES}; use brk_error::Result; use brk_types::{BasisPoints16, Cents, CentsCompact, CostBasisDistribution, Date, Dollars, Sats}; @@ -50,7 +50,7 @@ impl UTXOCohorts { push_cost_basis(<h, lth_d, &mut self.lth.metrics.cost_basis); let prof = self.fenwick.profitability(spot_price); - push_profitability(&prof, spot_price, &mut self.profitability); + push_profitability(&prof, &mut self.profitability); } /// K-way merge only for writing daily cost basis distributions to disk. @@ -100,93 +100,22 @@ fn push_cost_basis(percentiles: &PercentileResult, density_bps: u16, cost_basis: cost_basis.push_density(BasisPoints16::from(density_bps)); } -/// Convert raw (cents × sats) accumulator to Dollars (÷ 100 for cents→dollars, ÷ 1e8 for sats). #[inline(always)] fn raw_usd_to_dollars(raw: u128) -> Dollars { Dollars::from(raw as f64 / 1e10) } -/// Number of profit ranges (0..=14 are profit, 15..=24 are loss). -const PROFIT_RANGE_COUNT: usize = 15; - -/// Compute unrealized P&L from raw sats/usd for a given range. -/// Profit ranges: market_value - cost_basis. Loss ranges: cost_basis - market_value. -#[inline(always)] -fn compute_unrealized_pnl(spot_cents: u128, sats: u64, usd: u128, is_profit: bool) -> Dollars { - let market_value = spot_cents * sats as u128; - let raw = if is_profit { - market_value.saturating_sub(usd) - } else { - usd.saturating_sub(market_value) - }; - raw_usd_to_dollars(raw) -} - -/// Push profitability range + profit/loss aggregate values to vecs. fn push_profitability( buckets: &[ProfitabilityRangeResult; PROFITABILITY_RANGE_COUNT], - spot_price: Cents, metrics: &mut ProfitabilityMetrics, ) { - let spot_cents = spot_price.as_u128(); - - // Push 25 range buckets for (i, bucket) in metrics.range.as_array_mut().into_iter().enumerate() { let r = &buckets[i]; - let is_profit = i < PROFIT_RANGE_COUNT; bucket.push( Sats::from(r.all_sats), Sats::from(r.sth_sats), raw_usd_to_dollars(r.all_usd), raw_usd_to_dollars(r.sth_usd), - compute_unrealized_pnl(spot_cents, r.all_sats, r.all_usd, is_profit), - compute_unrealized_pnl(spot_cents, r.sth_sats, r.sth_usd, is_profit), - ); - } - - // Profit: forward cumulative sum over ranges[0..15], pushed in reverse. - // profit[0] (breakeven) = sum(0..=13), ..., profit[13] (_500pct) = ranges[0] - let profit_arr = metrics.profit.as_array_mut(); - let mut cum_sats = 0u64; - let mut cum_sth_sats = 0u64; - let mut cum_usd = 0u128; - let mut cum_sth_usd = 0u128; - for i in 0..PROFIT_COUNT { - cum_sats += buckets[i].all_sats; - cum_sth_sats += buckets[i].sth_sats; - cum_usd += buckets[i].all_usd; - cum_sth_usd += buckets[i].sth_usd; - profit_arr[PROFIT_COUNT - 1 - i].push( - Sats::from(cum_sats), - Sats::from(cum_sth_sats), - raw_usd_to_dollars(cum_usd), - raw_usd_to_dollars(cum_sth_usd), - compute_unrealized_pnl(spot_cents, cum_sats, cum_usd, true), - compute_unrealized_pnl(spot_cents, cum_sth_sats, cum_sth_usd, true), - ); - } - - // Loss: backward cumulative sum over ranges[15..25], pushed in reverse. - // loss[0] (breakeven) = sum(15..=24), ..., loss[8] (_80pct) = ranges[24] - let loss_arr = metrics.loss.as_array_mut(); - let loss_count = loss_arr.len(); - cum_sats = 0; - cum_sth_sats = 0; - cum_usd = 0; - cum_sth_usd = 0; - for i in 0..loss_count { - let r = &buckets[PROFITABILITY_RANGE_COUNT - 1 - i]; - cum_sats += r.all_sats; - cum_sth_sats += r.sth_sats; - cum_usd += r.all_usd; - cum_sth_usd += r.sth_usd; - loss_arr[loss_count - 1 - i].push( - Sats::from(cum_sats), - Sats::from(cum_sth_sats), - raw_usd_to_dollars(cum_usd), - raw_usd_to_dollars(cum_sth_usd), - compute_unrealized_pnl(spot_cents, cum_sats, cum_usd, false), - compute_unrealized_pnl(spot_cents, cum_sth_sats, cum_sth_usd, false), ); } } diff --git a/crates/brk_computer/src/distribution/metrics/profitability.rs b/crates/brk_computer/src/distribution/metrics/profitability.rs index 6e769ccdb..03de07a96 100644 --- a/crates/brk_computer/src/distribution/metrics/profitability.rs +++ b/crates/brk_computer/src/distribution/metrics/profitability.rs @@ -1,7 +1,7 @@ use brk_cohort::{Loss, Profit, ProfitabilityRange}; use brk_error::Result; use brk_traversable::Traversable; -use brk_types::{BasisPointsSigned32, Cents, Dollars, Indexes, Sats, Version}; +use brk_types::{BasisPointsSigned32, Bitcoin, Cents, Dollars, Indexes, Sats, Version}; use vecdb::{AnyStoredVec, AnyVec, Database, Exit, Rw, StorageMode, WritableVec}; use crate::{ @@ -32,7 +32,6 @@ impl ProfitabilityBucket { .height .len() .min(self.realized_cap.all.height.len()) - .min(self.unrealized_pnl.all.height.len()) } } @@ -104,21 +103,18 @@ impl ProfitabilityBucket { sth_supply: Sats, realized_cap: Dollars, sth_realized_cap: Dollars, - unrealized_pnl: Dollars, - sth_unrealized_pnl: Dollars, ) { self.supply.all.sats.height.push(supply); self.supply.sth.sats.height.push(sth_supply); self.realized_cap.all.height.push(realized_cap); self.realized_cap.sth.height.push(sth_realized_cap); - self.unrealized_pnl.all.height.push(unrealized_pnl); - self.unrealized_pnl.sth.height.push(sth_unrealized_pnl); } pub(crate) fn compute( &mut self, prices: &prices::Vecs, starting_indexes: &Indexes, + is_profit: bool, exit: &Exit, ) -> Result<()> { let max_from = starting_indexes.height; @@ -126,8 +122,33 @@ impl ProfitabilityBucket { self.supply.all.compute(prices, max_from, exit)?; self.supply.sth.compute(prices, max_from, exit)?; - // NUPL = (spot - realized_price) / spot - // where realized_price = realized_cap_cents × ONE_BTC / supply_sats + self.unrealized_pnl.all.height.compute_transform3( + max_from, + &prices.spot.cents.height, + &self.realized_cap.all.height, + &self.supply.all.sats.height, + |(i, spot, cap, supply, ..)| { + let mv = f64::from(Dollars::from(spot)) * f64::from(Bitcoin::from(supply)); + let rc = f64::from(cap); + let pnl = if is_profit { mv - rc } else { rc - mv }.max(0.0); + (i, Dollars::from(pnl)) + }, + exit, + )?; + self.unrealized_pnl.sth.height.compute_transform3( + max_from, + &prices.spot.cents.height, + &self.realized_cap.sth.height, + &self.supply.sth.sats.height, + |(i, spot, cap, supply, ..)| { + let mv = f64::from(Dollars::from(spot)) * f64::from(Bitcoin::from(supply)); + let rc = f64::from(cap); + let pnl = if is_profit { mv - rc } else { rc - mv }.max(0.0); + (i, Dollars::from(pnl)) + }, + exit, + )?; + self.nupl.bps.height.compute_transform3( max_from, &prices.spot.cents.height, @@ -150,6 +171,40 @@ impl ProfitabilityBucket { Ok(()) } + pub(crate) fn compute_from_ranges( + &mut self, + prices: &prices::Vecs, + starting_indexes: &Indexes, + is_profit: bool, + sources: &[&ProfitabilityBucket], + exit: &Exit, + ) -> Result<()> { + let max_from = starting_indexes.height; + + self.supply.all.sats.height.compute_sum_of_others( + max_from, + &sources.iter().map(|s| &s.supply.all.sats.height).collect::>(), + exit, + )?; + self.supply.sth.sats.height.compute_sum_of_others( + max_from, + &sources.iter().map(|s| &s.supply.sth.sats.height).collect::>(), + exit, + )?; + self.realized_cap.all.height.compute_sum_of_others( + max_from, + &sources.iter().map(|s| &s.realized_cap.all.height).collect::>(), + exit, + )?; + self.realized_cap.sth.height.compute_sum_of_others( + max_from, + &sources.iter().map(|s| &s.realized_cap.sth.height).collect::>(), + exit, + )?; + + self.compute(prices, starting_indexes, is_profit, exit) + } + pub(crate) fn collect_all_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> { vec![ &mut self.supply.all.inner.sats.height as &mut dyn AnyStoredVec, @@ -189,7 +244,7 @@ impl ProfitabilityMetrics { } pub(crate) fn min_stateful_len(&self) -> usize { - self.iter().map(|b| b.min_len()).min().unwrap_or(0) + self.range.iter().map(|b| b.min_len()).min().unwrap_or(0) } } @@ -204,12 +259,14 @@ impl ProfitabilityMetrics { ProfitabilityBucket::forced_import(db, name, version, indexes, cached_starts) })?; + let aggregate_version = version + Version::ONE; + let profit = Profit::try_new(|name| { - ProfitabilityBucket::forced_import(db, name, version, indexes, cached_starts) + ProfitabilityBucket::forced_import(db, name, aggregate_version, indexes, cached_starts) })?; let loss = Loss::try_new(|name| { - ProfitabilityBucket::forced_import(db, name, version, indexes, cached_starts) + ProfitabilityBucket::forced_import(db, name, aggregate_version, indexes, cached_starts) })?; Ok(Self { @@ -225,8 +282,20 @@ impl ProfitabilityMetrics { starting_indexes: &Indexes, exit: &Exit, ) -> Result<()> { - self.iter_mut() - .try_for_each(|b| b.compute(prices, starting_indexes, exit)) + for (is_profit, bucket) in self.range.iter_mut_with_is_profit() { + bucket.compute(prices, starting_indexes, is_profit, exit)?; + } + + let range_arr = self.range.as_array(); + + for (threshold, sources) in self.profit.iter_mut_with_growing_prefix(&range_arr) { + threshold.compute_from_ranges(prices, starting_indexes, true, sources, exit)?; + } + for (threshold, sources) in self.loss.iter_mut_with_growing_suffix(&range_arr) { + threshold.compute_from_ranges(prices, starting_indexes, false, sources, exit)?; + } + + Ok(()) } pub(crate) fn collect_all_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> { diff --git a/crates/brk_computer/src/supply/velocity/import.rs b/crates/brk_computer/src/supply/velocity/import.rs index a95cd164c..91310a219 100644 --- a/crates/brk_computer/src/supply/velocity/import.rs +++ b/crates/brk_computer/src/supply/velocity/import.rs @@ -12,8 +12,8 @@ impl Vecs { indexes: &indexes::Vecs, ) -> Result { Ok(Self { - native: PerBlock::forced_import(db, "velocity", version, indexes)?, - fiat: PerBlock::forced_import(db, "velocity_fiat", version, indexes)?, + native: PerBlock::forced_import(db, "velocity_btc", version, indexes)?, + fiat: PerBlock::forced_import(db, "velocity_usd", version, indexes)?, }) } } diff --git a/crates/brk_query/Cargo.toml b/crates/brk_query/Cargo.toml index 9aa27079b..ebf3be49b 100644 --- a/crates/brk_query/Cargo.toml +++ b/crates/brk_query/Cargo.toml @@ -25,7 +25,7 @@ derive_more = { workspace = true } jiff = { workspace = true } parking_lot = { workspace = true } # quickmatch = { path = "../../../quickmatch" } -quickmatch = "0.3.2" +quickmatch = "0.4.0" tokio = { workspace = true, optional = true } serde_json = { workspace = true } vecdb = { workspace = true } diff --git a/modules/brk-client/index.js b/modules/brk-client/index.js index aa6451310..8e8c12610 100644 --- a/modules/brk-client/index.js +++ b/modules/brk-client/index.js @@ -7926,8 +7926,8 @@ class BrkClient extends BrkClientBase { }, adjusted: { inflationRate: createBpsPercentRatioPattern(this, 'cointime_adj_inflation_rate'), - txVelocityNative: createSeriesPattern1(this, 'cointime_adj_tx_velocity'), - txVelocityFiat: createSeriesPattern1(this, 'cointime_adj_tx_velocity_fiat'), + txVelocityNative: createSeriesPattern1(this, 'cointime_adj_tx_velocity_btc'), + txVelocityFiat: createSeriesPattern1(this, 'cointime_adj_tx_velocity_usd'), }, reserveRisk: { value: createSeriesPattern1(this, 'reserve_risk'), @@ -8531,8 +8531,8 @@ class BrkClient extends BrkClientBase { burned: createBlockCumulativePattern(this, 'unspendable_supply'), inflationRate: createBpsPercentRatioPattern(this, 'inflation_rate'), velocity: { - native: createSeriesPattern1(this, 'velocity'), - fiat: createSeriesPattern1(this, 'velocity_fiat'), + native: createSeriesPattern1(this, 'velocity_btc'), + fiat: createSeriesPattern1(this, 'velocity_usd'), }, marketCap: createCentsDeltaUsdPattern(this, 'market_cap'), marketMinusRealizedCapGrowthRate: create_1m1w1y24hPattern(this, 'market_minus_realized_cap_growth_rate'), diff --git a/modules/quickmatch-js/0.3.1/src/index.js b/modules/quickmatch-js/0.3.1/src/index.js deleted file mode 100644 index 15d02dff7..000000000 --- a/modules/quickmatch-js/0.3.1/src/index.js +++ /dev/null @@ -1,441 +0,0 @@ -const DEFAULT_SEPARATORS = "_- ,:"; -const DEFAULT_TRIGRAM_BUDGET = 6; -const DEFAULT_LIMIT = 100; -const DEFAULT_MIN_SCORE = 2; - -/** - * Configuration for QuickMatch. - */ -export class QuickMatchConfig { - /** @type {string} Characters used to split items into words */ - separators = DEFAULT_SEPARATORS; - - /** @type {number} Maximum number of results to return */ - limit = DEFAULT_LIMIT; - - /** @type {number} Number of trigram lookups for fuzzy matching (0-20) */ - trigramBudget = DEFAULT_TRIGRAM_BUDGET; - - /** @type {number} Minimum trigram score required for fuzzy matches */ - minScore = DEFAULT_MIN_SCORE; - - /** - * Set maximum number of results. - * @param {number} n - */ - withLimit(n) { - this.limit = Math.max(1, n); - return this; - } - - /** - * Set trigram budget for fuzzy matching. - * Higher values find more typos but cost more. - * @param {number} n - Budget (0-20, default: 6) - */ - withTrigramBudget(n) { - this.trigramBudget = Math.max(0, Math.min(20, n)); - return this; - } - - /** - * Set word separator characters. - * @param {string} s - Separator characters (default: '_- ') - */ - withSeparators(s) { - this.separators = s; - return this; - } - - /** - * Set minimum trigram score for fuzzy matches. - * Higher values require more trigram overlap, reducing noise. - * @param {number} n - Minimum score (default: 2, min: 1) - */ - withMinScore(n) { - this.minScore = Math.max(1, n); - return this; - } -} - -/** - * Fast fuzzy string matcher using word and trigram indexing. - */ -export class QuickMatch { - /** - * Create a new matcher. - * @param {string[]} items - Items to index (should be lowercase) - * @param {QuickMatchConfig} [config] - Optional configuration - */ - constructor(items, config = new QuickMatchConfig()) { - this.config = config; - this.items = items; - /** @type {Map} */ - this.wordIndex = new Map(); - /** @type {Map} */ - this.trigramIndex = new Map(); - - let maxWordLength = 0; - let maxQueryLength = 0; - let maxWordCount = 0; - - const { separators } = config; - - for (let itemIndex = 0; itemIndex < items.length; itemIndex++) { - const item = items[itemIndex]; - - if (item.length > maxQueryLength) { - maxQueryLength = item.length; - } - - let wordCount = 0; - let wordStart = 0; - - for (let i = 0; i <= item.length; i++) { - const isEndOfWord = i === item.length || separators.includes(item[i]); - - if (isEndOfWord && i > wordStart) { - wordCount++; - const word = item.slice(wordStart, i); - - if (word.length > maxWordLength) { - maxWordLength = word.length; - } - - addToIndex(this.wordIndex, word, itemIndex); - addTrigramsToIndex(this.trigramIndex, word, itemIndex); - - wordStart = i + 1; - } else if (isEndOfWord) { - wordStart = i + 1; - } - } - - if (wordCount > maxWordCount) { - maxWordCount = wordCount; - } - } - - this.maxWordLength = maxWordLength + 4; - this.maxQueryLength = maxQueryLength + 6; - this.maxWordCount = maxWordCount + 2; - } - - /** - * Find matching items. Returns items sorted by relevance. - * @param {string} query - Search query - */ - matches(query) { - return this.matchesWith(query, this.config); - } - - /** - * Find matching items with custom config. Returns items sorted by relevance. - * @param {string} query - Search query - * @param {QuickMatchConfig} config - Configuration to use - */ - matchesWith(query, config) { - const { limit, trigramBudget, separators } = config; - - const normalizedQuery = normalizeQuery(query); - - if (!normalizedQuery || normalizedQuery.length > this.maxQueryLength) { - return []; - } - - const queryWords = parseWords( - normalizedQuery, - separators, - this.maxWordLength, - ); - - if (!queryWords.length || queryWords.length > this.maxWordCount) { - return []; - } - - const knownWords = []; - const unknownWords = []; - - for (const word of queryWords) { - const matchingItems = this.wordIndex.get(word); - - if (matchingItems) { - knownWords.push(matchingItems); - } else if (word.length >= 3 && unknownWords.length < trigramBudget) { - unknownWords.push(word); - } - } - - const exactMatches = intersectAll(knownWords); - const hasExactMatches = exactMatches.length > 0; - const needsFuzzyMatching = unknownWords.length > 0 && trigramBudget > 0; - - if (!needsFuzzyMatching) { - if (!hasExactMatches) return []; - return this.sortedByLength(exactMatches, limit); - } - - const scores = new Map(); - - if (hasExactMatches) { - for (const index of exactMatches) { - scores.set(index, 1); - } - } - - const minItemLength = Math.max(0, normalizedQuery.length - 3); - - const hitCount = this.scoreByTrigrams({ - unknownWords, - budget: trigramBudget, - scores, - hasExactMatches, - minItemLength, - }); - - const minScoreToInclude = Math.max(config.minScore, Math.ceil(hitCount / 2)); - - return this.rankedResults(scores, minScoreToInclude, limit); - } - - /** - * @private - * @param {{unknownWords: string[], budget: number, scores: Map, hasExactMatches: boolean, minItemLength: number}} args - */ - scoreByTrigrams({ - unknownWords, - budget, - scores, - hasExactMatches, - minItemLength, - }) { - const visitedTrigrams = new Set(); - let budgetRemaining = budget; - let hitCount = 0; - - outer: for (let round = 0; round < budget; round++) { - for (const word of unknownWords) { - if (budgetRemaining <= 0) break outer; - - const position = pickTrigramPosition(word.length, round); - if (position < 0) continue; - - const trigram = - word[position] + word[position + 1] + word[position + 2]; - - if (visitedTrigrams.has(trigram)) continue; - visitedTrigrams.add(trigram); - - budgetRemaining--; - - const matchingItems = this.trigramIndex.get(trigram); - if (!matchingItems) continue; - - hitCount++; - - for (const itemIndex of matchingItems) { - if (hasExactMatches) { - const currentScore = scores.get(itemIndex); - if (currentScore !== undefined) { - scores.set(itemIndex, currentScore + 1); - } - } else if (this.items[itemIndex].length >= minItemLength) { - scores.set(itemIndex, (scores.get(itemIndex) || 0) + 1); - } - } - } - } - - return hitCount; - } - - /** - * @private - * @param {number[]} indices - * @param {number} limit - */ - sortedByLength(indices, limit) { - const { items } = this; - indices.sort((a, b) => items[a].length - items[b].length); - if (indices.length > limit) indices.length = limit; - return indices.map((i) => items[i]); - } - - /** - * @private - * @param {Map} scores - * @param {number} minScore - * @param {number} limit - */ - rankedResults(scores, minScore, limit) { - const { items } = this; - const results = []; - - for (const [index, score] of scores) { - if (score >= minScore) { - results.push({ index, score }); - } - } - - results.sort((a, b) => { - if (b.score !== a.score) return b.score - a.score; - return items[a.index].length - items[b.index].length; - }); - - if (results.length > limit) results.length = limit; - - return results.map((r) => items[r.index]); - } -} - -/** @param {string} query */ -function normalizeQuery(query) { - let result = ""; - let start = 0; - let end = query.length; - - while (start < end && query.charCodeAt(start) <= 32) start++; - while (end > start && query.charCodeAt(end - 1) <= 32) end--; - - for (let i = start; i < end; i++) { - const code = query.charCodeAt(i); - if (code >= 128) continue; - result += - code >= 65 && code <= 90 ? String.fromCharCode(code + 32) : query[i]; - } - - return result; -} - -/** - * @param {string} text - * @param {string} separators - * @param {number} maxLength - */ -function parseWords(text, separators, maxLength) { - /** @type {string[]} */ - const words = []; - let start = 0; - - for (let i = 0; i <= text.length; i++) { - const isEnd = i === text.length || separators.includes(text[i]); - - if (isEnd && i > start) { - const word = text.slice(start, i); - if (word.length <= maxLength && !words.includes(word)) { - words.push(word); - } - start = i + 1; - } else if (isEnd) { - start = i + 1; - } - } - - return words; -} - -/** - * @param {Map} index - * @param {string} key - * @param {number} value - */ -function addToIndex(index, key, value) { - const existing = index.get(key); - if (existing) { - existing.push(value); - } else { - index.set(key, [value]); - } -} - -/** - * @param {Map} index - * @param {string} word - * @param {number} itemIndex - */ -function addTrigramsToIndex(index, word, itemIndex) { - if (word.length < 3) return; - - for (let i = 0; i <= word.length - 3; i++) { - const trigram = word[i] + word[i + 1] + word[i + 2]; - const existing = index.get(trigram); - - if (!existing) { - index.set(trigram, [itemIndex]); - } else if (existing[existing.length - 1] !== itemIndex) { - existing.push(itemIndex); - } - } -} - -/** @param {number[][]} arrays */ -function intersectAll(arrays) { - if (!arrays.length) return []; - - let smallestIndex = 0; - for (let i = 1; i < arrays.length; i++) { - if (arrays[i].length < arrays[smallestIndex].length) { - smallestIndex = i; - } - } - - const result = arrays[smallestIndex].slice(); - - for (let i = 0; i < arrays.length && result.length > 0; i++) { - if (i === smallestIndex) continue; - - let writeIndex = 0; - for (let j = 0; j < result.length; j++) { - if (binarySearch(arrays[i], result[j])) { - result[writeIndex++] = result[j]; - } - } - result.length = writeIndex; - } - - return result; -} - -/** - * @param {number[]} sortedArray - * @param {number} value - */ -function binarySearch(sortedArray, value) { - let low = 0; - let high = sortedArray.length - 1; - - while (low <= high) { - const mid = (low + high) >> 1; - const midValue = sortedArray[mid]; - - if (midValue === value) return true; - if (midValue < value) low = mid + 1; - else high = mid - 1; - } - - return false; -} - -/** - * @param {number} wordLength - * @param {number} round - */ -function pickTrigramPosition(wordLength, round) { - const maxPosition = wordLength - 3; - if (maxPosition < 0) return -1; - - if (round === 0) return 0; - if (round === 1 && maxPosition > 0) return maxPosition; - if (round === 2 && maxPosition > 1) return maxPosition >> 1; - if (maxPosition <= 2) return -1; - - const middle = maxPosition >> 1; - const offset = (round - 2) >> 1; - const position = round & 1 ? Math.max(0, middle - offset) : middle + offset; - - if (position === 0 || position >= maxPosition || position === middle) { - return -1; - } - - return position; -} diff --git a/modules/quickmatch-js/0.4.0/src/index.js b/modules/quickmatch-js/0.4.0/src/index.js new file mode 100644 index 000000000..d981b2993 --- /dev/null +++ b/modules/quickmatch-js/0.4.0/src/index.js @@ -0,0 +1,417 @@ +const DEFAULT_SEPARATORS = "_- :/"; +const DEFAULT_TRIGRAM_BUDGET = 6; +const DEFAULT_LIMIT = 100; +const DEFAULT_MIN_SCORE = 2; + +/** + * Search configuration. + * + * Defaults work well for most use cases. + * Tweak `trigramBudget` to trade speed for typo tolerance. + */ +export class QuickMatchConfig { + /** Characters that separate words in items (e.g. "hash_rate" → ["hash", "rate"]). + * @type {string} */ + separators = DEFAULT_SEPARATORS; + + /** Max results returned per query. + * @type {number} */ + limit = DEFAULT_LIMIT; + + /** How hard to try matching typos (0 = off, 3–6 = fast, 9–15 = thorough, max 20). + * @type {number} */ + trigramBudget = DEFAULT_TRIGRAM_BUDGET; + + /** Min overlap required for a typo match. Higher = fewer false positives. + * @type {number} */ + minScore = DEFAULT_MIN_SCORE; + + /** @param {number} n - Max results (default: 100, min: 1) */ + withLimit(n) { + this.limit = Math.max(1, n); + return this; + } + + /** @param {number} n - Trigram budget (0-20, default: 6) */ + withTrigramBudget(n) { + this.trigramBudget = Math.max(0, Math.min(20, n)); + return this; + } + + /** @param {string} s - Separator characters (default: '_- :/') */ + withSeparators(s) { + this.separators = s; + return this; + } + + /** @param {number} n - Min trigram score (default: 2, min: 1) */ + withMinScore(n) { + this.minScore = Math.max(1, n); + return this; + } +} + +/** + * Instant search over a list of strings. + * + * Supports exact words, prefixes ("dom" → "dominance"), joined words + * ("hashrate" → "hash_rate"), and typo tolerance ("suply" → "supply"). + * Results are ranked: exact matches first, then by specificity. + */ +export class QuickMatch { + /** @param {string[]} items - Searchable items (lowercase) @param {QuickMatchConfig} [config] */ + constructor(items, config = new QuickMatchConfig()) { + this.config = config; + this.items = items; + /** @type {Map} */ + this.wordIndex = new Map(); + /** @type {Map} */ + this.trigramIndex = new Map(); + this._sepLookup = sepLookup(config.separators); + this._scores = new Uint32Array(items.length); + /** @type {number[]} */ + this._dirty = []; + + let maxWordLen = 0; + let maxQueryLen = 0; + let maxWords = 0; + const sep = this._sepLookup; + + for (let idx = 0; idx < items.length; idx++) { + const item = items[idx]; + if (item.length > maxQueryLen) maxQueryLen = item.length; + + const words = []; + let start = 0; + + for (let i = 0; i <= item.length; i++) { + if (i < item.length && !sep[item.charCodeAt(i)]) continue; + if (i > start) { + const word = item.slice(start, i); + words.push(word); + if (word.length > maxWordLen) maxWordLen = word.length; + for (let len = 1; len <= word.length; len++) { + addToIndex(this.wordIndex, word.slice(0, len), idx); + } + for (let k = 0; k <= word.length - 3; k++) { + addToIndex(this.trigramIndex, word[k] + word[k + 1] + word[k + 2], idx); + } + } + start = i + 1; + } + + for (let i = 0; i < words.length - 1; i++) { + const compound = words[i] + words[i + 1]; + const from = words[i].length + 1; + for (let len = from; len <= compound.length; len++) { + addToIndex(this.wordIndex, compound.slice(0, len), idx); + } + } + + if (words.length > maxWords) maxWords = words.length; + } + + this.maxWordLen = maxWordLen + 4; + this.maxQueryLen = maxQueryLen + 6; + this.maxWords = maxWords + 2; + } + + /** @param {string} query */ + matches(query) { + return this.matchesWith(query, this.config); + } + + /** + * @param {string} query + * @param {QuickMatchConfig} config + */ + matchesWith(query, config) { + const { limit, trigramBudget } = config; + const sep = + config.separators === this.config.separators + ? this._sepLookup + : sepLookup(config.separators); + + const q = normalize(query); + if (!q || q.length > this.maxQueryLen) return []; + + const qwords = splitWords(q, sep, this.maxWordLen); + if (!qwords.length || qwords.length > this.maxWords) return []; + + const known = []; + const unknown = []; + + for (const w of qwords) { + const hits = this.wordIndex.get(w); + if (hits) { + known.push(hits); + } else if (w.length >= 3 && unknown.length < trigramBudget) { + unknown.push(w); + } + } + + const pool = intersect(known); + + // Try typo matching for unknown words + if (unknown.length && trigramBudget) { + const { _scores: scores, _dirty: dirty } = this; + + if (pool) { + for (const i of pool) { + scores[i] = 1; + dirty.push(i); + } + } + + const hitCount = this._scoreTrigrams( + unknown, + trigramBudget, + pool !== null, + Math.max(0, q.length - 3), + ); + const minScore = Math.max(config.minScore, Math.ceil(hitCount / 2)); + const result = this._rank(dirty, minScore, qwords, sep, limit); + + for (const i of dirty) scores[i] = 0; + dirty.length = 0; + + if (result.length > 0) return result; + } + + // Rank known candidates (intersection, or union as fallback) + const candidates = pool || union(known); + return candidates.length > 0 + ? this._rank(candidates, null, qwords, sep, limit) + : []; + } + + /** @private @param {string[]} unknown @param {number} budget @param {boolean} poolOnly @param {number} minLen */ + _scoreTrigrams(unknown, budget, poolOnly, minLen) { + const { _scores: scores, _dirty: dirty, items } = this; + const visited = new Set(); + const maxRounds = budget; + let hits = 0; + + outer: for (let round = 0; round < maxRounds; round++) { + for (const word of unknown) { + if (budget <= 0) break outer; + + const pos = trigramPosition(word.length, round); + if (pos < 0) continue; + + const tri = word[pos] + word[pos + 1] + word[pos + 2]; + if (visited.has(tri)) continue; + visited.add(tri); + budget--; + + const matched = this.trigramIndex.get(tri); + if (!matched) continue; + hits++; + + if (poolOnly) { + for (let j = 0; j < matched.length; j++) { + const i = matched[j]; + if (scores[i] > 0) scores[i]++; + } + } else { + for (let j = 0; j < matched.length; j++) { + const i = matched[j]; + if (items[i].length >= minLen) { + if (scores[i] === 0) dirty.push(i); + scores[i]++; + } + } + } + } + } + + return hits; + } + + /** + * @private + * @param {number[]} indices + * @param {number|null} minScore + * @param {string[]} qwords + * @param {Uint8Array} sep + * @param {number} limit + */ + _rank(indices, minScore, qwords, sep, limit) { + const { items, _scores: scores } = this; + const buckets = [[], [], []]; // ps=0, ps=1, ps=2 + + for (let i = 0; i < indices.length; i++) { + const idx = indices[i]; + if (minScore !== null && scores[idx] < minScore) continue; + buckets[prefixScore(items[idx], qwords, sep)].push(idx); + } + + const results = []; + for (let ps = 2; ps >= 0 && results.length < limit; ps--) { + const bucket = buckets[ps]; + if (!bucket.length) continue; + bucket.sort( + (a, b) => scores[b] - scores[a] || items[a].length - items[b].length, + ); + const take = Math.min(bucket.length, limit - results.length); + for (let i = 0; i < take; i++) results.push(items[bucket[i]]); + } + + return results; + } +} + +// --- Helpers --- + +/** @param {string} query */ +function normalize(query) { + let out = ""; + let start = 0; + let end = query.length; + while (start < end && query.charCodeAt(start) <= 32) start++; + while (end > start && query.charCodeAt(end - 1) <= 32) end--; + for (let i = start; i < end; i++) { + const c = query.charCodeAt(i); + if (c >= 128) continue; + out += c >= 65 && c <= 90 ? String.fromCharCode(c + 32) : query[i]; + } + return out; +} + +/** @param {string} separators */ +function sepLookup(separators) { + const t = new Uint8Array(128); + for (let i = 0; i < separators.length; i++) { + const c = separators.charCodeAt(i); + if (c < 128) t[c] = 1; + } + return t; +} + +/** + * @param {string} text + * @param {Uint8Array} sep + * @param {number} maxLen + */ +function splitWords(text, sep, maxLen) { + /** @type {string[]} */ + const words = []; + let start = 0; + for (let i = 0; i <= text.length; i++) { + if (i < text.length && !sep[text.charCodeAt(i)]) continue; + if (i > start) { + const w = text.slice(start, i); + if (w.length <= maxLen && !words.includes(w)) words.push(w); + } + start = i + 1; + } + return words; +} + +/** + * @param {Map} index + * @param {string} key + * @param {number} value + */ +function addToIndex(index, key, value) { + const arr = index.get(key); + if (arr) { + if (arr[arr.length - 1] !== value) arr.push(value); + } else { + index.set(key, [value]); + } +} + +/** @param {number[][]} arrays */ +function union(arrays) { + if (arrays.length <= 1) return arrays[0] || []; + const seen = new Set(); + const result = []; + for (const arr of arrays) { + for (const idx of arr) { + if (!seen.has(idx)) { + seen.add(idx); + result.push(idx); + } + } + } + return result; +} + +/** @param {number[][]} arrays @returns {number[]|null} */ +function intersect(arrays) { + if (arrays.length <= 1) return arrays[0] || null; + + let si = 0; + for (let i = 1; i < arrays.length; i++) { + if (arrays[i].length < arrays[si].length) si = i; + } + + const result = arrays[si].slice(); + for (let i = 0; i < arrays.length; i++) { + if (i === si) continue; + let w = 0; + for (let j = 0; j < result.length; j++) { + if (bsearch(arrays[i], result[j])) result[w++] = result[j]; + } + result.length = w; + if (!w) return null; + } + return result; +} + +/** + * @param {number[]} arr + * @param {number} val + */ +function bsearch(arr, val) { + let lo = 0, + hi = arr.length - 1; + while (lo <= hi) { + const mid = (lo + hi) >> 1; + if (arr[mid] === val) return true; + if (arr[mid] < val) lo = mid + 1; + else hi = mid - 1; + } + return false; +} + +/** @param {string} item @param {string[]} qwords @param {Uint8Array} sep */ +function prefixScore(item, qwords, sep) { + let qi = 0, + pos = 0; + const len = item.length; + + while (qi < qwords.length) { + while (pos < len && sep[item.charCodeAt(pos)]) pos++; + if (pos >= len) return 0; + + const ws = pos; + while (pos < len && !sep[item.charCodeAt(pos)]) pos++; + + const qw = qwords[qi]; + if (pos - ws < qw.length) return 0; + for (let j = 0; j < qw.length; j++) { + if (item.charCodeAt(ws + j) !== qw.charCodeAt(j)) return 0; + } + qi++; + } + + while (pos < len && sep[item.charCodeAt(pos)]) pos++; + return pos >= len ? 2 : 1; +} + +/** @param {number} len @param {number} round */ +function trigramPosition(len, round) { + const max = len - 3; + if (max < 0) return -1; + if (round === 0) return 0; + if (round === 1 && max > 0) return max; + if (round === 2 && max > 1) return max >> 1; + if (max <= 2) return -1; + + const mid = max >> 1; + const off = (round - 2) >> 1; + const pos = round & 1 ? Math.max(0, mid - off) : mid + off; + return pos === 0 || pos >= max || pos === mid ? -1 : pos; +} diff --git a/packages/brk_client/brk_client/__init__.py b/packages/brk_client/brk_client/__init__.py index 7cba09151..34731c159 100644 --- a/packages/brk_client/brk_client/__init__.py +++ b/packages/brk_client/brk_client/__init__.py @@ -3850,8 +3850,8 @@ class SeriesTree_Cointime_Adjusted: def __init__(self, client: BrkClientBase, base_path: str = ''): self.inflation_rate: BpsPercentRatioPattern = BpsPercentRatioPattern(client, 'cointime_adj_inflation_rate') - self.tx_velocity_native: SeriesPattern1[StoredF64] = SeriesPattern1(client, 'cointime_adj_tx_velocity') - self.tx_velocity_fiat: SeriesPattern1[StoredF64] = SeriesPattern1(client, 'cointime_adj_tx_velocity_fiat') + self.tx_velocity_native: SeriesPattern1[StoredF64] = SeriesPattern1(client, 'cointime_adj_tx_velocity_btc') + self.tx_velocity_fiat: SeriesPattern1[StoredF64] = SeriesPattern1(client, 'cointime_adj_tx_velocity_usd') class SeriesTree_Cointime_ReserveRisk: """Series tree node.""" @@ -4749,8 +4749,8 @@ class SeriesTree_Supply_Velocity: """Series tree node.""" def __init__(self, client: BrkClientBase, base_path: str = ''): - self.native: SeriesPattern1[StoredF64] = SeriesPattern1(client, 'velocity') - self.fiat: SeriesPattern1[StoredF64] = SeriesPattern1(client, 'velocity_fiat') + self.native: SeriesPattern1[StoredF64] = SeriesPattern1(client, 'velocity_btc') + self.fiat: SeriesPattern1[StoredF64] = SeriesPattern1(client, 'velocity_usd') class SeriesTree_Supply: """Series tree node.""" diff --git a/website/scripts/chart/index.js b/website/scripts/chart/index.js index 74c340f4a..233f02b66 100644 --- a/website/scripts/chart/index.js +++ b/website/scripts/chart/index.js @@ -52,7 +52,8 @@ import { Unit } from "../utils/units.js"; */ /** - * @typedef {Series} AnySeries + * @typedef {SingleValueData | CandlestickData | LineData | BaselineData | HistogramData | WhitespaceData} AnyChartData + * @typedef {Series} AnySeries */ /** @@ -69,7 +70,7 @@ import { Unit } from "../utils/units.js"; * @property {function(number): void} removeFrom */ -const lineWidth = /** @type {any} */ (1.5); +const lineWidth = /** @type {1} */ (/** @type {unknown} */ (1.5)); const MAX_SIZE = 10_000; @@ -140,7 +141,7 @@ export function createChart({ parent, brk, fitContent }) { if (cached) { this.data = cached; } - endpoint.slice(-MAX_SIZE).fetch((/** @type {any} */ result) => { + endpoint.slice(-MAX_SIZE).fetch((/** @type {AnySeriesData} */ result) => { if (currentGen !== generation) return; cache.set(endpoint.path, result); this.data = result; @@ -150,7 +151,7 @@ export function createChart({ parent, brk, fitContent }) { }; // Memory cache for instant index switching - /** @type {Map>} */ + /** @type {Map} */ const cache = new Map(); // Range state: localStorage stores all ranges per-index, URL stores current range only @@ -432,9 +433,9 @@ export function createChart({ parent, brk, fitContent }) { * @param {boolean} [args.defaultActive] * @param {(order: number) => void} args.setOrder * @param {(active: boolean, highlighted: boolean) => void} args.applyOptions - * @param {() => readonly any[]} args.getData - * @param {(data: any[]) => void} args.setData - * @param {(data: any) => void} args.update + * @param {() => readonly AnyChartData[]} args.getData + * @param {(data: AnyChartData[]) => void} args.setData + * @param {(data: AnyChartData) => void} args.update * @param {() => void} args.onRemove */ create({ @@ -791,8 +792,7 @@ export function createChart({ parent, brk, fitContent }) { const upColor = customColors?.[0] ?? colors.bi.p1[0]; const downColor = customColors?.[1] ?? colors.bi.p1[1]; - /** @type {CandlestickISeries} */ - const candlestickISeries = /** @type {any} */ ( + const candlestickISeries = /** @type {CandlestickISeries} */ ( ichart.addSeries( /** @type {SeriesDefinition<'Candlestick'>} */ (CandlestickSeries), { visible: false, borderVisible: false, ...options }, @@ -800,8 +800,7 @@ export function createChart({ parent, brk, fitContent }) { ) ); - /** @type {LineISeries} */ - const lineISeries = /** @type {any} */ ( + const lineISeries = /** @type {LineISeries} */ ( ichart.addSeries( /** @type {SeriesDefinition<'Line'>} */ (LineSeries), { visible: false, lineWidth, priceLineVisible: true }, @@ -851,9 +850,10 @@ export function createChart({ parent, brk, fitContent }) { }); }, setData: (data) => { - candlestickISeries.setData(data); + const cdata = /** @type {CandlestickData[]} */ (data); + candlestickISeries.setData(cdata); lineISeries.setData( - data.map((d) => ({ time: d.time, value: d.close })), + cdata.map((d) => ({ time: d.time, value: d.close })), ); requestAnimationFrame(() => { if (generation !== series.generation) return; @@ -862,8 +862,9 @@ export function createChart({ parent, brk, fitContent }) { }); }, update: (data) => { - candlestickISeries.update(data); - lineISeries.update({ time: data.time, value: data.close }); + const cd = /** @type {CandlestickData} */ (data); + candlestickISeries.update(cd); + lineISeries.update({ time: cd.time, value: cd.close }); }, getData: () => candlestickISeries.data(), onRemove: () => { @@ -903,8 +904,7 @@ export function createChart({ parent, brk, fitContent }) { const positiveColor = isDualColor ? color[0] : color; const negativeColor = isDualColor ? color[1] : color; - /** @type {HistogramISeries} */ - const iseries = /** @type {any} */ ( + const iseries = /** @type {HistogramISeries} */ ( ichart.addSeries( /** @type {SeriesDefinition<'Histogram'>} */ (HistogramSeries), { priceLineVisible: false, ...options }, @@ -972,8 +972,7 @@ export function createChart({ parent, brk, fitContent }) { defaultActive, options, }) { - /** @type {LineISeries} */ - const iseries = /** @type {any} */ ( + const iseries = /** @type {LineISeries} */ ( ichart.addSeries( /** @type {SeriesDefinition<'Line'>} */ (LineSeries), { lineWidth, priceLineVisible: false, ...options }, @@ -1029,8 +1028,7 @@ export function createChart({ parent, brk, fitContent }) { defaultActive, options, }) { - /** @type {LineISeries} */ - const iseries = /** @type {any} */ ( + const iseries = /** @type {LineISeries} */ ( ichart.addSeries( /** @type {SeriesDefinition<'Line'>} */ (LineSeries), { @@ -1110,8 +1108,7 @@ export function createChart({ parent, brk, fitContent }) { bottomColor = colors.bi.p1[1], options, }) { - /** @type {BaselineISeries} */ - const iseries = /** @type {any} */ ( + const iseries = /** @type {BaselineISeries} */ ( ichart.addSeries( /** @type {SeriesDefinition<'Baseline'>} */ (BaselineSeries), { @@ -1182,8 +1179,7 @@ export function createChart({ parent, brk, fitContent }) { bottomColor = colors.bi.p1[1], options, }) { - /** @type {BaselineISeries} */ - const iseries = /** @type {any} */ ( + const iseries = /** @type {BaselineISeries} */ ( ichart.addSeries( /** @type {SeriesDefinition<'Baseline'>} */ (BaselineSeries), { diff --git a/website/scripts/options/distribution/activity.js b/website/scripts/options/distribution/activity.js index 9b3b9af0b..b4c582ff5 100644 --- a/website/scripts/options/distribution/activity.js +++ b/website/scripts/options/distribution/activity.js @@ -115,7 +115,7 @@ function volumeFolder(activity, color, title) { /** * @param {{ transferVolume: TransferVolumePattern }} activity - * @param {CountPattern} adjustedTransferVolume + * @param {CountPattern} adjustedTransferVolume * @param {Color} color * @param {(name: string) => string} title * @returns {PartialOptionsGroup} @@ -171,7 +171,7 @@ function singleRollingSoprTree(ratio, title, prefix = "") { } /** - * @param {CountPattern} valueDestroyed + * @param {CountPattern} valueDestroyed * @param {(name: string) => string} title * @returns {PartialOptionsTree} */ @@ -180,7 +180,7 @@ function valueDestroyedTree(valueDestroyed, title) { } /** - * @param {CountPattern} valueDestroyed + * @param {CountPattern} valueDestroyed * @param {(name: string) => string} title * @returns {PartialOptionsGroup} */ @@ -189,8 +189,8 @@ function valueDestroyedFolder(valueDestroyed, title) { } /** - * @param {CountPattern} valueDestroyed - * @param {CountPattern} adjusted + * @param {CountPattern} valueDestroyed + * @param {CountPattern} adjusted * @param {(name: string) => string} title * @returns {PartialOptionsGroup} */ @@ -471,7 +471,7 @@ function groupedVolumeFolder(list, all, title, getTransferVolume) { * @param {A} all * @param {(name: string) => string} title * @param {(c: T | A) => { sum: Record, cumulative: AnyValuePattern, inProfit: { sum: Record, cumulative: AnyValuePattern }, inLoss: { sum: Record, cumulative: AnyValuePattern } }} getTransferVolume - * @param {(c: T | A) => CountPattern} getAdjustedTransferVolume + * @param {(c: T | A) => CountPattern} getAdjustedTransferVolume * @returns {PartialOptionsGroup} */ function groupedVolumeFolderWithAdjusted(list, all, title, getTransferVolume, getAdjustedTransferVolume) { @@ -528,7 +528,7 @@ function groupedSoprCharts(list, all, getRatio, title, prefix = "") { * @param {readonly T[]} list * @param {A} all * @param {(name: string) => string} title - * @param {(c: T | A) => CountPattern} getValueDestroyed + * @param {(c: T | A) => CountPattern} getValueDestroyed * @returns {PartialOptionsTree} */ function groupedValueDestroyedTree(list, all, title, getValueDestroyed) { @@ -546,7 +546,7 @@ function groupedValueDestroyedTree(list, all, title, getValueDestroyed) { * @param {readonly T[]} list * @param {A} all * @param {(name: string) => string} title - * @param {(c: T | A) => CountPattern} getValueDestroyed + * @param {(c: T | A) => CountPattern} getValueDestroyed * @returns {PartialOptionsGroup} */ function groupedValueDestroyedFolder(list, all, title, getValueDestroyed) { @@ -559,8 +559,8 @@ function groupedValueDestroyedFolder(list, all, title, getValueDestroyed) { * @param {readonly T[]} list * @param {A} all * @param {(name: string) => string} title - * @param {(c: T | A) => CountPattern} getValueDestroyed - * @param {(c: T | A) => CountPattern} getAdjustedValueDestroyed + * @param {(c: T | A) => CountPattern} getValueDestroyed + * @param {(c: T | A) => CountPattern} getAdjustedValueDestroyed * @returns {PartialOptionsGroup} */ function groupedValueDestroyedFolderWithAdjusted(list, all, title, getValueDestroyed, getAdjustedValueDestroyed) { diff --git a/website/scripts/options/distribution/holdings.js b/website/scripts/options/distribution/holdings.js index 4352fc14d..bec3aa236 100644 --- a/website/scripts/options/distribution/holdings.js +++ b/website/scripts/options/distribution/holdings.js @@ -100,6 +100,7 @@ function singleDeltaItems(delta, unit, title, name) { title, metric: `${name} Change`, unit, + legend: "Change", }), name: "Change", }, diff --git a/website/scripts/options/distribution/index.js b/website/scripts/options/distribution/index.js index 4d5bc44c9..61ec8400f 100644 --- a/website/scripts/options/distribution/index.js +++ b/website/scripts/options/distribution/index.js @@ -525,6 +525,7 @@ function singleBucketFolder({ name, color, pattern }, parentName) { title, metric: "Supply Change", unit: Unit.sats, + legend: "Change", }), name: "Change", }, @@ -610,63 +611,32 @@ function groupedBucketCharts(list, groupTitle) { }, { name: "Change", - tree: [ - { - name: "Compare", - title: title("Supply Change"), - bottom: ROLLING_WINDOWS.flatMap((w) => - list.map(({ name, color, pattern }) => - baseline({ - series: pattern.supply.all.delta.absolute[w.key], - name: `${name} ${w.name}`, - color, - unit: Unit.sats, - }), - ), - ), - }, - ...ROLLING_WINDOWS.map((w) => ({ - name: w.name, - title: title(`${w.title} Supply Change`), - bottom: list.map(({ name, color, pattern }) => - baseline({ - series: pattern.supply.all.delta.absolute[w.key], - name, - color, - unit: Unit.sats, - }), - ), - })), - ], + tree: ROLLING_WINDOWS.map((w) => ({ + name: w.name, + title: title(`${w.title} Supply Change`), + bottom: list.map(({ name, color, pattern }) => + baseline({ + series: pattern.supply.all.delta.absolute[w.key], + name, + color, + unit: Unit.sats, + }), + ), + })), }, { name: "Growth Rate", - tree: [ - { - name: "Compare", - title: title("Supply Growth Rate"), - bottom: ROLLING_WINDOWS.flatMap((w) => - list.flatMap(({ name, color, pattern }) => - percentRatio({ - pattern: pattern.supply.all.delta.rate[w.key], - name: `${name} ${w.name}`, - color, - }), - ), - ), - }, - ...ROLLING_WINDOWS.map((w) => ({ - name: w.name, - title: title(`${w.title} Supply Growth Rate`), - bottom: list.flatMap(({ name, color, pattern }) => - percentRatio({ - pattern: pattern.supply.all.delta.rate[w.key], - name, - color, - }), - ), - })), - ], + tree: ROLLING_WINDOWS.map((w) => ({ + name: w.name, + title: title(`${w.title} Supply Growth Rate`), + bottom: list.flatMap(({ name, color, pattern }) => + percentRatio({ + pattern: pattern.supply.all.delta.rate[w.key], + name, + color, + }), + ), + })), }, ], }, diff --git a/website/scripts/options/distribution/prices.js b/website/scripts/options/distribution/prices.js index bac02c8d6..b1a0615ad 100644 --- a/website/scripts/options/distribution/prices.js +++ b/website/scripts/options/distribution/prices.js @@ -31,7 +31,7 @@ export function createPricesSectionFull({ cohort, title }) { tree: [ { name: "Compare", - title: title("Prices"), + title: title("Realized Prices"), top: [ price({ series: tree.realized.price, name: "Realized", color: colors.realized }), price({ series: tree.realized.investor.price, name: "Investor", color: colors.investor }), diff --git a/website/scripts/options/distribution/profitability.js b/website/scripts/options/distribution/profitability.js index 2b6182309..cd6b7c9cc 100644 --- a/website/scripts/options/distribution/profitability.js +++ b/website/scripts/options/distribution/profitability.js @@ -399,6 +399,7 @@ function realizedNetFolder({ netPnl, title, extraChange = [] }) { title, metric: "Net Realized P&L Change", unit: Unit.usd, + legend: "Change", }), name: "Change", }, diff --git a/website/scripts/options/distribution/valuation.js b/website/scripts/options/distribution/valuation.js index 99c1cd2f1..907860792 100644 --- a/website/scripts/options/distribution/valuation.js +++ b/website/scripts/options/distribution/valuation.js @@ -19,7 +19,7 @@ import { ratioBottomSeries, mapCohortsWithAll, flatMapCohortsWithAll } from "../ */ function singleDeltaItems(tree, title) { return [ - { ...sumsTreeBaseline({ windows: mapWindows(tree.realized.cap.delta.absolute, (c) => c.usd), title, metric: "Realized Cap Change", unit: Unit.usd }), name: "Change" }, + { ...sumsTreeBaseline({ windows: mapWindows(tree.realized.cap.delta.absolute, (c) => c.usd), title, metric: "Realized Cap Change", unit: Unit.usd, legend: "Change" }), name: "Change" }, { ...rollingPercentRatioTree({ windows: tree.realized.cap.delta.rate, title, metric: "Realized Cap Growth Rate" }), name: "Growth Rate" }, ]; } diff --git a/website/scripts/options/investing.js b/website/scripts/options/investing.js index 6077f5d48..f5d4591c4 100644 --- a/website/scripts/options/investing.js +++ b/website/scripts/options/investing.js @@ -135,7 +135,7 @@ function createCompareFolder(context, items) { }, { name: "Accumulated", - title: `Accumulated Value: ${context}`, + title: `Accumulated Value ($100/day): ${context}`, top: topPane, bottom: items.flatMap(({ name, color, stack }) => satsBtcUsd({ pattern: stack, name, color }), @@ -188,7 +188,7 @@ function createLongCompareFolder(context, items) { }, { name: "Accumulated", - title: `Accumulated Value: ${context}`, + title: `Accumulated Value ($100/day): ${context}`, top: topPane, bottom: items.flatMap(({ name, color, stack }) => satsBtcUsd({ pattern: stack, name, color }), @@ -218,7 +218,7 @@ function createSingleEntryTree(item, returnsBottom) { }, { name: "Accumulated", - title: `Accumulated Value: ${titlePrefix}`, + title: `Accumulated Value ($100/day): ${titlePrefix}`, top, bottom: satsBtcUsd({ pattern: stack, name: "Value" }), }, @@ -259,7 +259,7 @@ function createLongSingleEntry(item) { }, { name: "Accumulated", - title: `Accumulated Value: ${titlePrefix}`, + title: `Accumulated Value ($100/day): ${titlePrefix}`, top, bottom: satsBtcUsd({ pattern: stack, name: "Value" }), }, diff --git a/website/scripts/options/market.js b/website/scripts/options/market.js index abb420d33..6fa5f4881 100644 --- a/website/scripts/options/market.js +++ b/website/scripts/options/market.js @@ -122,7 +122,7 @@ function returnsSubSection(name, periods) { tree: [ { name: "Compare", - title: `${name} Returns`, + title: `${name} Price Returns`, bottom: periods.flatMap((p) => percentRatioBaseline({ pattern: p.returns, @@ -133,8 +133,8 @@ function returnsSubSection(name, periods) { }, ...periods.map((p) => ({ name: periodIdToName(p.id, true), - title: `${periodIdToName(p.id, true)} Returns`, - bottom: percentRatioBaseline({ pattern: p.returns, name: "Total" }), + title: `${periodIdToName(p.id, true)} Price Returns`, + bottom: percentRatioBaseline({ pattern: p.returns, name: "Return" }), })), ], }; @@ -153,7 +153,7 @@ function returnsSubSectionWithCagr(name, periods) { tree: [ { name: "Compare", - title: `${name} Total Returns`, + title: `${name} Total Price Returns`, bottom: periods.flatMap((p) => percentRatioBaseline({ pattern: p.returns, @@ -164,8 +164,8 @@ function returnsSubSectionWithCagr(name, periods) { }, ...periods.map((p) => ({ name: periodIdToName(p.id, true), - title: `${periodIdToName(p.id, true)} Total Returns`, - bottom: percentRatioBaseline({ pattern: p.returns, name: "Total" }), + title: `${periodIdToName(p.id, true)} Total Price Returns`, + bottom: percentRatioBaseline({ pattern: p.returns, name: "Return" }), })), ], }, @@ -174,7 +174,7 @@ function returnsSubSectionWithCagr(name, periods) { tree: [ { name: "Compare", - title: `${name} CAGR`, + title: `${name} Price CAGR`, bottom: periods.flatMap((p) => percentRatioBaseline({ pattern: p.cagr, @@ -185,7 +185,7 @@ function returnsSubSectionWithCagr(name, periods) { }, ...periods.map((p) => ({ name: periodIdToName(p.id, true), - title: `${periodIdToName(p.id, true)} CAGR`, + title: `${periodIdToName(p.id, true)} Price CAGR`, bottom: percentRatioBaseline({ pattern: p.cagr, name: "CAGR" }), })), ], @@ -469,7 +469,7 @@ export function createMarketSection() { tree: [ { name: "Compare", - title: "Returns Comparison", + title: "Price Returns", bottom: [...shortPeriods, ...longPeriods].flatMap((p) => percentRatioBaseline({ pattern: p.returns, @@ -490,7 +490,7 @@ export function createMarketSection() { tree: [ { name: "Compare", - title: "Historical Price Comparison", + title: "Historical Prices", top: [...shortPeriods, ...longPeriods].map((p) => price({ series: p.lookback, @@ -592,7 +592,7 @@ export function createMarketSection() { bottom: [ baseline({ series: supply.marketMinusRealizedCapGrowthRate[w.key], - name: w.name, + name: "Spread", unit: Unit.percentage, }), ], @@ -615,7 +615,7 @@ export function createMarketSection() { tree: [ { name: "All Periods", - title: "SMA vs EMA Comparison", + title: "SMA vs EMA", top: smaVsEma.flatMap((p) => [ price({ series: p.sma, @@ -659,7 +659,7 @@ export function createMarketSection() { tree: [ { name: "Compare", - title: "RSI Comparison", + title: "RSI", bottom: [ ...ROLLING_WINDOWS_TO_1M.flatMap((w) => indexRatio({ @@ -726,7 +726,7 @@ export function createMarketSection() { tree: [ { name: "Compare", - title: "MACD Comparison", + title: "MACD", bottom: ROLLING_WINDOWS_TO_1M.map((w) => line({ series: technical.macd[w.key].line, @@ -789,7 +789,7 @@ export function createMarketSection() { bottom: [ line({ series: volatility[w.key], - name: w.name, + name: "Volatility", color: w.color, unit: Unit.percentage, }), diff --git a/website/scripts/options/mining.js b/website/scripts/options/mining.js index 0ff35813f..cadc0e87e 100644 --- a/website/scripts/options/mining.js +++ b/website/scripts/options/mining.js @@ -80,7 +80,7 @@ export function createMiningSection() { /** * @param {(metric: string) => string} title * @param {string} metric - * @param {{ _24h: any, _1w: any, _1m: any, _1y: any, percent: any, ratio: any }} dominance + * @param {DominancePattern} dominance */ const dominanceTree = (title, metric, dominance) => ({ name: "Dominance", @@ -98,12 +98,12 @@ export function createMiningSection() { ...ROLLING_WINDOWS.map((w) => ({ name: w.name, title: title(`${w.title} ${metric}`), - bottom: percentRatio({ pattern: dominance[w.key], name: w.name, color: w.color }), + bottom: percentRatio({ pattern: dominance[w.key], name: "Dominance", color: w.color }), })), { name: "All Time", - title: title(`${metric} All Time`), - bottom: percentRatio({ pattern: dominance, name: "All Time", color: colors.time.all }), + title: title(`All Time ${metric}`), + bottom: percentRatio({ pattern: dominance, name: "Dominance", color: colors.time.all }), }, ], }); @@ -251,7 +251,7 @@ export function createMiningSection() { }), dotted({ series: blocks.difficulty.hashrate, - name: "Difficulty", + name: "From Difficulty", color: colors.default, unit: Unit.hashRate, }), @@ -395,25 +395,25 @@ export function createMiningSection() { name: "Hash Price", title: "Hash Price", bottom: [ - line({ series: mining.hashrate.price.ths, name: "TH/s", color: colors.usd, unit: Unit.usdPerThsPerDay }), - line({ series: mining.hashrate.price.phs, name: "PH/s", color: colors.usd, unit: Unit.usdPerPhsPerDay }), - dotted({ series: mining.hashrate.price.thsMin, name: "TH/s ATL", color: colors.stat.min, unit: Unit.usdPerThsPerDay }), - dotted({ series: mining.hashrate.price.phsMin, name: "PH/s ATL", color: colors.stat.min, unit: Unit.usdPerPhsPerDay }), + line({ series: mining.hashrate.price.ths, name: "per TH/s", color: colors.usd, unit: Unit.usdPerThsPerDay }), + line({ series: mining.hashrate.price.phs, name: "per PH/s", color: colors.usd, unit: Unit.usdPerPhsPerDay }), + dotted({ series: mining.hashrate.price.thsMin, name: "per TH/s ATL", color: colors.stat.min, unit: Unit.usdPerThsPerDay }), + dotted({ series: mining.hashrate.price.phsMin, name: "per PH/s ATL", color: colors.stat.min, unit: Unit.usdPerPhsPerDay }), ], }, { name: "Hash Value", title: "Hash Value", bottom: [ - line({ series: mining.hashrate.value.ths, name: "TH/s", color: colors.bitcoin, unit: Unit.satsPerThsPerDay }), - line({ series: mining.hashrate.value.phs, name: "PH/s", color: colors.bitcoin, unit: Unit.satsPerPhsPerDay }), - dotted({ series: mining.hashrate.value.thsMin, name: "TH/s ATL", color: colors.stat.min, unit: Unit.satsPerThsPerDay }), - dotted({ series: mining.hashrate.value.phsMin, name: "PH/s ATL", color: colors.stat.min, unit: Unit.satsPerPhsPerDay }), + line({ series: mining.hashrate.value.ths, name: "per TH/s", color: colors.bitcoin, unit: Unit.satsPerThsPerDay }), + line({ series: mining.hashrate.value.phs, name: "per PH/s", color: colors.bitcoin, unit: Unit.satsPerPhsPerDay }), + dotted({ series: mining.hashrate.value.thsMin, name: "per TH/s ATL", color: colors.stat.min, unit: Unit.satsPerThsPerDay }), + dotted({ series: mining.hashrate.value.phsMin, name: "per PH/s ATL", color: colors.stat.min, unit: Unit.satsPerPhsPerDay }), ], }, { name: "Recovery", - title: "Mining Recovery", + title: "Hash Price & Value Recovery", bottom: [ ...percentRatio({ pattern: mining.hashrate.price.rebound, name: "Hash Price", color: colors.usd }), ...percentRatio({ pattern: mining.hashrate.value.rebound, name: "Hash Value", color: colors.bitcoin }), diff --git a/website/scripts/options/network.js b/website/scripts/options/network.js index 0e4a2d6e0..7c76a80ae 100644 --- a/website/scripts/options/network.js +++ b/website/scripts/options/network.js @@ -19,7 +19,7 @@ import { multiSeriesTree, percentRatioDots, } from "./series.js"; -import { satsBtcUsd, satsBtcUsdFrom, satsBtcUsdFullTree } from "./shared.js"; +import { satsBtcUsd, satsBtcUsdFrom, satsBtcUsdFullTree, formatCohortTitle } from "./shared.js"; /** * Create Network section @@ -114,15 +114,17 @@ export function createNetworkSection() { /** * @param {AddressableType | "all"} key - * @param {string} titlePrefix + * @param {string} [typeName] */ - const createAddressSeriesTree = (key, titlePrefix) => [ + const createAddressSeriesTree = (key, typeName) => { + const title = formatCohortTitle(typeName); + return [ { name: "Count", tree: [ { name: "Compare", - title: `${titlePrefix}Address Count`, + title: title("Address Count"), bottom: countMetrics.map((m) => line({ series: addrs[m.key][key], @@ -134,7 +136,7 @@ export function createNetworkSection() { }, ...countMetrics.map((m) => ({ name: m.name, - title: `${titlePrefix}${m.name} Addresses`, + title: title(`${m.name} Addresses`), bottom: [ line({ series: addrs[m.key][key], name: m.name, unit: Unit.count }), ], @@ -143,7 +145,7 @@ export function createNetworkSection() { }, ...simpleDeltaTree({ delta: addrs.delta[key], - title: (s) => `${titlePrefix}${s}`, + title, metric: "Address Count", unit: Unit.count, }), @@ -151,7 +153,7 @@ export function createNetworkSection() { name: "New", tree: chartsFromCount({ pattern: addrs.new[key], - title: (s) => `${titlePrefix}${s}`, + title, metric: "New Addresses", unit: Unit.count, }), @@ -163,7 +165,7 @@ export function createNetworkSection() { name: "Compare", tree: ROLLING_WINDOWS.map((w) => ({ name: w.name, - title: `${w.title} ${titlePrefix}Active Addresses`, + title: title(`${w.title} Active Addresses`), bottom: activityTypes.map((t, i) => line({ series: addrs.activity[key][t.key][w.key], @@ -178,7 +180,7 @@ export function createNetworkSection() { name: t.name, tree: averagesArray({ windows: addrs.activity[key][t.key], - title: (s) => `${titlePrefix}${s}`, + title, metric: `${t.name} Addresses`, unit: Unit.count, }), @@ -186,6 +188,7 @@ export function createNetworkSection() { ], }, ]; + }; /** @type {Record} */ const byKey = Object.fromEntries(scriptTypes.map((t) => [t.key, t])); @@ -560,7 +563,7 @@ export function createNetworkSection() { { name: "Addresses", tree: [ - ...createAddressSeriesTree("all", ""), + ...createAddressSeriesTree("all"), { name: "By Type", tree: [ @@ -582,7 +585,7 @@ export function createNetworkSection() { }, ...addressTypes.map((t) => ({ name: t.name, - tree: createAddressSeriesTree(t.key, `${t.name} `), + tree: createAddressSeriesTree(t.key, t.name), })), ], }, diff --git a/website/scripts/options/series.js b/website/scripts/options/series.js index e283999ee..9fc363a37 100644 --- a/website/scripts/options/series.js +++ b/website/scripts/options/series.js @@ -398,8 +398,8 @@ export function histogram({ /** * Create series from an AverageHeightMaxMedianMinP10P25P75P90Pattern (height + rolling stats) * @param {Object} args - * @param {{ height: AnySeriesPattern } & Record} args.pattern - Pattern with .height and rolling stats (p10/p25/p75/p90 as _1y24h30d7dPattern) - * @param {string} args.window - Rolling window key (e.g., '_24h', '_7d', '_30d', '_1y') + * @param {{ height: AnySeriesPattern } & WindowedStats} args.pattern - Pattern with .height and rolling stats + * @param {string} args.window - Rolling window key (e.g., '_24h', '_1w', '_1m', '_1y') * @param {Unit} args.unit * @param {string} [args.title] * @param {Color} [args.baseColor] @@ -417,7 +417,7 @@ export function fromBaseStatsPattern({ return [ dots({ series: pattern.height, - name: title || "base", + name: title || "Base", color: baseColor, unit, }), @@ -427,8 +427,10 @@ export function fromBaseStatsPattern({ /** * Extract stats at a specific rolling window - * @param {Record} pattern - Pattern with pct10/pct25/pct75/pct90 and median/max/min as _1y24h30d7dPattern + * @template T + * @param {WindowedStats} pattern - Pattern with pct10/pct25/pct75/pct90 and median/max/min at each rolling window * @param {string} window + * @returns {{ median: T, max: T, min: T, pct75: T, pct25: T, pct90: T, pct10: T }} */ export function statsAtWindow(pattern, window) { return { @@ -450,6 +452,7 @@ export function statsAtWindow(pattern, window) { * @param {(w: typeof ROLLING_WINDOWS[number]) => string} args.windowTitle * @param {Unit} args.unit * @param {string} args.name + * @param {string} args.legend * @returns {PartialOptionsGroup} */ function rollingWindowsTreeBaseline({ @@ -458,6 +461,7 @@ function rollingWindowsTreeBaseline({ windowTitle, unit, name, + legend, }) { return { name, @@ -477,7 +481,7 @@ function rollingWindowsTreeBaseline({ ...ROLLING_WINDOWS.map((w) => ({ name: w.name, title: windowTitle(w), - bottom: [baseline({ series: windows[w.key], name: w.name, unit })], + bottom: [baseline({ series: windows[w.key], name: legend, unit })], })), ], }; @@ -605,15 +609,17 @@ export function sumsAndAveragesCumulative({ * @param {(metric: string) => string} [args.title] * @param {string} args.metric * @param {Unit} args.unit + * @param {string} [args.legend] * @returns {PartialOptionsGroup} */ -export function sumsTreeBaseline({ windows, title = (s) => s, metric, unit }) { +export function sumsTreeBaseline({ windows, title = (s) => s, metric, unit, legend = "Sum" }) { return rollingWindowsTreeBaseline({ windows, title: title(metric), windowTitle: (w) => title(`${w.title} ${metric}`), unit, name: "Sums", + legend, }); } @@ -639,16 +645,16 @@ export function averagesArray({ windows, title = (s) => s, metric, unit }) { name: w.name, title: title(`${w.title} ${metric}`), bottom: [ - line({ series: windows[w.key], name: w.name, color: w.color, unit }), + line({ series: windows[w.key], name: "Average", color: w.color, unit }), ], })), ]; } /** - * Create a Distribution folder tree with stats at each rolling window (24h/7d/30d/1y) + * Create a Distribution folder tree with stats at each rolling window (24h/1w/1m/1y) * @param {Object} args - * @param {Record} args.pattern - Pattern with pct10/pct25/... and average/median/... as _1y24h30d7dPattern + * @param {WindowedStats} args.pattern - Pattern with pct10/pct25/... and average/median/... at each rolling window * @param {AnySeriesPattern} [args.base] - Optional base series to show as dots on each chart * @param {(metric: string) => string} [args.title] * @param {string} args.metric @@ -675,7 +681,7 @@ export function distributionWindowsTree({ pattern, base, title = (s) => s, metri name: w.name, title: title(`${w.title} ${metric} Distribution`), bottom: [ - ...(base ? [line({ series: base, name: "base", unit })] : []), + ...(base ? [line({ series: base, name: "Base", unit })] : []), ...percentileSeries({ pattern: statsAtWindow(pattern, w.key), unit }), ], })), @@ -871,7 +877,7 @@ export function rollingPercentRatioTree({ ...ROLLING_WINDOWS.map((w) => ({ name: w.name, title: title(`${w.title} ${metric}`), - bottom: percentRatioBaseline({ pattern: windows[w.key], name: w.name }), + bottom: percentRatioBaseline({ pattern: windows[w.key], name: "Rate" }), })), ], }; @@ -911,7 +917,7 @@ export function deltaTree({ delta, title = (s) => s, metric, unit, extract }) { bottom: [ baseline({ series: extract(delta.absolute[w.key]), - name: w.name, + name: "Change", unit, }), ], @@ -1068,7 +1074,7 @@ export function chartsFromBlockAnd6b({ pattern, title = (s) => s, metric, unit } /** * Averages + Sums + Cumulative charts * @param {Object} args - * @param {CountPattern} args.pattern + * @param {CountPattern} args.pattern * @param {(metric: string) => string} [args.title] * @param {string} args.metric * @param {Unit} args.unit @@ -1090,7 +1096,7 @@ export function chartsFromCount({ pattern, title = (s) => s, metric, unit, color /** * Windowed sums + cumulative for multiple named entries (e.g. transaction versions) * @param {Object} args - * @param {Array<[string, CountPattern]>} args.entries + * @param {Array<[string, CountPattern]>} args.entries * @param {(metric: string) => string} [args.title] * @param {string} args.metric * @param {Unit} args.unit diff --git a/website/scripts/options/shared.js b/website/scripts/options/shared.js index 475e59ef5..c655e3c32 100644 --- a/website/scripts/options/shared.js +++ b/website/scripts/options/shared.js @@ -203,7 +203,7 @@ export function revenueBtcSatsUsd({ coinbase, subsidy, fee, key }) { } /** - * Create sats/btc/usd series from a rolling window (24h/7d/30d/1y sum) + * Create sats/btc/usd series from a rolling window (24h/1w/1m/1y sum) * @param {Object} args * @param {AnyValuePattern} args.pattern - A BtcSatsUsdPattern (e.g., source.rolling._24h.sum) * @param {string} args.name @@ -540,14 +540,15 @@ export function ratioBottomSeries(ratio) { * @param {AnyRatioPattern} args.ratio * @param {Color} args.color * @param {string} [args.name] + * @param {string} [args.legend] * @returns {PartialChartOption} */ -export function createRatioChart({ title, pricePattern, ratio, color, name }) { +export function createRatioChart({ title, pricePattern, ratio, color, name, legend }) { return { name: name ?? "Ratio", title: title(name ?? "Ratio"), top: [ - price({ series: pricePattern, name: "Price", color }), + price({ series: pricePattern, name: legend ?? "Price", color }), ...percentileUsdMap(ratio).map(({ name, prop, color }) => price({ series: prop, @@ -742,6 +743,7 @@ export function createPriceRatioCharts({ pricePattern, ratio, color, + legend, }), createZScoresFolder({ formatTitle: (name) => diff --git a/website/scripts/options/unused.js b/website/scripts/options/unused.js index 8bfc9996f..9a6cd4745 100644 --- a/website/scripts/options/unused.js +++ b/website/scripts/options/unused.js @@ -100,9 +100,10 @@ export function logUnused(seriesTree, partialOptions) { if (!all.size) return; - /** @type {Record} */ + /** @type {Record} */ const tree = {}; for (const path of all.values()) { + /** @type {Record} */ let current = tree; for (let i = 0; i < path.length; i++) { const part = path[i]; @@ -110,7 +111,7 @@ export function logUnused(seriesTree, partialOptions) { current[part] = null; } else { current[part] = current[part] || {}; - current = current[part]; + current = /** @type {Record} */ (current[part]); } } } diff --git a/website/scripts/panes/search.js b/website/scripts/panes/search.js index e5f504a56..3fa5b076f 100644 --- a/website/scripts/panes/search.js +++ b/website/scripts/panes/search.js @@ -3,7 +3,7 @@ import { searchLabelElement, searchResultsElement, } from "../utils/elements.js"; -import { QuickMatch } from "../modules/quickmatch-js/0.3.1/src/index.js"; +import { QuickMatch } from "../modules/quickmatch-js/0.4.0/src/index.js"; /** * @param {Options} options @@ -24,11 +24,7 @@ export function initSearch(options) { searchResultsElement.scrollTo({ top: 0 }); searchResultsElement.innerHTML = ""; - if (needle.length < 3) { - const li = window.document.createElement("li"); - li.textContent = 'e.g. "BTC"'; - li.style.color = "var(--off-color)"; - searchResultsElement.appendChild(li); + if (!needle.length) { return; } diff --git a/website/scripts/panes/share.js b/website/scripts/panes/share.js index cb8f4d513..8a4eecf05 100644 --- a/website/scripts/panes/share.js +++ b/website/scripts/panes/share.js @@ -22,11 +22,8 @@ export function setQr(url) { ""; imgQrcode.src = - leanQr.generate(/** @type {any} */ (url))?.toDataURL({ - // @ts-ignore - padX: 0, - padY: 0, - }) || ""; + // @ts-ignore — lean-qr types don't resolve for file path import + leanQr.generate(url)?.toDataURL({ padX: 0, padY: 0 }) || ""; shareDiv.hidden = false; } diff --git a/website/scripts/types.js b/website/scripts/types.js index bab91c8c3..1739c36fb 100644 --- a/website/scripts/types.js +++ b/website/scripts/types.js @@ -160,6 +160,15 @@ * Distribution stats: min, max, median, pct10/25/75/90 * @typedef {{ min: AnySeriesPattern, max: AnySeriesPattern, median: AnySeriesPattern, pct10: AnySeriesPattern, pct25: AnySeriesPattern, pct75: AnySeriesPattern, pct90: AnySeriesPattern }} DistributionStats */ +/** + * Windowed distribution stats: each stat property is a rolling window record + * @template T + * @typedef {{ median: Record, max: Record, min: Record, pct75: Record, pct25: Record, pct90: Record, pct10: Record }} WindowedStats + */ +/** + * Dominance pattern: percent/ratio at top level + per rolling window + * @typedef {Brk._1m1w1y24hBpsPercentRatioPattern} DominancePattern + */ /** * diff --git a/website/scripts/utils/array.js b/website/scripts/utils/array.js index 585c5b282..2ea571c75 100644 --- a/website/scripts/utils/array.js +++ b/website/scripts/utils/array.js @@ -1,6 +1,6 @@ /** * Typed Object.entries that preserves key types - * @template {Record} T + * @template {Record} T * @param {T} obj * @returns {[keyof T & string, T[keyof T & string]][]} */ diff --git a/website/scripts/utils/dom.js b/website/scripts/utils/dom.js index 5f97ad1a4..613273971 100644 --- a/website/scripts/utils/dom.js +++ b/website/scripts/utils/dom.js @@ -183,8 +183,8 @@ export function createRadios({ choices, initialValue, onChange, - toKey = /** @type {(choice: T) => string} */ ((/** @type {any} */ c) => c), - toLabel = /** @type {(choice: T) => string} */ ((/** @type {any} */ c) => c), + toKey = /** @type {(choice: T) => string} */ ((c) => String(c)), + toLabel = /** @type {(choice: T) => string} */ ((c) => String(c)), toTitle, }) { const field = window.document.createElement("div"); @@ -247,8 +247,8 @@ export function createSelect({ initialValue, onChange, sorted, - toKey = /** @type {(choice: T) => string} */ ((/** @type {any} */ c) => c), - toLabel = /** @type {(choice: T) => string} */ ((/** @type {any} */ c) => c), + toKey = /** @type {(choice: T) => string} */ ((c) => String(c)), + toLabel = /** @type {(choice: T) => string} */ ((c) => String(c)), }) { const choices = sorted ? unsortedChoices.toSorted((a, b) => toLabel(a).localeCompare(toLabel(b))) diff --git a/website/scripts/utils/timing.js b/website/scripts/utils/timing.js index 48ffeaf9b..54cf7904d 100644 --- a/website/scripts/utils/timing.js +++ b/website/scripts/utils/timing.js @@ -22,7 +22,7 @@ export function idle(callback) { /** * - * @template {(...args: any[]) => any} F + * @template {(...args: never[]) => unknown} F * @param {F} callback * @param {number} [wait] */ @@ -51,7 +51,7 @@ export function throttle(callback, wait = 1000) { } /** - * @template {(...args: any[]) => any} F + * @template {(...args: never[]) => unknown} F * @param {F} callback * @param {number} [wait] * @returns {((...args: Parameters) => void) & { cancel: () => void }} diff --git a/website/service-worker.js b/website/service-worker.js index e6bd25957..17f0f569f 100644 --- a/website/service-worker.js +++ b/website/service-worker.js @@ -5,8 +5,7 @@ const API = "/api"; // Match hashed filenames: name.abc12345.js/mjs/css const HASHED_RE = /\.[0-9a-f]{8}\.(js|mjs|css)$/; -/** @type {ServiceWorkerGlobalScope} */ -const sw = /** @type {any} */ (self); +const sw = /** @type {ServiceWorkerGlobalScope} */ (/** @type {unknown} */ (self)); const offline = () => new Response("Offline", {