mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-04-25 23:29:58 -07:00
global: snap
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 }),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 }),
|
||||
),
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user