global: reused + mempool + favicon

This commit is contained in:
nym21
2026-04-23 23:13:39 +02:00
parent ce00de5da8
commit e4496742a4
77 changed files with 2631 additions and 1624 deletions
+1 -1
View File
@@ -9,7 +9,7 @@
"type": "openapi",
"url": "https://bitview.space/openapi.json"
},
"logo_url": "https://bitview.space/assets/favicon-196.png",
"logo_url": "https://bitview.space/assets/favicon/web-app-manifest-512x512.png",
"contact_email": "hello@bitcoinresearchkit.org",
"legal_info_url": "https://github.com/bitcoinresearchkit/brk/blob/main/docs/LICENSE.md"
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 511 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 594 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+43
View File
@@ -0,0 +1,43 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 173.2 200" role="img" aria-label="bitview" width="173.2" height="200"><metadata><rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:dc="http://purl.org/dc/elements/1.1/"><rdf:Description><dc:creator>RealFaviconGenerator</dc:creator><dc:source>https://realfavicongenerator.net</dc:source></rdf:Description></rdf:RDF></metadata><title>bitview</title><style>
svg {
--fill: 0.5;
--face-top: oklch(67.64% 0.191 44.41);
--face-left: oklch(64.34% 0.191 44.41);
--face-right: oklch(61.04% 0.191 44.41);
--face-bottom: oklch(57.74% 0.191 44.41);
}
svg[data-theme="light"] {
--face-top: oklch(90% 0 0);
--face-left: oklch(86.7% 0 0);
--face-right: oklch(83.4% 0 0);
--face-bottom: oklch(80.1% 0 0);
}
svg[data-theme="dark"] {
--face-top: oklch(26.6% 0 0);
--face-left: oklch(23.3% 0 0);
--face-right: oklch(20% 0 0);
--face-bottom: oklch(10.1% 0 0);
}
.glass { fill-opacity: 0.4; }
.glass-top, .liquid-top { fill: var(--face-top); }
.glass-right, .liquid-right { fill: var(--face-right); }
.glass-left, .liquid-left { fill: var(--face-left); }
.glass-bottom { fill: var(--face-bottom); }
.glass-rear-left { fill: var(--face-top); }
.glass-rear-right { fill: var(--face-left); }
.liquid {
transform-box: view-box;
transform: translateY(calc((1 - var(--fill)) * 50%));
opacity: ceil(var(--fill));
}
</style><defs>
<clipPath id="hex" clipPathUnits="userSpaceOnUse">
<polygon points="86.6,0 173.2,50 173.2,150 86.6,200 0,150 0,50"></polygon>
</clipPath>
</defs><polygon class="glass glass-bottom" points="86.6,100 173.2,150 86.6,200 0,150"></polygon><polygon class="glass glass-rear-left" points="86.6,100 0,150 0,50 86.6,0"></polygon><polygon class="glass glass-rear-right" points="86.6,100 173.2,150 173.2,50 86.6,0"></polygon><g clip-path="url(#hex)">
<g class="liquid">
<polygon class="liquid-top" points="86.6,0 173.2,50 86.6,100 0,50"></polygon>
<polygon class="liquid-right" points="173.2,50 173.2,150 86.6,200 86.6,100"></polygon>
<polygon class="liquid-left" points="0,50 0,150 86.6,200 86.6,100"></polygon>
</g>
</g><polygon class="glass glass-top" points="0,50 86.6,0 173.2,50 86.6,100"></polygon><polygon class="glass glass-right" points="173.2,50 173.2,150 86.6,200 86.6,100"></polygon><polygon class="glass glass-left" points="0,50 0,150 86.6,200 86.6,100"></polygon></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

