mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-06-10 06:53:33 -07:00
website: redesign part 17
This commit is contained in:
@@ -29,3 +29,4 @@ ALWAYS
|
||||
- easy to understand
|
||||
- maintainability
|
||||
- avoid defensive checks when the code itself guarantees correctness
|
||||
- prefer "@type {const}" + infer to "@type {TYPE}" or "@type {import().type}"
|
||||
|
||||
@@ -24,31 +24,21 @@ export const capitalizationSeries = createCohortSeries([
|
||||
},
|
||||
]);
|
||||
|
||||
export const marketCapSeries = createCohortSeries([
|
||||
{
|
||||
label: "Market cap",
|
||||
color: colors.green,
|
||||
metric: (client) => client.series.supply.marketCap.usd,
|
||||
},
|
||||
]);
|
||||
const [marketCap, realizedCap] = capitalizationSeries;
|
||||
|
||||
export const realizedCapSeries = createCohortSeries([
|
||||
{
|
||||
label: "Realized cap",
|
||||
color: colors.orange,
|
||||
metric: (client) => client.series.cohorts.utxo.all.realized.cap.usd,
|
||||
},
|
||||
]);
|
||||
export const marketCapSeries = [marketCap];
|
||||
|
||||
export const realizedCapSeries = [realizedCap];
|
||||
|
||||
export const marketCapTermSeries = createCohortSeries([
|
||||
{
|
||||
label: "STH",
|
||||
color: colors.sky,
|
||||
color: colors.yellow,
|
||||
metric: (client) => client.series.cohorts.utxo.sth.supply.total.usd,
|
||||
},
|
||||
{
|
||||
label: "LTH",
|
||||
color: colors.orange,
|
||||
color: colors.fuchsia,
|
||||
metric: (client) => client.series.cohorts.utxo.lth.supply.total.usd,
|
||||
},
|
||||
]);
|
||||
@@ -56,12 +46,12 @@ export const marketCapTermSeries = createCohortSeries([
|
||||
export const realizedCapTermSeries = createCohortSeries([
|
||||
{
|
||||
label: "STH",
|
||||
color: colors.sky,
|
||||
color: colors.yellow,
|
||||
metric: (client) => client.series.cohorts.utxo.sth.realized.cap.usd,
|
||||
},
|
||||
{
|
||||
label: "LTH",
|
||||
color: colors.orange,
|
||||
color: colors.fuchsia,
|
||||
metric: (client) => client.series.cohorts.utxo.lth.realized.cap.usd,
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
const suffixes = ["M", "B", "T", "P", "E", "Z", "Y"];
|
||||
|
||||
/**
|
||||
* @param {number} value
|
||||
* @param {number} digits
|
||||
@@ -11,6 +13,8 @@ function formatNumber(value, digits) {
|
||||
|
||||
/** @param {number} value */
|
||||
export function formatValue(value) {
|
||||
if (value === 0) return "0";
|
||||
|
||||
const absolute = Math.abs(value);
|
||||
|
||||
if (absolute < 10) return formatNumber(value, 3);
|
||||
@@ -20,7 +24,6 @@ export function formatValue(value) {
|
||||
if (absolute >= 1e27) return "Inf.";
|
||||
|
||||
const log = Math.floor(Math.log10(absolute) - 6);
|
||||
const suffixes = ["M", "B", "T", "P", "E", "Z", "Y"];
|
||||
const suffixIndex = Math.floor(log / 3);
|
||||
const digits = 3 - (log % 3);
|
||||
const scaled = value / (1_000_000 * 1_000 ** suffixIndex);
|
||||
|
||||
@@ -1,23 +1,28 @@
|
||||
/** @returns {SeriesNode} */
|
||||
function createSeriesNode() {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLElement[]} items
|
||||
* @param {HTMLElement} menu
|
||||
*/
|
||||
export function createSeriesHighlight(items) {
|
||||
const seriesNodes = items.map(createSeriesNode);
|
||||
export function createSeriesHighlight(items, menu) {
|
||||
const seriesNodes = /** @type {SeriesNode[]} */ (items.map(() => []));
|
||||
/** @type {number | undefined} */
|
||||
let previewIndex;
|
||||
|
||||
/** @param {number} index */
|
||||
function scrollToItem(index) {
|
||||
items[index].scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "nearest",
|
||||
inline: "nearest",
|
||||
});
|
||||
const itemRect = items[index].getBoundingClientRect();
|
||||
const menuRect = menu.getBoundingClientRect();
|
||||
|
||||
if (itemRect.left < menuRect.left) {
|
||||
menu.scrollBy({
|
||||
left: itemRect.left - menuRect.left,
|
||||
behavior: "smooth",
|
||||
});
|
||||
} else if (itemRect.right > menuRect.right) {
|
||||
menu.scrollBy({
|
||||
left: itemRect.right - menuRect.right,
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {number} index */
|
||||
|
||||
@@ -32,7 +32,7 @@ export function createChart(chart) {
|
||||
let currentTimeframe = getDefaultTimeframe(chartKey);
|
||||
let currentView = getDefaultView(chartKey);
|
||||
let currentScale = getDefaultScale(chartKey);
|
||||
const { legend, items, readout } = createLegend(chart);
|
||||
const { legend, menu, items, readout } = createLegend(chart);
|
||||
|
||||
figure.dataset.chart = "series";
|
||||
figure.dataset.timeframe = currentTimeframe;
|
||||
@@ -51,6 +51,7 @@ export function createChart(chart) {
|
||||
const renderer = createChartRenderer({
|
||||
svg,
|
||||
readout,
|
||||
menu,
|
||||
items,
|
||||
status,
|
||||
chart,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @param {Chart} chart
|
||||
* @returns {{ legend: HTMLElement, items: HTMLElement[], readout: Readout }}
|
||||
* @returns {{ legend: HTMLElement, menu: HTMLElement, items: HTMLElement[], readout: Readout }}
|
||||
*/
|
||||
export function createLegend(chart) {
|
||||
const legend = document.createElement("figcaption");
|
||||
@@ -30,7 +30,7 @@ export function createLegend(chart) {
|
||||
header.append(time);
|
||||
legend.append(header, menu);
|
||||
|
||||
return { legend, items, readout: { time, rows } };
|
||||
return { legend, menu, items, readout: { time, rows } };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,7 +4,7 @@ let groupId = 0;
|
||||
* @template {string} T
|
||||
* @param {Object} args
|
||||
* @param {string} args.legend
|
||||
* @param {{ value: T, label: string }[]} args.options
|
||||
* @param {readonly { value: T, label: string }[]} args.options
|
||||
* @param {T} args.currentValue
|
||||
* @param {(value: T) => void} args.onChange
|
||||
*/
|
||||
|
||||
@@ -48,6 +48,7 @@ function renderPlot(view, group, loadedSeries, height, highlight, scale) {
|
||||
* @param {Object} args
|
||||
* @param {SVGSVGElement} args.svg
|
||||
* @param {Readout} args.readout
|
||||
* @param {HTMLElement} args.menu
|
||||
* @param {HTMLElement[]} args.items
|
||||
* @param {HTMLElement} args.status
|
||||
* @param {Chart} args.chart
|
||||
@@ -58,6 +59,7 @@ function renderPlot(view, group, loadedSeries, height, highlight, scale) {
|
||||
export function createChartRenderer({
|
||||
svg,
|
||||
readout,
|
||||
menu,
|
||||
items,
|
||||
status,
|
||||
chart,
|
||||
@@ -66,7 +68,7 @@ export function createChartRenderer({
|
||||
getTimeframe,
|
||||
}) {
|
||||
const group = createSvgElement("g");
|
||||
const highlight = createSeriesHighlight(items);
|
||||
const highlight = createSeriesHighlight(items, menu);
|
||||
const loadSeries = createSeriesLoader(chart);
|
||||
/** @type {LoadedSeries[]} */
|
||||
let loadedSeries = [];
|
||||
|
||||
@@ -2,13 +2,11 @@ import { createRadioGroup } from "./radio.js";
|
||||
import { createChartStorage } from "./storage.js";
|
||||
|
||||
const storage = createChartStorage("scale");
|
||||
/** @type {ChartScale} */
|
||||
const defaultScale = "linear";
|
||||
/** @type {{ value: ChartScale, label: string }[]} */
|
||||
const scales = [
|
||||
const scales = /** @type {const} */ ([
|
||||
{ value: "linear", label: "Lin" },
|
||||
{ value: "log", label: "Log" },
|
||||
];
|
||||
]);
|
||||
|
||||
/** @param {string} chartKey */
|
||||
export function getDefaultScale(chartKey) {
|
||||
@@ -71,4 +69,4 @@ export function scaleY(value, bounds, height, scale) {
|
||||
* @property {number} minPositive
|
||||
*/
|
||||
|
||||
/** @typedef {"linear" | "log"} ChartScale */
|
||||
/** @typedef {(typeof scales)[number]["value"]} ChartScale */
|
||||
|
||||
@@ -6,16 +6,12 @@ main.learn {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 20rem;
|
||||
outline: 0;
|
||||
cursor: crosshair;
|
||||
overflow: visible;
|
||||
touch-action: pan-y;
|
||||
}
|
||||
|
||||
svg:focus-visible {
|
||||
outline: 1px solid var(--orange);
|
||||
outline-offset: 0.5rem;
|
||||
}
|
||||
|
||||
p[role="status"]:empty {
|
||||
display: none;
|
||||
}
|
||||
@@ -160,9 +156,9 @@ main.learn {
|
||||
|
||||
menu {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
padding: 0;
|
||||
padding-bottom: 1rem;
|
||||
padding-top: 0.25rem;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: thin;
|
||||
list-style: none;
|
||||
@@ -173,7 +169,7 @@ main.learn {
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0.25rem;
|
||||
padding: 0.25rem 0.375rem;
|
||||
border: 0;
|
||||
border-radius: 0.25rem;
|
||||
color: inherit;
|
||||
@@ -222,6 +218,8 @@ main.learn {
|
||||
> output {
|
||||
display: block;
|
||||
margin-top: 0.25rem;
|
||||
margin-left: auto;
|
||||
width: 7ch;
|
||||
min-height: 1em;
|
||||
color: var(--white);
|
||||
font-variant-numeric: tabular-nums;
|
||||
|
||||
@@ -2,10 +2,8 @@ import { createRadioGroup } from "./radio.js";
|
||||
import { createChartStorage } from "./storage.js";
|
||||
|
||||
const storage = createChartStorage("timeframe");
|
||||
/** @type {TimeframeValue} */
|
||||
const defaultTimeframe = "all";
|
||||
/** @type {Record<TimeframeValue, TimeframeConfig>} */
|
||||
const timeframes = {
|
||||
const timeframes = /** @type {const} */ ({
|
||||
"1d": { index: "minute10", count: 144 },
|
||||
"1w": { index: "hour1", count: 168 },
|
||||
"1m": { index: "hour4", count: 186 },
|
||||
@@ -13,9 +11,8 @@ const timeframes = {
|
||||
"4y": { index: "day3", count: 488 },
|
||||
"8y": { index: "week1", count: 418 },
|
||||
all: { index: "week1" },
|
||||
};
|
||||
/** @type {{ value: TimeframeValue, label: string }[]} */
|
||||
const options = [
|
||||
});
|
||||
const options = /** @type {const} */ ([
|
||||
{ value: "1d", label: "1d" },
|
||||
{ value: "1w", label: "1w" },
|
||||
{ value: "1m", label: "1m" },
|
||||
@@ -23,7 +20,7 @@ const options = [
|
||||
{ value: "4y", label: "4y" },
|
||||
{ value: "8y", label: "8y" },
|
||||
{ value: "all", label: "all" },
|
||||
];
|
||||
]);
|
||||
|
||||
/** @param {string} chartKey */
|
||||
export function getDefaultTimeframe(chartKey) {
|
||||
@@ -61,20 +58,16 @@ export function createTimeframeControl(currentTimeframe, onChange) {
|
||||
* @param {TimeframeValue} timeframe
|
||||
*/
|
||||
export function fetchTimeframe(metric, timeframe) {
|
||||
const { count, index } = timeframes[timeframe];
|
||||
const endpoint = metric.by[index];
|
||||
const config = timeframes[timeframe];
|
||||
const endpoint = metric.by[config.index];
|
||||
|
||||
return count ? endpoint.last(count).fetch() : endpoint.fetch();
|
||||
return "count" in config
|
||||
? endpoint.last(config.count).fetch()
|
||||
: endpoint.fetch();
|
||||
}
|
||||
|
||||
/** @typedef {"1d" | "1w" | "1m" | "1y" | "4y" | "8y" | "all"} TimeframeValue */
|
||||
/** @typedef {"minute10" | "hour1" | "hour4" | "day1" | "day3" | "week1"} TimeframeIndex */
|
||||
|
||||
/**
|
||||
* @typedef {Object} TimeframeConfig
|
||||
* @property {TimeframeIndex} index
|
||||
* @property {number} [count]
|
||||
*/
|
||||
/** @typedef {(typeof options)[number]["value"]} TimeframeValue */
|
||||
/** @typedef {(typeof timeframes)[TimeframeValue]["index"]} TimeframeIndex */
|
||||
|
||||
/**
|
||||
* @typedef {Object} TimeframeEndpoint
|
||||
|
||||
@@ -2,17 +2,15 @@ import { createRadioGroup } from "./radio.js";
|
||||
import { createChartStorage } from "./storage.js";
|
||||
|
||||
const storage = createChartStorage("view");
|
||||
/** @type {ChartView} */
|
||||
const defaultView = "stacked";
|
||||
/** @type {{ value: ChartView, label: string }[]} */
|
||||
const views = [
|
||||
const views = /** @type {const} */ ([
|
||||
{ value: "line", label: "Line" },
|
||||
{ value: "stacked", label: "Stack↑" },
|
||||
{ value: "stacked-reversed", label: "Stack↓" },
|
||||
{ value: "bar", label: "Bars↑" },
|
||||
{ value: "bar-reversed", label: "Bars↓" },
|
||||
{ value: "dots", label: "Dots" },
|
||||
];
|
||||
]);
|
||||
|
||||
/** @param {string} chartKey */
|
||||
export function getDefaultView(chartKey) {
|
||||
@@ -42,4 +40,4 @@ export function createViewControl(currentView, onChange) {
|
||||
});
|
||||
}
|
||||
|
||||
/** @typedef {"line" | "stacked" | "stacked-reversed" | "bar" | "bar-reversed" | "dots"} ChartView */
|
||||
/** @typedef {(typeof views)[number]["value"]} ChartView */
|
||||
|
||||
@@ -14,12 +14,12 @@ import { colors } from "../utils/colors.js";
|
||||
export const termSeries = createCohortSeries([
|
||||
{
|
||||
label: "STH",
|
||||
color: colors.sky,
|
||||
color: colors.yellow,
|
||||
metric: (client) => client.series.cohorts.utxo.sth.supply.total.btc,
|
||||
},
|
||||
{
|
||||
label: "LTH",
|
||||
color: colors.orange,
|
||||
color: colors.fuchsia,
|
||||
metric: (client) => client.series.cohorts.utxo.lth.supply.total.btc,
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -10,6 +10,7 @@ function createContentsItem(section, path) {
|
||||
const children = section.children ?? [];
|
||||
const sectionPath = [...path, section.title];
|
||||
|
||||
if (section.numbered === false) item.dataset.numbered = "false";
|
||||
anchor.href = `#${createPathId(sectionPath)}`;
|
||||
anchor.append(section.title);
|
||||
|
||||
@@ -44,5 +45,6 @@ export function createContents(sections) {
|
||||
/**
|
||||
* @typedef {Object} Section
|
||||
* @property {string} title
|
||||
* @property {boolean} [numbered]
|
||||
* @property {Section[]} [children]
|
||||
*/
|
||||
|
||||
@@ -22,10 +22,13 @@ main.learn {
|
||||
}
|
||||
|
||||
> ol > li {
|
||||
counter-increment: content-theme;
|
||||
counter-reset: content-topic;
|
||||
}
|
||||
|
||||
> ol > li:not([data-numbered="false"]) {
|
||||
counter-increment: content-theme;
|
||||
}
|
||||
|
||||
> ol > li > ol > li {
|
||||
counter-increment: content-topic;
|
||||
counter-reset: content-detail;
|
||||
@@ -83,6 +86,11 @@ main.learn {
|
||||
content: counter(content-theme, upper-roman) ". ";
|
||||
}
|
||||
|
||||
> ol > li[data-numbered="false"] > a::before {
|
||||
content: "I. ";
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
> ol > li > ol > li > a::before {
|
||||
content: counter(content-topic) ". ";
|
||||
}
|
||||
|
||||
+36
-235
@@ -44,10 +44,16 @@ function metricSupplyInLoss(client) {
|
||||
}
|
||||
|
||||
export const sections = [
|
||||
{
|
||||
title: "Introduction",
|
||||
numbered: false,
|
||||
description:
|
||||
"Bitcoin can be measured from many angles, but a single number rarely explains much on its own. This page introduces core Bitcoin concepts through data that changes over time. Each chart is meant to answer a simple question: what is being measured, how has it changed, and how does it compare across different groups? The goal is to make the system easier to read, from the supply itself to the way coins move, age, concentrate, and gain value.",
|
||||
},
|
||||
{
|
||||
title: "Supply",
|
||||
description:
|
||||
"How bitcoin moves from issuance into long-term ownership, profit, loss, and distribution.",
|
||||
"Bitcoin has a fixed issuance schedule. This chart shows how many BTC are in circulation over time, so you can see supply rising toward the 21 million limit.",
|
||||
chart: {
|
||||
title: "Circulating supply",
|
||||
series: [
|
||||
@@ -62,7 +68,7 @@ export const sections = [
|
||||
{
|
||||
title: "Profitability",
|
||||
description:
|
||||
"Which coins sit in profit or loss, and how that balance changes through cycles.",
|
||||
"Shows whether coins are in profit or loss based on the price when they last moved on-chain. A coin is in profit when today's price is higher than its last moved price, and in loss when today's price is lower.",
|
||||
chart: {
|
||||
title: "Profitability",
|
||||
series: [
|
||||
@@ -82,7 +88,7 @@ export const sections = [
|
||||
{
|
||||
title: "Term",
|
||||
description:
|
||||
"Supply split between recently moved coins and long-term holder coins.",
|
||||
"Splits supply between coins that moved recently and coins that have stayed still longer. This helps separate more active supply from long-term holder supply.",
|
||||
chart: {
|
||||
title: "Supply by term",
|
||||
series: termSeries,
|
||||
@@ -91,7 +97,7 @@ export const sections = [
|
||||
{
|
||||
title: "Age",
|
||||
description:
|
||||
"How long coins have remained still, from fresh movement to deep dormancy.",
|
||||
"Groups coins by how long they have stayed still since their last on-chain movement. Older coins are usually more dormant, while younger coins have moved more recently.",
|
||||
chart: {
|
||||
title: "Supply by age",
|
||||
series: ageSeries,
|
||||
@@ -99,7 +105,8 @@ export const sections = [
|
||||
},
|
||||
{
|
||||
title: "UTXO Balance",
|
||||
description: "Supply grouped by the amount held in each unspent output.",
|
||||
description:
|
||||
"Groups supply by the size of each unspent output. A UTXO is a spendable piece of bitcoin created by a transaction, so this shows the size distribution of coin fragments.",
|
||||
chart: {
|
||||
title: "Supply by UTXO balance",
|
||||
series: utxoBalanceSeries,
|
||||
@@ -107,7 +114,8 @@ export const sections = [
|
||||
},
|
||||
{
|
||||
title: "Address Balance",
|
||||
description: "Supply grouped by the balance held at each address.",
|
||||
description:
|
||||
"Groups supply by the total BTC held at each address. An address is not the same as a person or entity, but this still helps show how balances are distributed on-chain.",
|
||||
chart: {
|
||||
title: "Supply by address balance",
|
||||
series: addressBalanceSeries,
|
||||
@@ -115,7 +123,8 @@ export const sections = [
|
||||
},
|
||||
{
|
||||
title: "Type",
|
||||
description: "Supply grouped by output script type.",
|
||||
description:
|
||||
"Groups supply by Bitcoin output type. The output type is the script format that defines how coins can be spent.",
|
||||
chart: {
|
||||
title: "Supply by type",
|
||||
series: typeSeries,
|
||||
@@ -124,7 +133,7 @@ export const sections = [
|
||||
{
|
||||
title: "Epoch",
|
||||
description:
|
||||
"Supply grouped by the halving epoch in which coins were created.",
|
||||
"Groups supply by the halving epoch when coins were mined. A halving epoch is a period between two subsidy halvings, when the amount of new BTC paid to miners changes.",
|
||||
chart: {
|
||||
title: "Supply by epoch",
|
||||
series: epochSeries,
|
||||
@@ -133,7 +142,7 @@ export const sections = [
|
||||
{
|
||||
title: "Class",
|
||||
description:
|
||||
"Supply grouped by the calendar year in which coins were created.",
|
||||
"Groups supply by the calendar year when coins were mined. This shows how much of today's supply comes from each issuance year.",
|
||||
chart: {
|
||||
title: "Supply by class",
|
||||
series: classSeries,
|
||||
@@ -144,7 +153,7 @@ export const sections = [
|
||||
{
|
||||
title: "Capitalization",
|
||||
description:
|
||||
"Different ways to value the network by market price, realized cost, and accumulated flows.",
|
||||
"Shows ways to value Bitcoin in US dollars. Market cap uses today's price, while realized cap uses the price when coins last moved on-chain.",
|
||||
chart: {
|
||||
title: "Capitalization",
|
||||
series: capitalizationSeries,
|
||||
@@ -153,7 +162,7 @@ export const sections = [
|
||||
{
|
||||
title: "Market Cap",
|
||||
description:
|
||||
"The current market value of circulating bitcoin at spot price.",
|
||||
"Market cap is circulating supply multiplied by the current bitcoin price. It answers: what is all circulating BTC worth at today's market price?",
|
||||
chart: {
|
||||
title: "Market cap",
|
||||
series: marketCapSeries,
|
||||
@@ -162,7 +171,7 @@ export const sections = [
|
||||
{
|
||||
title: "Term",
|
||||
description:
|
||||
"Market value split between recently moved and long-term holder coins.",
|
||||
"Splits market cap between coins that moved recently and coins that have stayed still longer. This shows how much current market value sits with active supply versus long-term holder supply.",
|
||||
chart: {
|
||||
title: "Market cap by term",
|
||||
series: marketCapTermSeries,
|
||||
@@ -171,7 +180,7 @@ export const sections = [
|
||||
{
|
||||
title: "Age",
|
||||
description:
|
||||
"Market value grouped by how long coins have remained still.",
|
||||
"Groups market cap by how long coins have stayed still since their last on-chain movement. It shows which age bands hold the most current market value.",
|
||||
chart: {
|
||||
title: "Market cap by age",
|
||||
series: marketCapAgeSeries,
|
||||
@@ -180,7 +189,7 @@ export const sections = [
|
||||
{
|
||||
title: "UTXO Balance",
|
||||
description:
|
||||
"Market value grouped by the amount held in each unspent output.",
|
||||
"Groups market cap by the size of each unspent output. This shows how current market value is distributed across small and large spendable coin fragments.",
|
||||
chart: {
|
||||
title: "Market cap by UTXO balance",
|
||||
series: marketCapUtxoBalanceSeries,
|
||||
@@ -189,7 +198,7 @@ export const sections = [
|
||||
{
|
||||
title: "Address Balance",
|
||||
description:
|
||||
"Market value grouped by the balance held at each address.",
|
||||
"Groups market cap by the total BTC held at each address. Addresses are not people or entities, but this still helps show how current market value is distributed across address balances.",
|
||||
chart: {
|
||||
title: "Market cap by address balance",
|
||||
series: marketCapAddressBalanceSeries,
|
||||
@@ -197,7 +206,8 @@ export const sections = [
|
||||
},
|
||||
{
|
||||
title: "Type",
|
||||
description: "Market value grouped by spendable output script type.",
|
||||
description:
|
||||
"Groups market cap by Bitcoin output type. This shows how much current market value is held in each script format.",
|
||||
chart: {
|
||||
title: "Market cap by type",
|
||||
series: marketCapTypeSeries,
|
||||
@@ -206,7 +216,7 @@ export const sections = [
|
||||
{
|
||||
title: "Epoch",
|
||||
description:
|
||||
"Market value grouped by the halving epoch in which coins were created.",
|
||||
"Groups market cap by the halving epoch when coins were mined. This shows the current value of coins created during each issuance period.",
|
||||
chart: {
|
||||
title: "Market cap by epoch",
|
||||
series: marketCapEpochSeries,
|
||||
@@ -215,7 +225,7 @@ export const sections = [
|
||||
{
|
||||
title: "Class",
|
||||
description:
|
||||
"Market value grouped by the calendar year in which coins were created.",
|
||||
"Groups market cap by the calendar year when coins were mined. This shows the current value of supply created in each year.",
|
||||
chart: {
|
||||
title: "Market cap by class",
|
||||
series: marketCapClassSeries,
|
||||
@@ -226,7 +236,7 @@ export const sections = [
|
||||
{
|
||||
title: "Realized Cap",
|
||||
description:
|
||||
"The aggregate value of coins priced where they last moved on-chain.",
|
||||
"Realized cap values each coin at the price when it last moved on-chain. It is often used as a rough view of the market's aggregate cost basis.",
|
||||
chart: {
|
||||
title: "Realized cap",
|
||||
series: realizedCapSeries,
|
||||
@@ -235,7 +245,7 @@ export const sections = [
|
||||
{
|
||||
title: "Term",
|
||||
description:
|
||||
"Realized value split between recently moved and long-term holder coins.",
|
||||
"Splits realized cap between coins that moved recently and coins that have stayed still longer. This shows where the market's cost basis sits across active and long-term holder supply.",
|
||||
chart: {
|
||||
title: "Realized cap by term",
|
||||
series: realizedCapTermSeries,
|
||||
@@ -244,7 +254,7 @@ export const sections = [
|
||||
{
|
||||
title: "Age",
|
||||
description:
|
||||
"Realized value grouped by how long coins have remained still.",
|
||||
"Groups realized cap by how long coins have stayed still since their last on-chain movement. This shows which coin ages carry the largest share of the market's cost basis.",
|
||||
chart: {
|
||||
title: "Realized cap by age",
|
||||
series: realizedCapAgeSeries,
|
||||
@@ -253,7 +263,7 @@ export const sections = [
|
||||
{
|
||||
title: "UTXO Balance",
|
||||
description:
|
||||
"Realized value grouped by the amount held in each unspent output.",
|
||||
"Groups realized cap by the size of each unspent output. This shows how cost basis is distributed across small and large spendable coin fragments.",
|
||||
chart: {
|
||||
title: "Realized cap by UTXO balance",
|
||||
series: realizedCapUtxoBalanceSeries,
|
||||
@@ -262,7 +272,7 @@ export const sections = [
|
||||
{
|
||||
title: "Address Balance",
|
||||
description:
|
||||
"Realized value grouped by the balance held at each address.",
|
||||
"Groups realized cap by the total BTC held at each address. Addresses are not people or entities, but this still helps show how cost basis is distributed across address balances.",
|
||||
chart: {
|
||||
title: "Realized cap by address balance",
|
||||
series: realizedCapAddressBalanceSeries,
|
||||
@@ -271,7 +281,7 @@ export const sections = [
|
||||
{
|
||||
title: "Type",
|
||||
description:
|
||||
"Realized value grouped by spendable output script type.",
|
||||
"Groups realized cap by Bitcoin output type. This shows how much cost basis is held in each script format.",
|
||||
chart: {
|
||||
title: "Realized cap by type",
|
||||
series: realizedCapTypeSeries,
|
||||
@@ -280,7 +290,7 @@ export const sections = [
|
||||
{
|
||||
title: "Epoch",
|
||||
description:
|
||||
"Realized value grouped by the halving epoch in which coins were created.",
|
||||
"Groups realized cap by the halving epoch when coins were mined. This shows the cost basis of coins created during each issuance period.",
|
||||
chart: {
|
||||
title: "Realized cap by epoch",
|
||||
series: realizedCapEpochSeries,
|
||||
@@ -289,7 +299,7 @@ export const sections = [
|
||||
{
|
||||
title: "Class",
|
||||
description:
|
||||
"Realized value grouped by the calendar year in which coins were created.",
|
||||
"Groups realized cap by the calendar year when coins were mined. This shows the cost basis of supply created in each year.",
|
||||
chart: {
|
||||
title: "Realized cap by class",
|
||||
series: realizedCapClassSeries,
|
||||
@@ -299,213 +309,4 @@ export const sections = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Activity",
|
||||
description:
|
||||
"How often the chain is used, how value moves, and how demand appears " +
|
||||
"in fees and transactions.",
|
||||
chart: "Network activity",
|
||||
children: [
|
||||
{
|
||||
title: "Transactions",
|
||||
description:
|
||||
"Confirmed transaction count, throughput, and block-level settlement patterns.",
|
||||
chart: "Transaction count",
|
||||
},
|
||||
{
|
||||
title: "Fees",
|
||||
description:
|
||||
"The cost users pay for block space and what that reveals about demand.",
|
||||
chart: "Fee rate",
|
||||
},
|
||||
{
|
||||
title: "Addresses",
|
||||
description:
|
||||
"Address creation, reuse, activity, and balance changes across the network.",
|
||||
chart: "Active addresses",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Mining",
|
||||
description:
|
||||
"The security budget, difficulty adjustments, pool behavior, and miner revenue.",
|
||||
chart: "Mining overview",
|
||||
children: [
|
||||
{
|
||||
title: "Hashrate",
|
||||
description:
|
||||
"Estimated computational power securing the network over time.",
|
||||
chart: "Hashrate",
|
||||
},
|
||||
{
|
||||
title: "Difficulty",
|
||||
description:
|
||||
"How Bitcoin adjusts mining difficulty to keep block production steady.",
|
||||
chart: "Difficulty",
|
||||
},
|
||||
{
|
||||
title: "Rewards",
|
||||
description:
|
||||
"Subsidy, fees, and the changing economics of block production.",
|
||||
chart: "Miner rewards",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Market",
|
||||
description:
|
||||
"Price behavior, returns, volatility, and the market context around on-chain patterns.",
|
||||
chart: "Market overview",
|
||||
children: [
|
||||
{
|
||||
title: "Price",
|
||||
description:
|
||||
"Bitcoin price across time, cycles, drawdowns, and all-time highs.",
|
||||
chart: "Price",
|
||||
},
|
||||
{
|
||||
title: "Returns",
|
||||
description:
|
||||
"How returns vary by holding period, entry point, and cycle phase.",
|
||||
chart: "Returns",
|
||||
},
|
||||
{
|
||||
title: "Volatility",
|
||||
description:
|
||||
"The scale and rhythm of price movement across different windows.",
|
||||
chart: "Volatility",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Ownership",
|
||||
description:
|
||||
"How coins are held across balances, entities, custody patterns, and long-term cohorts.",
|
||||
chart: "Ownership overview",
|
||||
children: [
|
||||
{
|
||||
title: "Balances",
|
||||
description:
|
||||
"Address and entity balances grouped by size, concentration, and historical change.",
|
||||
chart: "Balance cohorts",
|
||||
},
|
||||
{
|
||||
title: "Entities",
|
||||
description:
|
||||
"Estimated ownership clusters and how their behavior changes through market regimes.",
|
||||
chart: "Entity supply",
|
||||
},
|
||||
{
|
||||
title: "Custody",
|
||||
description:
|
||||
"Coins associated with exchanges, funds, miners, and other observable custody groups.",
|
||||
chart: "Custody balances",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Liquidity",
|
||||
description:
|
||||
"How available supply changes as coins move between liquid, illiquid, and exchange venues.",
|
||||
chart: "Liquidity overview",
|
||||
children: [
|
||||
{
|
||||
title: "Liquid Supply",
|
||||
description:
|
||||
"Coins held by entities that tend to spend, trade, or redistribute frequently.",
|
||||
chart: "Liquid supply",
|
||||
},
|
||||
{
|
||||
title: "Illiquid Supply",
|
||||
description:
|
||||
"Coins held by entities with low spending history and stronger accumulation behavior.",
|
||||
chart: "Illiquid supply",
|
||||
},
|
||||
{
|
||||
title: "Exchange Flow",
|
||||
description:
|
||||
"Deposits, withdrawals, and balance changes across known exchange clusters.",
|
||||
chart: "Exchange netflow",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Risk",
|
||||
description:
|
||||
"Stress, leverage, drawdown, and valuation conditions that shape market fragility.",
|
||||
chart: "Risk overview",
|
||||
children: [
|
||||
{
|
||||
title: "Drawdown",
|
||||
description:
|
||||
"Distance from prior highs and the depth of cycle retracements over time.",
|
||||
chart: "Drawdown",
|
||||
},
|
||||
{
|
||||
title: "Stress",
|
||||
description:
|
||||
"Periods where losses, volatility, and fee pressure concentrate together.",
|
||||
chart: "Network stress",
|
||||
},
|
||||
{
|
||||
title: "Leverage",
|
||||
description:
|
||||
"Market conditions that indicate amplified exposure and forced positioning risk.",
|
||||
chart: "Leverage proxy",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Cycles",
|
||||
description:
|
||||
"How Bitcoin behaves across halvings, adoption waves, liquidity regimes, and market phases.",
|
||||
chart: "Cycle overview",
|
||||
children: [
|
||||
{
|
||||
title: "Halvings",
|
||||
description:
|
||||
"Supply issuance changes and their relationship to market and miner behavior.",
|
||||
chart: "Halving cycles",
|
||||
},
|
||||
{
|
||||
title: "Phases",
|
||||
description:
|
||||
"Bull, bear, recovery, and transition periods described through on-chain behavior.",
|
||||
chart: "Cycle phases",
|
||||
},
|
||||
{
|
||||
title: "Comparisons",
|
||||
description:
|
||||
"Cycle-to-cycle comparisons normalized by time, price, drawdown, or supply behavior.",
|
||||
chart: "Cycle comparison",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Cohorts",
|
||||
description:
|
||||
"Groups of market participants organized by age, balance, cost basis, and observed behavior.",
|
||||
chart: "Cohort overview",
|
||||
children: [
|
||||
{
|
||||
title: "Short Term",
|
||||
description:
|
||||
"Recently moved coins and holders more sensitive to price, volatility, and liquidity.",
|
||||
chart: "Short-term holder supply",
|
||||
},
|
||||
{
|
||||
title: "Long Term",
|
||||
description:
|
||||
"Older coins and holders with stronger dormancy, conviction, or lower spend frequency.",
|
||||
chart: "Long-term holder supply",
|
||||
},
|
||||
{
|
||||
title: "Cost Basis",
|
||||
description:
|
||||
"Estimated acquisition prices across cohorts and how they frame profit and loss.",
|
||||
chart: "Cohort cost basis",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -5,21 +5,6 @@ import { initHashLinks } from "./hash-links.js";
|
||||
import { initScrollSpy } from "./scroll-spy.js";
|
||||
import { createPathId } from "./path.js";
|
||||
|
||||
/** @param {Section["chart"]} chart */
|
||||
function createFigure(chart) {
|
||||
if (typeof chart !== "string") return createDataChart(chart);
|
||||
|
||||
const figure = document.createElement("figure");
|
||||
const placeholder = document.createElement("div");
|
||||
const caption = document.createElement("figcaption");
|
||||
|
||||
placeholder.append(chart);
|
||||
caption.append(chart);
|
||||
figure.append(placeholder, caption);
|
||||
|
||||
return figure;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Section} section
|
||||
* @param {readonly string[]} [path]
|
||||
@@ -35,11 +20,13 @@ function createSection(section, path = []) {
|
||||
const id = createPathId(sectionPath);
|
||||
|
||||
element.id = id;
|
||||
if (section.numbered === false) element.dataset.numbered = "false";
|
||||
anchor.href = `#${id}`;
|
||||
anchor.append(section.title);
|
||||
heading.append(anchor);
|
||||
description.append(section.description);
|
||||
element.append(heading, description, createFigure(section.chart));
|
||||
element.append(heading, description);
|
||||
if (section.chart) element.append(createDataChart(section.chart));
|
||||
|
||||
for (const child of children) {
|
||||
element.append(createSection(child, sectionPath));
|
||||
@@ -67,6 +54,7 @@ export function createLearnPage() {
|
||||
* @typedef {Object} Section
|
||||
* @property {string} title
|
||||
* @property {string} description
|
||||
* @property {string | import("./charts/index.js").Chart} chart
|
||||
* @property {import("./charts/index.js").Chart} [chart]
|
||||
* @property {boolean} [numbered]
|
||||
* @property {Section[]} [children]
|
||||
*/
|
||||
|
||||
@@ -36,16 +36,40 @@ main.learn {
|
||||
}
|
||||
|
||||
> section {
|
||||
counter-increment: theme;
|
||||
counter-reset: topic;
|
||||
width: min(100%, var(--content-width));
|
||||
margin-inline: auto;
|
||||
scroll-margin-top: var(--offset);
|
||||
}
|
||||
|
||||
> section:first-of-type {
|
||||
margin-top: calc(-1 * var(--offset));
|
||||
padding-top: var(--offset);
|
||||
> section:not([data-numbered="false"]) {
|
||||
counter-increment: theme;
|
||||
}
|
||||
|
||||
> section[data-numbered="false"] {
|
||||
display: grid;
|
||||
align-content: center;
|
||||
min-height: calc(100dvh - var(--offset));
|
||||
|
||||
> h1 {
|
||||
position: static;
|
||||
max-width: 10ch;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
font-size: 4rem;
|
||||
|
||||
a::before {
|
||||
content: none;
|
||||
}
|
||||
}
|
||||
|
||||
> p {
|
||||
max-width: 42rem;
|
||||
margin-top: 1.5rem;
|
||||
color: var(--white);
|
||||
font-size: var(--font-size-base);
|
||||
line-height: var(--line-height-base);
|
||||
}
|
||||
}
|
||||
|
||||
> section + section {
|
||||
@@ -155,21 +179,5 @@ main.learn {
|
||||
margin-top: 2rem;
|
||||
color: var(--gray);
|
||||
font-size: var(--font-size-xs);
|
||||
|
||||
&:not([data-chart]) {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
> div {
|
||||
height: 18rem;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border: 1px solid var(--dark-gray);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
figcaption {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createHeader } from "./header/index.js";
|
||||
import { createRoutePage, isRoute, normalizePath } from "./routes.js";
|
||||
import { createRoutePage, normalizePath, resolvePath } from "./routes.js";
|
||||
import { getEventAnchor, isPlainLeftClick } from "./utils/event.js";
|
||||
import { revealPage, transitionPage } from "./utils/transition.js";
|
||||
|
||||
@@ -62,10 +62,11 @@ document.addEventListener("click", (event) => {
|
||||
if (url.origin !== window.location.origin) return;
|
||||
if (url.pathname === window.location.pathname && url.hash) return;
|
||||
|
||||
if (!isRoute(url.pathname)) return;
|
||||
const pathname = resolvePath(url.pathname);
|
||||
if (!pathname) return;
|
||||
|
||||
event.preventDefault();
|
||||
navigate(`${url.pathname}${url.hash}`);
|
||||
navigate(`${pathname}${url.hash}`);
|
||||
});
|
||||
|
||||
window.addEventListener("popstate", renderPage);
|
||||
|
||||
+12
-3
@@ -12,13 +12,22 @@ const routes = {
|
||||
};
|
||||
|
||||
/** @param {string} pathname */
|
||||
export function isRoute(pathname) {
|
||||
return pathname in routes;
|
||||
function canonicalPath(pathname) {
|
||||
return pathname !== "/" && pathname.endsWith("/")
|
||||
? pathname.slice(0, -1)
|
||||
: pathname;
|
||||
}
|
||||
|
||||
/** @param {string} pathname */
|
||||
export function resolvePath(pathname) {
|
||||
const path = canonicalPath(pathname);
|
||||
|
||||
return path in routes ? path : undefined;
|
||||
}
|
||||
|
||||
/** @param {string} pathname */
|
||||
export function normalizePath(pathname) {
|
||||
return isRoute(pathname) ? pathname : "/";
|
||||
return resolvePath(pathname) ?? "/";
|
||||
}
|
||||
|
||||
/** @param {string} pathname */
|
||||
|
||||
@@ -26,12 +26,8 @@
|
||||
--fuchsia: oklch(0.629 0.294 322.523);
|
||||
--pink: oklch(0.624 0.245 357.444);
|
||||
--rose: oklch(0.6155 0.2495 17.012);
|
||||
--background-color: light-dark(var(--white), var(--black));
|
||||
--color: light-dark(var(--black), var(--white));
|
||||
--off-color: var(--gray);
|
||||
--border-color: light-dark(var(--light-gray), var(--dark-gray));
|
||||
--inv-border-color: light-dark(var(--dark-gray), var(--light-gray));
|
||||
--off-border-color: light-dark(var(--dark-white), var(--light-black));
|
||||
|
||||
--font-size-xs: 0.75rem;
|
||||
--line-height-xs: calc(1 / 0.75);
|
||||
@@ -39,15 +35,7 @@
|
||||
--line-height-sm: calc(1.25 / 0.875);
|
||||
--font-size-base: 1rem;
|
||||
--line-height-base: calc(1.5 / 1);
|
||||
--font-size-lg: 1.125rem;
|
||||
--line-height-lg: calc(1.75 / 1.125);
|
||||
--font-size-xl: 1.25rem;
|
||||
--line-height-xl: calc(1.75 / 1.25);
|
||||
|
||||
--page-x: 2rem;
|
||||
--main-padding: 2rem;
|
||||
--negative-main-padding: calc(-1 * var(--main-padding));
|
||||
--font-weight-base: 400;
|
||||
--max-main-width: 70dvw;
|
||||
--layer-header: 10;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user