diff --git a/.gitignore b/.gitignore index 76cffb2ea..d11c88af2 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ _* /filter_* /heatmaps* /oracle* +/playground # Logs *.log* diff --git a/crates/brk_cli/README.md b/crates/brk_cli/README.md index a9fd9f453..6d5d289f6 100644 --- a/crates/brk_cli/README.md +++ b/crates/brk_cli/README.md @@ -37,6 +37,8 @@ brk Indexes the blockchain, computes datasets, starts the server on `localhost:3110`, and waits for new blocks. +**Note:** When more than 10,000 blocks behind, indexing completes before the server starts to free up memory from fragmentation that occurs during large syncs. The web interface at `localhost:3110` won't be available until sync finishes. + ## Options ```bash diff --git a/crates/brk_cli/src/main.rs b/crates/brk_cli/src/main.rs index e2b0b46dd..f21d13684 100644 --- a/crates/brk_cli/src/main.rs +++ b/crates/brk_cli/src/main.rs @@ -57,7 +57,7 @@ pub fn run() -> anyhow::Result<()> { let chain_height = client.get_last_height()?; let indexed_height = indexer.vecs.starting_height(); let blocks_behind = chain_height.saturating_sub(*indexed_height); - if blocks_behind > 1000 { + if blocks_behind > 10_000 { info!("Indexing {blocks_behind} blocks before starting server..."); sleep(Duration::from_secs(3)); indexer.index(&blocks, &client, &exit)?; diff --git a/website/index.html b/website/index.html index 7397d9ba5..43e3e5554 100644 --- a/website/index.html +++ b/website/index.html @@ -1533,6 +1533,11 @@ "(prefers-color-scheme: dark)", ); + let storedTheme; + try { storedTheme = localStorage.getItem("theme"); } catch (_) {} + const isDark = storedTheme ? storedTheme === "dark" : preferredColorSchemeMatchMedia.matches; + document.documentElement.style.colorScheme = isDark ? "dark" : "light"; + const themeColor = window.document.createElement("meta"); themeColor.name = "theme-color"; window.document.getElementsByTagName("head")[0].appendChild(themeColor); @@ -1545,7 +1550,7 @@ themeColor.content = theme; } - updateThemeColor(preferredColorSchemeMatchMedia.matches); + updateThemeColor(isDark); preferredColorSchemeMatchMedia.addEventListener( "change", ({ matches }) => { @@ -1832,6 +1837,7 @@ + diff --git a/website/scripts/chart/index.js b/website/scripts/chart/index.js index 6e20fca4d..dc8a86626 100644 --- a/website/scripts/chart/index.js +++ b/website/scripts/chart/index.js @@ -33,16 +33,18 @@ import { resources } from "../resources.js"; /** * @template T * @typedef {Object} Series + * @property {string} key * @property {string} id * @property {number} paneIndex * @property {Signal} active + * @property {(value: boolean) => void} setActive * @property {() => void} show * @property {() => void} hide * @property {(order: number) => void} setOrder * @property {() => void} highlight * @property {() => void} tame * @property {() => boolean} hasData - * @property {Signal} url + * @property {string | null} url * @property {() => readonly T[]} getData * @property {(data: T) => void} update * @property {VoidFunction} remove @@ -102,14 +104,18 @@ export function createChart({ /** @type {Map>} */ const sharedActiveSignals = new Map(); - const legendTop = createLegend(signals); + // Registry for linked series (same key = linked across panes) + /** @type {Map>} */ + const seriesByKey = new Map(); + + const legendTop = createLegend(); div.append(legendTop.element); const chartDiv = window.document.createElement("div"); chartDiv.classList.add("lightweight-chart"); div.append(chartDiv); - const legendBottom = createLegend(signals); + const legendBottom = createLegend(); div.append(legendBottom.element); const ichart = lcCreateChart( @@ -175,15 +181,18 @@ export function createChart({ /** @type {Set} */ const onZoomChange = new Set(); - ichart.timeScale().subscribeVisibleLogicalRangeChange( - throttle((range) => { - if (!range) return; - const count = range.to - range.from; - if (count === visibleBarsCount) return; - visibleBarsCount = count; - onZoomChange.forEach((cb) => cb(count)); - }, 100), - ); + /** @param {{ from: number, to: number } | null} range */ + function updateVisibleBarsCount(range) { + if (!range) return; + const count = range.to - range.from; + if (count === visibleBarsCount) return; + visibleBarsCount = count; + onZoomChange.forEach((cb) => cb(count)); + } + + ichart + .timeScale() + .subscribeVisibleLogicalRangeChange(throttle(updateVisibleBarsCount, 100)); signals.createEffect( () => ({ @@ -391,27 +400,24 @@ export function createChart({ onRemove, }) { return signals.createRoot((dispose) => { - const id = `${stringToId(name)}-${paneIndex}`; - const urlId = stringToId(name); + const key = stringToId(name); + const id = `${key}-${paneIndex}`; // Reuse existing signal if same name (links legends across panes) - let active = sharedActiveSignals.get(urlId); + let active = sharedActiveSignals.get(key); if (!active) { active = signals.createPersistedSignal({ defaultValue: defaultActive ?? true, storageKey: id, - urlKey: urlId, + urlKey: key, ...serdeBool, }); - sharedActiveSignals.set(urlId, active); + sharedActiveSignals.set(key, active); } setOrder(-order); - // Bridge signal to series methods - signals.createEffect(active, (isActive) => { - isActive ? show() : hide(); - }); + active() ? show() : hide(); let hasData = false; let lastTime = -Infinity; @@ -422,26 +428,47 @@ export function createChart({ /** @type {AnySeries} */ const series = { active, + setActive(value) { + active.set(value); + seriesByKey.get(key)?.forEach((s) => { + value ? s.show() : s.hide(); + }); + document.querySelectorAll(`[data-series="${key}"]`).forEach((el) => { + if (el instanceof HTMLInputElement && el.type === "checkbox") { + el.checked = value; + } + }); + }, setOrder, show, hide, highlight, tame, hasData: () => hasData, + key, id, paneIndex, - url: signals.createSignal(/** @type {string | null} */ (null)), + url: null, getData, update, remove() { dispose(); onRemove(); + seriesByKey.get(key)?.delete(series); if (_valuesResource) { activeResources.delete(_valuesResource); } }, }; + // Register series for cross-pane linking + let keySet = seriesByKey.get(key); + if (!keySet) { + keySet = new Set(); + seriesByKey.set(key, keySet); + } + keySet.add(series); + if (metric) { signals.createScopedEffect(index, (index) => { // Get timestamp metric from tree based on index type @@ -462,11 +489,15 @@ export function createChart({ const valuesResource = resources.useMetricEndpoint(valuesNode); _valuesResource = valuesResource; - series.url.set(() => { - const base = brk.baseUrl.endsWith("/") - ? brk.baseUrl.slice(0, -1) - : brk.baseUrl; - return `${base}${valuesResource.path}`; + series.url = `${ + brk.baseUrl.endsWith("/") ? brk.baseUrl.slice(0, -1) : brk.baseUrl + }${valuesResource.path}`; + + (paneIndex ? legendBottom : legendTop).addOrReplace({ + series, + name, + colors, + order, }); // Create memo outside active check (cheap, just checks data existence) @@ -608,22 +639,24 @@ export function createChart({ }, ); }); - } else if (data) { - signals.createEffect(data, (data) => { - setData(data); - hasData = true; - if (fitContent) { - ichart.timeScale().fitContent(); - } + } else { + (paneIndex ? legendBottom : legendTop).addOrReplace({ + series, + name, + colors, + order, }); - } - (paneIndex ? legendBottom : legendTop).addOrReplace({ - series, - name, - colors, - order, - }); + if (data) { + signals.createEffect(data, (data) => { + setData(data); + hasData = true; + if (fitContent) { + ichart.timeScale().fitContent(); + } + }); + } + } addPriceScaleSelectorIfNeeded({ paneIndex, @@ -693,7 +726,6 @@ export function createChart({ wickUpColor: upColor(), wickDownColor: downColor(), borderVisible: false, - visible: defaultActive !== false, ...options, }, paneIndex, @@ -707,14 +739,13 @@ export function createChart({ { color: colors.default(), lineWidth, - visible: false, priceLineVisible: true, }, paneIndex, ) ); - let active = true; + let active = defaultActive !== false; let highlighted = true; let showLine = visibleBarsCount > 500; @@ -733,11 +764,11 @@ export function createChart({ color: colors.default.highlight(highlighted), }); } + update(); /** @type {ZoomChangeCallback} */ function handleZoom(count) { const newShowLine = count > 500; - if (newShowLine === showLine) return; showLine = newShowLine; update(); } @@ -827,8 +858,6 @@ export function createChart({ ichart.addSeries( /** @type {SeriesDefinition<'Histogram'>} */ (HistogramSeries), { - color: positiveColor(), - visible: defaultActive !== false, priceLineVisible: false, ...options, }, @@ -836,7 +865,7 @@ export function createChart({ ) ); - let active = true; + let active = defaultActive !== false; let highlighted = true; function update() { @@ -846,6 +875,7 @@ export function createChart({ color: positiveColor.highlight(highlighted), }); } + update(); const series = addSeries({ colors: isDualColor ? [positiveColor, negativeColor] : [positiveColor], @@ -931,16 +961,14 @@ export function createChart({ /** @type {SeriesDefinition<'Line'>} */ (LineSeries), { lineWidth, - visible: defaultActive !== false, priceLineVisible: false, - color: color(), ...options, }, paneIndex, ) ); - let active = true; + let active = defaultActive !== false; let highlighted = true; function update() { @@ -950,6 +978,7 @@ export function createChart({ color: color.highlight(highlighted), }); } + update(); const series = addSeries({ colors: [color], @@ -1020,9 +1049,7 @@ export function createChart({ ichart.addSeries( /** @type {SeriesDefinition<'Line'>} */ (LineSeries), { - visible: defaultActive !== false, priceLineVisible: false, - color: color(), lineVisible: false, pointMarkersVisible: true, pointMarkersRadius: 1, @@ -1032,7 +1059,7 @@ export function createChart({ ) ); - let active = true; + let active = defaultActive !== false; let highlighted = true; let radius = visibleBarsCount > 1000 ? 1 : visibleBarsCount > 200 ? 1.5 : 2; @@ -1044,6 +1071,7 @@ export function createChart({ color: color.highlight(highlighted), }); } + update(); /** @type {ZoomChangeCallback} */ function handleZoom(count) { @@ -1128,13 +1156,10 @@ export function createChart({ /** @type {SeriesDefinition<'Baseline'>} */ (BaselineSeries), { lineWidth, - visible: defaultActive !== false, baseValue: { price: options?.baseValue?.price ?? 0, }, ...options, - topLineColor: topColor(), - bottomLineColor: bottomColor(), priceLineVisible: false, bottomFillColor1: "transparent", bottomFillColor2: "transparent", @@ -1146,7 +1171,7 @@ export function createChart({ ) ); - let active = true; + let active = defaultActive !== false; let highlighted = true; function update() { @@ -1157,6 +1182,7 @@ export function createChart({ bottomLineColor: bottomColor.highlight(highlighted), }); } + update(); const series = addSeries({ colors: [topColor, bottomColor], diff --git a/website/scripts/chart/legend.js b/website/scripts/chart/legend.js index 786a69aa4..b8203fa9e 100644 --- a/website/scripts/chart/legend.js +++ b/website/scripts/chart/legend.js @@ -1,10 +1,7 @@ import { createLabeledInput, createSpanName } from "../utils/dom.js"; import { stringToId } from "../utils/format.js"; -/** - * @param {Signals} signals - */ -export function createLegend(signals) { +export function createLegend() { const element = window.document.createElement("legend"); /** @type {AnySeries | null} */ @@ -60,15 +57,11 @@ export function createLegend(signals) { title: "Click to toggle", inputChecked: series.active(), onClick: () => { - series.active.set(input.checked); + series.setActive(input.checked); }, type: "checkbox", }); - - // Sync checkbox with signal (for shared signals across panes) - signals.createEffect(series.active, (active) => { - input.checked = active; - }); + input.dataset.series = series.key; const spanMain = window.document.createElement("span"); spanMain.classList.add("main"); @@ -94,17 +87,14 @@ export function createLegend(signals) { }); seriesColorSpans.set(series, colorSpans); - const anchor = window.document.createElement("a"); - - signals.createEffect(series.url, (url) => { - if (url) { - anchor.href = url; - anchor.target = "_blank"; - anchor.rel = "noopener noreferrer"; - anchor.title = "Click to view data"; - div.append(anchor); - } - }); + if (series.url) { + const anchor = window.document.createElement("a"); + anchor.href = series.url; + anchor.target = "_blank"; + anchor.rel = "noopener noreferrer"; + anchor.title = "Click to view data"; + div.append(anchor); + } }, /** * @param {number} start diff --git a/website/scripts/entry.js b/website/scripts/entry.js index f5b0cd172..2badf2fc4 100644 --- a/website/scripts/entry.js +++ b/website/scripts/entry.js @@ -18,7 +18,6 @@ * * @import { Option, PartialChartOption, ChartOption, AnyPartialOption, ProcessedOptionAddons, OptionsTree, SimulationOption, AnySeriesBlueprint, SeriesType, AnyFetchedSeriesBlueprint, TableOption, ExplorerOption, UrlOption, PartialOptionsGroup, OptionsGroup, PartialOptionsTree, UtxoCohortObject, AddressCohortObject, CohortObject, CohortGroupObject, FetchedLineSeriesBlueprint, FetchedBaselineSeriesBlueprint, FetchedHistogramSeriesBlueprint, PartialContext, PatternAll, PatternFull, PatternWithAdjusted, PatternWithPercentiles, PatternBasic, CohortAll, CohortFull, CohortWithAdjusted, CohortWithPercentiles, CohortBasic, CohortGroupFull, CohortGroupWithAdjusted, CohortGroupWithPercentiles, CohortGroupBasic, UtxoCohortGroupObject, AddressCohortGroupObject, FetchedDotsSeriesBlueprint, FetchedCandlestickSeriesBlueprint } from "./options/partial.js" * - * @import { line as LineSeriesFn, dots as DotsSeriesFn, candlestick as CandlestickSeriesFn, baseline as BaselineSeriesFn, histogram as HistogramSeriesFn } from "./options/series.js" * * @import { UnitObject as Unit } from "./utils/units.js" * diff --git a/website/scripts/main.js b/website/scripts/main.js index a01129ed4..626958f3d 100644 --- a/website/scripts/main.js +++ b/website/scripts/main.js @@ -14,6 +14,7 @@ import { init as initSimulation } from "./panes/_simulation.js"; import { next } from "./utils/timing.js"; import { replaceHistory } from "./utils/url.js"; import { removeStored, writeToStorage } from "./utils/storage.js"; +import { dark } from "./utils/theme.js"; import { asideElement, asideLabelElement, @@ -121,18 +122,6 @@ signals.createRoot(() => { console.log(`VERSION = ${brk.VERSION}`); - function initDark() { - const preferredColorSchemeMatchMedia = window.matchMedia( - "(prefers-color-scheme: dark)", - ); - const dark = signals.createSignal(preferredColorSchemeMatchMedia.matches); - preferredColorSchemeMatchMedia.addEventListener("change", ({ matches }) => { - dark.set(matches); - }); - return dark; - } - const dark = initDark(); - const qrcode = signals.createSignal(/** @type {string | null} */ (null)); signals.createEffect(webSockets.kraken1dCandle.latest, (latest) => { @@ -552,6 +541,7 @@ signals.createRoot(() => { qrcode.set(window.location.href); }); + shareDiv.addEventListener("click", () => { qrcode.set(null); }); diff --git a/website/scripts/options/chain.js b/website/scripts/options/chain.js index 67a60b9ec..c9e1df214 100644 --- a/website/scripts/options/chain.js +++ b/website/scripts/options/chain.js @@ -1,6 +1,7 @@ /** Chain section builder - typed tree-based patterns */ import { Unit } from "../utils/units.js"; +import { line, baseline, dots } from "./series.js"; import { satsBtcUsd } from "./shared.js"; /** @@ -12,9 +13,6 @@ export function createChainSection(ctx) { const { colors, brk, - line, - baseline, - dots, createPriceLine, fromSizePattern, fromFullnessPattern, @@ -240,7 +238,7 @@ export function createChainSection(ctx) { name: "Volume", title: "Transaction Volume", bottom: [ - ...satsBtcUsd(ctx, transactions.volume.sentSum, "Sent"), + ...satsBtcUsd( transactions.volume.sentSum, "Sent"), line({ metric: transactions.volume.annualizedVolume.sats, name: "annualized", diff --git a/website/scripts/options/cohorts/address.js b/website/scripts/options/cohorts/address.js index e79f18c8d..eba3b37ab 100644 --- a/website/scripts/options/cohorts/address.js +++ b/website/scripts/options/cohorts/address.js @@ -5,6 +5,7 @@ */ import { Unit } from "../../utils/units.js"; +import { line, baseline } from "../series.js"; import { createSingleSupplySeries, createGroupedSupplyTotalSeries, @@ -54,12 +55,12 @@ export function createAddressCohortFolder(ctx, args) { { name: "in profit", title: `Supply In Profit ${title}`, - bottom: createGroupedSupplyInProfitSeries(ctx, list), + bottom: createGroupedSupplyInProfitSeries(list), }, { name: "in loss", title: `Supply In Loss ${title}`, - bottom: createGroupedSupplyInLossSeries(ctx, list), + bottom: createGroupedSupplyInLossSeries(list), }, ], }, @@ -68,7 +69,7 @@ export function createAddressCohortFolder(ctx, args) { { name: "utxo count", title: `UTXO Count ${title}`, - bottom: createUtxoCountSeries(ctx, list, useGroupName), + bottom: createUtxoCountSeries(list, useGroupName), }, // Address count (ADDRESS COHORTS ONLY - fully type safe!) @@ -87,7 +88,7 @@ export function createAddressCohortFolder(ctx, args) { { name: "Price", title: `Realized Price ${title}`, - top: createRealizedPriceSeries(ctx, list), + top: createRealizedPriceSeries(list), }, { name: "Ratio", @@ -96,7 +97,6 @@ export function createAddressCohortFolder(ctx, args) { }, ] : createRealizedPriceOptions( - ctx, /** @type {AddressCohortObject} */ (args), title, )), @@ -119,24 +119,22 @@ export function createAddressCohortFolder(ctx, args) { ...createUnrealizedSection(ctx, list, useGroupName, title), // Cost basis section (no percentiles for address cohorts) - ...createCostBasisSection(ctx, list, useGroupName, title), + ...createCostBasisSection(list, useGroupName, title), // Activity section - ...createActivitySection(ctx, list, useGroupName, title), + ...createActivitySection(list, useGroupName, title), ], }; } /** * Create realized price options for single cohort - * @param {PartialContext} ctx * @param {AddressCohortObject} args * @param {string} title * @returns {PartialOptionsTree} */ -function createRealizedPriceOptions(ctx, args, title) { - const { line } = ctx; - const { tree, color } = args; +function createRealizedPriceOptions(args, title) { + const { tree, color } = args; return [ { @@ -163,7 +161,7 @@ function createRealizedPriceOptions(ctx, args, title) { * @returns {AnyFetchedSeriesBlueprint[]} */ function createRealizedCapWithExtras(ctx, list, args, useGroupName) { - const { line, baseline, createPriceLine } = ctx; + const { createPriceLine } = ctx; const isSingle = !("list" in args); return list.flatMap(({ color, name, tree }) => [ @@ -196,7 +194,7 @@ function createRealizedCapWithExtras(ctx, list, args, useGroupName) { * @returns {PartialOptionsTree} */ function createRealizedPnlSection(ctx, args, title) { - const { colors, line } = ctx; + const { colors } = ctx; const { realized } = args.tree; return [ @@ -251,7 +249,7 @@ function createRealizedPnlSection(ctx, args, title) { * @returns {PartialOptionsTree} */ function createUnrealizedSection(ctx, list, useGroupName, title) { - const { colors, line, baseline } = ctx; + const { colors } = ctx; return [ { @@ -301,15 +299,12 @@ function createUnrealizedSection(ctx, list, useGroupName, title) { /** * Create cost basis section (no percentiles for address cohorts) - * @param {PartialContext} ctx * @param {readonly AddressCohortObject[]} list * @param {boolean} useGroupName * @param {string} title * @returns {PartialOptionsTree} */ -function createCostBasisSection(ctx, list, useGroupName, title) { - const { line } = ctx; - +function createCostBasisSection(list, useGroupName, title) { return [ { name: "Cost Basis", @@ -345,15 +340,12 @@ function createCostBasisSection(ctx, list, useGroupName, title) { /** * Create activity section - * @param {PartialContext} ctx * @param {readonly AddressCohortObject[]} list * @param {boolean} useGroupName * @param {string} title * @returns {PartialOptionsTree} */ -function createActivitySection(ctx, list, useGroupName, title) { - const { line } = ctx; - +function createActivitySection(list, useGroupName, title) { return [ { name: "Activity", diff --git a/website/scripts/options/cohorts/shared.js b/website/scripts/options/cohorts/shared.js index f8da67d69..2b92d0303 100644 --- a/website/scripts/options/cohorts/shared.js +++ b/website/scripts/options/cohorts/shared.js @@ -1,6 +1,7 @@ /** Shared cohort chart section builders */ import { Unit } from "../../utils/units.js"; +import { line } from "../series.js"; import { satsBtcUsd } from "../shared.js"; /** @@ -10,11 +11,11 @@ import { satsBtcUsd } from "../shared.js"; * @returns {AnyFetchedSeriesBlueprint[]} */ export function createSingleSupplySeries(ctx, cohort) { - const { colors, line, createPriceLine } = ctx; + const { colors, createPriceLine } = ctx; const { tree } = cohort; return [ - ...satsBtcUsd(ctx, tree.supply.total, "Supply", colors.default), + ...satsBtcUsd( tree.supply.total, "Supply", colors.default), ...("supplyRelToCirculatingSupply" in tree.relative ? [ line({ @@ -25,9 +26,9 @@ export function createSingleSupplySeries(ctx, cohort) { }), ] : []), - ...satsBtcUsd(ctx, tree.unrealized.supplyInProfit, "In Profit", colors.green), - ...satsBtcUsd(ctx, tree.unrealized.supplyInLoss, "In Loss", colors.red), - ...satsBtcUsd(ctx, tree.supply.halved, "half", colors.gray).map((s) => ({ + ...satsBtcUsd( tree.unrealized.supplyInProfit, "In Profit", colors.green), + ...satsBtcUsd( tree.unrealized.supplyInLoss, "In Loss", colors.red), + ...satsBtcUsd( tree.supply.halved, "half", colors.gray).map((s) => ({ ...s, options: { lineStyle: 4 }, })), @@ -76,11 +77,11 @@ export function createSingleSupplySeries(ctx, cohort) { * @returns {AnyFetchedSeriesBlueprint[]} */ export function createGroupedSupplyTotalSeries(ctx, list) { - const { line, brk } = ctx; + const { brk } = ctx; const constant100 = brk.metrics.constants.constant100; return list.flatMap(({ color, name, tree }) => [ - ...satsBtcUsd(ctx, tree.supply.total, name, color), + ...satsBtcUsd( tree.supply.total, name, color), line({ metric: "supplyRelToCirculatingSupply" in tree.relative @@ -95,15 +96,13 @@ export function createGroupedSupplyTotalSeries(ctx, list) { /** * Create supply in profit series for grouped cohorts - * @param {PartialContext} ctx * @param {readonly CohortObject[]} list * @returns {AnyFetchedSeriesBlueprint[]} */ -export function createGroupedSupplyInProfitSeries(ctx, list) { - const { line } = ctx; - +export function createGroupedSupplyInProfitSeries(list) { + return list.flatMap(({ color, name, tree }) => [ - ...satsBtcUsd(ctx, tree.unrealized.supplyInProfit, name, color), + ...satsBtcUsd( tree.unrealized.supplyInProfit, name, color), ...("supplyInProfitRelToCirculatingSupply" in tree.relative ? [ line({ @@ -119,15 +118,13 @@ export function createGroupedSupplyInProfitSeries(ctx, list) { /** * Create supply in loss series for grouped cohorts - * @param {PartialContext} ctx * @param {readonly CohortObject[]} list * @returns {AnyFetchedSeriesBlueprint[]} */ -export function createGroupedSupplyInLossSeries(ctx, list) { - const { line } = ctx; - +export function createGroupedSupplyInLossSeries(list) { + return list.flatMap(({ color, name, tree }) => [ - ...satsBtcUsd(ctx, tree.unrealized.supplyInLoss, name, color), + ...satsBtcUsd( tree.unrealized.supplyInLoss, name, color), ...("supplyInLossRelToCirculatingSupply" in tree.relative ? [ line({ @@ -143,14 +140,11 @@ export function createGroupedSupplyInLossSeries(ctx, list) { /** * Create UTXO count series - * @param {PartialContext} ctx * @param {readonly CohortObject[]} list * @param {boolean} useGroupName * @returns {AnyFetchedSeriesBlueprint[]} */ -export function createUtxoCountSeries(ctx, list, useGroupName) { - const { line } = ctx; - +export function createUtxoCountSeries(list, useGroupName) { return list.flatMap(({ color, name, tree }) => [ line({ metric: tree.outputs.utxoCount, @@ -169,7 +163,7 @@ export function createUtxoCountSeries(ctx, list, useGroupName) { * @returns {AnyFetchedSeriesBlueprint[]} */ export function createAddressCountSeries(ctx, list, useGroupName) { - const { line, colors } = ctx; + const { colors } = ctx; return list.flatMap(({ color, name, tree }) => [ line({ @@ -183,13 +177,10 @@ export function createAddressCountSeries(ctx, list, useGroupName) { /** * Create realized price series for grouped cohorts - * @param {PartialContext} ctx * @param {readonly CohortObject[]} list * @returns {AnyFetchedSeriesBlueprint[]} */ -export function createRealizedPriceSeries(ctx, list) { - const { line } = ctx; - +export function createRealizedPriceSeries(list) { return list.map(({ color, name, tree }) => line({ metric: tree.realized.realizedPrice, name, color, unit: Unit.usd }), ); @@ -202,7 +193,7 @@ export function createRealizedPriceSeries(ctx, list) { * @returns {AnyFetchedSeriesBlueprint[]} */ export function createRealizedPriceRatioSeries(ctx, list) { - const { line, createPriceLine } = ctx; + const { createPriceLine } = ctx; return [ ...list.map(({ color, name, tree }) => @@ -219,14 +210,11 @@ export function createRealizedPriceRatioSeries(ctx, list) { /** * Create realized capitalization series - * @param {PartialContext} ctx * @param {readonly CohortObject[]} list * @param {boolean} useGroupName * @returns {AnyFetchedSeriesBlueprint[]} */ -export function createRealizedCapSeries(ctx, list, useGroupName) { - const { line } = ctx; - +export function createRealizedCapSeries(list, useGroupName) { return list.flatMap(({ color, name, tree }) => [ line({ metric: tree.realized.realizedCap, @@ -239,14 +227,11 @@ export function createRealizedCapSeries(ctx, list, useGroupName) { /** * Create cost basis min/max series (available on all cohorts) - * @param {PartialContext} ctx * @param {readonly CohortObject[]} list * @param {boolean} useGroupName * @returns {AnyFetchedSeriesBlueprint[]} */ -export function createCostBasisMinMaxSeries(ctx, list, useGroupName) { - const { line } = ctx; - +export function createCostBasisMinMaxSeries(list, useGroupName) { return list.flatMap(({ color, name, tree }) => [ line({ metric: tree.costBasis.min, @@ -265,14 +250,11 @@ export function createCostBasisMinMaxSeries(ctx, list, useGroupName) { /** * Create cost basis percentile series (only for cohorts with CostBasisPattern2) - * @param {PartialContext} ctx * @param {readonly CohortWithCostBasisPercentiles[]} list * @param {boolean} useGroupName * @returns {AnyFetchedSeriesBlueprint[]} */ -export function createCostBasisPercentilesSeries(ctx, list, useGroupName) { - const { line } = ctx; - +export function createCostBasisPercentilesSeries(list, useGroupName) { return list.flatMap(({ color, name, tree }) => { const percentiles = tree.costBasis.percentiles; return [ diff --git a/website/scripts/options/cohorts/utxo.js b/website/scripts/options/cohorts/utxo.js index 36e5c8491..393101c74 100644 --- a/website/scripts/options/cohorts/utxo.js +++ b/website/scripts/options/cohorts/utxo.js @@ -34,6 +34,7 @@ import { createCostBasisPercentilesSeries, } from "./shared.js"; import { Unit } from "../../utils/units.js"; +import { line, baseline } from "../series.js"; // ============================================================================ // Folder Builders (4 variants based on pattern capabilities) @@ -51,10 +52,10 @@ export function createCohortFolderAll(ctx, args) { name: args.name || "all", tree: [ createSingleSupplyChart(ctx, args, title), - createSingleUtxoCountChart(ctx, args, title), + createSingleUtxoCountChart(args, title), createSingleRealizedSectionWithAdjusted(ctx, args, title), createSingleUnrealizedSectionAll(ctx, args, title), - createSingleCostBasisSectionWithPercentiles(ctx, args, title), + createSingleCostBasisSectionWithPercentiles(args, title), ...createSingleActivitySectionWithAdjusted(ctx, args, title), ], }; @@ -74,11 +75,11 @@ export function createCohortFolderFull(ctx, args) { name: args.name || "all", tree: [ createGroupedSupplySection(ctx, list, title), - createGroupedUtxoCountChart(ctx, list, title), + createGroupedUtxoCountChart(list, title), createGroupedRealizedSectionWithAdjusted(ctx, list, title), createGroupedUnrealizedSectionFull(ctx, list, title), - createGroupedCostBasisSectionWithPercentiles(ctx, list, title), - ...createGroupedActivitySectionWithAdjusted(ctx, list, title), + createGroupedCostBasisSectionWithPercentiles(list, title), + ...createGroupedActivitySectionWithAdjusted(list, title), ], }; } @@ -87,10 +88,10 @@ export function createCohortFolderFull(ctx, args) { name: args.name || "all", tree: [ createSingleSupplyChart(ctx, args, title), - createSingleUtxoCountChart(ctx, args, title), + createSingleUtxoCountChart(args, title), createSingleRealizedSectionWithAdjusted(ctx, args, title), createSingleUnrealizedSectionFull(ctx, args, title), - createSingleCostBasisSectionWithPercentiles(ctx, args, title), + createSingleCostBasisSectionWithPercentiles(args, title), ...createSingleActivitySectionWithAdjusted(ctx, args, title), ], }; @@ -110,11 +111,11 @@ export function createCohortFolderWithAdjusted(ctx, args) { name: args.name || "all", tree: [ createGroupedSupplySection(ctx, list, title), - createGroupedUtxoCountChart(ctx, list, title), + createGroupedUtxoCountChart(list, title), createGroupedRealizedSectionWithAdjusted(ctx, list, title), createGroupedUnrealizedSectionWithMarketCap(ctx, list, title), - createGroupedCostBasisSection(ctx, list, title), - ...createGroupedActivitySectionWithAdjusted(ctx, list, title), + createGroupedCostBasisSection(list, title), + ...createGroupedActivitySectionWithAdjusted(list, title), ], }; } @@ -123,10 +124,10 @@ export function createCohortFolderWithAdjusted(ctx, args) { name: args.name || "all", tree: [ createSingleSupplyChart(ctx, args, title), - createSingleUtxoCountChart(ctx, args, title), + createSingleUtxoCountChart(args, title), createSingleRealizedSectionWithAdjusted(ctx, args, title), createSingleUnrealizedSectionWithMarketCap(ctx, args, title), - createSingleCostBasisSection(ctx, args, title), + createSingleCostBasisSection(args, title), ...createSingleActivitySectionWithAdjusted(ctx, args, title), ], }; @@ -146,11 +147,11 @@ export function createCohortFolderWithPercentiles(ctx, args) { name: args.name || "all", tree: [ createGroupedSupplySection(ctx, list, title), - createGroupedUtxoCountChart(ctx, list, title), + createGroupedUtxoCountChart(list, title), createGroupedRealizedSectionBasic(ctx, list, title), createGroupedUnrealizedSectionWithOwnCaps(ctx, list, title), - createGroupedCostBasisSectionWithPercentiles(ctx, list, title), - ...createGroupedActivitySectionBasic(ctx, list, title), + createGroupedCostBasisSectionWithPercentiles(list, title), + ...createGroupedActivitySectionBasic(list, title), ], }; } @@ -159,10 +160,10 @@ export function createCohortFolderWithPercentiles(ctx, args) { name: args.name || "all", tree: [ createSingleSupplyChart(ctx, args, title), - createSingleUtxoCountChart(ctx, args, title), + createSingleUtxoCountChart(args, title), createSingleRealizedSectionBasic(ctx, args, title), createSingleUnrealizedSectionWithOwnCaps(ctx, args, title), - createSingleCostBasisSectionWithPercentiles(ctx, args, title), + createSingleCostBasisSectionWithPercentiles(args, title), ...createSingleActivitySectionBasic(ctx, args, title), ], }; @@ -182,11 +183,11 @@ export function createCohortFolderBasic(ctx, args) { name: args.name || "all", tree: [ createGroupedSupplySection(ctx, list, title), - createGroupedUtxoCountChart(ctx, list, title), + createGroupedUtxoCountChart(list, title), createGroupedRealizedSectionBasic(ctx, list, title), createGroupedUnrealizedSectionBase(ctx, list, title), - createGroupedCostBasisSection(ctx, list, title), - ...createGroupedActivitySectionBasic(ctx, list, title), + createGroupedCostBasisSection(list, title), + ...createGroupedActivitySectionBasic(list, title), ], }; } @@ -195,10 +196,10 @@ export function createCohortFolderBasic(ctx, args) { name: args.name || "all", tree: [ createSingleSupplyChart(ctx, args, title), - createSingleUtxoCountChart(ctx, args, title), + createSingleUtxoCountChart(args, title), createSingleRealizedSectionBasic(ctx, args, title), createSingleUnrealizedSectionBase(ctx, args, title), - createSingleCostBasisSection(ctx, args, title), + createSingleCostBasisSection(args, title), ...createSingleActivitySectionBasic(ctx, args, title), ], }; @@ -238,12 +239,12 @@ function createGroupedSupplySection(ctx, list, title) { { name: "in profit", title: `Supply In Profit ${title}`, - bottom: createGroupedSupplyInProfitSeries(ctx, list), + bottom: createGroupedSupplyInProfitSeries(list), }, { name: "in loss", title: `Supply In Loss ${title}`, - bottom: createGroupedSupplyInLossSeries(ctx, list), + bottom: createGroupedSupplyInLossSeries(list), }, ], }; @@ -251,31 +252,29 @@ function createGroupedSupplySection(ctx, list, title) { /** * Create UTXO count chart for single cohort - * @param {PartialContext} ctx * @param {UtxoCohortObject} cohort * @param {string} title * @returns {PartialChartOption} */ -function createSingleUtxoCountChart(ctx, cohort, title) { +function createSingleUtxoCountChart(cohort, title) { return { name: "utxo count", title: `UTXO Count ${title}`, - bottom: createUtxoCountSeries(ctx, [cohort], false), + bottom: createUtxoCountSeries( [cohort], false), }; } /** * Create UTXO count chart for grouped cohorts - * @param {PartialContext} ctx * @param {readonly UtxoCohortObject[]} list * @param {string} title * @returns {PartialChartOption} */ -function createGroupedUtxoCountChart(ctx, list, title) { +function createGroupedUtxoCountChart(list, title) { return { name: "utxo count", title: `UTXO Count ${title}`, - bottom: createUtxoCountSeries(ctx, list, true), + bottom: createUtxoCountSeries( list, true), }; } @@ -290,7 +289,7 @@ function createSingleRealizedSectionWithAdjusted(ctx, cohort, title) { return { name: "Realized", tree: [ - createSingleRealizedPriceChart(ctx, cohort, title), + createSingleRealizedPriceChart(cohort, title), { name: "capitalization", title: `Realized Capitalization ${title}`, @@ -316,7 +315,7 @@ function createGroupedRealizedSectionWithAdjusted(ctx, list, title) { { name: "Price", title: `Realized Price ${title}`, - top: createRealizedPriceSeries(ctx, list), + top: createRealizedPriceSeries(list), }, { name: "Ratio", @@ -326,7 +325,7 @@ function createGroupedRealizedSectionWithAdjusted(ctx, list, title) { { name: "capitalization", title: `Realized Capitalization ${title}`, - bottom: createGroupedRealizedCapSeries(ctx, list), + bottom: createGroupedRealizedCapSeries(list), }, ...createGroupedRealizedPnlSections(ctx, list, title), createGroupedSoprSectionWithAdjusted(ctx, list, title), @@ -345,7 +344,7 @@ function createSingleRealizedSectionBasic(ctx, cohort, title) { return { name: "Realized", tree: [ - createSingleRealizedPriceChart(ctx, cohort, title), + createSingleRealizedPriceChart(cohort, title), { name: "capitalization", title: `Realized Capitalization ${title}`, @@ -371,7 +370,7 @@ function createGroupedRealizedSectionBasic(ctx, list, title) { { name: "Price", title: `Realized Price ${title}`, - top: createRealizedPriceSeries(ctx, list), + top: createRealizedPriceSeries(list), }, { name: "Ratio", @@ -381,7 +380,7 @@ function createGroupedRealizedSectionBasic(ctx, list, title) { { name: "capitalization", title: `Realized Capitalization ${title}`, - bottom: createGroupedRealizedCapSeries(ctx, list), + bottom: createGroupedRealizedCapSeries(list), }, ...createGroupedRealizedPnlSections(ctx, list, title), createGroupedSoprSectionBasic(ctx, list, title), @@ -391,14 +390,12 @@ function createGroupedRealizedSectionBasic(ctx, list, title) { /** * Create realized price chart for single cohort - * @param {PartialContext} ctx * @param {UtxoCohortObject} cohort * @param {string} title * @returns {PartialChartOption} */ -function createSingleRealizedPriceChart(ctx, cohort, title) { - const { line } = ctx; - const { tree, color } = cohort; +function createSingleRealizedPriceChart(cohort, title) { + const { tree, color } = cohort; return { name: "price", @@ -421,7 +418,7 @@ function createSingleRealizedPriceChart(ctx, cohort, title) { * @returns {AnyFetchedSeriesBlueprint[]} */ function createSingleRealizedCapSeries(ctx, cohort) { - const { colors, line, baseline, createPriceLine } = ctx; + const { colors, createPriceLine } = ctx; const { color, tree } = cohort; return [ @@ -459,13 +456,10 @@ function createSingleRealizedCapSeries(ctx, cohort) { /** * Create realized cap series for grouped cohorts - * @param {PartialContext} ctx * @param {readonly UtxoCohortObject[]} list * @returns {AnyFetchedSeriesBlueprint[]} */ -function createGroupedRealizedCapSeries(ctx, list) { - const { line } = ctx; - +function createGroupedRealizedCapSeries(list) { return list.map(({ color, name, tree }) => line({ metric: tree.realized.realizedCap, @@ -486,8 +480,6 @@ function createGroupedRealizedCapSeries(ctx, list) { function createSingleRealizedPnlSection(ctx, cohort, title) { const { colors, - line, - baseline, createPriceLine, fromBlockCountWithUnit, fromBitcoinPatternWithUnit, @@ -598,7 +590,7 @@ function createSingleRealizedPnlSection(ctx, cohort, title) { * @returns {PartialOptionsTree} */ function createGroupedRealizedPnlSections(ctx, list, title) { - const { line, baseline, createPriceLine } = ctx; + const { createPriceLine } = ctx; return [ { @@ -795,7 +787,7 @@ function createGroupedRealizedPnlSections(ctx, list, title) { * @returns {PartialChartOption} */ function createSingleBaseSoprChart(ctx, cohort, title) { - const { colors, baseline, createPriceLine } = ctx; + const { colors, createPriceLine } = ctx; const { tree } = cohort; return { @@ -837,7 +829,7 @@ function createSingleBaseSoprChart(ctx, cohort, title) { * @returns {PartialChartOption} */ function createSingleAdjustedSoprChart(ctx, cohort, title) { - const { colors, baseline, createPriceLine } = ctx; + const { colors, createPriceLine } = ctx; const { tree } = cohort; return { @@ -880,7 +872,7 @@ function createSingleAdjustedSoprChart(ctx, cohort, title) { * @returns {PartialChartOption} */ function createGroupedBaseSoprChart(ctx, list, title) { - const { baseline, createPriceLine } = ctx; + const { createPriceLine } = ctx; return { name: "Normal", @@ -924,7 +916,7 @@ function createGroupedBaseSoprChart(ctx, list, title) { * @returns {PartialChartOption} */ function createGroupedAdjustedSoprChart(ctx, list, title) { - const { baseline, createPriceLine } = ctx; + const { createPriceLine } = ctx; return { name: "Adjusted", @@ -1035,7 +1027,7 @@ function createGroupedSoprSectionBasic(ctx, list, title) { * @param {RelativeWithMarketCap} rel */ function createUnrealizedPnlRelToMarketCapMetrics(ctx, rel) { - const { colors, line } = ctx; + const { colors } = ctx; return [ line({ metric: rel.unrealizedProfitRelToMarketCap, @@ -1064,7 +1056,7 @@ function createUnrealizedPnlRelToMarketCapMetrics(ctx, rel) { * @param {RelativeWithOwnMarketCap} rel */ function createUnrealizedPnlRelToOwnMarketCapMetrics(ctx, rel) { - const { colors, line, createPriceLine } = ctx; + const { colors, createPriceLine } = ctx; return [ line({ metric: rel.unrealizedProfitRelToOwnMarketCap, @@ -1095,7 +1087,7 @@ function createUnrealizedPnlRelToOwnMarketCapMetrics(ctx, rel) { * @param {RelativeWithOwnPnl} rel */ function createUnrealizedPnlRelToOwnPnlMetrics(ctx, rel) { - const { colors, line, createPriceLine } = ctx; + const { colors, createPriceLine } = ctx; return [ line({ metric: rel.unrealizedProfitRelToOwnTotalUnrealizedPnl, @@ -1122,12 +1114,10 @@ function createUnrealizedPnlRelToOwnPnlMetrics(ctx, rel) { } /** - * @param {PartialContext} ctx * @param {RelativeWithMarketCap} rel */ -function createNetUnrealizedPnlRelToMarketCapMetrics(ctx, rel) { - const { baseline } = ctx; - return [ +function createNetUnrealizedPnlRelToMarketCapMetrics(rel) { + return [ baseline({ metric: rel.netUnrealizedPnlRelToMarketCap, name: "Net", @@ -1141,7 +1131,7 @@ function createNetUnrealizedPnlRelToMarketCapMetrics(ctx, rel) { * @param {RelativeWithOwnMarketCap} rel */ function createNetUnrealizedPnlRelToOwnMarketCapMetrics(ctx, rel) { - const { baseline, createPriceLine } = ctx; + const { createPriceLine } = ctx; return [ baseline({ metric: rel.netUnrealizedPnlRelToOwnMarketCap, @@ -1157,7 +1147,7 @@ function createNetUnrealizedPnlRelToOwnMarketCapMetrics(ctx, rel) { * @param {RelativeWithOwnPnl} rel */ function createNetUnrealizedPnlRelToOwnPnlMetrics(ctx, rel) { - const { baseline, createPriceLine } = ctx; + const { createPriceLine } = ctx; return [ baseline({ metric: rel.netUnrealizedPnlRelToOwnTotalUnrealizedPnl, @@ -1174,7 +1164,7 @@ function createNetUnrealizedPnlRelToOwnPnlMetrics(ctx, rel) { * @param {{ unrealized: { totalUnrealizedPnl: AnyMetricPattern, unrealizedProfit: AnyMetricPattern, unrealizedLoss: AnyMetricPattern, negUnrealizedLoss: AnyMetricPattern } }} tree */ function createUnrealizedPnlBaseMetrics(ctx, tree) { - const { colors, line } = ctx; + const { colors } = ctx; return [ line({ metric: tree.unrealized.totalUnrealizedPnl, @@ -1206,12 +1196,10 @@ function createUnrealizedPnlBaseMetrics(ctx, tree) { /** * Base net unrealized metric (always present) - * @param {PartialContext} ctx * @param {{ unrealized: { netUnrealizedPnl: AnyMetricPattern } }} tree */ -function createNetUnrealizedPnlBaseMetric(ctx, tree) { - const { baseline } = ctx; - return baseline({ +function createNetUnrealizedPnlBaseMetric(tree) { + return baseline({ metric: tree.unrealized.netUnrealizedPnl, name: "Net", unit: Unit.ratio, @@ -1249,7 +1237,7 @@ function createSingleUnrealizedSectionAll(ctx, cohort, title) { name: "Net pnl", title: `Net Unrealized Profit And Loss ${title}`, bottom: [ - createNetUnrealizedPnlBaseMetric(ctx, tree), + createNetUnrealizedPnlBaseMetric(tree), ...createNetUnrealizedPnlRelToOwnPnlMetrics(ctx, tree.relative), createPriceLine({ unit: Unit.usd }), ], @@ -1287,8 +1275,8 @@ function createSingleUnrealizedSectionFull(ctx, cohort, title) { name: "Net pnl", title: `Net Unrealized Profit And Loss ${title}`, bottom: [ - createNetUnrealizedPnlBaseMetric(ctx, tree), - ...createNetUnrealizedPnlRelToMarketCapMetrics(ctx, tree.relative), + createNetUnrealizedPnlBaseMetric(tree), + ...createNetUnrealizedPnlRelToMarketCapMetrics(tree.relative), ...createNetUnrealizedPnlRelToOwnMarketCapMetrics(ctx, tree.relative), ...createNetUnrealizedPnlRelToOwnPnlMetrics(ctx, tree.relative), createPriceLine({ unit: Unit.usd }), @@ -1326,8 +1314,8 @@ function createSingleUnrealizedSectionWithMarketCap(ctx, cohort, title) { name: "Net pnl", title: `Net Unrealized Profit And Loss ${title}`, bottom: [ - createNetUnrealizedPnlBaseMetric(ctx, tree), - ...createNetUnrealizedPnlRelToMarketCapMetrics(ctx, tree.relative), + createNetUnrealizedPnlBaseMetric(tree), + ...createNetUnrealizedPnlRelToMarketCapMetrics(tree.relative), createPriceLine({ unit: Unit.usd }), createPriceLine({ unit: Unit.pctMcap }), ], @@ -1364,7 +1352,7 @@ function createSingleUnrealizedSectionWithOwnCaps(ctx, cohort, title) { name: "Net pnl", title: `Net Unrealized Profit And Loss ${title}`, bottom: [ - createNetUnrealizedPnlBaseMetric(ctx, tree), + createNetUnrealizedPnlBaseMetric(tree), ...createNetUnrealizedPnlRelToOwnMarketCapMetrics(ctx, tree.relative), ...createNetUnrealizedPnlRelToOwnPnlMetrics(ctx, tree.relative), createPriceLine({ unit: Unit.usd }), @@ -1400,7 +1388,7 @@ function createSingleUnrealizedSectionBase(ctx, cohort, title) { name: "Net pnl", title: `Net Unrealized Profit And Loss ${title}`, bottom: [ - createNetUnrealizedPnlBaseMetric(ctx, tree), + createNetUnrealizedPnlBaseMetric(tree), createPriceLine({ unit: Unit.usd }), ], }, @@ -1410,13 +1398,11 @@ function createSingleUnrealizedSectionBase(ctx, cohort, title) { /** * Grouped unrealized base charts (profit, loss, total pnl) - * @param {PartialContext} ctx * @param {readonly { color: Color, name: string, tree: { unrealized: PatternAll["unrealized"] } }[]} list * @param {string} title */ -function createGroupedUnrealizedBaseCharts(ctx, list, title) { - const { line } = ctx; - return [ +function createGroupedUnrealizedBaseCharts(list, title) { + return [ { name: "profit", title: `Unrealized Profit ${title}`, @@ -1464,11 +1450,11 @@ function createGroupedUnrealizedBaseCharts(ctx, list, title) { * @returns {PartialOptionsGroup} */ function createGroupedUnrealizedSectionFull(ctx, list, title) { - const { baseline, createPriceLine } = ctx; + const { createPriceLine } = ctx; return { name: "Unrealized", tree: [ - ...createGroupedUnrealizedBaseCharts(ctx, list, title), + ...createGroupedUnrealizedBaseCharts(list, title), { name: "Net pnl", title: `Net Unrealized Profit And Loss ${title}`, @@ -1517,11 +1503,11 @@ function createGroupedUnrealizedSectionFull(ctx, list, title) { * @returns {PartialOptionsGroup} */ function createGroupedUnrealizedSectionWithMarketCap(ctx, list, title) { - const { baseline, createPriceLine } = ctx; + const { createPriceLine } = ctx; return { name: "Unrealized", tree: [ - ...createGroupedUnrealizedBaseCharts(ctx, list, title), + ...createGroupedUnrealizedBaseCharts(list, title), { name: "Net pnl", title: `Net Unrealized Profit And Loss ${title}`, @@ -1556,11 +1542,11 @@ function createGroupedUnrealizedSectionWithMarketCap(ctx, list, title) { * @returns {PartialOptionsGroup} */ function createGroupedUnrealizedSectionWithOwnCaps(ctx, list, title) { - const { baseline, createPriceLine } = ctx; + const { createPriceLine } = ctx; return { name: "Unrealized", tree: [ - ...createGroupedUnrealizedBaseCharts(ctx, list, title), + ...createGroupedUnrealizedBaseCharts(list, title), { name: "Net pnl", title: `Net Unrealized Profit And Loss ${title}`, @@ -1602,11 +1588,11 @@ function createGroupedUnrealizedSectionWithOwnCaps(ctx, list, title) { * @returns {PartialOptionsGroup} */ function createGroupedUnrealizedSectionBase(ctx, list, title) { - const { baseline, createPriceLine } = ctx; + const { createPriceLine } = ctx; return { name: "Unrealized", tree: [ - ...createGroupedUnrealizedBaseCharts(ctx, list, title), + ...createGroupedUnrealizedBaseCharts(list, title), { name: "Net pnl", title: `Net Unrealized Profit And Loss ${title}`, @@ -1628,14 +1614,12 @@ function createGroupedUnrealizedSectionBase(ctx, list, title) { /** * Create cost basis section for single cohort WITH percentiles - * @param {PartialContext} ctx * @param {CohortAll | CohortFull | CohortWithPercentiles} cohort * @param {string} title * @returns {PartialOptionsGroup} */ -function createSingleCostBasisSectionWithPercentiles(ctx, cohort, title) { - const { line } = ctx; - const { color, tree } = cohort; +function createSingleCostBasisSectionWithPercentiles(cohort, title) { + const { color, tree } = cohort; return { name: "Cost Basis", @@ -1668,7 +1652,7 @@ function createSingleCostBasisSectionWithPercentiles(ctx, cohort, title) { { name: "percentiles", title: `Cost Basis Percentiles ${title}`, - top: createCostBasisPercentilesSeries(ctx, [cohort], false), + top: createCostBasisPercentilesSeries( [cohort], false), }, ], }; @@ -1676,14 +1660,11 @@ function createSingleCostBasisSectionWithPercentiles(ctx, cohort, title) { /** * Create cost basis section for grouped cohorts WITH percentiles - * @param {PartialContext} ctx * @param {readonly (CohortFull | CohortWithPercentiles)[]} list * @param {string} title * @returns {PartialOptionsGroup} */ -function createGroupedCostBasisSectionWithPercentiles(ctx, list, title) { - const { line } = ctx; - +function createGroupedCostBasisSectionWithPercentiles(list, title) { return { name: "Cost Basis", tree: [ @@ -1719,14 +1700,12 @@ function createGroupedCostBasisSectionWithPercentiles(ctx, list, title) { /** * Create cost basis section for single cohort (no percentiles) - * @param {PartialContext} ctx * @param {CohortWithAdjusted | CohortBasic} cohort * @param {string} title * @returns {PartialOptionsGroup} */ -function createSingleCostBasisSection(ctx, cohort, title) { - const { line } = ctx; - const { color, tree } = cohort; +function createSingleCostBasisSection(cohort, title) { + const { color, tree } = cohort; return { name: "Cost Basis", @@ -1762,14 +1741,11 @@ function createSingleCostBasisSection(ctx, cohort, title) { /** * Create cost basis section for grouped cohorts (no percentiles) - * @param {PartialContext} ctx * @param {readonly (CohortWithAdjusted | CohortBasic)[]} list * @param {string} title * @returns {PartialOptionsGroup} */ -function createGroupedCostBasisSection(ctx, list, title) { - const { line } = ctx; - +function createGroupedCostBasisSection(list, title) { return { name: "Cost Basis", tree: [ @@ -1811,7 +1787,7 @@ function createGroupedCostBasisSection(ctx, list, title) { * @returns {PartialOptionsTree} */ function createSingleActivitySectionWithAdjusted(ctx, cohort, title) { - const { colors, line } = ctx; + const { colors } = ctx; const { tree, color } = cohort; return [ @@ -1925,7 +1901,7 @@ function createSingleActivitySectionWithAdjusted(ctx, cohort, title) { * @returns {PartialOptionsTree} */ function createSingleActivitySectionBasic(ctx, cohort, title) { - const { colors, line } = ctx; + const { colors } = ctx; const { tree, color } = cohort; return [ @@ -2021,14 +1997,11 @@ function createSingleActivitySectionBasic(ctx, cohort, title) { /** * Create activity section for grouped cohorts with adjusted values (for cohorts with RealizedPattern3/4) - * @param {PartialContext} ctx * @param {readonly (CohortFull | CohortWithAdjusted)[]} list * @param {string} title * @returns {PartialOptionsTree} */ -function createGroupedActivitySectionWithAdjusted(ctx, list, title) { - const { line } = ctx; - +function createGroupedActivitySectionWithAdjusted(list, title) { return [ { name: "Sell Side Risk", @@ -2151,14 +2124,11 @@ function createGroupedActivitySectionWithAdjusted(ctx, list, title) { /** * Create activity section for grouped cohorts without adjusted values (for cohorts with RealizedPattern/2) - * @param {PartialContext} ctx * @param {readonly (CohortWithPercentiles | CohortBasic)[]} list * @param {string} title * @returns {PartialOptionsTree} */ -function createGroupedActivitySectionBasic(ctx, list, title) { - const { line } = ctx; - +function createGroupedActivitySectionBasic(list, title) { return [ { name: "Sell Side Risk", diff --git a/website/scripts/options/cointime.js b/website/scripts/options/cointime.js index 0b84dca70..675b39f52 100644 --- a/website/scripts/options/cointime.js +++ b/website/scripts/options/cointime.js @@ -1,6 +1,7 @@ /** Cointime section builder - typed tree-based patterns */ import { Unit } from "../utils/units.js"; +import { line, baseline } from "./series.js"; import { satsBtcUsd, priceLines, @@ -25,7 +26,7 @@ function createCointimePriceWithRatioOptions( ctx, { title, legend, price, ratio, color }, ) { - const { line, colors, createPriceLine } = ctx; + const { colors, createPriceLine } = ctx; const pctUsdMap = percentileUsdMap(colors, ratio); const pctMap = percentileMap(colors, ratio); @@ -54,42 +55,53 @@ function createCointimePriceWithRatioOptions( ), ], bottom: [ - line({ metric: ratio.ratio, name: "Ratio", color, unit: Unit.ratio }), + baseline({ + metric: ratio.ratio, + name: "Ratio", + color, + unit: Unit.ratio, + }), line({ metric: ratio.ratio1wSma, name: "1w SMA", color: colors.lime, unit: Unit.ratio, + defaultActive: false, }), line({ metric: ratio.ratio1mSma, name: "1m SMA", color: colors.teal, unit: Unit.ratio, + defaultActive: false, }), line({ metric: ratio.ratio1ySd.sma, name: "1y SMA", color: colors.sky, unit: Unit.ratio, + defaultActive: false, }), line({ metric: ratio.ratio2ySd.sma, name: "2y SMA", color: colors.indigo, unit: Unit.ratio, + defaultActive: false, }), line({ metric: ratio.ratio4ySd.sma, name: "4y SMA", color: colors.purple, unit: Unit.ratio, + defaultActive: false, }), line({ metric: ratio.ratioSd.sma, name: "All SMA", color: colors.rose, unit: Unit.ratio, + defaultActive: false, }), ...pctMap.map(({ name: pctName, prop, color: pctColor }) => line({ @@ -174,13 +186,14 @@ function createCointimePriceWithRatioOptions( ...sdPats.map(({ nameAddon, titleAddon, sd }) => ({ name: nameAddon, title: `${title} ${titleAddon} Z-Score`, - top: sdBands(colors, sd).map(({ name: bandName, prop, color: bandColor }) => - line({ - metric: prop, - name: bandName, - color: bandColor, - unit: Unit.usd, - }), + top: sdBands(colors, sd).map( + ({ name: bandName, prop, color: bandColor }) => + line({ + metric: prop, + name: bandName, + color: bandColor, + unit: Unit.usd, + }), ), bottom: [ line({ metric: sd.zscore, name: "Z-Score", color, unit: Unit.sd }), @@ -198,7 +211,7 @@ function createCointimePriceWithRatioOptions( * @returns {PartialOptionsGroup} */ export function createCointimeSection(ctx) { - const { colors, brk, line } = ctx; + const { colors, brk } = ctx; const { cointime, distribution, supply } = brk.metrics; const { pricing, cap, activity, supply: cointimeSupply, adjusted } = cointime; const { all } = distribution.utxoCohorts; @@ -348,9 +361,17 @@ export function createCointimeSection(ctx) { name: "Supply", title: "Cointime Supply", bottom: [ - ...satsBtcUsd(ctx, all.supply.total, "All", colors.orange), - ...satsBtcUsd(ctx, cointimeSupply.vaultedSupply, "Vaulted", colors.lime), - ...satsBtcUsd(ctx, cointimeSupply.activeSupply, "Active", colors.rose), + ...satsBtcUsd( all.supply.total, "All", colors.orange), + ...satsBtcUsd( + cointimeSupply.vaultedSupply, + "Vaulted", + colors.lime, + ), + ...satsBtcUsd( + cointimeSupply.activeSupply, + "Active", + colors.rose, + ), ], }, diff --git a/website/scripts/options/context.js b/website/scripts/options/context.js index 011bfbf5a..d98b47048 100644 --- a/website/scripts/options/context.js +++ b/website/scripts/options/context.js @@ -1,9 +1,4 @@ import { - line, - dots, - candlestick, - baseline, - histogram, fromBlockCount, fromBitcoin, fromBlockSize, @@ -37,12 +32,6 @@ export function createContext({ colors, brk }) { colors, brk, - // Series helpers - line, - dots, - candlestick, - baseline, - histogram, fromBlockCount: (pattern, title, color) => fromBlockCount(colors, pattern, title, color), fromBitcoin: (pattern, title, color) => diff --git a/website/scripts/options/market/averages.js b/website/scripts/options/market/averages.js index f6029114f..d1b9da692 100644 --- a/website/scripts/options/market/averages.js +++ b/website/scripts/options/market/averages.js @@ -1,6 +1,7 @@ /** Moving averages section */ import { Unit } from "../../utils/units.js"; +import { line, baseline } from "../series.js"; import { priceLines, percentileUsdMap, @@ -55,7 +56,7 @@ export function createPriceWithRatioOptions( ctx, { title, legend, ratio, color }, ) { - const { line, colors, createPriceLine } = ctx; + const { colors, createPriceLine } = ctx; const priceMetric = ratio.price; const pctUsdMap = percentileUsdMap(colors, ratio); @@ -85,42 +86,54 @@ export function createPriceWithRatioOptions( ), ], bottom: [ - line({ metric: ratio.ratio, name: "Ratio", color, unit: Unit.ratio }), + baseline({ + metric: ratio.ratio, + name: "Ratio", + base: 1, + color, + unit: Unit.ratio, + }), line({ metric: ratio.ratio1wSma, name: "1w SMA", color: colors.lime, unit: Unit.ratio, + defaultActive: false, }), line({ metric: ratio.ratio1mSma, name: "1m SMA", color: colors.teal, unit: Unit.ratio, + defaultActive: false, }), line({ metric: ratio.ratio1ySd.sma, name: "1y SMA", color: colors.sky, unit: Unit.ratio, + defaultActive: false, }), line({ metric: ratio.ratio2ySd.sma, name: "2y SMA", color: colors.indigo, unit: Unit.ratio, + defaultActive: false, }), line({ metric: ratio.ratio4ySd.sma, name: "4y SMA", color: colors.purple, unit: Unit.ratio, + defaultActive: false, }), line({ metric: ratio.ratioSd.sma, name: "All SMA", color: colors.rose, unit: Unit.ratio, + defaultActive: false, }), ...pctMap.map(({ name: pctName, prop, color: pctColor }) => line({ @@ -140,13 +153,14 @@ export function createPriceWithRatioOptions( tree: sdPats.map(({ nameAddon, titleAddon, sd }) => ({ name: nameAddon, title: `${title} ${titleAddon} Z-Score`, - top: sdBands(colors, sd).map(({ name: bandName, prop, color: bandColor }) => - line({ - metric: prop, - name: bandName, - color: bandColor, - unit: Unit.usd, - }), + top: sdBands(colors, sd).map( + ({ name: bandName, prop, color: bandColor }) => + line({ + metric: prop, + name: bandName, + color: bandColor, + unit: Unit.usd, + }), ), bottom: [ line({ metric: sd.zscore, name: "Z-Score", color, unit: Unit.sd }), @@ -163,8 +177,7 @@ export function createPriceWithRatioOptions( * @param {ReturnType} averages */ export function createAveragesSection(ctx, averages) { - const { line } = ctx; - + return { name: "Averages", tree: [ diff --git a/website/scripts/options/market/index.js b/website/scripts/options/market/index.js index fc3440552..921fccc6a 100644 --- a/website/scripts/options/market/index.js +++ b/website/scripts/options/market/index.js @@ -2,6 +2,7 @@ import { localhost } from "../../utils/env.js"; import { Unit } from "../../utils/units.js"; +import { line } from "../series.js"; import { buildAverages, createAveragesSection } from "./averages.js"; import { createPerformanceSection } from "./performance.js"; import { createIndicatorsSection } from "./indicators/index.js"; @@ -13,7 +14,7 @@ import { createInvestingSection } from "./investing.js"; * @returns {PartialOptionsGroup} */ export function createMarketSection(ctx) { - const { colors, brk, line } = ctx; + const { colors, brk } = ctx; const { market, supply, price } = brk.metrics; const { movingAverage, diff --git a/website/scripts/options/market/indicators/bands.js b/website/scripts/options/market/indicators/bands.js index 3a52bec57..21eeac599 100644 --- a/website/scripts/options/market/indicators/bands.js +++ b/website/scripts/options/market/indicators/bands.js @@ -1,6 +1,7 @@ /** Bands indicators (MinMax, Mayer Multiple) */ import { Unit } from "../../../utils/units.js"; +import { line } from "../../series.js"; /** * Create Bands section @@ -10,7 +11,7 @@ import { Unit } from "../../../utils/units.js"; * @param {Market["movingAverage"]} args.movingAverage */ export function createBandsSection(ctx, { range, movingAverage }) { - const { line, colors } = ctx; + const { colors } = ctx; return { name: "Bands", diff --git a/website/scripts/options/market/indicators/momentum.js b/website/scripts/options/market/indicators/momentum.js index 87ae6c215..f032fc0df 100644 --- a/website/scripts/options/market/indicators/momentum.js +++ b/website/scripts/options/market/indicators/momentum.js @@ -1,6 +1,7 @@ /** Momentum indicators (RSI, StochRSI, Stochastic, MACD) */ import { Unit } from "../../../utils/units.js"; +import { line, histogram } from "../../series.js"; /** * Create Momentum section @@ -8,7 +9,7 @@ import { Unit } from "../../../utils/units.js"; * @param {Market["indicators"]} indicators */ export function createMomentumSection(ctx, indicators) { - const { line, histogram, colors, createPriceLine } = ctx; + const { colors, createPriceLine } = ctx; return { name: "Momentum", diff --git a/website/scripts/options/market/indicators/onchain.js b/website/scripts/options/market/indicators/onchain.js index b72d00319..52c4c1a5a 100644 --- a/website/scripts/options/market/indicators/onchain.js +++ b/website/scripts/options/market/indicators/onchain.js @@ -1,6 +1,7 @@ /** On-chain indicators (Pi Cycle, Puell, NVT, Gini) */ import { Unit } from "../../../utils/units.js"; +import { line } from "../../series.js"; /** * Create On-chain section @@ -10,7 +11,7 @@ import { Unit } from "../../../utils/units.js"; * @param {Market["movingAverage"]} args.movingAverage */ export function createOnchainSection(ctx, { indicators, movingAverage }) { - const { line, colors, createPriceLine } = ctx; + const { colors, createPriceLine } = ctx; return { name: "On-chain", diff --git a/website/scripts/options/market/indicators/volatility.js b/website/scripts/options/market/indicators/volatility.js index a0d44c522..78ad8a5b7 100644 --- a/website/scripts/options/market/indicators/volatility.js +++ b/website/scripts/options/market/indicators/volatility.js @@ -1,6 +1,7 @@ /** Volatility indicators (Index, True Range, Choppiness, Sharpe, Sortino) */ import { Unit } from "../../../utils/units.js"; +import { line } from "../../series.js"; /** * Create Volatility section @@ -10,7 +11,7 @@ import { Unit } from "../../../utils/units.js"; * @param {Market["range"]} args.range */ export function createVolatilitySection(ctx, { volatility, range }) { - const { line, colors, createPriceLine } = ctx; + const { colors, createPriceLine } = ctx; return { name: "Volatility", diff --git a/website/scripts/options/market/investing.js b/website/scripts/options/market/investing.js index e790ef894..61d671e53 100644 --- a/website/scripts/options/market/investing.js +++ b/website/scripts/options/market/investing.js @@ -1,6 +1,7 @@ /** Investing section (DCA) */ import { Unit } from "../../utils/units.js"; +import { line, baseline } from "../series.js"; import { satsBtcUsd } from "../shared.js"; import { periodIdToName } from "./utils.js"; @@ -41,7 +42,7 @@ export function buildDcaClasses(colors, dca) { * @param {Market["returns"]} args.returns */ export function createInvestingSection(ctx, { dca, lookback, returns }) { - const { line, baseline, colors, createPriceLine } = ctx; + const { colors, createPriceLine } = ctx; const dcaClasses = buildDcaClasses(colors, dca); return { @@ -114,8 +115,8 @@ export function createInvestingSection(ctx, { dca, lookback, returns }) { name: "Stack", title: `${name} DCA vs Lump Sum Stack ($100/day)`, bottom: [ - ...satsBtcUsd(ctx, dcaStack, "DCA", colors.green), - ...satsBtcUsd(ctx, lumpSumStack, "Lump sum", colors.orange), + ...satsBtcUsd( dcaStack, "DCA", colors.green), + ...satsBtcUsd( lumpSumStack, "Lump sum", colors.orange), ], }, ], @@ -164,7 +165,7 @@ export function createInvestingSection(ctx, { dca, lookback, returns }) { title: "DCA Stack by Year ($100/day)", bottom: dcaClasses.flatMap( ({ year, color, defaultActive, stack }) => - satsBtcUsd(ctx, stack, `${year}`, color, { defaultActive }), + satsBtcUsd( stack, `${year}`, color, { defaultActive }), ), }, ], @@ -200,7 +201,7 @@ export function createInvestingSection(ctx, { dca, lookback, returns }) { { name: "Stack", title: `DCA Class ${year} Stack ($100/day)`, - bottom: satsBtcUsd(ctx, stack, "Stack", color), + bottom: satsBtcUsd( stack, "Stack", color), }, ], })), diff --git a/website/scripts/options/market/performance.js b/website/scripts/options/market/performance.js index d6e84ff59..20c045920 100644 --- a/website/scripts/options/market/performance.js +++ b/website/scripts/options/market/performance.js @@ -1,6 +1,7 @@ /** Performance section */ import { Unit } from "../../utils/units.js"; +import { baseline } from "../series.js"; import { periodIdToName } from "./utils.js"; /** @@ -9,7 +10,7 @@ import { periodIdToName } from "./utils.js"; * @param {Market["returns"]} returns */ export function createPerformanceSection(ctx, returns) { - const { colors, baseline, createPriceLine } = ctx; + const { colors, createPriceLine } = ctx; return { name: "Performance", diff --git a/website/scripts/options/series.js b/website/scripts/options/series.js index f7212adf8..54cc8a4c0 100644 --- a/website/scripts/options/series.js +++ b/website/scripts/options/series.js @@ -85,6 +85,8 @@ export function candlestick({ * @param {Unit} args.unit * @param {Color | [Color, Color]} [args.color] * @param {boolean} [args.defaultActive] + * @param {boolean} [args.defaultActive] + * @param {number | undefined} [args.base] * @param {BaselineSeriesPartialOptions} [args.options] * @returns {FetchedBaselineSeriesBlueprint} */ @@ -94,6 +96,7 @@ export function baseline({ color, defaultActive, unit, + base, options, }) { const isTuple = Array.isArray(color); @@ -105,7 +108,12 @@ export function baseline({ colors: isTuple ? color : undefined, unit, defaultActive, - options, + options: { + baseValue: { + price: base, + }, + ...options, + }, }; } diff --git a/website/scripts/options/shared.js b/website/scripts/options/shared.js index 9bd5da545..36a195937 100644 --- a/website/scripts/options/shared.js +++ b/website/scripts/options/shared.js @@ -1,22 +1,22 @@ /** Shared helpers for options */ import { Unit } from "../utils/units.js"; +import { line } from "./series.js"; /** * Create sats/btc/usd line series from a pattern with .sats/.bitcoin/.dollars - * @param {PartialContext} ctx * @param {{ sats: AnyMetricPattern, bitcoin: AnyMetricPattern, dollars: AnyMetricPattern }} pattern * @param {string} name * @param {Color} [color] * @param {{ defaultActive?: boolean }} [options] * @returns {FetchedLineSeriesBlueprint[]} */ -export function satsBtcUsd(ctx, pattern, name, color, options) { +export function satsBtcUsd(pattern, name, color, options) { const { defaultActive } = options || {}; return [ - ctx.line({ metric: pattern.sats, name, color, unit: Unit.sats, defaultActive }), - ctx.line({ metric: pattern.bitcoin, name, color, unit: Unit.btc, defaultActive }), - ctx.line({ metric: pattern.dollars, name, color, unit: Unit.usd, defaultActive }), + line({ metric: pattern.sats, name, color, unit: Unit.sats, defaultActive }), + line({ metric: pattern.bitcoin, name, color, unit: Unit.btc, defaultActive }), + line({ metric: pattern.dollars, name, color, unit: Unit.usd, defaultActive }), ]; } diff --git a/website/scripts/options/types.js b/website/scripts/options/types.js index d7eb0a058..641e1b26f 100644 --- a/website/scripts/options/types.js +++ b/website/scripts/options/types.js @@ -241,11 +241,6 @@ * @typedef {Object} PartialContext * @property {Colors} colors * @property {BrkClient} brk - * @property {LineSeriesFn} line - * @property {DotsSeriesFn} dots - * @property {CandlestickSeriesFn} candlestick - * @property {BaselineSeriesFn} baseline - * @property {HistogramSeriesFn} histogram * @property {(pattern: BlockCountPattern, title: string, color?: Color) => AnyFetchedSeriesBlueprint[]} fromBlockCount * @property {(pattern: FullnessPattern, title: string, color?: Color) => AnyFetchedSeriesBlueprint[]} fromBitcoin * @property {(pattern: AnyStatsPattern, title: string, color?: Color) => AnyFetchedSeriesBlueprint[]} fromBlockSize diff --git a/website/scripts/utils/theme.js b/website/scripts/utils/theme.js new file mode 100644 index 000000000..ea28a60bb --- /dev/null +++ b/website/scripts/utils/theme.js @@ -0,0 +1,36 @@ +import signals from "../signals.js"; +import { readStored, removeStored, writeToStorage } from "./storage.js"; + +const preferredColorSchemeMatchMedia = window.matchMedia( + "(prefers-color-scheme: dark)", +); +const stored = readStored("theme"); +const initial = stored ? stored === "dark" : preferredColorSchemeMatchMedia.matches; + +export const dark = signals.createSignal(initial); + +/** @param {boolean} isDark */ +function apply(isDark) { + document.documentElement.style.colorScheme = isDark ? "dark" : "light"; +} +apply(initial); + +preferredColorSchemeMatchMedia.addEventListener("change", ({ matches }) => { + if (!readStored("theme")) { + dark.set(matches); + apply(matches); + } +}); + +function invert() { + const newValue = !dark(); + dark.set(newValue); + apply(newValue); + if (newValue === preferredColorSchemeMatchMedia.matches) { + removeStored("theme"); + } else { + writeToStorage("theme", newValue ? "dark" : "light"); + } +} + +document.getElementById("invert-button")?.addEventListener("click", invert);