+21
View File
@@ -0,0 +1,21 @@
{
"name": "bitview",
"short_name": "bitview",
"icons": [
{
"src": "/web-app-manifest-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/web-app-manifest-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"theme_color": "#EEE",
"background_color": "#EEE",
"display": "standalone"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

+99 -159
View File
@@ -26,16 +26,9 @@
--cube: calc(var(--cube-rem) * 1rem);
--cube-px: calc(var(--cube-rem) * 16);
--face-scale: calc(100 / var(--cube-px));
--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.4;
--face-step: 0.033;
--iso-scale: 0.866;
--font-size-xs: 0.75rem;
--font-size-sm: 0.875rem;
@@ -113,6 +106,8 @@
font-weight: 500;
pointer-events: none;
}
.face-text { color: var(--black); }
svg[data-theme="dark"] .face-text { color: var(--white); }
.face-text.top,
.face-text.right {
display: flex;
@@ -165,51 +160,65 @@
.face-text .pool img.marapool { content: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iMjU2IiBoZWlnaHQ9IjI1NiIgdmlld0JveD0iMCAwIDI1NiAyNTYiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik0wIDMuNjAwOEMwIDIuMzQzMTMgMCAxLjcxNDMgMC4yNDQ3NTcgMS4yMzM5NEMwLjQ2MDA1MiAwLjgxMTQgMC44MDM1ODcgMC40Njc4NjQgMS4yMjYxMyAwLjI1MjU3QzEuNzA2NDkgMC4wMDc4MTI1IDIuMzM1MzIgMC4wMDc4MTI1IDMuNTkyOTggMC4wMDc4MTI1SDI1Mi40MDdDMjUzLjY2NSAwLjAwNzgxMjUgMjU0LjI5NCAwLjAwNzgxMjUgMjU0Ljc3NCAwLjI1MjU3QzI1NS4xOTYgMC40Njc4NjQgMjU1LjU0IDAuODExNCAyNTUuNzU1IDEuMjMzOTRDMjU2IDEuNzE0MyAyNTYgMi4zNDMxMyAyNTYgMy42MDA4VjI1Mi40MTVDMjU2IDI1My42NzIgMjU2IDI1NC4zMDEgMjU1Ljc1NSAyNTQuNzgyQzI1NS41NCAyNTUuMjA0IDI1NS4xOTYgMjU1LjU0OCAyNTQuNzc0IDI1NS43NjNDMjU0LjI5NCAyNTYuMDA4IDI1My42NjUgMjU2LjAwOCAyNTIuNDA3IDI1Ni4wMDhIMy41OTI5OUMyLjMzNTMyIDI1Ni4wMDggMS43MDY0OSAyNTYuMDA4IDEuMjI2MTMgMjU1Ljc2M0MwLjgwMzU4NyAyNTUuNTQ4IDAuNDYwMDUyIDI1NS4yMDQgMC4yNDQ3NTcgMjU0Ljc4MkMwIDI1NC4zMDEgMCAyNTMuNjcyIDAgMjUyLjQxNVYzLjYwMDhaIiBmaWxsPSIjMjcyNTI1Ii8+CjxwYXRoIGQ9Ik01OS4yODUgNDguMDYyNUM1OC43ODg5IDQ4LjA2MjUgNTguMzg2NyA0OC40NjQ3IDU4LjM4NjcgNDguOTYwN1YxNTkuNDQ1QzU4LjM4NjcgMTU5Ljk0MSA1OC43ODg5IDE2MC4zNDMgNTkuMjg1IDE2MC4zNDNIODAuMDc0OUM4MC41NzA5IDE2MC4zNDMgODAuOTczMSAxNTkuOTQxIDgwLjk3MzEgMTU5LjQ0NVY4Mi4zODlIODQuNjMzNUwxMTcuNjQ5IDE1OS43OTdDMTE3Ljc5IDE2MC4xMjggMTE4LjExNiAxNjAuMzQzIDExOC40NzUgMTYwLjM0M0gxMzcuODA5QzEzOC4xNjkgMTYwLjM0MyAxMzguNDk0IDE2MC4xMjggMTM4LjYzNSAxNTkuNzk3TDE3MS42NTEgODIuMzg5SDE3NS4zMTFWMTU5LjQ0NUMxNzUuMzExIDE1OS45NDEgMTc1LjcxNCAxNjAuMzQzIDE3Ni4yMSAxNjAuMzQzSDE5N0MxOTcuNDk2IDE2MC4zNDMgMTk3Ljg5OCAxNTkuOTQxIDE5Ny44OTggMTU5LjQ0NVY0OC45NjA3QzE5Ny44OTggNDguNDY0NyAxOTcuNDk2IDQ4LjA2MjUgMTk3IDQ4LjA2MjVIMTY0LjI5QzE2My45MzEgNDguMDYyNSAxNjMuNjA1IDQ4LjI3NzMgMTYzLjQ2NCA0OC42MDg0TDEyOS45NzIgMTI3LjE0SDEyNi4zMTJMOTIuODIwMiA0OC42MDg0QzkyLjY3OSA0OC4yNzczIDkyLjM1MzkgNDguMDYyNSA5MS45OTQgNDguMDYyNUg1OS4yODVaIiBmaWxsPSIjRUVFQ0U4Ii8+CjxwYXRoIGQ9Ik01OC4zODY3IDE5NC45MjZDNTguMzg2NyAxOTQuNDMgNTguNzg4OSAxOTQuMDI3IDU5LjI4NSAxOTQuMDI3SDE5Ni45OTlDMTk3LjQ5NiAxOTQuMDI3IDE5Ny44OTggMTk0LjQzIDE5Ny44OTggMTk0LjkyNlYyMTUuNTg1QzE5Ny44OTggMjE2LjA4MSAxOTcuNDk2IDIxNi40ODQgMTk2Ljk5OSAyMTYuNDg0SDU5LjI4NUM1OC43ODg5IDIxNi40ODQgNTguMzg2NyAyMTYuMDgxIDU4LjM4NjcgMjE1LjU4NVYxOTQuOTI2WiIgZmlsbD0iI0YyQTkwMCIvPgo8L3N2Zz4K"); }
.face-text .pool img.unknown { content: url("data:image/svg+xml;base64,PHN2ZyBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjUuMjcgMyAxMy40OCAxOCI+IDxwYXRoIGQ9Ik01LjY0MTcgMTguMzY5NkM1LjE1ODM0IDE3Ljg4NzEgNS4xNTgzMyAxNy4xMTMgNS42NDE2OSAxNi42MzA0QzcuMjY5OTIgMTUuMDA1MSA5LjUxNzYxIDE0IDEyIDE0QzE0LjQ4MjUgMTQgMTYuNzMwMSAxNS4wMDUgMTguMzU4MyAxNi42MzA0QzE4Ljg0MTcgMTcuMTEyOSAxOC44NDE3IDE3Ljg4NzEgMTguMzU4NCAxOC4zNjk2QzE2LjczMDEgMTkuOTk0OSAxNC40ODI0IDIxIDEyIDIxQzkuNTE3NTkgMjEgNy4yNjk5MyAxOS45OTUgNS42NDE3IDE4LjM2OTZaIiBmaWxsPSIjYjRiNGI0Ij48L3BhdGg+IDxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNOC41MDAwMyA3LjVIMTUuNVY4SDE2QzE2IDEwLjYzNzIgMTQuMzE4OCAxMyAxMiAxM0M5LjY4MTIzIDEzIDguMDAwMDMgMTAuNjM3MiA4LjAwMDAzIDhIOC41MDAwM1Y3LjVaTTkuMDIyNzMgOC41QzkuMjEyNjYgMTAuNTY3OCAxMC41NjU4IDEyIDEyIDEyQzEzLjQzNDMgMTIgMTQuNzg3NCAxMC41Njc4IDE0Ljk3NzMgOC41SDkuMDIyNzNaIiBmaWxsPSIjYjRiNGI0Ij48L3BhdGg+IDxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTIgM0MxNC40ODUzIDMgMTYuNSA1LjIzODU4IDE2LjUgOEg3LjUwMDAzQzcuNTAwMDMgNS4yMzg1OCA5LjUxNDc1IDMgMTIgM1pNMTIgNy41QzEyLjgyODUgNy41IDEzLjUgNi44Mjg0MyAxMy41IDZDMTMuNSA1LjE3MTU3IDEyLjgyODUgNC41IDEyIDQuNUMxMS4xNzE2IDQuNSAxMC41IDUuMTcxNTcgMTAuNSA2QzEwLjUgNi44Mjg0MyAxMS4xNzE2IDcuNSAxMiA3LjVaIiBmaWxsPSIjYjRiNGI0Ij48L3BhdGg+IDxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNNi41MDAwMyA4LjVDNi41MDAwMyA4LjIyMzg2IDYuNzIzODkgOCA3LjAwMDAzIDhIMTdDMTcuMjc2MiA4IDE3LjUgOC4yMjM4NiAxNy41IDguNUMxNy41IDguNzc2MTQgMTcuMjc2MiA5IDE3IDlINy4wMDAwM0M2LjcyMzg5IDkgNi41MDAwMyA4Ljc3NjE0IDYuNTAwMDMgOC41WiIgZmlsbD0iI2I0YjRiNCI+PC9wYXRoPiA8L3N2Zz4="); }
/* ---- 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 polygons: translucent. Liquid polygons: opaque. Rear-face
colors mirror demo.html (bottom → face-bottom, rear-left →
face-top, rear-right → face-left). */
svg.cube .glass { fill-opacity: var(--empty-alpha); }
svg.cube .glass-top, svg.cube .liquid-top { fill: var(--face-top); }
svg.cube .glass-right, svg.cube .liquid-right { fill: var(--face-right); }
svg.cube .glass-left, svg.cube .liquid-left { fill: var(--face-left); }
svg.cube .glass-bottom { fill: var(--face-bottom); }
svg.cube .glass-rear-left { fill: var(--face-top); }
svg.cube .glass-rear-right { fill: var(--face-left); }
</style>
</head>
<body>
<div class="wrap">
<!-- SVG shell. All polygons + text-face transforms are generated
in JS from the cube constants (ISO, OX, OY, CUBE), so the
geometry has a single source of truth. DOM order defines
z-order: rear polygons, liquid, front polygons, text. -->
<template id="logo-template">
<div class="logo-slot">
<svg class="cube"></svg>
</div>
</template>
<template id="logo-template"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 173.2 200" role="img" aria-label="bitview">
<title>bitview</title>
<style>
svg {
--fill: 0.5;
--face-top: oklch(67.64% 0.191 44.41);
--face-left: oklch(64.34% 0.191 44.41);
--face-right: oklch(61.04% 0.191 44.41);
--face-bottom: oklch(57.74% 0.191 44.41);
}
svg[data-theme="light"] {
--face-top: oklch(90% 0 0);
--face-left: oklch(86.7% 0 0);
--face-right: oklch(83.4% 0 0);
--face-bottom: oklch(80.1% 0 0);
}
svg[data-theme="dark"] {
--face-top: oklch(26.6% 0 0);
--face-left: oklch(23.3% 0 0);
--face-right: oklch(20% 0 0);
--face-bottom: oklch(10.1% 0 0);
}
.glass { fill-opacity: 0.4; }
.glass-top, .liquid-top { fill: var(--face-top); }
.glass-right, .liquid-right { fill: var(--face-right); }
.glass-left, .liquid-left { fill: var(--face-left); }
.glass-bottom { fill: var(--face-bottom); }
.glass-rear-left { fill: var(--face-top); }
.glass-rear-right { fill: var(--face-left); }
.liquid {
transform-box: view-box;
transform: translateY(calc((1 - var(--fill)) * 50%));
opacity: ceil(var(--fill));
}
</style>
<defs>
<clipPath id="hex" clipPathUnits="userSpaceOnUse">
<polygon points="86.6,0 173.2,50 173.2,150 86.6,200 0,150 0,50"/>
</clipPath>
</defs>
<polygon class="glass glass-bottom" points="86.6,100 173.2,150 86.6,200 0,150"/>
<polygon class="glass glass-rear-left" points="86.6,100 0,150 0,50 86.6,0"/>
<polygon class="glass glass-rear-right" points="86.6,100 173.2,150 173.2,50 86.6,0"/>
<g clip-path="url(#hex)">
<g class="liquid">
<polygon class="liquid-top" points="86.6,0 173.2,50 86.6,100 0,50"/>
<polygon class="liquid-right" points="173.2,50 173.2,150 86.6,200 86.6,100"/>
<polygon class="liquid-left" points="0,50 0,150 86.6,200 86.6,100"/>
</g>
</g>
<polygon class="glass glass-top" points="0,50 86.6,0 173.2,50 86.6,100"/>
<polygon class="glass glass-right" points="173.2,50 173.2,150 86.6,200 86.6,100"/>
<polygon class="glass glass-left" points="0,50 0,150 86.6,200 86.6,100"/>
</svg></template>
<div class="wrap">
<section>
<div class="row" id="row"></div>
</section>
@@ -224,61 +233,18 @@
</section>
</div>
<script>
// --- Cube geometry: everything derives from these 4 constants.
// Change ISO/OX/OY/CUBE and the entire cube reprojects. ---
<script type="module">
const ISO = 0.866, OX = 0.3, OY = 0.6, CUBE = 100;
const W = 2 * ISO * CUBE, H = 2 * CUBE; // viewBox 173.2 × 200
const W = 2 * ISO * CUBE, H = 2 * CUBE;
// Hex vertices (iso projection of the 8 cube vertices; parallel
// pairs collapse into V.C, then these 7 outline the silhouette).
const V = {
T: [W/2, 0 ],
UR: [W, H/4 ], UL: [0, H/4 ],
C: [W/2, H/2 ],
LR: [W, 3*H/4 ], LL: [0, 3*H/4 ],
B: [W/2, H ],
};
const lerp = ([ax,ay], [bx,by], t) => [ax + (bx-ax)*t, ay + (by-ay)*t];
// Static glass polygons. DOM order sets z-order (rear polygons
// first, then liquid, then front). All share the .glass class so
// the single fill-opacity rule applies to both rear and front.
const GLASS_REAR = [
{ cls: 'glass glass-bottom', pts: [V.C, V.LR, V.B, V.LL] },
{ cls: 'glass glass-rear-left', pts: [V.C, V.LL, V.UL, V.T] },
{ cls: 'glass glass-rear-right', pts: [V.C, V.LR, V.UR, V.T] },
];
const GLASS_FRONT = [
{ cls: 'glass glass-top', pts: [V.UL, V.T, V.UR, V.C] },
{ cls: 'glass glass-right', pts: [V.UR, V.LR, V.B, V.C] },
{ cls: 'glass glass-left', pts: [V.UL, V.LL, V.B, V.C] },
];
// Liquid surface at fill f is a linear interp of the cube's
// bottom-face corners (f=0) toward its top-face corners (f=1).
// The 3 liquid sub-polygons clip the visible front faces at it.
const surface = (f) => ({
back: lerp(V.C, V.T, f),
tr: lerp(V.LR, V.UR, f),
front: lerp(V.B, V.C, f),
tl: lerp(V.LL, V.UL, f),
});
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, V.LR, V.B, s.front] },
{ cls: 'liquid-left', pts: [s.tl, V.LL, V.B, s.front] },
];
const SVGNS = 'http://www.w3.org/2000/svg';
const XHTMLNS = 'http://www.w3.org/1999/xhtml';
const svgEl = (tag, attrs = {}) => {
const e = document.createElementNS(SVGNS, tag);
for (const [k, v] of Object.entries(attrs)) e.setAttribute(k, String(v));
return e;
};
// Face-text transforms: outer translate re-anchors the cube
// origin (HTML demo's (0,0)) to the viewBox, then per-face
// `rotate skewX translate scaleY(ISO)` matches the HTML demo.
// Composed into a single transform per face and set directly on
// each <foreignObject> — no wrapping <g>.
const OUTER_TF =
`translate(${W/2 - ISO*(OX+1)*CUBE} ${H/2 - (0.5*(OX+1) + OY/ISO)*CUBE})`;
const tf = (rot, skew, tx, ty) =>
@@ -289,38 +255,15 @@
['left', tf( 30, 30, OX * CUBE, OY * CUBE)],
];
const SVGNS = 'http://www.w3.org/2000/svg';
const XHTMLNS = 'http://www.w3.org/1999/xhtml';
const svgEl = (tag, attrs = {}) => {
const e = document.createElementNS(SVGNS, tag);
for (const [k, v] of Object.entries(attrs)) e.setAttribute(k, String(v));
return e;
};
const ptsAttr = (pts) => pts.map(p => p.join(',')).join(' ');
const addPoly = (parent, cls, pts) =>
parent.appendChild(svgEl('polygon', { class: cls, points: ptsAttr(pts) }));
const logoSvg = document.getElementById('logo-template').content.querySelector('svg');
const template = document.getElementById('logo-template');
const makeLogo = ({ fill = 0.5, faceColor, cubeScheme } = {}) => {
const slot = template.content.cloneNode(true).firstElementChild;
const svg = slot.querySelector('svg');
svg.setAttribute('viewBox', `0 0 ${W} ${H}`);
svg.style.setProperty('--fill', String(fill));
if (faceColor) svg.style.setProperty('--face-color', faceColor);
svg.style.setProperty('color-scheme', cubeScheme ?? 'light');
for (const f of GLASS_REAR) addPoly(svg, f.cls, f.pts);
for (const f of liquidPolygons(fill)) addPoly(svg, f.cls, f.pts);
for (const f of GLASS_FRONT) addPoly(svg, f.cls, f.pts);
for (const [face, transform] of TEXT_FACES) {
const fo = svgEl('foreignObject', { width: CUBE, height: CUBE, transform });
const div = document.createElementNS(XHTMLNS, 'div');
div.setAttribute('class', `face-text ${face}`);
fo.appendChild(div);
svg.appendChild(fo);
}
const makeLogo = ({ fill, theme } = {}) => {
const slot = document.createElement('div');
slot.className = 'logo-slot';
const svg = logoSvg.cloneNode(true);
if (fill !== undefined) svg.style.setProperty('--fill', String(fill));
if (theme) svg.setAttribute('data-theme', theme);
slot.appendChild(svg);
return slot;
};
@@ -335,37 +278,30 @@
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-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 })));
// Row 3: cube variants × bg theme (same matrix as demo.html).
const variants = document.getElementById('variants');
const neutLight = { faceColor: 'var(--border-color)', cubeScheme: 'light' };
const neutDark = { faceColor: 'var(--border-color)', cubeScheme: 'dark' };
const variantTiles = [
{ label: 'light / light', bg: 'scheme-light', opts: neutLight },
{ label: 'light / dark', bg: 'scheme-dark', opts: neutLight },
{ label: 'dark / light', bg: 'scheme-light', opts: neutDark },
{ label: 'dark / dark', bg: 'scheme-dark', opts: neutDark },
{ label: 'light / light', bg: 'scheme-light', opts: { theme: 'light' } },
{ label: 'light / dark', bg: 'scheme-dark', opts: { theme: 'light' } },
{ label: 'dark / light', bg: 'scheme-light', opts: { theme: 'dark' } },
{ label: 'dark / dark', bg: 'scheme-dark', opts: { theme: 'dark' } },
{ label: 'orange / light', bg: 'scheme-light', opts: {} },
{ label: 'orange / dark', bg: 'scheme-dark', opts: {} },
];
for (const v of variantTiles)
variants.appendChild(makeTile(`bg-auto ${v.bg}`, v.label, makeLogo(v.opts)));
// Row 4: text-in-cube — same variant matrix as row 3 with realistic
// block content (same as demo.html's `with-text` section).
const sampleBlocks = [
{ height: 912345, date: '2026-04-17', time: '12:00:00', miner: 'Foundry USA', mid: 12, min: 1, max: 64 },
{ height: 912346, date: '2026-04-17', time: '12:10:23', miner: 'AntPool', mid: 8, min: 2, max: 21 },
@@ -379,19 +315,16 @@
const [, m, d] = iso.split('-').map(Number);
return `${MONTHS[m - 1]} ${d}`;
};
// Tiny DOM builders.
const el = (tag, cls, text) => {
const e = document.createElement(tag);
if (cls) e.className = cls;
if (text != null) e.textContent = text;
return e;
};
const p = (text, cls) => el('p', cls, text);
const span = (text, cls) => el('span', cls, text);
const slug = (s) => s.toLowerCase().replace(/\s+/g, '');
const p = (text, cls) => el('p', cls, text);
const span = (text, cls) => el('span', cls, text);
const slug = (s) => s.toLowerCase().replace(/\s+/g, '');
// Mirror of website/scripts/explorer/render.js createHeightElement:
// dimmed "#000…" prefix padding the height to 7 digits.
const heightSpan = (h) => {
const s = String(h);
const prefix = span('#' + '0'.repeat(Math.max(0, 7 - s.length)), 'dim');
@@ -403,37 +336,44 @@
const makeBlockCube = (opts, block) => {
const slot = makeLogo(opts);
const q = (sel) => slot.querySelector(sel);
const [hh, mm] = block.time.slice(0, 5).split(':');
const svg = slot.querySelector('svg');
// Top: date / HH:MM (colon dimmed).
const faces = {};
for (const [face, transform] of TEXT_FACES) {
const fo = svgEl('foreignObject', { width: CUBE, height: CUBE, transform });
const div = document.createElementNS(XHTMLNS, 'div');
div.setAttribute('class', `face-text ${face}`);
fo.appendChild(div);
svg.appendChild(fo);
faces[face] = div;
}
const [hh, mm] = block.time.slice(0, 5).split(':');
const timeP = p();
timeP.append(hh, span(':', 'dim'), mm);
q('.face-text.top').append(p(shortDate(block.date)), timeP);
faces.top.append(p(shortDate(block.date)), timeP);
// Right: height (sm) / raw pool-logo + miner name (ellipsis-clipped).
const heightP = p(null, 'height');
heightP.appendChild(heightSpan(block.height));
const poolDiv = el('div', 'pool');
const logo = el('img', slug(block.miner));
const nameSpan = span(block.miner.replace(/\s+(Pool|USA)$/i, '').trim());
poolDiv.append(logo, nameSpan);
q('.face-text.right').append(heightP, poolDiv);
faces.right.append(heightP, poolDiv);
// Left: ~median / min-max / sat/vB (dash + unit dimmed).
const range = p();
range.append(String(block.min), span('-', 'dim'), String(block.max));
const fees = el('div', 'fees');
fees.append(p(`~${block.mid}`), range, p('sat/vB', 'dim'));
q('.face-text.left').append(fees);
faces.left.append(fees);
return slot;
};
const withText = document.getElementById('with-text');
variantTiles.forEach((v, i) =>
withText.appendChild(makeTile(`bg-auto ${v.bg}`, v.label, makeBlockCube(v.opts, sampleBlocks[i])))
);
</script>
</body>
</html>
+37
View File
@@ -0,0 +1,37 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 173.2 200" role="img" aria-label="bitview">
<title>bitview</title>
<style>
.glass { fill-opacity: 0.4; }
.glass-top, .liquid-top { fill: oklch(26.6% 0 0); }
.glass-right, .liquid-right { fill: oklch(20% 0 0); }
.glass-left, .liquid-left { fill: oklch(23.3% 0 0); }
.glass-bottom { fill: oklch(10.1% 0 0); }
.glass-rear-left { fill: oklch(26.6% 0 0); }
.glass-rear-right { fill: oklch(23.3% 0 0); }
.liquid {
transform-box: view-box;
transform: translateY(25%);
}
</style>
<defs>
<clipPath id="hex" clipPathUnits="userSpaceOnUse">
<polygon points="86.6,0 173.2,50 173.2,150 86.6,200 0,150 0,50"/>
</clipPath>
</defs>
<polygon class="glass glass-bottom" points="86.6,100 173.2,150 86.6,200 0,150"/>
<polygon class="glass glass-rear-left" points="86.6,100 0,150 0,50 86.6,0"/>
<polygon class="glass glass-rear-right" points="86.6,100 173.2,150 173.2,50 86.6,0"/>
<g clip-path="url(#hex)">
<g class="liquid">
<polygon class="liquid-top" points="86.6,0 173.2,50 86.6,100 0,50"/>
<polygon class="liquid-right" points="173.2,50 173.2,150 86.6,200 86.6,100"/>
<polygon class="liquid-left" points="0,50 0,150 86.6,200 86.6,100"/>
</g>
</g>
<polygon class="glass glass-top" points="0,50 86.6,0 173.2,50 86.6,100"/>
<polygon class="glass glass-right" points="173.2,50 173.2,150 86.6,200 86.6,100"/>
<polygon class="glass glass-left" points="0,50 0,150 86.6,200 86.6,100"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

+37
View File
@@ -0,0 +1,37 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 173.2 200" role="img" aria-label="bitview">
<title>bitview</title>
<style>
.glass { fill-opacity: 0.4; }
.glass-top, .liquid-top { fill: oklch(90% 0 0); }
.glass-right, .liquid-right { fill: oklch(83.4% 0 0); }
.glass-left, .liquid-left { fill: oklch(86.7% 0 0); }
.glass-bottom { fill: oklch(80.1% 0 0); }
.glass-rear-left { fill: oklch(90% 0 0); }
.glass-rear-right { fill: oklch(86.7% 0 0); }
.liquid {
transform-box: view-box;
transform: translateY(25%);
}
</style>
<defs>
<clipPath id="hex" clipPathUnits="userSpaceOnUse">
<polygon points="86.6,0 173.2,50 173.2,150 86.6,200 0,150 0,50"/>
</clipPath>
</defs>
<polygon class="glass glass-bottom" points="86.6,100 173.2,150 86.6,200 0,150"/>
<polygon class="glass glass-rear-left" points="86.6,100 0,150 0,50 86.6,0"/>
<polygon class="glass glass-rear-right" points="86.6,100 173.2,150 173.2,50 86.6,0"/>
<g clip-path="url(#hex)">
<g class="liquid">
<polygon class="liquid-top" points="86.6,0 173.2,50 86.6,100 0,50"/>
<polygon class="liquid-right" points="173.2,50 173.2,150 86.6,200 86.6,100"/>
<polygon class="liquid-left" points="0,50 0,150 86.6,200 86.6,100"/>
</g>
</g>
<polygon class="glass glass-top" points="0,50 86.6,0 173.2,50 86.6,100"/>
<polygon class="glass glass-right" points="173.2,50 173.2,150 86.6,200 86.6,100"/>
<polygon class="glass glass-left" points="0,50 0,150 86.6,200 86.6,100"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

+37
View File
@@ -0,0 +1,37 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 173.2 200" role="img" aria-label="bitview">
<title>bitview</title>
<style>
.glass { fill-opacity: 0.4; }
.glass-top, .liquid-top { fill: oklch(67.64% 0.191 44.41); }
.glass-right, .liquid-right { fill: oklch(61.04% 0.191 44.41); }
.glass-left, .liquid-left { fill: oklch(64.34% 0.191 44.41); }
.glass-bottom { fill: oklch(57.74% 0.191 44.41); }
.glass-rear-left { fill: oklch(67.64% 0.191 44.41); }
.glass-rear-right { fill: oklch(64.34% 0.191 44.41); }
.liquid {
transform-box: view-box;
transform: translateY(25%);
}
</style>
<defs>
<clipPath id="hex" clipPathUnits="userSpaceOnUse">
<polygon points="86.6,0 173.2,50 173.2,150 86.6,200 0,150 0,50"/>
</clipPath>
</defs>
<polygon class="glass glass-bottom" points="86.6,100 173.2,150 86.6,200 0,150"/>
<polygon class="glass glass-rear-left" points="86.6,100 0,150 0,50 86.6,0"/>
<polygon class="glass glass-rear-right" points="86.6,100 173.2,150 173.2,50 86.6,0"/>
<g clip-path="url(#hex)">
<g class="liquid">
<polygon class="liquid-top" points="86.6,0 173.2,50 86.6,100 0,50"/>
<polygon class="liquid-right" points="173.2,50 173.2,150 86.6,200 86.6,100"/>
<polygon class="liquid-left" points="0,50 0,150 86.6,200 86.6,100"/>
</g>
</g>
<polygon class="glass glass-top" points="0,50 86.6,0 173.2,50 86.6,100"/>
<polygon class="glass glass-right" points="173.2,50 173.2,150 86.6,200 86.6,100"/>
<polygon class="glass glass-left" points="0,50 0,150 86.6,200 86.6,100"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

+57
View File
@@ -0,0 +1,57 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 173.2 200" role="img" aria-label="bitview">
<title>bitview</title>
<style>
svg {
--fill: 0.5;
--face-top: oklch(67.64% 0.191 44.41);
--face-left: oklch(64.34% 0.191 44.41);
--face-right: oklch(61.04% 0.191 44.41);
--face-bottom: oklch(57.74% 0.191 44.41);
}
svg[data-theme="light"] {
--face-top: oklch(90% 0 0);
--face-left: oklch(86.7% 0 0);
--face-right: oklch(83.4% 0 0);
--face-bottom: oklch(80.1% 0 0);
}
svg[data-theme="dark"] {
--face-top: oklch(26.6% 0 0);
--face-left: oklch(23.3% 0 0);
--face-right: oklch(20% 0 0);
--face-bottom: oklch(10.1% 0 0);
}
.glass { fill-opacity: 0.4; }
.glass-top, .liquid-top { fill: var(--face-top); }
.glass-right, .liquid-right { fill: var(--face-right); }
.glass-left, .liquid-left { fill: var(--face-left); }
.glass-bottom { fill: var(--face-bottom); }
.glass-rear-left { fill: var(--face-top); }
.glass-rear-right { fill: var(--face-left); }
.liquid {
transform-box: view-box;
transform: translateY(calc((1 - var(--fill)) * 50%));
opacity: ceil(var(--fill));
}
</style>
<defs>
<clipPath id="hex" clipPathUnits="userSpaceOnUse">
<polygon points="86.6,0 173.2,50 173.2,150 86.6,200 0,150 0,50"/>
</clipPath>
</defs>
<polygon class="glass glass-bottom" points="86.6,100 173.2,150 86.6,200 0,150"/>
<polygon class="glass glass-rear-left" points="86.6,100 0,150 0,50 86.6,0"/>
<polygon class="glass glass-rear-right" points="86.6,100 173.2,150 173.2,50 86.6,0"/>
<g clip-path="url(#hex)">
<g class="liquid">
<polygon class="liquid-top" points="86.6,0 173.2,50 86.6,100 0,50"/>
<polygon class="liquid-right" points="173.2,50 173.2,150 86.6,200 86.6,100"/>
<polygon class="liquid-left" points="0,50 0,150 86.6,200 86.6,100"/>
</g>
</g>
<polygon class="glass glass-top" points="0,50 86.6,0 173.2,50 86.6,100"/>
<polygon class="glass glass-right" points="173.2,50 173.2,150 86.6,200 86.6,100"/>
<polygon class="glass glass-left" points="0,50 0,150 86.6,200 86.6,100"/>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 562 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

+5 -7
View File
@@ -96,13 +96,11 @@
</script>
<script type="module" src="/scripts/entry.js"></script>
<link
rel="icon"
type="image/png"
sizes="196x196"
href="/assets/favicon-196.png"
/>
<link rel="apple-touch-icon" href="/assets/apple-icon-180.png" />
<link rel="icon" type="image/svg+xml" href="/assets/favicon/favicon.svg" />
<link rel="icon" type="image/png" sizes="96x96" href="/assets/favicon/favicon-96x96.png" />
<link rel="shortcut icon" href="/assets/favicon/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/assets/favicon/apple-touch-icon.png" />
<meta name="apple-mobile-web-app-title" content="bitview" />
<meta name="apple-mobile-web-app-capable" content="yes" />
</head>
<body>
+4 -4
View File
@@ -16,25 +16,25 @@
"lang": "en",
"icons": [
{
"src": "/assets/manifest-icon-192.maskable.png",
"src": "/assets/favicon/web-app-manifest-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "/assets/manifest-icon-192.maskable.png",
"src": "/assets/favicon/web-app-manifest-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/assets/manifest-icon-512.maskable.png",
"src": "/assets/favicon/web-app-manifest-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
"src": "/assets/manifest-icon-512.maskable.png",
"src": "/assets/favicon/web-app-manifest-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
+1
View File
@@ -131,6 +131,7 @@
* @typedef {Brk.AddrUtxoPattern} AvgAmountPattern
* @typedef {Brk.SeriesTree_Addrs_Exposed} ExposedTree
* @typedef {Brk.SeriesTree_Addrs_Reused} ReusedTree
* @typedef {Brk.SeriesTree_Addrs_Respent} RespentTree
*/
/**
@@ -178,6 +178,7 @@ export function buildCohortData() {
avgAmount: addrs.avgAmount[key],
exposed: addrs.exposed,
reused: addrs.reused,
respent: addrs.respent,
};
});
@@ -264,7 +264,7 @@ export function createCohortFolderAddress(cohort) {
createProfitabilitySectionWithProfitLoss({ cohort, title }),
createActivitySectionMinimal({ cohort, title }),
avgHoldingsSubtree(cohort.avgAmount, title),
reusedSubtree(cohort.reused, cohort.key, title),
reusedSubtree(cohort.reused, cohort.respent, cohort.key, title),
exposedSubtree(cohort.exposed, cohort.key, title),
],
};
+364 -259
View File
@@ -103,154 +103,254 @@ export function createNetworkSection() {
{ key: "total", name: "Total", color: colors.default },
]);
const reusedSetEntries =
/**
* @param {AddressableType | "all"} key
* @param {(name: string) => string} title
*/
(key, title) => [
{
name: "Compare",
title: title("Reused Address Count"),
bottom: [
line({
series: addrs.reused.count.funded[key],
name: "Funded",
unit: Unit.count,
}),
line({
series: addrs.reused.count.total[key],
name: "Total",
color: colors.gray,
unit: Unit.count,
}),
],
},
{
name: "Funded",
title: title("Funded Reused Addresses"),
bottom: [
line({
series: addrs.reused.count.funded[key],
name: "Funded Reused",
unit: Unit.count,
}),
],
},
{
name: "Total",
title: title("Total Reused Addresses"),
bottom: [
line({
series: addrs.reused.count.total[key],
name: "Total Reused",
color: colors.gray,
unit: Unit.count,
}),
],
},
];
const reusedInputsSubtree =
/**
* @param {AddressableType | "all"} key
* @param {(name: string) => string} title
*/
(key, title) => ({
name: "Inputs",
tree: [
{
name: "Count",
tree: chartsFromCount({
pattern: addrs.reused.events.inputFromReusedAddrCount[key],
title,
metric: "Transaction Inputs from Reused Addresses",
unit: Unit.count,
}),
},
{
name: "Share",
tree: chartsFromPercentCumulative({
pattern: addrs.reused.events.inputFromReusedAddrShare[key],
title,
metric: "Share of Transaction Inputs from Reused Addresses",
}),
},
],
});
const reusedOutputsSubtreeForAll =
/** @param {(name: string) => string} title */
(title) => ({
name: "Outputs",
tree: [
{
name: "Count",
tree: chartsFromCount({
pattern: addrs.reused.events.outputToReusedAddrCount.all,
title,
metric: "Transaction Outputs to Reused Addresses",
unit: Unit.count,
}),
},
{
name: "Share",
tree: chartsFromPercentCumulativeEntries({
entries: [
{
name: "All",
pattern: addrs.reused.events.outputToReusedAddrShare.all,
},
{
name: "Spendable",
pattern: addrs.reused.events.spendableOutputToReusedAddrShare,
color: colors.gray,
},
],
title,
metric: "Share of Transaction Outputs to Reused Addresses",
}),
},
],
});
const reusedActiveSubtreeForAll =
/** @param {(name: string) => string} title */
(title) => ({
name: "Active",
tree: [
{
name: "Count",
tree: averagesArray({
windows: addrs.reused.events.activeReusedAddrCount,
title,
metric: "Active Reused Addresses",
unit: Unit.count,
}),
},
{
name: "Share",
tree: averagesArray({
windows: addrs.reused.events.activeReusedAddrShare,
title,
metric: "Active Reused Address Share",
unit: Unit.percentage,
}),
},
],
});
const reusedSubtreeForAll =
/** @param {(name: string) => string} title */
(title) => ({
name: "Reused",
tree: [
...reusedSetEntries("all", title),
reusedActiveSubtreeForAll(title),
reusedOutputsSubtreeForAll(title),
reusedInputsSubtree("all", title),
],
});
(title) => {
const reused = addrs.reused;
const respent = addrs.respent;
const key = /** @type {const} */ ("all");
/**
* Windowed sums + cumulative, overlaying reused (primary) and respent (gray).
* @param {CountPattern<number>} reusedPattern
* @param {CountPattern<number>} respentPattern
* @param {string} metric
* @returns {PartialOptionsTree}
*/
const countPair = (reusedPattern, respentPattern, metric) => [
...ROLLING_WINDOWS.map((w) => ({
name: w.name,
title: title(`${w.title} ${metric}`),
bottom: [
line({
series: reusedPattern.sum[w.key],
name: "2+ Funded",
unit: Unit.count,
}),
line({
series: respentPattern.sum[w.key],
name: "2+ Spent",
color: colors.gray,
unit: Unit.count,
}),
],
})),
{
name: "Cumulative",
title: title(`Cumulative ${metric}`),
bottom: [
line({
series: reusedPattern.cumulative,
name: "2+ Funded",
unit: Unit.count,
}),
line({
series: respentPattern.cumulative,
name: "2+ Spent",
color: colors.gray,
unit: Unit.count,
}),
],
},
];
return {
name: "Reused",
tree: [
{
name: "Funded",
title: title("Funded Reused Addresses"),
bottom: [
line({
series: reused.count.funded[key],
name: "2+ Funded",
unit: Unit.count,
}),
line({
series: respent.count.funded[key],
name: "2+ Spent",
color: colors.gray,
unit: Unit.count,
}),
],
},
{
name: "Total",
title: title("Total Reused Addresses"),
bottom: [
line({
series: reused.count.total[key],
name: "2+ Funded",
unit: Unit.count,
}),
line({
series: respent.count.total[key],
name: "2+ Spent",
color: colors.gray,
unit: Unit.count,
}),
],
},
{
name: "Active",
tree: [
{
name: "Count",
tree: ROLLING_WINDOWS.map((w) => ({
name: w.name,
title: title(`${w.title} Active Reused Addresses`),
bottom: [
line({
series: reused.events.activeReusedAddrCount[w.key],
name: "2+ Funded",
unit: Unit.count,
}),
line({
series: respent.events.activeReusedAddrCount[w.key],
name: "2+ Spent",
color: colors.gray,
unit: Unit.count,
}),
],
})),
},
{
name: "Share",
tree: ROLLING_WINDOWS.map((w) => ({
name: w.name,
title: title(`${w.title} Active Reused Address Share`),
bottom: [
line({
series: reused.events.activeReusedAddrShare[w.key],
name: "2+ Funded",
unit: Unit.percentage,
}),
line({
series: respent.events.activeReusedAddrShare[w.key],
name: "2+ Spent",
color: colors.gray,
unit: Unit.percentage,
}),
],
})),
},
],
},
{
name: "Outputs",
tree: [
{
name: "Count",
tree: countPair(
reused.events.outputToReusedAddrCount[key],
respent.events.outputToReusedAddrCount[key],
"Transaction Outputs to Reused Addresses",
),
},
{
name: "Share",
tree: [
{
name: "All",
tree: chartsFromPercentCumulativeEntries({
entries: [
{
name: "2+ Funded",
pattern: reused.events.outputToReusedAddrShare[key],
},
{
name: "2+ Spent",
pattern: respent.events.outputToReusedAddrShare[key],
color: colors.gray,
},
],
title,
metric: "Share of Transaction Outputs to Reused Addresses",
}),
},
{
name: "Spendable",
tree: chartsFromPercentCumulativeEntries({
entries: [
{
name: "2+ Funded",
pattern: reused.events.spendableOutputToReusedAddrShare,
},
{
name: "2+ Spent",
pattern: respent.events.spendableOutputToReusedAddrShare,
color: colors.gray,
},
],
title,
metric: "Share of Spendable Transaction Outputs to Reused Addresses",
}),
},
],
},
],
},
{
name: "Inputs",
tree: [
{
name: "Count",
tree: countPair(
reused.events.inputFromReusedAddrCount[key],
respent.events.inputFromReusedAddrCount[key],
"Transaction Inputs from Reused Addresses",
),
},
{
name: "Share",
tree: chartsFromPercentCumulativeEntries({
entries: [
{
name: "2+ Funded",
pattern: reused.events.inputFromReusedAddrShare[key],
},
{
name: "2+ Spent",
pattern: respent.events.inputFromReusedAddrShare[key],
color: colors.gray,
},
],
title,
metric: "Share of Transaction Inputs from Reused Addresses",
}),
},
],
},
{
name: "Supply",
title: title("Supply in Reused Addresses"),
bottom: [
...satsBtcUsd({ pattern: reused.supply[key], name: "2+ Funded" }),
...satsBtcUsd({
pattern: respent.supply[key],
name: "2+ Spent",
color: colors.gray,
}),
],
},
{
name: "Share",
title: title("Share of Supply in Reused Addresses"),
bottom: [
...percentRatio({
pattern: reused.supply.share[key],
name: "2+ Funded",
}),
...percentRatio({
pattern: respent.supply.share[key],
name: "2+ Spent",
color: colors.gray,
}),
],
},
],
};
};
const countSubtree =
@@ -328,6 +428,12 @@ export function createNetworkSection() {
name: "Reused Share",
unit: Unit.percentage,
}),
line({
series: addrs.respent.events.activeReusedAddrShare[w.key],
name: "Respent Share",
color: colors.gray,
unit: Unit.percentage,
}),
],
})),
},
@@ -413,7 +519,7 @@ export function createNetworkSection() {
unit: Unit.count,
}),
activitySubtreeForType(addrType, title),
reusedSubtree(addrs.reused, addrType, title),
reusedSubtree(addrs.reused, addrs.respent, addrType, title),
exposedSubtree(addrs.exposed, addrType, title),
avgHoldingsSubtree(addrs.avgAmount[addrType], title),
];
@@ -458,6 +564,113 @@ export function createNetworkSection() {
}),
);
const reuseCompareByTypeFolder =
/**
* @param {string} label - "Reused" or "Respent"
* @param {ReusedTree | RespentTree} tree
* @returns {PartialOptionsGroup}
*/
(label, tree) => ({
name: label,
tree: [
{
name: "Funded",
title: `Funded ${label} Address Count by Type`,
bottom: typeLines((t) => tree.count.funded[t.key]),
},
{
name: "Total",
title: `Total ${label} Address Count by Type`,
bottom: typeLines((t) => tree.count.total[t.key]),
},
{
name: "Outputs",
tree: [
{
name: "Count",
tree: groupedWindowsCumulative({
list: addressTypes,
title: (s) => s,
metricTitle: `Transaction Outputs to ${label} Addresses by Type`,
getWindowSeries: (t, key) =>
tree.events.outputToReusedAddrCount[t.key].sum[key],
getCumulativeSeries: (t) =>
tree.events.outputToReusedAddrCount[t.key].cumulative,
seriesFn: line,
unit: Unit.count,
}),
},
{
name: "Share",
tree: [
...ROLLING_WINDOWS.map((w) => ({
name: w.name,
title: `${w.title} Share of Transaction Outputs to ${label} Addresses by Type`,
bottom: typeLines(
(t) =>
tree.events.outputToReusedAddrShare[t.key][w.key]
.percent,
Unit.percentage,
),
})),
{
name: "Cumulative",
title: `Cumulative Share of Transaction Outputs to ${label} Addresses by Type`,
bottom: typeLines(
(t) =>
tree.events.outputToReusedAddrShare[t.key].percent,
Unit.percentage,
),
},
],
},
],
},
{
name: "Inputs",
tree: [
{
name: "Count",
tree: groupedWindowsCumulative({
list: addressTypes,
title: (s) => s,
metricTitle: `Transaction Inputs from ${label} Addresses by Type`,
getWindowSeries: (t, key) =>
tree.events.inputFromReusedAddrCount[t.key].sum[key],
getCumulativeSeries: (t) =>
tree.events.inputFromReusedAddrCount[t.key].cumulative,
seriesFn: line,
unit: Unit.count,
}),
},
{
name: "Share",
tree: [
...ROLLING_WINDOWS.map((w) => ({
name: w.name,
title: `${w.title} Share of Transaction Inputs from ${label} Addresses by Type`,
bottom: typeLines(
(t) =>
tree.events.inputFromReusedAddrShare[t.key][w.key]
.percent,
Unit.percentage,
),
})),
{
name: "Cumulative",
title: `Cumulative Share of Transaction Inputs from ${label} Addresses by Type`,
bottom: typeLines(
(t) => tree.events.inputFromReusedAddrShare[t.key].percent,
Unit.percentage,
),
},
],
},
],
},
],
});
return {
name: "Compare",
tree: [
@@ -525,120 +738,12 @@ export function createNetworkSection() {
})),
},
// Reused
// Address Reuse: receive-side (Reused) + spend-side (Respent)
{
name: "Reused",
name: "Address Reuse",
tree: [
{
name: "Funded",
title: "Funded Reused Address Count by Type",
bottom: typeLines((t) => addrs.reused.count.funded[t.key]),
},
{
name: "Total",
title: "Total Reused Address Count by Type",
bottom: typeLines((t) => addrs.reused.count.total[t.key]),
},
{
name: "Outputs",
tree: [
{
name: "Count",
tree: groupedWindowsCumulative({
list: addressTypes,
title: (s) => s,
metricTitle:
"Transaction Outputs to Reused Addresses by Type",
getWindowSeries: (t, key) =>
addrs.reused.events.outputToReusedAddrCount[t.key].sum[
key
],
getCumulativeSeries: (t) =>
addrs.reused.events.outputToReusedAddrCount[t.key]
.cumulative,
seriesFn: line,
unit: Unit.count,
}),
},
{
name: "Share",
tree: [
...ROLLING_WINDOWS.map((w) => ({
name: w.name,
title: `${w.title} Share of Transaction Outputs to Reused Addresses by Type`,
bottom: typeLines(
(t) =>
addrs.reused.events.outputToReusedAddrShare[t.key][
w.key
].percent,
Unit.percentage,
),
})),
{
name: "Cumulative",
title:
"Cumulative Share of Transaction Outputs to Reused Addresses by Type",
bottom: typeLines(
(t) =>
addrs.reused.events.outputToReusedAddrShare[t.key]
.percent,
Unit.percentage,
),
},
],
},
],
},
{
name: "Inputs",
tree: [
{
name: "Count",
tree: groupedWindowsCumulative({
list: addressTypes,
title: (s) => s,
metricTitle:
"Transaction Inputs from Reused Addresses by Type",
getWindowSeries: (t, key) =>
addrs.reused.events.inputFromReusedAddrCount[t.key].sum[
key
],
getCumulativeSeries: (t) =>
addrs.reused.events.inputFromReusedAddrCount[t.key]
.cumulative,
seriesFn: line,
unit: Unit.count,
}),
},
{
name: "Share",
tree: [
...ROLLING_WINDOWS.map((w) => ({
name: w.name,
title: `${w.title} Share of Transaction Inputs from Reused Addresses by Type`,
bottom: typeLines(
(t) =>
addrs.reused.events.inputFromReusedAddrShare[t.key][
w.key
].percent,
Unit.percentage,
),
})),
{
name: "Cumulative",
title:
"Cumulative Share of Transaction Inputs from Reused Addresses by Type",
bottom: typeLines(
(t) =>
addrs.reused.events.inputFromReusedAddrShare[t.key]
.percent,
Unit.percentage,
),
},
],
},
],
},
reuseCompareByTypeFolder("Reused", addrs.reused),
reuseCompareByTypeFolder("Respent", addrs.respent),
],
},
+110 -36
View File
@@ -7,8 +7,7 @@ import {
baseline,
price,
percentRatio,
chartsFromCount,
chartsFromPercentCumulative,
chartsFromPercentCumulativeEntries,
sumsAndAveragesCumulativeWith,
} from "./series.js";
import { priceLine, priceLines } from "./constants.js";
@@ -365,45 +364,78 @@ export function exposedSubtree(exposed, key, title) {
}
/**
* "Reused" subtree (per-type / per-cohort no "Active" window, since that
* data is only tracked globally). Shape:
* Compare (funded + total) / Funded / Total / Outputs / Inputs.
* "Reused" subtree (per-type / per-cohort, no "Active" window since that
* data is only tracked globally). Respent (addresses whose outputs have
* been spent more than once) is a subset of reused, so each chart layers
* both series in two colors: reused in the primary color, respent in
* gray. Shape: Funded / Total / Outputs / Inputs / Supply / Share.
* @param {ReusedTree} reused
* @param {RespentTree} respent
* @param {AddressableType | "all"} key
* @param {(name: string) => string} title
* @returns {PartialOptionsGroup}
*/
export function reusedSubtree(reused, key, title) {
export function reusedSubtree(reused, respent, key, title) {
/**
* Windowed sums + cumulative, overlaying reused (primary) and respent (gray).
* @param {CountPattern<number>} reusedPattern
* @param {CountPattern<number>} respentPattern
* @param {string} metric
* @returns {PartialOptionsTree}
*/
const countPair = (reusedPattern, respentPattern, metric) => [
...ROLLING_WINDOWS.map((w) => ({
name: w.name,
title: title(`${w.title} ${metric}`),
bottom: [
line({ series: reusedPattern.sum[w.key], name: "2+ Funded", unit: Unit.count }),
line({
series: respentPattern.sum[w.key],
name: "2+ Spent",
color: colors.gray,
unit: Unit.count,
}),
],
})),
{
name: "Cumulative",
title: title(`Cumulative ${metric}`),
bottom: [
line({ series: reusedPattern.cumulative, name: "2+ Funded", unit: Unit.count }),
line({
series: respentPattern.cumulative,
name: "2+ Spent",
color: colors.gray,
unit: Unit.count,
}),
],
},
];
return {
name: "Reused",
tree: [
{
name: "Compare",
title: title("Reused Address Count"),
name: "Funded",
title: title("Funded Reused Addresses"),
bottom: [
line({ series: reused.count.funded[key], name: "Funded", unit: Unit.count }),
line({ series: reused.count.funded[key], name: "2+ Funded", unit: Unit.count }),
line({
series: reused.count.total[key],
name: "Total",
series: respent.count.funded[key],
name: "2+ Spent",
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: "2+ Funded", unit: Unit.count }),
line({
series: reused.count.total[key],
name: "Total Reused",
series: respent.count.total[key],
name: "2+ Spent",
color: colors.gray,
unit: Unit.count,
}),
@@ -414,17 +446,26 @@ export function reusedSubtree(reused, key, title) {
tree: [
{
name: "Count",
tree: chartsFromCount({
pattern: reused.events.outputToReusedAddrCount[key],
title,
metric: "Transaction Outputs to Reused Addresses",
unit: Unit.count,
}),
tree: countPair(
reused.events.outputToReusedAddrCount[key],
respent.events.outputToReusedAddrCount[key],
"Transaction Outputs to Reused Addresses",
),
},
{
name: "Share",
tree: chartsFromPercentCumulative({
pattern: reused.events.outputToReusedAddrShare[key],
tree: chartsFromPercentCumulativeEntries({
entries: [
{
name: "2+ Funded",
pattern: reused.events.outputToReusedAddrShare[key],
},
{
name: "2+ Spent",
pattern: respent.events.outputToReusedAddrShare[key],
color: colors.gray,
},
],
title,
metric: "Share of Transaction Outputs to Reused Addresses",
}),
@@ -436,23 +477,56 @@ export function reusedSubtree(reused, key, title) {
tree: [
{
name: "Count",
tree: chartsFromCount({
pattern: reused.events.inputFromReusedAddrCount[key],
title,
metric: "Transaction Inputs from Reused Addresses",
unit: Unit.count,
}),
tree: countPair(
reused.events.inputFromReusedAddrCount[key],
respent.events.inputFromReusedAddrCount[key],
"Transaction Inputs from Reused Addresses",
),
},
{
name: "Share",
tree: chartsFromPercentCumulative({
pattern: reused.events.inputFromReusedAddrShare[key],
tree: chartsFromPercentCumulativeEntries({
entries: [
{
name: "2+ Funded",
pattern: reused.events.inputFromReusedAddrShare[key],
},
{
name: "2+ Spent",
pattern: respent.events.inputFromReusedAddrShare[key],
color: colors.gray,
},
],
title,
metric: "Share of Transaction Inputs from Reused Addresses",
}),
},
],
},
{
name: "Supply",
title: title("Supply in Reused Addresses"),
bottom: [
...satsBtcUsd({ pattern: reused.supply[key], name: "2+ Funded" }),
...satsBtcUsd({
pattern: respent.supply[key],
name: "2+ Spent",
color: colors.gray,
}),
],
},
{
name: "Share",
title: title("Share of Supply in Reused Addresses"),
bottom: [
...percentRatio({ pattern: reused.supply.share[key], name: "2+ Funded" }),
...percentRatio({
pattern: respent.supply.share[key],
name: "2+ Spent",
color: colors.gray,
}),
],
},
],
};
}
+1 -1
View File
@@ -252,7 +252,7 @@
* ============================================================================
*
* Addressable cohort with address count (for "type" cohorts - uses OutputsRealizedSupplyUnrealizedPattern2)
* @typedef {{ name: string, key: AddressableType, title: string, color: Color, tree: EmptyPattern, addressCount: AddrCountPattern, avgAmount: AvgAmountPattern, exposed: ExposedTree, reused: ReusedTree }} CohortAddr
* @typedef {{ name: string, key: AddressableType, title: string, color: Color, tree: EmptyPattern, addressCount: AddrCountPattern, avgAmount: AvgAmountPattern, exposed: ExposedTree, reused: ReusedTree, respent: RespentTree }} CohortAddr
*
* ============================================================================
* Cohort Group Types (by capability)