global: snap

This commit is contained in:
nym21
2026-04-17 21:23:11 +02:00
parent 008143ff00
commit 2a93f51e81
47 changed files with 2075 additions and 389 deletions
+276
View File
@@ -0,0 +1,276 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<title>bitview logo (svg)</title>
<style>
/* Website's Lilex (the site's --font-mono). */
@font-face {
font-family: Lilex;
src: url("../fonts/Lilex[wght]-v2_620.woff2") format("woff2");
font-style: normal;
font-weight: 100 700;
font-display: block;
}
@font-face {
font-family: Lilex;
src: url("../fonts/Lilex-Italic[wght]-v2_620.woff2.woff2") format("woff2");
font-style: italic;
font-weight: 100 700;
font-display: block;
}
:root {
color-scheme: light dark;
--cube: 4.5rem;
--orange: oklch(67.64% 0.191 44.41);
--white: oklch(95% 0 0);
--black: oklch(15% 0 0);
--light-gray: oklch(90% 0 0);
--dark-gray: oklch(20% 0 0);
--border-color: light-dark(var(--light-gray), var(--dark-gray));
--background-color: light-dark(var(--white), var(--black));
--fill: 0.5;
--empty-alpha: 0.3;
--face-step: 0.033;
--iso-scale: 0.866;
--blur-stddev: 3;
--font-size-xs: 0.75rem;
--font-size-sm: 0.875rem;
}
/* ---- page chrome ---- */
html, body { margin: 0; padding: 0; }
body {
font: 14px/1.4 -apple-system, system-ui, sans-serif;
color: #111;
background: #f7f8fa;
}
.wrap { max-width: 1440px; margin: 0 auto; padding: 40px 24px 80px; }
section { margin: 0 0 12px; }
.row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 12px;
}
.controls {
display: flex;
gap: 16px;
align-items: center;
margin: 0 0 24px;
font-size: 13px;
color: #444;
}
.controls select { font-size: 13px; padding: 2px 6px; }
.tile {
position: relative;
aspect-ratio: 1;
border-radius: 14px;
display: grid; place-items: center;
overflow: hidden;
box-shadow: 0 0 0 1px rgba(0,0,0,0.08);
}
.tile .label {
position: absolute; left: 10px; bottom: 8px;
font: 11px/1.4 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
color: rgba(0,0,0,0.55);
background: rgba(255,255,255,0.7); padding: 2px 6px; border-radius: 4px;
backdrop-filter: blur(6px);
}
.tile.scheme-light { color-scheme: light; }
.tile.scheme-dark { color-scheme: dark; color: #e8e8ea; }
.tile.scheme-dark .label { color: rgba(255,255,255,0.75); background: rgba(0,0,0,0.4); }
.bg-auto { background: var(--background-color); }
.bg-paper { background: light-dark(#f0ece3, #2a2823); }
.bg-gradient { background: linear-gradient(135deg, oklch(70% 0.16 260), oklch(72% 0.19 330)); }
.bg-image-1 { background: url('https://picsum.photos/seed/brk1/440/440') center/cover; }
.bg-image-2 { background: url('https://picsum.photos/seed/brk2/440/440') center/cover; }
/* ---- cube slot: just holds the svg ---- */
.logo-slot {
position: relative;
width: 160px; height: 160px;
display: grid; place-items: center;
}
.logo-slot svg {
/* Iso cube silhouette is 2·iso × 2 (taller than wide, aspect
sqrt(3)/2 : 1). The viewBox is square 0 0 200 200 for clean
polygon coordinates; preserveAspectRatio="none" stretches it
into the right aspect at render time. */
width: calc(var(--cube) * var(--iso-scale) * 2);
height: calc(var(--cube) * 2);
/* Frost: backdrop-filter on the non-transformed <svg> (Safari
honours it) clipped to the hex silhouette so the blur only
shows where the cube's translucent faces show through. */
clip-path: polygon(
50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%
);
backdrop-filter: blur(3px);
-webkit-backdrop-filter: blur(3px);
}
/* ---- cube face colors (derived from --face-color, same logic
as website/styles/panes/explorer.css) ---- */
svg.cube {
--face-color: var(--orange);
--face-right: light-dark(
oklch(from var(--face-color) calc(l - var(--face-step) * 2) c h),
var(--face-color)
);
--face-left: 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: light-dark(
var(--face-color),
oklch(from var(--face-color) calc(l + var(--face-step) * 2) c h)
);
--face-bottom: oklch(from var(--face-color) calc(l - var(--face-step) * 3) c h);
color: light-dark(var(--black), var(--white));
}
/* Glass fills — translucent (--empty-alpha).
Front faces (visible in iso): top, right, left.
Rear faces (hidden in iso but visible through the translucent
fronts as "interior depth"): bottom (y-), rear-left (x-),
rear-right (z-). Colors mirror the HTML demo:
rear bottom → face-bottom
rear-left (x-) → face-top
rear-right (z-) → face-left
Liquid polygons are opaque, drawn between rear and front. */
svg.cube .glass-top { fill: var(--face-top); fill-opacity: var(--empty-alpha); }
svg.cube .glass-right { fill: var(--face-right); fill-opacity: var(--empty-alpha); }
svg.cube .glass-left { fill: var(--face-left); fill-opacity: var(--empty-alpha); }
svg.cube .glass-bottom { fill: var(--face-bottom); fill-opacity: var(--empty-alpha); }
svg.cube .glass-rear-left { fill: var(--face-top); fill-opacity: var(--empty-alpha); }
svg.cube .glass-rear-right { fill: var(--face-left); fill-opacity: var(--empty-alpha); }
svg.cube .liquid-top { fill: var(--face-top); }
svg.cube .liquid-right { fill: var(--face-right); }
svg.cube .liquid-left { fill: var(--face-left); }
</style>
</head>
<body>
<div class="wrap">
<div class="controls">
<span>SVG cube — blur via SVG <code>feGaussianBlur</code> with <code>BackgroundImage</code>.</span>
</div>
<!-- Cube template.
viewBox 0 0 200 200, hex center = (100, 100), iso half-width = 86.6.
Points: V_top=(100,0) VR_upper=(187,50) VR_lower=(187,150)
V_bottom=(100,200) VL_lower=(13,150) VL_upper=(13,50)
V_center=(100,100)
Face polygons (3 visible):
top = V_upper_left, V_top, V_upper_right, V_center
right = V_upper_right, V_lower_right, V_bottom, V_center
left = V_upper_left, V_lower_left, V_bottom, V_center
Liquid surface at fill f in [0,1] — 4 corners:
(13, 150 - 100f) (187, 150 - 100f) (100, 100 - 100f) (100, 200 - 100f)
JS computes liquid sub-polygons per tile based on --fill. -->
<template id="logo-template">
<div class="logo-slot">
<svg class="cube" viewBox="0 0 200 200" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
<!-- Rear glass (hidden 3D faces at their iso positions).
Drawn first so translucent front faces composite over
them for a "depth" look. -->
<g class="glass-rear">
<polygon class="glass-bottom" points="100,100 200,150 100,200 0,150"/>
<polygon class="glass-rear-left" points="100,100 0,150 0,50 100,0"/>
<polygon class="glass-rear-right" points="100,100 200,150 200,50 100,0"/>
</g>
<!-- Liquid (opaque): JS injects polygons sized from --fill. -->
<g class="liquid"></g>
<!-- Front glass (visible 3D faces). -->
<g class="glass-front">
<polygon class="glass-top" points="0,50 100,0 200,50 100,100"/>
<polygon class="glass-right" points="200,50 200,150 100,200 100,100"/>
<polygon class="glass-left" points="0,50 0,150 100,200 100,100"/>
</g>
</svg>
</div>
</template>
<section>
<div class="row" id="row"></div>
</section>
<section>
<div class="row" id="fills"></div>
</section>
</div>
<script>
// Liquid surface at fill f in [0,1]: horizontal cross-section of
// the cube at y=f, projected to viewBox (hex fills 200×200).
const surface = (f) => ({
tl: [0, 150 - 100 * f], // left side at surface
tr: [200, 150 - 100 * f], // right side at surface
back: [100, 100 - 100 * f], // back vertex (upper)
front: [100, 200 - 100 * f], // front vertex (lower)
});
// Liquid = 3 sub-polygons of the 3 visible faces, clipped at the
// surface line. The surface rhombus has 4 corners:
// back — back vertex of the cross-section
// tr/tl — right/left side vertices of the cross-section (shared
// with the right/left visible faces)
// front — front vertex of the cross-section (shared with both
// side faces' front edge)
// Each side face's upper edge at the surface runs from its side
// corner (tr or tl) to the shared front corner.
const liquidPolygons = (f) => {
if (f <= 0) return [];
const s = surface(f);
return [
{ cls: 'liquid-top', pts: [s.back, s.tr, s.front, s.tl] },
{ cls: 'liquid-right', pts: [s.tr, [200, 150], [100, 200], s.front] },
{ cls: 'liquid-left', pts: [s.tl, [0, 150], [100, 200], s.front] },
];
};
const template = document.getElementById('logo-template');
const SVGNS = 'http://www.w3.org/2000/svg';
const makeLogo = ({ fill = 0.5, faceColor, cubeScheme } = {}) => {
const slot = template.content.cloneNode(true).firstElementChild;
const svg = slot.querySelector('svg');
svg.style.setProperty('--fill', String(fill));
if (faceColor) svg.style.setProperty('--face-color', faceColor);
svg.style.setProperty('color-scheme', cubeScheme ?? 'light');
const liquid = svg.querySelector('.liquid');
for (const { cls, pts } of liquidPolygons(fill)) {
const poly = document.createElementNS(SVGNS, 'polygon');
poly.setAttribute('class', cls);
poly.setAttribute('points', pts.map(p => p.join(',')).join(' '));
liquid.appendChild(poly);
}
return slot;
};
const makeTile = (cls, label, content) => {
const tile = document.createElement('div');
tile.className = `tile ${cls}`;
tile.appendChild(content);
const lbl = document.createElement('span');
lbl.className = 'label';
lbl.textContent = label;
tile.appendChild(lbl);
return tile;
};
// Row 1: cube on each background.
const row = document.getElementById('row');
row.appendChild(makeTile('bg-auto scheme-light', 'light', makeLogo()));
row.appendChild(makeTile('bg-auto scheme-dark', 'dark', makeLogo()));
row.appendChild(makeTile('bg-paper scheme-light', 'paper', makeLogo()));
row.appendChild(makeTile('bg-gradient scheme-light', 'gradient', makeLogo()));
row.appendChild(makeTile('bg-image-1 scheme-light', 'img 1', makeLogo()));
row.appendChild(makeTile('bg-image-2 scheme-light', 'img 2', makeLogo()));
// Row 2: fill levels.
const fills = document.getElementById('fills');
for (const f of [0, 0.1, 0.25, 0.5, 0.75, 1])
fills.appendChild(makeTile('bg-auto scheme-light', `fill ${f}`, makeLogo({ fill: f })));
</script>
</body>
</html>
File diff suppressed because one or more lines are too long
+3
View File
@@ -128,6 +128,9 @@
*
* Address count pattern (base + delta with absolute + rate)
* @typedef {Brk.BaseDeltaPattern} AddrCountPattern
* @typedef {Brk.AddrUtxoPattern} AvgAmountPattern
* @typedef {Brk.SeriesTree_Addrs_Exposed} ExposedTree
* @typedef {Brk.SeriesTree_Addrs_Reused} ReusedTree
*/
/**
+9
View File
@@ -273,6 +273,15 @@ function createBlockCube(block) {
function createCube() {
const cubeElement = document.createElement("div");
cubeElement.classList.add("cube");
const bottomElement = document.createElement("div");
bottomElement.classList.add("face", "bottom");
cubeElement.append(bottomElement);
const rearRightElement = document.createElement("div");
rearRightElement.classList.add("face", "rear-right");
cubeElement.append(rearRightElement);
const rearLeftElement = document.createElement("div");
rearLeftElement.classList.add("face", "rear-left");
cubeElement.append(rearLeftElement);
const innerTopElement = document.createElement("div");
innerTopElement.classList.add("face", "inner-top");
cubeElement.append(innerTopElement);
@@ -47,6 +47,7 @@ export function buildCohortData() {
base: addrs.funded.all,
delta: addrs.delta.all,
},
avgAmount: addrs.avgAmount.all,
};
const shortNames = TERM_NAMES.short;
@@ -174,6 +175,9 @@ export function buildCohortData() {
base: addrs.funded[key],
delta: addrs.delta[key],
},
avgAmount: addrs.avgAmount[key],
exposed: addrs.exposed,
reused: addrs.reused,
};
});
@@ -28,7 +28,7 @@ import {
groupedWindowsCumulativeWithAll,
} from "../shared.js";
import { colors } from "../../utils/colors.js";
import { priceLines } from "../constants.js";
import { priceLine } from "../constants.js";
/**
* Simple supply series (total + half only, no profit/loss)
@@ -165,41 +165,44 @@ function groupedDeltaItems(list, all, getDelta, unit, title, name) {
// ============================================================================
/**
* Profitability chart (in profit + in loss supply)
* Amount chart: total + halved + in profit + in loss in sats/btc/usd.
* @param {{ total: AnyValuePattern, half: AnyValuePattern, inProfit: AnyValuePattern, inLoss: AnyValuePattern }} supply
* @param {(name: string) => string} title
* @returns {PartialChartOption}
*/
function profitabilityChart(supply, title) {
function profitabilityAmountChart(supply, title) {
return {
name: "Profitability",
name: "Amount",
title: title("Supply Profitability"),
bottom: [
...satsBtcUsd({
pattern: supply.total,
name: "Total",
color: colors.default,
}),
...satsBtcUsd({
pattern: supply.inProfit,
name: "In Profit",
color: colors.profit,
}),
...satsBtcUsd({
pattern: supply.inLoss,
name: "In Loss",
color: colors.loss,
}),
...satsBtcUsd({
pattern: supply.half,
name: "Halved",
color: colors.gray,
style: 4,
}),
...satsBtcUsd({ pattern: supply.total, name: "Total", color: colors.default }),
...satsBtcUsd({ pattern: supply.inProfit, name: "In Profit", color: colors.profit }),
...satsBtcUsd({ pattern: supply.inLoss, name: "In Loss", color: colors.loss }),
...satsBtcUsd({ pattern: supply.half, name: "Halved", color: colors.gray, style: 4 }),
],
};
}
/**
* Share chart: in profit / in loss as % of own supply.
* @param {{ inProfit: { toOwn: { percent: AnySeriesPattern, ratio: AnySeriesPattern } }, inLoss: { toOwn: { percent: AnySeriesPattern, ratio: AnySeriesPattern } } }} supply
* @param {(name: string) => string} title
* @returns {PartialChartOption}
*/
function profitabilityShareChart(supply, title) {
return {
name: "Share",
title: title("Supply Profitability"),
bottom: [
...percentRatio({ pattern: supply.inProfit.toOwn, name: "In Profit", color: colors.profit }),
...percentRatio({ pattern: supply.inLoss.toOwn, name: "In Loss", color: colors.loss }),
priceLine({ number: 100, color: colors.default, style: 0, unit: Unit.percentage }),
priceLine({ number: 50, unit: Unit.percentage }),
],
};
}
/**
* @param {{ toCirculating: PercentRatioPattern, inProfit: { toCirculating: PercentRatioPattern }, inLoss: { toCirculating: PercentRatioPattern } }} supply
* @param {(name: string) => string} title
@@ -207,8 +210,8 @@ function profitabilityChart(supply, title) {
*/
function circulatingChart(supply, title) {
return {
name: "% of Circulating",
title: title("Supply (% of Circulating)"),
name: "Dominance",
title: title("Supply Dominance"),
bottom: [
...percentRatio({ pattern: supply.toCirculating, name: "Total", color: colors.default }),
...percentRatio({ pattern: supply.inProfit.toCirculating, name: "In Profit", color: colors.profit }),
@@ -217,23 +220,6 @@ function circulatingChart(supply, title) {
};
}
/**
* @param {{ inProfit: { toOwn: { percent: AnySeriesPattern, ratio: AnySeriesPattern } }, inLoss: { toOwn: { percent: AnySeriesPattern, ratio: AnySeriesPattern } } }} supply
* @param {(name: string) => string} title
* @returns {PartialChartOption}
*/
function ownSupplyChart(supply, title) {
return {
name: "% of Own Supply",
title: title("Supply (% of Own)"),
bottom: [
...percentRatio({ pattern: supply.inProfit.toOwn, name: "In Profit", color: colors.profit }),
...percentRatio({ pattern: supply.inLoss.toOwn, name: "In Loss", color: colors.loss }),
...priceLines({ numbers: [100, 50, 0], unit: Unit.percentage }),
],
};
}
/**
* @param {OutputsPattern} outputs
* @param {Color} color
@@ -330,8 +316,13 @@ export function createHoldingsSectionAll({ cohort, title }) {
title: title("Supply"),
bottom: simpleSupplySeries(supply),
},
profitabilityChart(supply, title),
ownSupplyChart(supply, title),
{
name: "Profitability",
tree: [
profitabilityAmountChart(supply, title),
profitabilityShareChart(supply, title),
],
},
...singleDeltaItems(supply.delta, Unit.sats, title, "Supply"),
],
},
@@ -355,9 +346,14 @@ export function createHoldingsSectionWithRelative({ cohort, title }) {
title: title("Supply"),
bottom: simpleSupplySeries(supply),
},
profitabilityChart(supply, title),
circulatingChart(supply, title),
ownSupplyChart(supply, title),
{
name: "Profitability",
tree: [
profitabilityAmountChart(supply, title),
profitabilityShareChart(supply, title),
circulatingChart(supply, title),
],
},
...singleDeltaItems(supply.delta, Unit.sats, title, "Supply"),
],
},
@@ -380,8 +376,13 @@ export function createHoldingsSectionWithOwnSupply({ cohort, title }) {
title: title("Supply"),
bottom: simpleSupplySeries(supply),
},
profitabilityChart(supply, title),
circulatingChart(supply, title),
{
name: "Profitability",
tree: [
profitabilityAmountChart(supply, title),
circulatingChart(supply, title),
],
},
...singleDeltaItems(supply.delta, Unit.sats, title, "Supply"),
],
},
@@ -404,7 +405,10 @@ export function createHoldingsSectionWithProfitLoss({ cohort, title }) {
title: title("Supply"),
bottom: simpleSupplySeries(supply),
},
profitabilityChart(supply, title),
{
name: "Profitability",
tree: [profitabilityAmountChart(supply, title)],
},
...singleDeltaItems(supply.delta, Unit.sats, title, "Supply"),
],
},
@@ -427,7 +431,10 @@ export function createHoldingsSectionAddress({ cohort, title }) {
title: title("Supply"),
bottom: simpleSupplySeries(supply),
},
profitabilityChart(supply, title),
{
name: "Profitability",
tree: [profitabilityAmountChart(supply, title)],
},
...singleDeltaItems(supply.delta, Unit.sats, title, "Supply"),
],
},
@@ -502,7 +509,10 @@ export function createGroupedHoldingsSectionAddress({ list, all, title }) {
name: "Supply",
tree: [
groupedSupplyTotal(list, all, title),
...groupedSupplyProfitLoss(list, all, title),
{
name: "Profitability",
tree: groupedSupplyProfitLoss(list, all, title),
},
...groupedDeltaItems(list, all, (c) => c.tree.supply.delta, Unit.sats, title, "Supply"),
],
},
@@ -520,6 +530,25 @@ export function createGroupedHoldingsSectionAddress({ list, all, title }) {
...groupedDeltaItems(list, all, (c) => c.addressCount.delta, Unit.count, title, "Address Count"),
],
},
{
name: "Average Holdings",
tree: [
{
name: "Per UTXO",
title: title("Average Holdings per UTXO"),
bottom: flatMapCohortsWithAll(list, all, ({ name, color, avgAmount }) =>
satsBtcUsd({ pattern: avgAmount.utxo, name, color }),
),
},
{
name: "Per Address",
title: title("Average Holdings per Funded Address"),
bottom: flatMapCohortsWithAll(list, all, ({ name, color, avgAmount }) =>
satsBtcUsd({ pattern: avgAmount.addr, name, color }),
),
},
],
},
];
}
@@ -14,6 +14,9 @@ import {
formatCohortTitle,
satsBtcUsd,
satsBtcUsdFullTree,
avgHoldingsSubtree,
exposedSubtree,
reusedSubtree,
} from "../shared.js";
import {
ROLLING_WINDOWS,
@@ -103,6 +106,7 @@ export function createCohortFolderAll(cohort) {
createCostBasisSectionWithPercentiles({ cohort, title }),
createProfitabilitySectionAll({ cohort, title }),
createActivitySectionWithAdjusted({ cohort, title }),
avgHoldingsSubtree(cohort.avgAmount, title),
],
};
}
@@ -259,6 +263,9 @@ export function createCohortFolderAddress(cohort) {
createPricesSectionBasic({ cohort, title }),
createProfitabilitySectionWithProfitLoss({ cohort, title }),
createActivitySectionMinimal({ cohort, title }),
avgHoldingsSubtree(cohort.avgAmount, title),
reusedSubtree(cohort.reused, cohort.key, title),
exposedSubtree(cohort.exposed, cohort.key, title),
],
};
}
@@ -6,6 +6,7 @@ import { Unit } from "../../utils/units.js";
import { colors } from "../../utils/colors.js";
import { ROLLING_WINDOWS, line, baseline, mapWindows, sumsTreeBaseline, rollingPercentRatioTree, percentRatio, percentRatioBaseline } from "../series.js";
import { ratioBottomSeries, mapCohortsWithAll, flatMapCohortsWithAll } from "../shared.js";
import { priceLine } from "../constants.js";
// ============================================================================
// Shared building blocks
@@ -80,11 +81,26 @@ export function createValuationSectionFull({ cohort, title }) {
{ name: "Total", title: title("Realized Cap"), bottom: [line({ series: tree.realized.cap.usd, name: "Realized Cap", color, unit: Unit.usd })] },
{
name: "Profitability",
title: title("Invested Capital"),
bottom: [
line({ series: tree.realized.cap.usd, name: "Total", color: colors.default, unit: Unit.usd }),
line({ series: tree.unrealized.investedCapital.inProfit.usd, name: "In Profit", color: colors.profit, unit: Unit.usd }),
line({ series: tree.unrealized.investedCapital.inLoss.usd, name: "In Loss", color: colors.loss, unit: Unit.usd }),
tree: [
{
name: "Amount",
title: title("Invested Capital"),
bottom: [
line({ series: tree.realized.cap.usd, name: "Total", color: colors.default, unit: Unit.usd }),
line({ series: tree.unrealized.investedCapital.inProfit.usd, name: "In Profit", color: colors.profit, unit: Unit.usd }),
line({ series: tree.unrealized.investedCapital.inLoss.usd, name: "In Loss", color: colors.loss, unit: Unit.usd }),
],
},
{
name: "Share",
title: title("Invested Capital Profitability"),
bottom: [
...percentRatio({ pattern: tree.investedCapital.inProfit.toOwn, name: "In Profit", color: colors.profit }),
...percentRatio({ pattern: tree.investedCapital.inLoss.toOwn, name: "In Loss", color: colors.loss }),
priceLine({ number: 100, color: colors.default, style: 0, unit: Unit.percentage }),
priceLine({ number: 50, unit: Unit.percentage }),
],
},
],
},
{ name: "MVRV", title: title("MVRV"), bottom: ratioBottomSeries(tree.realized.price) },
+52 -102
View File
@@ -21,6 +21,7 @@ import {
ROLLING_WINDOWS,
chartsFromBlockAnd6b,
multiSeriesTree,
percentRatio,
percentRatioDots,
} from "./series.js";
import {
@@ -29,6 +30,9 @@ import {
satsBtcUsdFullTree,
formatCohortTitle,
groupedWindowsCumulative,
avgHoldingsSubtree,
exposedSubtree,
reusedSubtree,
} from "./shared.js";
/**
@@ -210,34 +214,6 @@ export function createNetworkSection() {
],
});
const reusedOutputsSubtreeForType =
/**
* @param {AddressableType} key
* @param {(name: string) => string} title
*/
(key, title) => ({
name: "Outputs",
tree: [
{
name: "Count",
tree: chartsFromCount({
pattern: addrs.reused.events.outputToReusedAddrCount[key],
title,
metric: "Transaction Outputs to Reused Addresses",
unit: Unit.count,
}),
},
{
name: "Share",
tree: chartsFromPercentCumulative({
pattern: addrs.reused.events.outputToReusedAddrShare[key],
title,
metric: "Share of Transaction Outputs to Reused Addresses",
}),
},
],
});
const reusedActiveSubtreeForAll =
/** @param {(name: string) => string} title */
(title) => ({
@@ -276,19 +252,6 @@ export function createNetworkSection() {
],
});
const reusedSubtreeForType =
/**
* @param {AddressableType} key
* @param {(name: string) => string} title
*/
(key, title) => ({
name: "Reused",
tree: [
...reusedSetEntries(key, title),
reusedOutputsSubtreeForType(key, title),
reusedInputsSubtree(key, title),
],
});
const countSubtree =
/**
@@ -324,64 +287,6 @@ export function createNetworkSection() {
],
});
const exposedSubtree =
/**
* @param {AddressableType | "all"} key
* @param {(name: string) => string} title
*/
(key, title) => ({
name: "Exposed",
tree: [
{
name: "Compare",
title: title("Exposed Address Count"),
bottom: [
line({
series: addrs.exposed.count.funded[key],
name: "Funded",
unit: Unit.count,
}),
line({
series: addrs.exposed.count.total[key],
name: "Total",
color: colors.gray,
unit: Unit.count,
}),
],
},
{
name: "Funded",
title: title("Funded Exposed Address Count"),
bottom: [
line({
series: addrs.exposed.count.funded[key],
name: "Funded Exposed",
unit: Unit.count,
}),
],
},
{
name: "Total",
title: title("Total Exposed Address Count"),
bottom: [
line({
series: addrs.exposed.count.total[key],
name: "Total Exposed",
color: colors.gray,
unit: Unit.count,
}),
],
},
{
name: "Supply",
title: title("Supply in Exposed Addresses"),
bottom: satsBtcUsd({
pattern: addrs.exposed.supply[key],
name: "Supply",
}),
},
],
});
const activityPerTypeEntries =
/**
@@ -478,7 +383,8 @@ export function createNetworkSection() {
}),
activitySubtreeForAll(title),
reusedSubtreeForAll(title),
exposedSubtree("all", title),
exposedSubtree(addrs.exposed, "all", title),
avgHoldingsSubtree(addrs.avgAmount.all, title),
];
};
@@ -507,8 +413,9 @@ export function createNetworkSection() {
unit: Unit.count,
}),
activitySubtreeForType(addrType, title),
reusedSubtreeForType(addrType, title),
exposedSubtree(addrType, title),
reusedSubtree(addrs.reused, addrType, title),
exposedSubtree(addrs.exposed, addrType, title),
avgHoldingsSubtree(addrs.avgAmount[addrType], title),
];
};
@@ -761,6 +668,49 @@ export function createNetworkSection() {
}),
),
},
{
name: "Share",
title: "Share of Supply in Exposed Addresses by Type",
bottom: addressTypes.flatMap((t) =>
percentRatio({
pattern: addrs.exposed.supply.share[t.key],
name: t.name,
color: t.color,
defaultActive: t.defaultActive,
}),
),
},
],
},
// Average Holdings
{
name: "Average Holdings",
tree: [
{
name: "Per UTXO",
title: "Average Holdings per UTXO by Type",
bottom: addressTypes.flatMap((t) =>
satsBtcUsd({
pattern: addrs.avgAmount[t.key].utxo,
name: t.name,
color: t.color,
defaultActive: t.defaultActive,
}),
),
},
{
name: "Per Address",
title: "Average Holdings per Funded Address by Type",
bottom: addressTypes.flatMap((t) =>
satsBtcUsd({
pattern: addrs.avgAmount[t.key].addr,
name: t.name,
color: t.color,
defaultActive: t.defaultActive,
}),
),
},
],
},
],
+195
View File
@@ -6,6 +6,9 @@ import {
line,
baseline,
price,
percentRatio,
chartsFromCount,
chartsFromPercentCumulative,
sumsAndAveragesCumulativeWith,
} from "./series.js";
import { priceLine, priceLines } from "./constants.js";
@@ -243,6 +246,198 @@ export function satsBtcUsdFullTree({ pattern, title, metric, color }) {
});
}
/**
* "Exposed" subtree (quantum-risk / revealed-pubkey addresses).
* Shape: Compare (funded + total) / Funded / Total / Supply / Share.
* Shared between Network and Distribution (per-type cohort view).
* @param {ExposedTree} exposed
* @param {AddressableType | "all"} key
* @param {(name: string) => string} title
* @returns {PartialOptionsGroup}
*/
export function exposedSubtree(exposed, key, title) {
return {
name: "Exposed",
tree: [
{
name: "Compare",
title: title("Exposed Address Count"),
bottom: [
line({ series: exposed.count.funded[key], name: "Funded", unit: Unit.count }),
line({
series: exposed.count.total[key],
name: "Total",
color: colors.gray,
unit: Unit.count,
}),
],
},
{
name: "Funded",
title: title("Funded Exposed Address Count"),
bottom: [
line({ series: exposed.count.funded[key], name: "Funded Exposed", unit: Unit.count }),
],
},
{
name: "Total",
title: title("Total Exposed Address Count"),
bottom: [
line({
series: exposed.count.total[key],
name: "Total Exposed",
color: colors.gray,
unit: Unit.count,
}),
],
},
{
name: "Supply",
title: title("Supply in Exposed Addresses"),
bottom: satsBtcUsd({ pattern: exposed.supply[key], name: "Supply" }),
},
{
name: "Share",
title: title("Share of Supply in Exposed Addresses"),
bottom: percentRatio({ pattern: exposed.supply.share[key], name: "Supply" }),
},
],
};
}
/**
* "Reused" subtree (per-type / per-cohort — no "Active" window, since that
* data is only tracked globally). Shape:
* Compare (funded + total) / Funded / Total / Outputs / Inputs.
* @param {ReusedTree} reused
* @param {AddressableType | "all"} key
* @param {(name: string) => string} title
* @returns {PartialOptionsGroup}
*/
export function reusedSubtree(reused, key, title) {
return {
name: "Reused",
tree: [
{
name: "Compare",
title: title("Reused Address Count"),
bottom: [
line({ series: reused.count.funded[key], name: "Funded", unit: Unit.count }),
line({
series: reused.count.total[key],
name: "Total",
color: colors.gray,
unit: Unit.count,
}),
],
},
{
name: "Funded",
title: title("Funded Reused Addresses"),
bottom: [
line({ series: reused.count.funded[key], name: "Funded Reused", unit: Unit.count }),
],
},
{
name: "Total",
title: title("Total Reused Addresses"),
bottom: [
line({
series: reused.count.total[key],
name: "Total Reused",
color: colors.gray,
unit: Unit.count,
}),
],
},
{
name: "Outputs",
tree: [
{
name: "Count",
tree: chartsFromCount({
pattern: reused.events.outputToReusedAddrCount[key],
title,
metric: "Transaction Outputs to Reused Addresses",
unit: Unit.count,
}),
},
{
name: "Share",
tree: chartsFromPercentCumulative({
pattern: reused.events.outputToReusedAddrShare[key],
title,
metric: "Share of Transaction Outputs to Reused Addresses",
}),
},
],
},
{
name: "Inputs",
tree: [
{
name: "Count",
tree: chartsFromCount({
pattern: reused.events.inputFromReusedAddrCount[key],
title,
metric: "Transaction Inputs from Reused Addresses",
unit: Unit.count,
}),
},
{
name: "Share",
tree: chartsFromPercentCumulative({
pattern: reused.events.inputFromReusedAddrShare[key],
title,
metric: "Share of Transaction Inputs from Reused Addresses",
}),
},
],
},
],
};
}
/**
* "Average Holdings" subtree: Compare (both) + Per UTXO + Per Funded Address.
* Shared between Network and Distribution.
* @param {AvgAmountPattern} pattern
* @param {(name: string) => string} title
* @returns {PartialOptionsGroup}
*/
export function avgHoldingsSubtree(pattern, title) {
return {
name: "Average Holdings",
tree: [
{
name: "Compare",
title: title("Average Holdings"),
bottom: [
...satsBtcUsd({ pattern: pattern.utxo, name: "Per UTXO" }),
...satsBtcUsd({
pattern: pattern.addr,
name: "Per Funded Address",
color: colors.gray,
}),
],
},
{
name: "Per UTXO",
title: title("Average Holdings per UTXO"),
bottom: satsBtcUsd({ pattern: pattern.utxo, name: "Per UTXO" }),
},
{
name: "Per Address",
title: title("Average Holdings per Funded Address"),
bottom: satsBtcUsd({
pattern: pattern.addr,
name: "Per Funded Address",
}),
},
],
};
}
/**
* Create Price + Ratio charts from a simple price pattern (BpsCentsRatioSatsUsdPattern)
* @param {Object} args
+2 -1
View File
@@ -183,6 +183,7 @@
* @property {Color} color
* @property {PatternAll} tree
* @property {AddrCountPattern} addressCount
* @property {AvgAmountPattern} avgAmount
*
* Full cohort: adjustedSopr + percentiles + RelToMarketCap (term.short)
* @typedef {Object} CohortFull
@@ -251,7 +252,7 @@
* ============================================================================
*
* Addressable cohort with address count (for "type" cohorts - uses OutputsRealizedSupplyUnrealizedPattern2)
* @typedef {{ name: string, title: string, color: Color, tree: EmptyPattern, addressCount: AddrCountPattern }} CohortAddr
* @typedef {{ name: string, key: AddressableType, title: string, color: Color, tree: EmptyPattern, addressCount: AddrCountPattern, avgAmount: AvgAmountPattern, exposed: ExposedTree, reused: ReusedTree }} CohortAddr
*
* ============================================================================
* Cohort Group Types (by capability)
+2 -1
View File
@@ -29,7 +29,8 @@ nav {
}
a:visited {
color: transparent;
font-style: italic;
/*color: var(--orange);*/
}
ul {
+75 -50
View File
@@ -72,7 +72,11 @@
--block-gap: calc(
var(--min-gap) + var(--t) * (var(--max-gap) - var(--min-gap))
);
--empty-alpha: 0.5;
--empty-alpha: 0.3;
--iso-scale: 0.866;
--ox: 0.3;
--oy: 0.6;
--fill-pct: calc(var(--fill, 1) * 100%);
--face-step: 0.033;
--face-right-color: light-dark(
oklch(from var(--face-color) calc(l - var(--face-step) * 2) c h),
@@ -86,7 +90,6 @@
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;
@@ -97,29 +100,9 @@
line-height: var(--line-height-sm);
--face-color: var(--border-color);
color: var(--color);
transition:
color,
background-color,
border-color,
outline-color,
text-decoration-color,
fill,
stroke,
opacity,
box-shadow,
transform,
translate,
scale,
rotate,
filter,
-webkit-backdrop-filter,
backdrop-filter,
display,
content-visibility,
overlay,
pointer-events;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-property: color, background-color;
transition-duration: 50ms;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
user-select: none;
pointer-events: none;
@@ -143,33 +126,74 @@
position: absolute;
width: var(--cube);
height: var(--cube);
}
.right,
.left,
.top {
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);*/
.right,
.left,
.rear-right,
.rear-left {
background: linear-gradient(
to top,
var(--fc) var(--fill-pct),
oklch(from var(--fc) l c h / var(--empty-alpha)) var(--fill-pct)
);
}
.bottom {
background-color: var(--face-color);
transform: rotate(30deg) skew(-30deg)
translate(
calc(var(--cube) * (1.99 - var(--fill, 1))),
calc(var(--cube) * (0.599 - 0.864 * var(--fill, 1)))
calc(var(--cube) * (var(--ox) + 1 + var(--oy) / var(--iso-scale))),
calc(var(--cube) * var(--oy))
)
scaleY(0.864);
scaleY(var(--iso-scale));
}
.rear-right {
--fc: var(--face-left-color);
transform: rotate(30deg) skewX(30deg)
translate(
calc(var(--cube) * (var(--ox) + 1)),
calc(var(--cube) * (var(--oy) - var(--iso-scale)))
)
scaleY(var(--iso-scale));
}
.rear-left {
--fc: var(--face-top-color);
transform: rotate(-30deg) skewX(-30deg)
translate(
calc(var(--cube) * (var(--ox) + 1)),
calc(var(--cube) * (var(--ox) * var(--iso-scale) + var(--oy)))
)
scale(-1, var(--iso-scale));
}
.inner-top {
background-color: var(--face-top-color);
transform: rotate(30deg) skew(-30deg)
translate(
calc(var(--cube) * (var(--ox) + 1 + var(--oy) / var(--iso-scale) - var(--fill, 1))),
calc(var(--cube) * (var(--oy) - var(--iso-scale) * var(--fill, 1)))
)
scaleY(var(--iso-scale));
}
.right {
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%)
);
--fc: var(--face-right-color);
transform: rotate(-30deg) skewX(-30deg)
translate(calc(var(--cube) * 1.3), calc(var(--cube) * 1.725))
scaleY(0.864);
translate(
calc(var(--cube) * (var(--ox) + 1)),
calc(var(--cube) * ((var(--ox) + 1) * var(--iso-scale) + var(--oy)))
)
scaleY(var(--iso-scale));
}
.top {
@@ -179,8 +203,11 @@
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);
translate(
calc(var(--cube) * (var(--ox) + var(--oy) / var(--iso-scale))),
calc(var(--cube) * (var(--oy) - var(--iso-scale)))
)
scaleY(var(--iso-scale));
justify-content: center;
align-items: center;
text-align: center;
@@ -192,17 +219,15 @@
}
.left {
--fc: var(--face-left-color);
font-size: var(--font-size-xs);
line-height: var(--line-height-xs);
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);
translate(
calc(var(--cube) * var(--ox)),
calc(var(--cube) * var(--oy))
)
scaleY(var(--iso-scale));
}
&.skeleton {