global: snap

This commit is contained in:
nym21
2026-04-16 22:17:41 +02:00
parent 78d6d9d6f1
commit d340855c8b
42 changed files with 850 additions and 493 deletions

View File

@@ -86,7 +86,7 @@
* Profitability bucket pattern (supply + realized_cap + unrealized_pnl + nupl)
* @typedef {Brk.NuplRealizedSupplyUnrealizedPattern} RealizedSupplyPattern
*
* Realized pattern (full: cap + gross + investor + loss + mvrv + net + peak + price + profit + sell + sopr)
* Realized pattern (full: cap + gross + capitalized + loss + mvrv + net + peak + price + profit + sell + sopr)
* @typedef {Brk.CapGrossInvestorLossMvrvNetPeakPriceProfitSellSoprPattern} RealizedPattern
*
* Transfer volume pattern (block + cumulative + inProfit/inLoss + sum windows)
@@ -256,9 +256,9 @@
* @typedef {Brk.AbsoluteRatePattern} DeltaPattern
* @typedef {Brk.AbsoluteRatePattern2} FiatDeltaPattern
*
* Investor price percentiles (pct1/2/5/95/98/99)
* @typedef {Brk.Pct0Pct1Pct2Pct5Pct95Pct98Pct99Pattern} InvestorPercentilesPattern
* @typedef {Brk.BpsPriceRatioPattern} InvestorPercentileEntry
* Capitalized price percentiles (pct1/2/5/95/98/99)
* @typedef {Brk.Pct0Pct1Pct2Pct5Pct95Pct98Pct99Pattern} CapitalizedPercentilesPattern
* @typedef {Brk.BpsPriceRatioPattern} CapitalizedPercentileEntry
*
* Generic tree node type for walking
* @typedef {AnySeriesPattern | Record<string, unknown>} TreeNode

View File

