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
+4 -4
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
+33 -3
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);
}
+3 -3
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 }),
@@ -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,
+13 -13
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 }),
),
},
],
+63 -14
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);
+1
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,
+4
View File
@@ -28,6 +28,10 @@ nav {
padding: 0.25rem 0;
}
a:visited {
color: transparent;
}
ul {
color: var(--off-color);
overflow: hidden;
+95 -8
View File
@@ -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;
}
}
}
}
}