mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-06-12 07:53:32 -07:00
website: redesign part 22
This commit is contained in:
@@ -100,6 +100,9 @@
|
||||
<link rel="stylesheet" href="/explore/style.css" />
|
||||
<link rel="stylesheet" href="/learn/style.css" />
|
||||
<link rel="stylesheet" href="/learn/charts/style.css" />
|
||||
<link rel="stylesheet" href="/learn/charts/controls/style.css" />
|
||||
<link rel="stylesheet" href="/learn/charts/legend/style.css" />
|
||||
<link rel="stylesheet" href="/learn/charts/scrubber/style.css" />
|
||||
<link rel="stylesheet" href="/learn/charts/bar/style.css" />
|
||||
<link rel="stylesheet" href="/learn/charts/line/style.css" />
|
||||
<link rel="stylesheet" href="/learn/charts/dots/style.css" />
|
||||
|
||||
@@ -1,16 +1,8 @@
|
||||
import { createLinePathData, formatCoordinate } from "../path.js";
|
||||
import { createSvgElement } from "../svg.js";
|
||||
import { VIEWBOX_WIDTH } from "../viewbox.js";
|
||||
import { createStackedSeries } from "../stacked/series.js";
|
||||
|
||||
/**
|
||||
* @param {number} value
|
||||
* @param {number} min
|
||||
* @param {number} max
|
||||
*/
|
||||
function clamp(value, min, max) {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
import { clamp } from "../math.js";
|
||||
import { appendSeriesPath } from "../series-path.js";
|
||||
|
||||
/** @param {{ x: number, y0: number, y1: number }[]} points */
|
||||
function getBarWidth(points) {
|
||||
@@ -63,26 +55,26 @@ export function renderBarPlot(
|
||||
|
||||
for (const index of stackIndexes) {
|
||||
const { color, points } = plottedSeries[index];
|
||||
const path = createSvgElement("path");
|
||||
|
||||
path.dataset.chart = "bar";
|
||||
path.dataset.series = index.toString();
|
||||
path.style.setProperty("--color", color);
|
||||
path.setAttribute("d", createBarPathData(points, getBarWidth(points)));
|
||||
highlight.addNode(path, index);
|
||||
group.append(path);
|
||||
appendSeriesPath({
|
||||
group,
|
||||
highlight,
|
||||
index,
|
||||
chart: "bar",
|
||||
color,
|
||||
d: createBarPathData(points, getBarWidth(points)),
|
||||
});
|
||||
}
|
||||
|
||||
for (const index of lineIndexes) {
|
||||
const { color, points } = plottedSeries[index];
|
||||
const path = createSvgElement("path");
|
||||
|
||||
path.dataset.chart = "line";
|
||||
path.dataset.series = index.toString();
|
||||
path.style.setProperty("--color", color);
|
||||
path.setAttribute("d", createLinePathData(points));
|
||||
highlight.addNode(path, index);
|
||||
group.append(path);
|
||||
appendSeriesPath({
|
||||
group,
|
||||
highlight,
|
||||
index,
|
||||
chart: "line",
|
||||
color,
|
||||
d: createLinePathData(points),
|
||||
});
|
||||
}
|
||||
|
||||
return plottedSeries;
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
main.learn {
|
||||
figure[data-chart="series"] {
|
||||
> footer {
|
||||
> div {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.125rem 0.5rem;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
text-transform: uppercase;
|
||||
|
||||
legend {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
clip-path: inset(50%);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
label {
|
||||
position: relative;
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
margin: 0;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
span {
|
||||
display: block;
|
||||
padding: 0.25rem;
|
||||
border-radius: 0.25rem;
|
||||
color: var(--gray);
|
||||
}
|
||||
|
||||
label:hover span {
|
||||
color: var(--black);
|
||||
background: var(--white);
|
||||
}
|
||||
|
||||
label:has(:checked):not(:hover) span {
|
||||
color: var(--black);
|
||||
background: var(--gray);
|
||||
}
|
||||
|
||||
label:active span {
|
||||
color: var(--black);
|
||||
background: var(--orange);
|
||||
}
|
||||
|
||||
label:has(:focus-visible) span {
|
||||
outline: 1px solid var(--orange);
|
||||
outline-offset: 0.125rem;
|
||||
}
|
||||
}
|
||||
|
||||
button[data-chart="fullscreen"] {
|
||||
padding: 0.25rem;
|
||||
border: 0;
|
||||
border-radius: 0.25rem;
|
||||
color: var(--gray);
|
||||
background: none;
|
||||
font: inherit;
|
||||
line-height: inherit;
|
||||
text-transform: uppercase;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--black);
|
||||
background: var(--white);
|
||||
}
|
||||
|
||||
&[aria-pressed="true"] {
|
||||
color: var(--black);
|
||||
background: var(--green);
|
||||
}
|
||||
|
||||
&:active {
|
||||
color: var(--black);
|
||||
background: var(--orange);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 1px solid var(--orange);
|
||||
outline-offset: 0.125rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { formatCoordinate } from "../path.js";
|
||||
import { createSvgElement } from "../svg.js";
|
||||
import { appendSeriesPath } from "../series-path.js";
|
||||
import { createLineSeries } from "../line/series.js";
|
||||
|
||||
const radius = 1;
|
||||
@@ -27,14 +27,14 @@ export function renderDotsPlot(group, loadedSeries, height, highlight, scale) {
|
||||
const plottedSeries = createLineSeries(loadedSeries, height, scale);
|
||||
|
||||
plottedSeries.forEach(({ color, points }, index) => {
|
||||
const path = createSvgElement("path");
|
||||
|
||||
path.dataset.chart = "dots";
|
||||
path.dataset.series = index.toString();
|
||||
path.style.setProperty("--color", color);
|
||||
path.setAttribute("d", createDotsPathData(points));
|
||||
highlight.addNode(path, index);
|
||||
group.append(path);
|
||||
appendSeriesPath({
|
||||
group,
|
||||
highlight,
|
||||
index,
|
||||
chart: "dots",
|
||||
color,
|
||||
d: createDotsPathData(points),
|
||||
});
|
||||
});
|
||||
|
||||
return plottedSeries;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createFullscreenButton } from "./fullscreen.js";
|
||||
import { onChartVisibility } from "./intersection.js";
|
||||
import { createLegend } from "./legend.js";
|
||||
import { createLegend } from "./legend/index.js";
|
||||
import { createChartRenderer } from "./renderer.js";
|
||||
import {
|
||||
createScaleControl,
|
||||
|
||||
@@ -47,4 +47,4 @@ export function createLegend(chart) {
|
||||
* @property {{ value: HTMLOutputElement }[]} rows
|
||||
*/
|
||||
|
||||
/** @typedef {import("./index.js").Chart} Chart */
|
||||
/** @typedef {import("../index.js").Chart} Chart */
|
||||
@@ -0,0 +1,114 @@
|
||||
main.learn {
|
||||
figure[data-chart="series"] {
|
||||
figcaption {
|
||||
text-transform: uppercase;
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
align-items: start;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
time {
|
||||
color: var(--off-color);
|
||||
}
|
||||
|
||||
span:is([data-chart="unit"], [data-chart="separator"]) {
|
||||
color: var(--off-color);
|
||||
}
|
||||
|
||||
menu {
|
||||
--shadow-size: 1rem;
|
||||
|
||||
display: flex;
|
||||
margin-inline: calc(-1 * var(--shadow-size));
|
||||
padding: 0 var(--shadow-size);
|
||||
padding-bottom: 1rem;
|
||||
padding-top: 0.25rem;
|
||||
overflow-x: auto;
|
||||
list-style: none;
|
||||
mask-image: linear-gradient(
|
||||
to right,
|
||||
transparent,
|
||||
black var(--shadow-size),
|
||||
black calc(100% - var(--shadow-size)),
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
li {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0.25rem 0.375rem;
|
||||
border: 0;
|
||||
border-radius: 0.25rem;
|
||||
color: inherit;
|
||||
background: none;
|
||||
font: inherit;
|
||||
text-align: inherit;
|
||||
text-transform: inherit;
|
||||
cursor: pointer;
|
||||
|
||||
&:is(:hover, :focus-visible, [data-active], [data-preview]) {
|
||||
color: var(--black);
|
||||
background: var(--color);
|
||||
|
||||
span,
|
||||
output {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 1px solid var(--orange);
|
||||
outline-offset: 0.125rem;
|
||||
}
|
||||
|
||||
&[data-muted] {
|
||||
opacity: 0.35;
|
||||
}
|
||||
|
||||
> span {
|
||||
display: block;
|
||||
color: var(--color);
|
||||
text-align: left;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
width: 0.5em;
|
||||
height: 0.5em;
|
||||
margin-right: 0.35em;
|
||||
margin-bottom: 0.1rem;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
}
|
||||
}
|
||||
|
||||
> output {
|
||||
display: block;
|
||||
margin-top: 0.25rem;
|
||||
margin-left: auto;
|
||||
width: 7ch;
|
||||
min-height: 1em;
|
||||
color: var(--white);
|
||||
font-variant-numeric: tabular-nums;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:fullscreen {
|
||||
figcaption menu {
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
svg [data-series][data-muted] {
|
||||
opacity: 0.2;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createLinePathData } from "../path.js";
|
||||
import { createSvgElement } from "../svg.js";
|
||||
import { appendSeriesPath } from "../series-path.js";
|
||||
import { createLineSeries } from "./series.js";
|
||||
|
||||
/**
|
||||
@@ -13,14 +13,14 @@ export function renderLinePlot(group, loadedSeries, height, highlight, scale) {
|
||||
const plottedSeries = createLineSeries(loadedSeries, height, scale);
|
||||
|
||||
plottedSeries.forEach(({ color, points }, index) => {
|
||||
const path = createSvgElement("path");
|
||||
|
||||
path.dataset.chart = "line";
|
||||
path.dataset.series = index.toString();
|
||||
path.style.setProperty("--color", color);
|
||||
path.setAttribute("d", createLinePathData(points));
|
||||
highlight.addNode(path, index);
|
||||
group.append(path);
|
||||
appendSeriesPath({
|
||||
group,
|
||||
highlight,
|
||||
index,
|
||||
chart: "line",
|
||||
color,
|
||||
d: createLinePathData(points),
|
||||
});
|
||||
});
|
||||
|
||||
return plottedSeries;
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* @param {number} value
|
||||
* @param {number} min
|
||||
* @param {number} max
|
||||
*/
|
||||
export function clamp(value, min, max) {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { renderBarPlot } from "./bar/index.js";
|
||||
import { renderDotsPlot } from "./dots/index.js";
|
||||
import { renderLinePlot } from "./line/index.js";
|
||||
import { renderStackedPlot } from "./stacked/index.js";
|
||||
|
||||
/**
|
||||
* @param {ChartView} view
|
||||
* @param {SVGGElement} group
|
||||
* @param {LoadedSeries[]} loadedSeries
|
||||
* @param {number} height
|
||||
* @param {SeriesHighlight} highlight
|
||||
* @param {ChartScale} scale
|
||||
*/
|
||||
export function renderPlot(view, group, loadedSeries, height, highlight, scale) {
|
||||
switch (view) {
|
||||
case "line":
|
||||
return renderLinePlot(group, loadedSeries, height, highlight, scale);
|
||||
case "bar":
|
||||
case "bar-reversed":
|
||||
return renderBarPlot(
|
||||
group,
|
||||
loadedSeries,
|
||||
height,
|
||||
highlight,
|
||||
{ reversed: view === "bar-reversed" },
|
||||
scale,
|
||||
);
|
||||
case "dots":
|
||||
return renderDotsPlot(group, loadedSeries, height, highlight, scale);
|
||||
default:
|
||||
return renderStackedPlot(
|
||||
group,
|
||||
loadedSeries,
|
||||
height,
|
||||
highlight,
|
||||
{ reversed: view === "stacked-reversed" },
|
||||
scale,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** @typedef {import("./highlight.js").SeriesHighlight} SeriesHighlight */
|
||||
/** @typedef {import("./index.js").LoadedSeries} LoadedSeries */
|
||||
/** @typedef {import("./scale.js").ChartScale} ChartScale */
|
||||
/** @typedef {import("./views.js").ChartView} ChartView */
|
||||
@@ -1,49 +1,10 @@
|
||||
import { renderBarPlot } from "./bar/index.js";
|
||||
import { createSeriesHighlight } from "./highlight.js";
|
||||
import { createSeriesLoader } from "./loader.js";
|
||||
import { renderLinePlot } from "./line/index.js";
|
||||
import { createScrubber } from "./scrubber.js";
|
||||
import { renderDotsPlot } from "./dots/index.js";
|
||||
import { renderPlot } from "./plot.js";
|
||||
import { createScrubber } from "./scrubber/index.js";
|
||||
import { createSvgElement } from "./svg.js";
|
||||
import { renderStackedPlot } from "./stacked/index.js";
|
||||
import { getViewBoxHeight, VIEWBOX_WIDTH } from "./viewbox.js";
|
||||
|
||||
/**
|
||||
* @param {ChartView} view
|
||||
* @param {SVGGElement} group
|
||||
* @param {LoadedSeries[]} loadedSeries
|
||||
* @param {number} height
|
||||
* @param {SeriesHighlight} highlight
|
||||
* @param {ChartScale} scale
|
||||
*/
|
||||
function renderPlot(view, group, loadedSeries, height, highlight, scale) {
|
||||
switch (view) {
|
||||
case "line":
|
||||
return renderLinePlot(group, loadedSeries, height, highlight, scale);
|
||||
case "bar":
|
||||
case "bar-reversed":
|
||||
return renderBarPlot(
|
||||
group,
|
||||
loadedSeries,
|
||||
height,
|
||||
highlight,
|
||||
{ reversed: view === "bar-reversed" },
|
||||
scale,
|
||||
);
|
||||
case "dots":
|
||||
return renderDotsPlot(group, loadedSeries, height, highlight, scale);
|
||||
default:
|
||||
return renderStackedPlot(
|
||||
group,
|
||||
loadedSeries,
|
||||
height,
|
||||
highlight,
|
||||
{ reversed: view === "stacked-reversed" },
|
||||
scale,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} args
|
||||
* @param {SVGSVGElement} args.svg
|
||||
@@ -80,6 +41,16 @@ export function createChartRenderer({
|
||||
|
||||
svg.append(group);
|
||||
|
||||
function clearStatus() {
|
||||
status.textContent = "";
|
||||
svg.removeAttribute("aria-busy");
|
||||
}
|
||||
|
||||
/** @param {string} message */
|
||||
function setStatus(message) {
|
||||
status.textContent = message;
|
||||
}
|
||||
|
||||
function renderCurrent() {
|
||||
if (!active || !loadedSeries.length) return;
|
||||
|
||||
@@ -105,9 +76,10 @@ export function createChartRenderer({
|
||||
async function loadCurrent() {
|
||||
const id = (loadId += 1);
|
||||
const loadingTimer = setTimeout(() => {
|
||||
if (id === loadId && active) status.textContent = "Loading";
|
||||
if (id === loadId && active) setStatus("Loading");
|
||||
}, 250);
|
||||
|
||||
setStatus("");
|
||||
svg.setAttribute("aria-busy", "true");
|
||||
|
||||
try {
|
||||
@@ -117,11 +89,11 @@ export function createChartRenderer({
|
||||
|
||||
loadedSeries = nextSeries;
|
||||
renderCurrent();
|
||||
status.textContent = "";
|
||||
clearStatus();
|
||||
} catch (error) {
|
||||
if (id !== loadId) return;
|
||||
console.error(error);
|
||||
status.textContent = "Chart unavailable";
|
||||
setStatus("Chart unavailable");
|
||||
} finally {
|
||||
clearTimeout(loadingTimer);
|
||||
if (id === loadId) svg.removeAttribute("aria-busy");
|
||||
@@ -146,8 +118,7 @@ export function createChartRenderer({
|
||||
group.replaceChildren();
|
||||
highlight.clearNodes();
|
||||
scrubber?.clear();
|
||||
status.textContent = "";
|
||||
svg.removeAttribute("aria-busy");
|
||||
clearStatus();
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -160,7 +131,7 @@ export function createChartRenderer({
|
||||
|
||||
/** @typedef {import("./index.js").Chart} Chart */
|
||||
/** @typedef {import("./index.js").LoadedSeries} LoadedSeries */
|
||||
/** @typedef {import("./legend.js").Readout} Readout */
|
||||
/** @typedef {import("./legend/index.js").Readout} Readout */
|
||||
/** @typedef {import("./scale.js").ChartScale} ChartScale */
|
||||
/** @typedef {import("./timeframes.js").TimeframeValue} TimeframeValue */
|
||||
/** @typedef {import("./views.js").ChartView} ChartView */
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
import { createRadioGroup } from "./radio.js";
|
||||
import { createChartStorage } from "./storage.js";
|
||||
import { createChartSetting } from "./setting.js";
|
||||
|
||||
const storage = createChartStorage("scale");
|
||||
const defaultScale = "linear";
|
||||
const scales = /** @type {const} */ ([
|
||||
{ value: "linear", label: "Lin" },
|
||||
{ value: "log", label: "Log" },
|
||||
]);
|
||||
const defaultScale = "linear";
|
||||
const setting = createChartSetting({
|
||||
storageKey: "scale",
|
||||
legend: "Scale",
|
||||
options: scales,
|
||||
defaultValue: defaultScale,
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {string} chartKey
|
||||
* @param {ChartScale} [fallback]
|
||||
*/
|
||||
export function getDefaultScale(chartKey, fallback = defaultScale) {
|
||||
const value = storage.get(chartKey);
|
||||
|
||||
return scales.find((scale) => scale.value === value)?.value ?? fallback;
|
||||
return setting.get(chartKey, fallback);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -23,7 +25,7 @@ export function getDefaultScale(chartKey, fallback = defaultScale) {
|
||||
* @param {ChartScale} scale
|
||||
*/
|
||||
export function saveScale(chartKey, scale) {
|
||||
storage.set(chartKey, scale);
|
||||
setting.save(chartKey, scale);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -31,12 +33,7 @@ export function saveScale(chartKey, scale) {
|
||||
* @param {(scale: ChartScale) => void} onChange
|
||||
*/
|
||||
export function createScaleControl(currentScale, onChange) {
|
||||
return createRadioGroup({
|
||||
legend: "Scale",
|
||||
options: scales,
|
||||
currentValue: currentScale,
|
||||
onChange,
|
||||
});
|
||||
return setting.create(currentScale, onChange);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+6
-14
@@ -1,9 +1,10 @@
|
||||
import { formatValue } from "./format.js";
|
||||
import { createSvgElement } from "./svg.js";
|
||||
import { VIEWBOX_WIDTH } from "./viewbox.js";
|
||||
import { formatValue } from "../format.js";
|
||||
import { clamp } from "../math.js";
|
||||
import { createSvgElement } from "../svg.js";
|
||||
import { VIEWBOX_WIDTH } from "../viewbox.js";
|
||||
|
||||
/** @typedef {import("./highlight.js").SeriesHighlight} SeriesHighlight */
|
||||
/** @typedef {import("./legend.js").Readout} Readout */
|
||||
/** @typedef {import("../highlight.js").SeriesHighlight} SeriesHighlight */
|
||||
/** @typedef {import("../legend/index.js").Readout} Readout */
|
||||
|
||||
const dateFormat = new Intl.DateTimeFormat("en-US", {
|
||||
day: "2-digit",
|
||||
@@ -11,15 +12,6 @@ const dateFormat = new Intl.DateTimeFormat("en-US", {
|
||||
year: "numeric",
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {number} value
|
||||
* @param {number} min
|
||||
* @param {number} max
|
||||
*/
|
||||
function clamp(value, min, max) {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ScrubberSeries} series
|
||||
* @param {number} ratio
|
||||
@@ -0,0 +1,25 @@
|
||||
main.learn {
|
||||
figure[data-chart="series"] {
|
||||
[data-scrubber] {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
svg[data-scrubbing="true"] [data-scrubber] {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[data-scrubber="guide"] {
|
||||
stroke: var(--white);
|
||||
stroke-dasharray: 2 4;
|
||||
vector-effect: non-scaling-stroke;
|
||||
}
|
||||
|
||||
[data-scrubber="marker"] {
|
||||
fill: var(--black);
|
||||
stroke: var(--color, var(--orange));
|
||||
stroke-width: 1.5;
|
||||
vector-effect: non-scaling-stroke;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { createSvgElement } from "./svg.js";
|
||||
|
||||
/**
|
||||
* @param {Object} args
|
||||
* @param {SVGGElement} args.group
|
||||
* @param {SeriesHighlight} args.highlight
|
||||
* @param {number} args.index
|
||||
* @param {string} args.chart
|
||||
* @param {string} args.color
|
||||
* @param {string} args.d
|
||||
*/
|
||||
export function appendSeriesPath(args) {
|
||||
const path = createSvgElement("path");
|
||||
|
||||
path.dataset.chart = args.chart;
|
||||
path.dataset.series = args.index.toString();
|
||||
path.style.setProperty("--color", args.color);
|
||||
path.setAttribute("d", args.d);
|
||||
args.highlight.addNode(path, args.index);
|
||||
args.group.append(path);
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
/** @typedef {import("./highlight.js").SeriesHighlight} SeriesHighlight */
|
||||
@@ -0,0 +1,50 @@
|
||||
import { createRadioGroup } from "./radio.js";
|
||||
import { createChartStorage } from "./storage.js";
|
||||
|
||||
/**
|
||||
* @template {string} T
|
||||
* @param {Object} config
|
||||
* @param {string} config.storageKey
|
||||
* @param {string} config.legend
|
||||
* @param {readonly { value: T, label: string }[]} config.options
|
||||
* @param {T} config.defaultValue
|
||||
*/
|
||||
export function createChartSetting(config) {
|
||||
const storage = createChartStorage(config.storageKey);
|
||||
|
||||
return {
|
||||
/**
|
||||
* @param {string} chartKey
|
||||
* @param {T} [fallback]
|
||||
*/
|
||||
get(chartKey, fallback = config.defaultValue) {
|
||||
const value = storage.get(chartKey);
|
||||
|
||||
return (
|
||||
config.options.find((option) => option.value === value)?.value ??
|
||||
fallback
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} chartKey
|
||||
* @param {T} value
|
||||
*/
|
||||
save(chartKey, value) {
|
||||
storage.set(chartKey, value);
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {T} currentValue
|
||||
* @param {(value: T) => void} onChange
|
||||
*/
|
||||
create(currentValue, onChange) {
|
||||
return createRadioGroup({
|
||||
legend: config.legend,
|
||||
options: config.options,
|
||||
currentValue,
|
||||
onChange,
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createAreaPathData, createLinePathData } from "../path.js";
|
||||
import { createSvgElement } from "../svg.js";
|
||||
import { appendSeriesPath } from "../series-path.js";
|
||||
import { createStackedSeries } from "./series.js";
|
||||
|
||||
/**
|
||||
@@ -27,26 +27,26 @@ export function renderStackedPlot(
|
||||
|
||||
for (const index of stackIndexes) {
|
||||
const { color, points } = plottedSeries[index];
|
||||
const path = createSvgElement("path");
|
||||
|
||||
path.dataset.chart = "stacked";
|
||||
path.dataset.series = index.toString();
|
||||
path.style.setProperty("--color", color);
|
||||
path.setAttribute("d", createAreaPathData(points));
|
||||
highlight.addNode(path, index);
|
||||
group.append(path);
|
||||
appendSeriesPath({
|
||||
group,
|
||||
highlight,
|
||||
index,
|
||||
chart: "stacked",
|
||||
color,
|
||||
d: createAreaPathData(points),
|
||||
});
|
||||
}
|
||||
|
||||
for (const index of lineIndexes) {
|
||||
const { color, points } = plottedSeries[index];
|
||||
const path = createSvgElement("path");
|
||||
|
||||
path.dataset.chart = "line";
|
||||
path.dataset.series = index.toString();
|
||||
path.style.setProperty("--color", color);
|
||||
path.setAttribute("d", createLinePathData(points));
|
||||
highlight.addNode(path, index);
|
||||
group.append(path);
|
||||
appendSeriesPath({
|
||||
group,
|
||||
highlight,
|
||||
index,
|
||||
chart: "line",
|
||||
color,
|
||||
d: createLinePathData(points),
|
||||
});
|
||||
}
|
||||
|
||||
return plottedSeries;
|
||||
|
||||
@@ -43,103 +43,6 @@ main.learn {
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem 1rem;
|
||||
margin: 0.5rem 0 0;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.125rem 0.5rem;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
text-transform: uppercase;
|
||||
|
||||
legend {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
clip-path: inset(50%);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
label {
|
||||
position: relative;
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
margin: 0;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
span {
|
||||
display: block;
|
||||
padding: 0.25rem;
|
||||
border-radius: 0.25rem;
|
||||
color: var(--gray);
|
||||
}
|
||||
|
||||
label:hover span {
|
||||
color: var(--black);
|
||||
background: var(--white);
|
||||
}
|
||||
|
||||
label:has(:checked):not(:hover) span {
|
||||
color: var(--black);
|
||||
background: var(--gray);
|
||||
}
|
||||
|
||||
label:active span {
|
||||
color: var(--black);
|
||||
background: var(--orange);
|
||||
}
|
||||
|
||||
label:has(:focus-visible) span {
|
||||
outline: 1px solid var(--orange);
|
||||
outline-offset: 0.125rem;
|
||||
}
|
||||
}
|
||||
|
||||
button[data-chart="fullscreen"] {
|
||||
padding: 0.25rem;
|
||||
border: 0;
|
||||
border-radius: 0.25rem;
|
||||
color: var(--gray);
|
||||
background: none;
|
||||
font: inherit;
|
||||
line-height: inherit;
|
||||
text-transform: uppercase;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--black);
|
||||
background: var(--white);
|
||||
}
|
||||
|
||||
&[aria-pressed="true"] {
|
||||
color: var(--black);
|
||||
background: var(--green);
|
||||
}
|
||||
|
||||
&:active {
|
||||
color: var(--black);
|
||||
background: var(--orange);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 1px solid var(--orange);
|
||||
outline-offset: 0.125rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:fullscreen {
|
||||
@@ -154,137 +57,6 @@ main.learn {
|
||||
height: auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
figcaption menu {
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
figcaption {
|
||||
text-transform: uppercase;
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
align-items: start;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
time {
|
||||
color: var(--off-color);
|
||||
}
|
||||
|
||||
span:is([data-chart="unit"], [data-chart="separator"]) {
|
||||
color: var(--off-color);
|
||||
}
|
||||
|
||||
menu {
|
||||
--shadow-size: 1rem;
|
||||
|
||||
display: flex;
|
||||
margin-inline: calc(-1 * var(--shadow-size));
|
||||
padding: 0 var(--shadow-size);
|
||||
padding-bottom: 1rem;
|
||||
padding-top: 0.25rem;
|
||||
overflow-x: auto;
|
||||
list-style: none;
|
||||
mask-image: linear-gradient(
|
||||
to right,
|
||||
transparent,
|
||||
black var(--shadow-size),
|
||||
black calc(100% - var(--shadow-size)),
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
li {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0.25rem 0.375rem;
|
||||
border: 0;
|
||||
border-radius: 0.25rem;
|
||||
color: inherit;
|
||||
background: none;
|
||||
font: inherit;
|
||||
text-align: inherit;
|
||||
text-transform: inherit;
|
||||
cursor: pointer;
|
||||
|
||||
&:is(:hover, :focus-visible, [data-active], [data-preview]) {
|
||||
color: var(--black);
|
||||
background: var(--color);
|
||||
|
||||
span,
|
||||
output {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 1px solid var(--orange);
|
||||
outline-offset: 0.125rem;
|
||||
}
|
||||
|
||||
&[data-muted] {
|
||||
opacity: 0.35;
|
||||
}
|
||||
|
||||
> span {
|
||||
display: block;
|
||||
color: var(--color);
|
||||
text-align: left;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
width: 0.5em;
|
||||
height: 0.5em;
|
||||
margin-right: 0.35em;
|
||||
margin-bottom: 0.1rem;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
}
|
||||
}
|
||||
|
||||
> output {
|
||||
display: block;
|
||||
margin-top: 0.25rem;
|
||||
margin-left: auto;
|
||||
width: 7ch;
|
||||
min-height: 1em;
|
||||
color: var(--white);
|
||||
font-variant-numeric: tabular-nums;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
svg [data-series][data-muted] {
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
[data-scrubber] {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
svg[data-scrubbing="true"] [data-scrubber] {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[data-scrubber="guide"] {
|
||||
stroke: var(--white);
|
||||
stroke-dasharray: 2 4;
|
||||
vector-effect: non-scaling-stroke;
|
||||
}
|
||||
|
||||
[data-scrubber="marker"] {
|
||||
fill: var(--black);
|
||||
stroke: var(--color, var(--orange));
|
||||
stroke-width: 1.5;
|
||||
vector-effect: non-scaling-stroke;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { createRadioGroup } from "./radio.js";
|
||||
import { createChartStorage } from "./storage.js";
|
||||
import { createChartSetting } from "./setting.js";
|
||||
|
||||
const storage = createChartStorage("timeframe");
|
||||
const defaultTimeframe = "all";
|
||||
const timeframes = /** @type {const} */ ({
|
||||
"1d": { index: "minute10", count: 144 },
|
||||
"1w": { index: "hour1", count: 168 },
|
||||
@@ -21,15 +18,16 @@ const options = /** @type {const} */ ([
|
||||
{ value: "8y", label: "8y" },
|
||||
{ value: "all", label: "all" },
|
||||
]);
|
||||
const setting = createChartSetting({
|
||||
storageKey: "timeframe",
|
||||
legend: "Time",
|
||||
options,
|
||||
defaultValue: "all",
|
||||
});
|
||||
|
||||
/** @param {string} chartKey */
|
||||
export function getDefaultTimeframe(chartKey) {
|
||||
const value = storage.get(chartKey);
|
||||
|
||||
return (
|
||||
options.find((timeframe) => timeframe.value === value)?.value ??
|
||||
defaultTimeframe
|
||||
);
|
||||
return setting.get(chartKey);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -37,7 +35,7 @@ export function getDefaultTimeframe(chartKey) {
|
||||
* @param {TimeframeValue} timeframe
|
||||
*/
|
||||
export function saveTimeframe(chartKey, timeframe) {
|
||||
storage.set(chartKey, timeframe);
|
||||
setting.save(chartKey, timeframe);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -45,12 +43,7 @@ export function saveTimeframe(chartKey, timeframe) {
|
||||
* @param {(timeframe: TimeframeValue) => void} onChange
|
||||
*/
|
||||
export function createTimeframeControl(currentTimeframe, onChange) {
|
||||
return createRadioGroup({
|
||||
legend: "Time",
|
||||
options,
|
||||
currentValue: currentTimeframe,
|
||||
onChange,
|
||||
});
|
||||
return setting.create(currentTimeframe, onChange);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,25 +1,35 @@
|
||||
import { createRadioGroup } from "./radio.js";
|
||||
import { createChartStorage } from "./storage.js";
|
||||
import { createChartSetting } from "./setting.js";
|
||||
|
||||
const storage = createChartStorage("view");
|
||||
const defaultView = "stacked";
|
||||
export const viewTypes = /** @type {const} */ ({
|
||||
line: "line",
|
||||
stacked: "stacked",
|
||||
stackedReversed: "stacked-reversed",
|
||||
bar: "bar",
|
||||
barReversed: "bar-reversed",
|
||||
dots: "dots",
|
||||
});
|
||||
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" },
|
||||
{ value: viewTypes.line, label: "Line" },
|
||||
{ value: viewTypes.stacked, label: "Stack↑" },
|
||||
{ value: viewTypes.stackedReversed, label: "Stack↓" },
|
||||
{ value: viewTypes.bar, label: "Bars↑" },
|
||||
{ value: viewTypes.barReversed, label: "Bars↓" },
|
||||
{ value: viewTypes.dots, label: "Dots" },
|
||||
]);
|
||||
const defaultView = viewTypes.stacked;
|
||||
const setting = createChartSetting({
|
||||
storageKey: "view",
|
||||
legend: "View",
|
||||
options: views,
|
||||
defaultValue: defaultView,
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {string} chartKey
|
||||
* @param {ChartView} [fallback]
|
||||
*/
|
||||
export function getDefaultView(chartKey, fallback = defaultView) {
|
||||
const value = storage.get(chartKey);
|
||||
|
||||
return views.find((view) => view.value === value)?.value ?? fallback;
|
||||
return setting.get(chartKey, fallback);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -27,7 +37,7 @@ export function getDefaultView(chartKey, fallback = defaultView) {
|
||||
* @param {ChartView} view
|
||||
*/
|
||||
export function saveView(chartKey, view) {
|
||||
storage.set(chartKey, view);
|
||||
setting.save(chartKey, view);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -35,12 +45,7 @@ export function saveView(chartKey, view) {
|
||||
* @param {(view: ChartView) => void} onChange
|
||||
*/
|
||||
export function createViewControl(currentView, onChange) {
|
||||
return createRadioGroup({
|
||||
legend: "View",
|
||||
options: views,
|
||||
currentValue: currentView,
|
||||
onChange,
|
||||
});
|
||||
return setting.create(currentView, onChange);
|
||||
}
|
||||
|
||||
/** @typedef {(typeof views)[number]["value"]} ChartView */
|
||||
|
||||
@@ -11,6 +11,7 @@ main.learn {
|
||||
font-size: var(--font-size-xs);
|
||||
line-height: var(--line-height-xs);
|
||||
text-transform: uppercase;
|
||||
color: var(--gray);
|
||||
padding-left: 0.5rem;
|
||||
margin-left: -0.5rem;
|
||||
|
||||
@@ -40,7 +41,6 @@ main.learn {
|
||||
ol ol {
|
||||
margin-top: 0.25rem;
|
||||
margin-left: 1rem;
|
||||
color: var(--gray);
|
||||
}
|
||||
|
||||
li + li {
|
||||
@@ -66,15 +66,15 @@ main.learn {
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
&[aria-current="location"] {
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--black);
|
||||
background-color: var(--white);
|
||||
}
|
||||
|
||||
&[aria-current="location"] {
|
||||
color: var(--orange);
|
||||
}
|
||||
|
||||
&:active {
|
||||
color: var(--black);
|
||||
background-color: var(--orange);
|
||||
|
||||
+6
-386
@@ -1,389 +1,9 @@
|
||||
import {
|
||||
capitalizationSeries,
|
||||
marketCapAddressBalanceSeries,
|
||||
marketCapAgeSeries,
|
||||
marketCapClassSeries,
|
||||
marketCapEpochSeries,
|
||||
marketCapProfitabilitySeries,
|
||||
marketCapSeries,
|
||||
marketCapTermSeries,
|
||||
marketCapTypeSeries,
|
||||
marketCapUtxoBalanceSeries,
|
||||
realizedCapAddressBalanceSeries,
|
||||
realizedCapAgeSeries,
|
||||
realizedCapClassSeries,
|
||||
realizedCapEpochSeries,
|
||||
realizedCapProfitabilitySeries,
|
||||
realizedCapSeries,
|
||||
realizedCapTermSeries,
|
||||
realizedCapTypeSeries,
|
||||
realizedCapUtxoBalanceSeries,
|
||||
} from "./capitalization.js";
|
||||
import {
|
||||
addressBalanceSeries,
|
||||
ageSeries,
|
||||
classSeries,
|
||||
epochSeries,
|
||||
exposedSupplySeries,
|
||||
exposedSupplyTypeSeries,
|
||||
termSeries,
|
||||
typeSeries,
|
||||
utxoBalanceSeries,
|
||||
} from "./cohorts.js";
|
||||
import { colors } from "../utils/colors.js";
|
||||
import { units } from "./charts/units.js";
|
||||
|
||||
const lineType = /** @type {const} */ ("line");
|
||||
|
||||
/** @param {typeof import("../utils/client.js").brk} client */
|
||||
function metricCirculatingSupply(client) {
|
||||
return client.series.supply.circulating.btc;
|
||||
}
|
||||
|
||||
/** @param {typeof import("../utils/client.js").brk} client */
|
||||
function metricSupplyInProfit(client) {
|
||||
return client.series.cohorts.utxo.profitability.profit.all.supply.all.btc;
|
||||
}
|
||||
|
||||
/** @param {typeof import("../utils/client.js").brk} client */
|
||||
function metricSupplyInLoss(client) {
|
||||
return client.series.cohorts.utxo.profitability.loss.all.supply.all.btc;
|
||||
}
|
||||
import { capitalizationSection } from "./sections/capitalization.js";
|
||||
import { introductionSection } from "./sections/introduction.js";
|
||||
import { supplySection } from "./sections/supply.js";
|
||||
|
||||
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:
|
||||
"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",
|
||||
unit: units.btc,
|
||||
series: [
|
||||
{
|
||||
label: "Circulating",
|
||||
color: colors.orange,
|
||||
metric: metricCirculatingSupply,
|
||||
},
|
||||
],
|
||||
},
|
||||
children: [
|
||||
{
|
||||
title: "Profitability",
|
||||
description:
|
||||
"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",
|
||||
unit: units.btc,
|
||||
series: [
|
||||
{
|
||||
label: "In profit",
|
||||
color: colors.green,
|
||||
metric: metricSupplyInProfit,
|
||||
},
|
||||
{
|
||||
label: "In loss",
|
||||
color: colors.red,
|
||||
metric: metricSupplyInLoss,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Exposed",
|
||||
description:
|
||||
"Shows BTC held by addresses whose public key is already visible on-chain. This can happen because the address type exposes the key directly, or because coins were spent from that address before.",
|
||||
chart: {
|
||||
title: "Exposed supply",
|
||||
unit: units.btc,
|
||||
defaultType: lineType,
|
||||
series: exposedSupplySeries,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
title: "Type",
|
||||
description:
|
||||
"Splits exposed supply by address type. This shows which script formats account for the visible-public-key supply.",
|
||||
chart: {
|
||||
title: "Exposed supply by type",
|
||||
unit: units.btc,
|
||||
series: exposedSupplyTypeSeries,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Term",
|
||||
description:
|
||||
"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",
|
||||
unit: units.btc,
|
||||
series: termSeries,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Age",
|
||||
description:
|
||||
"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",
|
||||
unit: units.btc,
|
||||
series: ageSeries,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "UTXO Balance",
|
||||
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",
|
||||
unit: units.btc,
|
||||
series: utxoBalanceSeries,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Address Balance",
|
||||
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",
|
||||
unit: units.btc,
|
||||
series: addressBalanceSeries,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "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",
|
||||
unit: units.btc,
|
||||
series: typeSeries,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Epoch",
|
||||
description:
|
||||
"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",
|
||||
unit: units.btc,
|
||||
series: epochSeries,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Class",
|
||||
description:
|
||||
"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",
|
||||
unit: units.btc,
|
||||
series: classSeries,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Capitalization",
|
||||
description:
|
||||
"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",
|
||||
unit: units.usd,
|
||||
defaultType: lineType,
|
||||
series: capitalizationSeries,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
title: "Market Cap",
|
||||
description:
|
||||
"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",
|
||||
unit: units.usd,
|
||||
series: marketCapSeries,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
title: "Profitability",
|
||||
description:
|
||||
"Splits market cap between coins that are currently in profit and coins that are currently in loss. This shows how much current market value sits above or below each coin's last moved price.",
|
||||
chart: {
|
||||
title: "Market cap by profitability",
|
||||
unit: units.usd,
|
||||
series: marketCapProfitabilitySeries,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Term",
|
||||
description:
|
||||
"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",
|
||||
unit: units.usd,
|
||||
series: marketCapTermSeries,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Age",
|
||||
description:
|
||||
"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",
|
||||
unit: units.usd,
|
||||
series: marketCapAgeSeries,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "UTXO Balance",
|
||||
description:
|
||||
"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",
|
||||
unit: units.usd,
|
||||
series: marketCapUtxoBalanceSeries,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Address Balance",
|
||||
description:
|
||||
"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",
|
||||
unit: units.usd,
|
||||
series: marketCapAddressBalanceSeries,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "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",
|
||||
unit: units.usd,
|
||||
series: marketCapTypeSeries,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Epoch",
|
||||
description:
|
||||
"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",
|
||||
unit: units.usd,
|
||||
series: marketCapEpochSeries,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Class",
|
||||
description:
|
||||
"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",
|
||||
unit: units.usd,
|
||||
series: marketCapClassSeries,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Realized Cap",
|
||||
description:
|
||||
"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",
|
||||
unit: units.usd,
|
||||
series: realizedCapSeries,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
title: "Profitability",
|
||||
description:
|
||||
"Splits realized cap between coins that are currently in profit and coins that are currently in loss. This shows how the market's cost basis is distributed across coins above or below their last moved price.",
|
||||
chart: {
|
||||
title: "Realized cap by profitability",
|
||||
unit: units.usd,
|
||||
series: realizedCapProfitabilitySeries,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Term",
|
||||
description:
|
||||
"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",
|
||||
unit: units.usd,
|
||||
series: realizedCapTermSeries,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Age",
|
||||
description:
|
||||
"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",
|
||||
unit: units.usd,
|
||||
series: realizedCapAgeSeries,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "UTXO Balance",
|
||||
description:
|
||||
"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",
|
||||
unit: units.usd,
|
||||
series: realizedCapUtxoBalanceSeries,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Address Balance",
|
||||
description:
|
||||
"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",
|
||||
unit: units.usd,
|
||||
series: realizedCapAddressBalanceSeries,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Type",
|
||||
description:
|
||||
"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",
|
||||
unit: units.usd,
|
||||
series: realizedCapTypeSeries,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Epoch",
|
||||
description:
|
||||
"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",
|
||||
unit: units.usd,
|
||||
series: realizedCapEpochSeries,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Class",
|
||||
description:
|
||||
"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",
|
||||
unit: units.usd,
|
||||
series: realizedCapClassSeries,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
introductionSection,
|
||||
supplySection,
|
||||
capitalizationSection,
|
||||
];
|
||||
|
||||
@@ -18,17 +18,18 @@ export function initScrollSpy(main) {
|
||||
let scheduled = false;
|
||||
|
||||
function getViewportTop() {
|
||||
return Number.parseFloat(getComputedStyle(main).getPropertyValue("--offset"));
|
||||
return Number.parseFloat(getComputedStyle(main).scrollPaddingTop);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Element} section
|
||||
* @param {Element | null} firstChild
|
||||
* @param {number} viewportTop
|
||||
*/
|
||||
function getOwnVisibleHeight(section, firstChild) {
|
||||
function getOwnVisibleHeight(section, firstChild, viewportTop) {
|
||||
const sectionRect = section.getBoundingClientRect();
|
||||
const childRect = firstChild?.getBoundingClientRect();
|
||||
const top = Math.max(sectionRect.top, getViewportTop());
|
||||
const top = Math.max(sectionRect.top, viewportTop);
|
||||
const bottom = Math.min(
|
||||
childRect?.top ?? sectionRect.bottom,
|
||||
window.innerHeight,
|
||||
@@ -80,9 +81,14 @@ export function initScrollSpy(main) {
|
||||
/** @type {{ section: Element, firstChild: Element | null } | undefined} */
|
||||
let currentState;
|
||||
let currentHeight = 0;
|
||||
const viewportTop = getViewportTop();
|
||||
|
||||
for (const state of sectionStates) {
|
||||
const height = getOwnVisibleHeight(state.section, state.firstChild);
|
||||
const height = getOwnVisibleHeight(
|
||||
state.section,
|
||||
state.firstChild,
|
||||
viewportTop,
|
||||
);
|
||||
|
||||
if (height > currentHeight) {
|
||||
currentState = state;
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { capitalizationSeries } from "../capitalization.js";
|
||||
import { units } from "../charts/units.js";
|
||||
import { viewTypes } from "../charts/views.js";
|
||||
import { marketCapSection } from "./capitalization/market.js";
|
||||
import { realizedCapSection } from "./capitalization/realized.js";
|
||||
|
||||
export const capitalizationSection = {
|
||||
title: "Capitalization",
|
||||
description:
|
||||
"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",
|
||||
unit: units.usd,
|
||||
defaultType: viewTypes.line,
|
||||
series: capitalizationSeries,
|
||||
},
|
||||
children: [
|
||||
marketCapSection,
|
||||
realizedCapSection,
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,105 @@
|
||||
import {
|
||||
marketCapAddressBalanceSeries,
|
||||
marketCapAgeSeries,
|
||||
marketCapClassSeries,
|
||||
marketCapEpochSeries,
|
||||
marketCapProfitabilitySeries,
|
||||
marketCapSeries,
|
||||
marketCapTermSeries,
|
||||
marketCapTypeSeries,
|
||||
marketCapUtxoBalanceSeries,
|
||||
} from "../../capitalization.js";
|
||||
import { units } from "../../charts/units.js";
|
||||
|
||||
export const marketCapSection = {
|
||||
title: "Market Cap",
|
||||
description:
|
||||
"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",
|
||||
unit: units.usd,
|
||||
series: marketCapSeries,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
title: "Profitability",
|
||||
description:
|
||||
"Splits market cap between coins that are currently in profit and coins that are currently in loss. This shows how much current market value sits above or below each coin's last moved price.",
|
||||
chart: {
|
||||
title: "Market cap by profitability",
|
||||
unit: units.usd,
|
||||
series: marketCapProfitabilitySeries,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Term",
|
||||
description:
|
||||
"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",
|
||||
unit: units.usd,
|
||||
series: marketCapTermSeries,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Age",
|
||||
description:
|
||||
"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",
|
||||
unit: units.usd,
|
||||
series: marketCapAgeSeries,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "UTXO Balance",
|
||||
description:
|
||||
"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",
|
||||
unit: units.usd,
|
||||
series: marketCapUtxoBalanceSeries,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Address Balance",
|
||||
description:
|
||||
"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",
|
||||
unit: units.usd,
|
||||
series: marketCapAddressBalanceSeries,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "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",
|
||||
unit: units.usd,
|
||||
series: marketCapTypeSeries,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Epoch",
|
||||
description:
|
||||
"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",
|
||||
unit: units.usd,
|
||||
series: marketCapEpochSeries,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Class",
|
||||
description:
|
||||
"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",
|
||||
unit: units.usd,
|
||||
series: marketCapClassSeries,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,105 @@
|
||||
import {
|
||||
realizedCapAddressBalanceSeries,
|
||||
realizedCapAgeSeries,
|
||||
realizedCapClassSeries,
|
||||
realizedCapEpochSeries,
|
||||
realizedCapProfitabilitySeries,
|
||||
realizedCapSeries,
|
||||
realizedCapTermSeries,
|
||||
realizedCapTypeSeries,
|
||||
realizedCapUtxoBalanceSeries,
|
||||
} from "../../capitalization.js";
|
||||
import { units } from "../../charts/units.js";
|
||||
|
||||
export const realizedCapSection = {
|
||||
title: "Realized Cap",
|
||||
description:
|
||||
"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",
|
||||
unit: units.usd,
|
||||
series: realizedCapSeries,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
title: "Profitability",
|
||||
description:
|
||||
"Splits realized cap between coins that are currently in profit and coins that are currently in loss. This shows how the market's cost basis is distributed across coins above or below their last moved price.",
|
||||
chart: {
|
||||
title: "Realized cap by profitability",
|
||||
unit: units.usd,
|
||||
series: realizedCapProfitabilitySeries,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Term",
|
||||
description:
|
||||
"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",
|
||||
unit: units.usd,
|
||||
series: realizedCapTermSeries,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Age",
|
||||
description:
|
||||
"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",
|
||||
unit: units.usd,
|
||||
series: realizedCapAgeSeries,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "UTXO Balance",
|
||||
description:
|
||||
"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",
|
||||
unit: units.usd,
|
||||
series: realizedCapUtxoBalanceSeries,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Address Balance",
|
||||
description:
|
||||
"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",
|
||||
unit: units.usd,
|
||||
series: realizedCapAddressBalanceSeries,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Type",
|
||||
description:
|
||||
"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",
|
||||
unit: units.usd,
|
||||
series: realizedCapTypeSeries,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Epoch",
|
||||
description:
|
||||
"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",
|
||||
unit: units.usd,
|
||||
series: realizedCapEpochSeries,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Class",
|
||||
description:
|
||||
"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",
|
||||
unit: units.usd,
|
||||
series: realizedCapClassSeries,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
export const introductionSection = {
|
||||
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.",
|
||||
};
|
||||
@@ -0,0 +1,133 @@
|
||||
import {
|
||||
addressBalanceSeries,
|
||||
ageSeries,
|
||||
classSeries,
|
||||
epochSeries,
|
||||
exposedSupplySeries,
|
||||
exposedSupplyTypeSeries,
|
||||
termSeries,
|
||||
typeSeries,
|
||||
utxoBalanceSeries,
|
||||
} from "../cohorts.js";
|
||||
import {
|
||||
circulatingSupplySeries,
|
||||
supplyProfitabilitySeries,
|
||||
} from "../supply.js";
|
||||
import { units } from "../charts/units.js";
|
||||
import { viewTypes } from "../charts/views.js";
|
||||
|
||||
export const supplySection = {
|
||||
title: "Supply",
|
||||
description:
|
||||
"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",
|
||||
unit: units.btc,
|
||||
series: circulatingSupplySeries,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
title: "Profitability",
|
||||
description:
|
||||
"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",
|
||||
unit: units.btc,
|
||||
series: supplyProfitabilitySeries,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Exposed",
|
||||
description:
|
||||
"Shows BTC held by addresses whose public key is already visible on-chain. This can happen because the address type exposes the key directly, or because coins were spent from that address before.",
|
||||
chart: {
|
||||
title: "Exposed supply",
|
||||
unit: units.btc,
|
||||
defaultType: viewTypes.line,
|
||||
series: exposedSupplySeries,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
title: "Type",
|
||||
description:
|
||||
"Splits exposed supply by address type. This shows which script formats account for the visible-public-key supply.",
|
||||
chart: {
|
||||
title: "Exposed supply by type",
|
||||
unit: units.btc,
|
||||
series: exposedSupplyTypeSeries,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Term",
|
||||
description:
|
||||
"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",
|
||||
unit: units.btc,
|
||||
series: termSeries,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Age",
|
||||
description:
|
||||
"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",
|
||||
unit: units.btc,
|
||||
series: ageSeries,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "UTXO Balance",
|
||||
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",
|
||||
unit: units.btc,
|
||||
series: utxoBalanceSeries,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Address Balance",
|
||||
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",
|
||||
unit: units.btc,
|
||||
series: addressBalanceSeries,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "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",
|
||||
unit: units.btc,
|
||||
series: typeSeries,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Epoch",
|
||||
description:
|
||||
"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",
|
||||
unit: units.btc,
|
||||
series: epochSeries,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Class",
|
||||
description:
|
||||
"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",
|
||||
unit: units.btc,
|
||||
series: classSeries,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -16,6 +16,7 @@ main.learn {
|
||||
grid-template-columns: 14rem minmax(0, 1fr);
|
||||
gap: 4rem;
|
||||
padding: 0 var(--page-x);
|
||||
scroll-padding-top: var(--offset);
|
||||
|
||||
article {
|
||||
counter-reset: theme;
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { createCohortSeries } from "./cohort-series.js";
|
||||
import { colors } from "../utils/colors.js";
|
||||
|
||||
export const circulatingSupplySeries = createCohortSeries([
|
||||
{
|
||||
label: "Circulating",
|
||||
color: colors.orange,
|
||||
metric: (client) => client.series.supply.circulating.btc,
|
||||
},
|
||||
]);
|
||||
|
||||
export const supplyProfitabilitySeries = createCohortSeries([
|
||||
{
|
||||
label: "In profit",
|
||||
color: colors.green,
|
||||
metric: (client) =>
|
||||
client.series.cohorts.utxo.profitability.profit.all.supply.all.btc,
|
||||
},
|
||||
{
|
||||
label: "In loss",
|
||||
color: colors.red,
|
||||
metric: (client) =>
|
||||
client.series.cohorts.utxo.profitability.loss.all.supply.all.btc,
|
||||
},
|
||||
]);
|
||||
@@ -4,7 +4,6 @@
|
||||
--white: oklch(95% 0 0);
|
||||
--dark-white: oklch(92.5% 0 0);
|
||||
--gray: oklch(55% 0 0);
|
||||
--light-black: oklch(17.5% 0 0);
|
||||
--black: oklch(15% 0 0);
|
||||
--red: oklch(0.607 0.241 26.328);
|
||||
--orange: oklch(67.64% 0.191 44.41);
|
||||
@@ -24,7 +23,6 @@
|
||||
--fuchsia: oklch(0.629 0.294 322.523);
|
||||
--pink: oklch(0.624 0.245 357.444);
|
||||
--rose: oklch(0.6155 0.2495 17.012);
|
||||
--color: light-dark(var(--black), var(--white));
|
||||
--off-color: var(--gray);
|
||||
|
||||
--font-size-xs: 0.75rem;
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
* @param {string} selector
|
||||
*/
|
||||
export function getEventTarget(event, selector) {
|
||||
return /** @type {HTMLElement | null} */ (
|
||||
/** @type {HTMLElement} */ (event.target).closest(selector)
|
||||
);
|
||||
const target = event.target;
|
||||
|
||||
return target instanceof Element ? target.closest(selector) : null;
|
||||
}
|
||||
|
||||
/** @param {Event} event */
|
||||
|
||||
Reference in New Issue
Block a user