@@ -117,12 +117,13 @@ function appendNewerBlocks(blocks) {
for (let i = blocks.length - 1; i >= 0; i--) {
const b = blocks[i];
if (b.height > newestHeight) {
blocksEl.append(createBlockCube(b));
appendCube(createBlockCube(b));
} else {
blocksByHash.set(b.id, b);
}
}
newestHeight = Math.max(newestHeight, blocks[0].height);
if (anchor && anchorRect) {
const r = anchor.getBoundingClientRect();
chainEl.scrollTop += r.top - anchorRect.top;
@@ -139,11 +140,12 @@ async function loadInitial(height) {
: await brk.getBlocksV1();
clear();
for (const b of blocks) blocksEl.prepend(createBlockCube(b));
for (const b of blocks) prependCube(createBlockCube(b));
newestHeight = blocks[0].height;
oldestHeight = blocks[blocks.length - 1].height;
reachedTip = height == null;
observeOldestEdge();
if (!reachedTip) await loadNewer();
return blocks[0].id;
}
@@ -197,11 +199,12 @@ async function loadOlder() {
loadingOlder = true;
try {
const blocks = await brk.getBlocksV1FromHeight(oldestHeight - 1);
for (const block of blocks) blocksEl.prepend(createBlockCube(block));
for (const block of blocks) prependCube(createBlockCube(block));
if (blocks.length) {
oldestHeight = blocks[blocks.length - 1].height;
observeOldestEdge();
}
} catch (e) {
console.error("explorer loadOlder:", e);
}
@@ -227,6 +230,8 @@ function createBlockCube(block) {
cubeElement.dataset.hash = block.id;
cubeElement.dataset.height = String(block.height);
cubeElement.dataset.timestamp = String(block.timestamp);
cubeElement.style.setProperty("--fill", String(Math.min(1, block.weight / 3_990_000)));
blocksByHash.set(block.id, block);
cubeElement.addEventListener("click", () => onCubeClick(cubeElement));
@@ -268,6 +273,9 @@ function createBlockCube(block) {
function createCube() {
const cubeElement = document.createElement("div");
cubeElement.classList.add("cube");
const innerTopElement = document.createElement("div");
innerTopElement.classList.add("face", "inner-top");
cubeElement.append(innerTopElement);
const rightFaceElement = document.createElement("div");
rightFaceElement.classList.add("face", "right");
cubeElement.append(rightFaceElement);
@@ -279,3 +287,25 @@ function createCube() {
cubeElement.append(topFaceElement);
return { cubeElement, leftFaceElement, rightFaceElement, topFaceElement };
}
/** @param {HTMLElement} cube */
function setGap(cube) {
const prev = /** @type {HTMLElement | null} */ (cube.previousElementSibling);
if (!prev) return;
const dt = Math.max(0, Number(cube.dataset.timestamp) - Number(prev.dataset.timestamp));
cube.style.setProperty("--dt", String(dt));
}
/** @param {HTMLDivElement} cube */
function prependCube(cube) {
const next = /** @type {HTMLElement | null} */ (blocksEl.firstElementChild);
blocksEl.prepend(cube);
if (next) setGap(next);
}
/** @param {HTMLDivElement} cube */
function appendCube(cube) {
blocksEl.append(cube);
setGap(cube);
}

View File

@@ -178,9 +178,9 @@ export function createCointimeSection() {
color: colors.realized,
}),
price({
series: all.realized.investor.price,
name: "Investor",
color: colors.investor,
series: all.realized.capitalized.price,
name: "Capitalized",
color: colors.capitalized,
}),
...prices.map(({ pattern, name, color, defaultActive }) =>
price({ series: pattern, name, color, defaultActive }),

View File

@@ -42,7 +42,7 @@ function percentileSeries(p, n = (x) => x) {
/**
* Per Coin or Per Dollar folder for a single cohort
* @param {Object} args
* @param {AnyPricePattern} args.avgPrice - realized price (per coin) or investor price (per dollar)
* @param {AnyPricePattern} args.avgPrice - realized price (per coin) or capitalized price (per dollar)
* @param {string} args.avgName
* @param {AnyPricePattern} args.inProfit
* @param {AnyPricePattern} args.inLoss
@@ -100,7 +100,7 @@ export function createCostBasisSectionWithPercentiles({ cohort, title }) {
{
name: "Per Dollar",
tree: singleWeightFolder({
avgPrice: tree.realized.investor.price, avgName: "All",
avgPrice: tree.realized.capitalized.price, avgName: "All",
inProfit: cb.inProfit.perDollar, inLoss: cb.inLoss.perDollar,
percentiles: cb.perDollar, color, weightLabel: "USD-weighted", title,
}),
@@ -192,7 +192,7 @@ export function createGroupedCostBasisSectionWithPercentiles({ list, all, title
name: "Per Dollar",
tree: groupedWeightFolder({
list, all, title,
getAvgPrice: (c) => c.tree.realized.investor.price,
getAvgPrice: (c) => c.tree.realized.capitalized.price,
getInProfit: (c) => c.tree.costBasis.inProfit.perDollar,
getInLoss: (c) => c.tree.costBasis.inLoss.perDollar,
getPercentiles: (c) => c.tree.costBasis.perDollar,

View File

@@ -4,11 +4,11 @@
* Structure (single cohort):
* - Compare: Both prices on one chart
* - Realized: Price + Ratio (MVRV) + Z-Scores (for full cohorts)
* - Investor: Price + Ratio + Z-Scores (for full cohorts)
* - Capitalized: Price + Ratio + Z-Scores (for full cohorts)
*
* Structure (grouped cohorts):
* - Realized: Price + Ratio comparison across cohorts
* - Investor: Price + Ratio comparison across cohorts
* - Capitalized: Price + Ratio comparison across cohorts
*
* For cohorts WITHOUT full ratio patterns: basic Price/Ratio charts only (no Z-Scores)
*/
@@ -34,7 +34,7 @@ export function createPricesSectionFull({ cohort, title }) {
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 }),
price({ series: tree.realized.capitalized.price, name: "Capitalized", color: colors.capitalized }),
],
},
{
@@ -50,12 +50,12 @@ export function createPricesSectionFull({ cohort, title }) {
}),
},
{
name: "Investor",
name: "Capitalized",
tree: priceRatioPercentilesTree({
pattern: tree.realized.investor.price,
title: title("Investor Price"),
ratioTitle: title("Investor Price Ratio"),
legend: "Investor",
pattern: tree.realized.capitalized.price,
title: title("Capitalized Price"),
ratioTitle: title("Capitalized Price Ratio"),
legend: "Capitalized",
color,
}),
},
@@ -150,20 +150,20 @@ export function createGroupedPricesSectionFull({ list, all, title }) {
tree: [
...groupedRealizedPriceItems(list, all, title),
{
name: "Investor",
name: "Capitalized",
tree: [
{
name: "Price",
title: title("Investor Price"),
title: title("Capitalized Price"),
top: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
price({ series: tree.realized.investor.price, name, color }),
price({ series: tree.realized.capitalized.price, name, color }),
),
},
{
name: "Ratio",
title: title("Investor Price Ratio"),
title: title("Capitalized Price Ratio"),
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
baseline({ series: tree.realized.investor.price.ratio, name, color, unit: Unit.ratio, base: 1 }),
baseline({ series: tree.realized.capitalized.price.ratio, name, color, unit: Unit.ratio, base: 1 }),
),
},
],

View File

@@ -4,6 +4,7 @@ import {
searchResultsElement,
} from "../utils/elements.js";
import { QuickMatch } from "../modules/quickmatch-js/0.4.1/src/index.js";
import { brk } from "../utils/client.js";
/**
* @param {Options} options
@@ -28,9 +29,67 @@ export function init(options) {
if (li) li.dataset.highlight = "";
}
const HEX64_RE = /^[0-9a-f]{64}$/i;
const ADDR_RE = /^([13][a-km-zA-HJ-NP-Z1-9]{25,34}|bc1[a-z0-9]{8,87})$/;
/** @param {string} label @param {string} href @param {Element | null} [before] */
function createResultLink(label, href, before) {
const li = window.document.createElement("li");
const a = window.document.createElement("a");
a.href = href;
a.textContent = label;
a.title = label;
if (href === window.location.pathname) setHighlight(li);
a.addEventListener("click", (e) => {
e.preventDefault();
setHighlight(li);
history.pushState(null, "", href);
options.resolveUrl();
});
li.append(a);
searchResultsElement.insertBefore(li, before ?? null);
}
/** @type {AbortController | undefined} */
let lookupController;
/** @param {string} needle @param {AbortSignal} signal */
async function lookup(needle, signal) {
/** @type {Array<[string, string]>} */
const results = [];
if (HEX64_RE.test(needle)) {
const [blockRes, txRes] = await Promise.allSettled([
brk.getBlock(needle, { signal }),
brk.getTx(needle, { signal }),
]);
if (signal.aborted) return;
if (blockRes.status === "fulfilled") results.push(["Block", `/block/${needle}`]);
if (txRes.status === "fulfilled") results.push(["Transaction", `/tx/${needle}`]);
} else if (ADDR_RE.test(needle)) {
try {
const { isvalid } = await brk.validateAddress(needle, { signal });
if (signal.aborted || !isvalid) return;
results.push(["Address", `/address/${needle}`]);
} catch { return; }
} else {
return;
}
const before = searchResultsElement.firstElementChild;
for (const [label, href] of results) {
createResultLink(`${label} ${needle}`, href, before);
}
// Remove "No results" placeholder if present
const last = searchResultsElement.lastElementChild;
if (last && !last.querySelector("a")) last.remove();
}
function inputEvent() {
const needle = /** @type {string} */ (searchInput.value).trim();
if (lookupController) lookupController.abort();
searchResultsElement.scrollTo({ top: 0 });
searchResultsElement.innerHTML = "";
setHighlight();
@@ -54,23 +113,13 @@ export function init(options) {
["Transaction", `/tx/${num}`],
];
for (const [label, href] of entries) {
const li = window.document.createElement("li");
const a = window.document.createElement("a");
a.href = href;
a.textContent = `${label} #${num}`;
a.title = `${label} #${num}`;
if (href === window.location.pathname) setHighlight(li);
a.addEventListener("click", (e) => {
e.preventDefault();
setHighlight(li);
history.pushState(null, "", href);
options.resolveUrl();
});
li.append(a);
searchResultsElement.appendChild(li);
createResultLink(`${label} #${num}`, href);
}
}
lookupController = new AbortController();
lookup(needle, lookupController.signal);
if (matches.length) {
matches.forEach((title) => {
const option = titleToOption.get(title);

View File

@@ -162,6 +162,7 @@ export const colors = {
// Valuations
realized: palette.orange,
investor: palette.fuchsia,
capitalized: palette.green,
thermo: palette.emerald,
trueMarketMean: palette.blue,
vocdd: palette.purple,