global: snapshot

This commit is contained in:
nym21
2026-01-14 20:09:51 +01:00
parent d75c2a881b
commit 1c7434ff83
25 changed files with 4059 additions and 22606 deletions
+4 -1
View File
@@ -653,7 +653,10 @@ export function createChartElement({
({ count, active }) => {
showLine = count > 500;
candlestickISeries.applyOptions({ visible: active && !showLine });
lineISeries.applyOptions({ visible: active && showLine });
lineISeries.applyOptions({
visible: active && showLine,
priceLineVisible: active && showLine,
});
},
);
},
+1 -1
View File
@@ -52,7 +52,7 @@
* @typedef {Brk.AnyMetricEndpointBuilder} AnyMetricEndpoint
* @typedef {Brk.AnyMetricData} AnyMetricData
* @typedef {Brk.AddrCountPattern} AddrCountPattern
* @typedef {Brk.MetricsTree_Blocks_Interval} IntervalPattern
* @typedef {FullnessPattern<any>} IntervalPattern
* @typedef {Brk.MetricsTree_Supply_Circulating} SupplyPattern
* @typedef {Brk.RelativePattern} GlobalRelativePattern
* @typedef {Brk.RelativePattern2} OwnRelativePattern
+5
View File
@@ -545,6 +545,11 @@ signals.createRoot(() => {
function initShare() {
const shareDiv = getElementById("share-div");
const shareContentDiv = getElementById("share-content-div");
const shareButton = getElementById("share-button");
shareButton.addEventListener("click", () => {
qrcode.set(window.location.href);
});
shareDiv.addEventListener("click", () => {
qrcode.set(null);
+12
View File
@@ -12,6 +12,7 @@ export function createChainSection(ctx) {
colors,
brk,
line,
baseline,
dots,
createPriceLine,
fromSizePattern,
@@ -638,6 +639,17 @@ export function createChainSection(ctx) {
}),
],
},
{
name: "Adjustment",
title: "Difficulty Adjustment",
bottom: [
baseline({
metric: blocks.difficulty.adjustment,
name: "Difficulty Change",
unit: Unit.percentage,
}),
],
},
{
name: "Hash Price",
title: "Hash Price",
+15
View File
@@ -10,6 +10,7 @@ import {
ltAmountColors,
amountRangeColors,
spendableTypeColors,
yearColors,
} from "../colors/index.js";
/**
@@ -40,6 +41,7 @@ export function buildCohortData(colors, brk) {
LT_AMOUNT_NAMES,
AMOUNT_RANGE_NAMES,
SPENDABLE_TYPE_NAMES,
YEAR_NAMES,
} = brk;
// Base cohort representing "all" - CohortAll (adjustedSopr + percentiles but no RelToMarketCap)
@@ -210,6 +212,18 @@ export function buildCohortData(colors, brk) {
};
});
// Year cohorts - CohortBasic (neither adjustedSopr nor percentiles)
/** @type {readonly CohortBasic[]} */
const year = entries(utxoCohorts.year).map(([key, tree]) => {
const names = YEAR_NAMES[key];
return {
name: names.short,
title: names.long,
color: colors[yearColors[key]],
tree,
};
});
return {
cohortAll,
termShort,
@@ -225,5 +239,6 @@ export function buildCohortData(colors, brk) {
utxosAmountRanges,
addressesAmountRanges,
type,
year,
};
}
+96 -22
View File
@@ -139,28 +139,102 @@ function createCointimePriceWithRatioOptions(
},
{
name: "ZScores",
tree: sdPatterns.map(({ nameAddon, titleAddon, sd }) => ({
name: nameAddon,
title: `${title} ${titleAddon} Z-Score`,
top: getSdBands(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 }),
createPriceLine({ unit: Unit.sd, number: 3 }),
createPriceLine({ unit: Unit.sd, number: 2 }),
createPriceLine({ unit: Unit.sd, number: 1 }),
createPriceLine({ unit: Unit.sd, number: 0 }),
createPriceLine({ unit: Unit.sd, number: -1 }),
createPriceLine({ unit: Unit.sd, number: -2 }),
createPriceLine({ unit: Unit.sd, number: -3 }),
],
})),
tree: [
// Compare all Z-Scores
{
name: "Compare",
title: `Compare ${title} Z-Scores`,
top: [
line({ metric: price, name: legend, color, unit: Unit.usd }),
line({
metric: ratio.ratio1ySd._0sdUsd,
name: "1y 0sd",
color: colors.fuchsia,
defaultActive: false,
unit: Unit.usd,
}),
line({
metric: ratio.ratio2ySd._0sdUsd,
name: "2y 0sd",
color: colors.purple,
defaultActive: false,
unit: Unit.usd,
}),
line({
metric: ratio.ratio4ySd._0sdUsd,
name: "4y 0sd",
color: colors.violet,
defaultActive: false,
unit: Unit.usd,
}),
line({
metric: ratio.ratioSd._0sdUsd,
name: "0sd",
color: colors.indigo,
defaultActive: false,
unit: Unit.usd,
}),
],
bottom: [
line({
metric: ratio.ratioSd.zscore,
name: "All",
color: colors.default,
unit: Unit.sd,
}),
line({
metric: ratio.ratio4ySd.zscore,
name: "4y",
color: colors.lime,
unit: Unit.sd,
}),
line({
metric: ratio.ratio2ySd.zscore,
name: "2y",
color: colors.avocado,
unit: Unit.sd,
}),
line({
metric: ratio.ratio1ySd.zscore,
name: "1y",
color: colors.yellow,
unit: Unit.sd,
}),
createPriceLine({ unit: Unit.sd, number: 4 }),
createPriceLine({ unit: Unit.sd, number: 3 }),
createPriceLine({ unit: Unit.sd, number: 2 }),
createPriceLine({ unit: Unit.sd, number: 1 }),
createPriceLine({ unit: Unit.sd, number: 0 }),
createPriceLine({ unit: Unit.sd, number: -1 }),
createPriceLine({ unit: Unit.sd, number: -2 }),
createPriceLine({ unit: Unit.sd, number: -3 }),
createPriceLine({ unit: Unit.sd, number: -4 }),
],
},
// Individual Z-Score charts
...sdPatterns.map(({ nameAddon, titleAddon, sd }) => ({
name: nameAddon,
title: `${title} ${titleAddon} Z-Score`,
top: getSdBands(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 }),
createPriceLine({ unit: Unit.sd, number: 3 }),
createPriceLine({ unit: Unit.sd, number: 2 }),
createPriceLine({ unit: Unit.sd, number: 1 }),
createPriceLine({ unit: Unit.sd, number: 0 }),
createPriceLine({ unit: Unit.sd, number: -1 }),
createPriceLine({ unit: Unit.sd, number: -2 }),
createPriceLine({ unit: Unit.sd, number: -3 }),
],
})),
],
},
];
}
+22
View File
@@ -150,3 +150,25 @@ export const spendableTypeColors = {
unknown: "violet",
empty: "fuchsia",
};
/** @type {Readonly<Record<string, ColorName>>} */
export const yearColors = {
_2009: "red",
_2010: "orange",
_2011: "amber",
_2012: "yellow",
_2013: "lime",
_2014: "green",
_2015: "teal",
_2016: "cyan",
_2017: "sky",
_2018: "blue",
_2019: "indigo",
_2020: "violet",
_2021: "purple",
_2022: "fuchsia",
_2023: "pink",
_2024: "rose",
_2025: "red",
_2026: "orange",
};
+1
View File
@@ -9,6 +9,7 @@ export {
ltAmountColors,
amountRangeColors,
spendableTypeColors,
yearColors,
} from "./cohorts.js";
export { averageColors, dcaColors } from "./misc.js";
+187 -242
View File
@@ -1,6 +1,5 @@
/** Partial options - Main entry point */
import { localhost } from "../utils/env.js";
import { createContext } from "./context.js";
import {
buildCohortData,
@@ -45,6 +44,7 @@ export function createPartialOptions({ colors, brk }) {
utxosAmountRanges,
addressesAmountRanges,
type,
year,
} = buildCohortData(colors, brk);
// Helpers to map cohorts by capability type
@@ -58,16 +58,16 @@ export function createPartialOptions({ colors, brk }) {
const mapAddressCohorts = (cohort) => createAddressCohortFolder(ctx, cohort);
return [
// Debug explorer (localhost only)
...(localhost
? [
{
kind: /** @type {const} */ ("explorer"),
name: "Explorer",
title: "Debug explorer",
},
]
: []),
// Debug explorer (disabled)
// ...(localhost
// ? [
// {
// kind: /** @type {const} */ ("explorer"),
// name: "Explorer",
// title: "Debug explorer",
// },
// ]
// : []),
// Charts section
{
@@ -88,7 +88,7 @@ export function createPartialOptions({ colors, brk }) {
// Terms (STH/LTH) - Short is Full, Long is WithPercentiles
{
name: "terms",
name: "Terms",
tree: [
// Individual cohorts with their specific capabilities
createCohortFolderFull(ctx, termShort),
@@ -96,7 +96,149 @@ export function createPartialOptions({ colors, brk }) {
],
},
// Epochs - CohortBasic (neither adjustedSopr nor percentiles)
// Types - CohortBasic
{
name: "Types",
tree: [
createCohortFolderBasic(ctx, {
name: "Compare",
title: "Type",
list: type,
}),
...type.map(mapBasic),
],
},
// Age cohorts
{
name: "Age",
tree: [
// Up To (< X old)
{
name: "Up To",
tree: [
createCohortFolderWithAdjusted(ctx, {
name: "Compare",
title: "Age Up To",
list: upToDate,
}),
...upToDate.map(mapWithAdjusted),
],
},
// At Least (≥ X old)
{
name: "At Least",
tree: [
createCohortFolderBasic(ctx, {
name: "Compare",
title: "Age At Least",
list: fromDate,
}),
...fromDate.map(mapBasic),
],
},
// Range
{
name: "Range",
tree: [
createCohortFolderWithPercentiles(ctx, {
name: "Compare",
title: "Age Range",
list: dateRange,
}),
...dateRange.map(mapWithPercentiles),
],
},
],
},
// Amount cohorts (UTXO size)
{
name: "Amount",
tree: [
// Under (< X sats)
{
name: "Under",
tree: [
createCohortFolderBasic(ctx, {
name: "Compare",
title: "Amount Under",
list: utxosUnderAmount,
}),
...utxosUnderAmount.map(mapBasic),
],
},
// Above (≥ X sats)
{
name: "Above",
tree: [
createCohortFolderBasic(ctx, {
name: "Compare",
title: "Amount Above",
list: utxosAboveAmount,
}),
...utxosAboveAmount.map(mapBasic),
],
},
// Range
{
name: "Range",
tree: [
createCohortFolderBasic(ctx, {
name: "Compare",
title: "Amount Range",
list: utxosAmountRanges,
}),
...utxosAmountRanges.map(mapBasic),
],
},
],
},
// Balance cohorts (Address balance)
{
name: "Balance",
tree: [
// Under (< X sats)
{
name: "Under",
tree: [
createAddressCohortFolder(ctx, {
name: "Compare",
title: "Balance Under",
list: addressesUnderAmount,
}),
...addressesUnderAmount.map(mapAddressCohorts),
],
},
// Above (≥ X sats)
{
name: "Above",
tree: [
createAddressCohortFolder(ctx, {
name: "Compare",
title: "Balance Above",
list: addressesAboveAmount,
}),
...addressesAboveAmount.map(mapAddressCohorts),
],
},
// Range
{
name: "Range",
tree: [
createAddressCohortFolder(ctx, {
name: "Compare",
title: "Balance Range",
list: addressesAmountRanges,
}),
...addressesAmountRanges.map(mapAddressCohorts),
],
},
],
},
// Epochs - CohortBasic
{
name: "Epochs",
tree: [
@@ -109,133 +251,16 @@ export function createPartialOptions({ colors, brk }) {
],
},
// Types - CohortBasic
// Years - CohortBasic
{
name: "types",
name: "Years",
tree: [
createCohortFolderBasic(ctx, {
name: "Compare",
title: "Type",
list: type,
title: "Year",
list: year,
}),
...type.map(mapBasic),
],
},
// UTXOs Up to age - CohortWithAdjusted (adjustedSopr only)
{
name: "UTXOs Up to age",
tree: [
createCohortFolderWithAdjusted(ctx, {
name: "Compare",
title: "UTXOs Up To Age",
list: upToDate,
}),
...upToDate.map(mapWithAdjusted),
],
},
// UTXOs from age - CohortBasic
{
name: "UTXOs from age",
tree: [
createCohortFolderBasic(ctx, {
name: "Compare",
title: "UTXOs from age",
list: fromDate,
}),
...fromDate.map(mapBasic),
],
},
// UTXOs age ranges - CohortWithPercentiles (percentiles only)
{
name: "UTXOs age Ranges",
tree: [
createCohortFolderWithPercentiles(ctx, {
name: "Compare",
title: "UTXOs Age Range",
list: dateRange,
}),
...dateRange.map(mapWithPercentiles),
],
},
// UTXOs under amounts - CohortBasic
{
name: "UTXOs under amounts",
tree: [
createCohortFolderBasic(ctx, {
name: "Compare",
title: "UTXOs under amount",
list: utxosUnderAmount,
}),
...utxosUnderAmount.map(mapBasic),
],
},
// UTXOs above amounts - CohortBasic
{
name: "UTXOs Above Amounts",
tree: [
createCohortFolderBasic(ctx, {
name: "Compare",
title: "UTXOs Above Amount",
list: utxosAboveAmount,
}),
...utxosAboveAmount.map(mapBasic),
],
},
// UTXOs between amounts - CohortBasic
{
name: "UTXOs between amounts",
tree: [
createCohortFolderBasic(ctx, {
name: "Compare",
title: "UTXOs between amounts",
list: utxosAmountRanges,
}),
...utxosAmountRanges.map(mapBasic),
],
},
// Addresses under amount (TYPE SAFE - uses createAddressCohortFolder!)
{
name: "Addresses under amount",
tree: [
createAddressCohortFolder(ctx, {
name: "Compare",
title: "Addresses under Amount",
list: addressesUnderAmount,
}),
...addressesUnderAmount.map(mapAddressCohorts),
],
},
// Addresses above amount (TYPE SAFE - uses createAddressCohortFolder!)
{
name: "Addresses above amount",
tree: [
createAddressCohortFolder(ctx, {
name: "Compare",
title: "Addresses above amount",
list: addressesAboveAmount,
}),
...addressesAboveAmount.map(mapAddressCohorts),
],
},
// Addresses between amounts (TYPE SAFE - uses createAddressCohortFolder!)
{
name: "Addresses between amounts",
tree: [
createAddressCohortFolder(ctx, {
name: "Compare",
title: "Addresses between amounts",
list: addressesAmountRanges,
}),
...addressesAmountRanges.map(mapAddressCohorts),
...year.map(mapBasic),
],
},
],
@@ -246,117 +271,37 @@ export function createPartialOptions({ colors, brk }) {
],
},
// Table section
// Table section (disabled)
// {
// kind: /** @type {const} */ ("table"),
// title: "Table",
// name: "Table",
// },
// Simulations section (disabled)
// {
// name: "Simulations",
// tree: [
// {
// kind: /** @type {const} */ ("simulation"),
// name: "Save In Bitcoin",
// title: "Save In Bitcoin",
// },
// ],
// },
// API documentation
{
kind: /** @type {const} */ ("table"),
title: "Table",
name: "Table",
name: "API",
url: () => "/api",
title: "API documentation",
},
// Simulations section
// Project link
{
name: "Simulations",
tree: [
{
kind: /** @type {const} */ ("simulation"),
name: "Save In Bitcoin",
title: "Save In Bitcoin",
},
],
},
// Tools section
{
name: "Tools",
tree: [
{
name: "Documentation",
tree: [
{
name: "API",
url: () => "/api",
title: "API documentation",
},
{
name: "MCP",
url: () =>
"https://github.com/bitcoinresearchkit/brk/blob/main/crates/brk_mcp/README.md#brk_mcp",
title: "Model Context Protocol documentation",
},
{
name: "Crate",
url: () => "/crate",
title: "View on crates.io",
},
{
name: "Source",
url: () => "/github",
title: "Source code and issues",
},
{
name: "Changelog",
url: () => "/changelog",
title: "Release notes and changelog",
},
],
},
{
name: "Hosting",
tree: [
{
name: "Status",
url: () => "/status",
title: "Service status and uptime",
},
{
name: "Self-host",
url: () => "/install",
title: "Install and run yourself",
},
{
name: "Service",
url: () => "/service",
title: "Hosted service offering",
},
],
},
{
name: "Community",
tree: [
{
name: "Discord",
url: () => "/discord",
title: "Join the Discord server",
},
{
name: "GitHub",
url: () => "/github",
title: "Source code and issues",
},
{
name: "Nostr",
url: () => "/nostr",
title: "Follow on Nostr",
},
],
},
],
},
// Donate
{
name: "Donate",
qrcode: true,
url: () => "bitcoin:bc1q098zsm89m7kgyze338vfejhpdt92ua9p3peuve",
title: "Bitcoin address for donations",
},
// Share
{
name: "Share",
qrcode: true,
url: () => window.location.href,
title: "Share",
name: "Source",
url: () => "https://bitcoinresearchkit.org",
title: "Bitcoin Research Kit",
},
];
}
+18 -17
View File
@@ -11,6 +11,7 @@ import { Unit } from "../../utils/units.js";
import signals from "../../signals.js";
import { createChartElement } from "../../chart/index.js";
import { webSockets } from "../../utils/ws.js";
import { screenshot } from "./screenshot.js";
const keyPrefix = "chart";
const ONE_BTC_IN_SATS = 100_000_000;
@@ -82,21 +83,20 @@ export function init({ colors, option, brk }) {
});
if (!(ios && !canShare)) {
const chartBottomRightCanvas = Array.from(
chart.inner.chartElement().getElementsByTagName("tr"),
).at(-1)?.lastChild?.firstChild?.firstChild;
if (chartBottomRightCanvas) {
const domain = window.document.createElement("p");
domain.innerText = `${window.location.host}`;
domain.id = "domain";
const screenshotButton = window.document.createElement("button");
screenshotButton.id = "screenshot";
const camera = "[ ◉¯]";
screenshotButton.innerHTML = camera;
screenshotButton.title = "Screenshot";
chartBottomRightCanvas.replaceWith(screenshotButton);
screenshotButton.addEventListener("click", () => {
import("./screenshot").then(async ({ screenshot }) => {
const domain = window.document.createElement("p");
domain.innerText = `${window.location.host}`;
domain.id = "domain";
chart.addFieldsetIfNeeded({
id: "capture",
paneIndex: 0,
position: "ne",
createChild() {
const button = window.document.createElement("button");
button.id = "capture";
button.innerText = "capture";
button.title = "Capture chart as image";
button.addEventListener("click", async () => {
chartElement.dataset.screenshot = "true";
chartElement.append(domain);
try {
@@ -109,8 +109,9 @@ export function init({ colors, option, brk }) {
chartElement.removeChild(domain);
chartElement.dataset.screenshot = "false";
});
});
}
return button;
},
});
}
chart.inner.timeScale().subscribeVisibleLogicalRangeChange(