mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-06-21 03:58:24 -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,
|
||||
|
||||
@@ -28,6 +28,10 @@ nav {
|
||||
padding: 0.25rem 0;
|
||||
}
|
||||
|
||||
a:visited {
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
ul {
|
||||
color: var(--off-color);
|
||||
overflow: hidden;
|
||||
|
||||
@@ -40,13 +40,16 @@
|
||||
.blocks {
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
--gap: 0.8;
|
||||
gap: calc(var(--cube) * var(--gap));
|
||||
--min-gap: 0rem;
|
||||
--max-gap: calc(var(--cube) * 6);
|
||||
--min-dt: 0;
|
||||
--max-dt: 10800;
|
||||
margin-right: var(--cube);
|
||||
margin-top: calc(var(--cube) * -0.25);
|
||||
|
||||
@media (max-width: 767px) {
|
||||
--gap: 1.25;
|
||||
--min-gap: 0rem;
|
||||
--max-gap: calc(var(--cube) * 1.5);
|
||||
flex-direction: row-reverse;
|
||||
height: 11.5rem;
|
||||
width: max-content;
|
||||
@@ -58,7 +61,32 @@
|
||||
}
|
||||
|
||||
.cube {
|
||||
margin-top: -0.375rem;
|
||||
--t: pow(
|
||||
clamp(
|
||||
0,
|
||||
(var(--dt, 600) - var(--min-dt)) / (var(--max-dt) - var(--min-dt)),
|
||||
1
|
||||
),
|
||||
0.7
|
||||
);
|
||||
--block-gap: calc(
|
||||
var(--min-gap) + var(--t) * (var(--max-gap) - var(--min-gap))
|
||||
);
|
||||
--empty-alpha: 0.5;
|
||||
--face-step: 0.033;
|
||||
--face-right-color: light-dark(
|
||||
oklch(from var(--face-color) calc(l - var(--face-step) * 2) c h),
|
||||
var(--face-color)
|
||||
);
|
||||
--face-left-color: light-dark(
|
||||
oklch(from var(--face-color) calc(l - var(--face-step)) c h),
|
||||
oklch(from var(--face-color) calc(l + var(--face-step)) c h)
|
||||
);
|
||||
--face-top-color: light-dark(
|
||||
var(--face-color),
|
||||
oklch(from var(--face-color) calc(l + var(--face-step) * 2) c h)
|
||||
);
|
||||
/*margin-top: -0.375rem;*/
|
||||
margin-left: calc(var(--cube) * -0.25);
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
@@ -116,17 +144,40 @@
|
||||
width: var(--cube);
|
||||
height: var(--cube);
|
||||
padding: 0.1rem;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.inner-top {
|
||||
backdrop-filter: none;
|
||||
background-color: var(--face-top-color);
|
||||
/*-webkit-mask-image: linear-gradient(transparent, black 0.5rem, black calc(100% - 0.5rem), transparent);
|
||||
mask-image: linear-gradient(transparent, black 0.5rem, black calc(100% - 0.5rem), transparent);*/
|
||||
transform: rotate(30deg) skew(-30deg)
|
||||
translate(
|
||||
calc(var(--cube) * (1.99 - var(--fill, 1))),
|
||||
calc(var(--cube) * (0.599 - 0.864 * var(--fill, 1)))
|
||||
)
|
||||
scaleY(0.864);
|
||||
}
|
||||
|
||||
.right {
|
||||
background-color: oklch(from var(--face-color) calc(l - 0.05) c h);
|
||||
background: linear-gradient(
|
||||
to top,
|
||||
var(--face-right-color) calc(var(--fill, 1) * 100%),
|
||||
oklch(from var(--face-right-color) l c h / var(--empty-alpha))
|
||||
calc(var(--fill, 1) * 100%)
|
||||
);
|
||||
transform: rotate(-30deg) skewX(-30deg)
|
||||
translate(calc(var(--cube) * 1.3), calc(var(--cube) * 1.725))
|
||||
scaleY(0.864);
|
||||
}
|
||||
|
||||
.top {
|
||||
background-color: oklch(from var(--face-color) calc(l + 0.05) c h);
|
||||
--is-full: round(down, calc(var(--fill, 1) + 0.0025), 1);
|
||||
background-color: oklch(
|
||||
from var(--face-top-color) l c h /
|
||||
calc(var(--empty-alpha) + var(--is-full) * (1 - var(--empty-alpha)))
|
||||
);
|
||||
transform: rotate(30deg) skew(-30deg)
|
||||
translate(calc(var(--cube) * 0.99), calc(var(--cube) * -0.265))
|
||||
scaleY(0.864);
|
||||
@@ -143,7 +194,12 @@
|
||||
.left {
|
||||
font-size: var(--font-size-xs);
|
||||
line-height: var(--line-height-xs);
|
||||
background-color: var(--face-color);
|
||||
background: linear-gradient(
|
||||
to top,
|
||||
var(--face-left-color) calc(var(--fill, 1) * 100%),
|
||||
oklch(from var(--face-left-color) l c h / var(--empty-alpha))
|
||||
calc(var(--fill, 1) * 100%)
|
||||
);
|
||||
transform: rotate(30deg) skewX(30deg)
|
||||
translate(calc(var(--cube) * 0.3), calc(var(--cube) * 0.6))
|
||||
scaleY(0.864);
|
||||
@@ -151,7 +207,9 @@
|
||||
|
||||
&.skeleton {
|
||||
pointer-events: none;
|
||||
.face { color: transparent; }
|
||||
.face {
|
||||
color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.fees {
|
||||
@@ -161,6 +219,35 @@
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
& + & {
|
||||
margin-bottom: var(--block-gap);
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: calc(var(--cube) * 1.75);
|
||||
left: calc(var(--cube) * 1.12);
|
||||
width: 1px;
|
||||
height: var(--block-gap);
|
||||
background: var(--border-color);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
margin-bottom: 0;
|
||||
margin-right: var(--block-gap);
|
||||
|
||||
&::before {
|
||||
bottom: auto;
|
||||
left: auto;
|
||||
right: calc(-1 * var(--block-gap));
|
||||
top: 50%;
|
||||
width: var(--block-gap);
|
||||
height: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user