website: redesign part 24

This commit is contained in:
nym21
2026-06-08 16:37:53 +02:00
parent 6786be296d
commit b0b261fe9f
7 changed files with 111 additions and 61 deletions
+8 -4
View File
@@ -1,14 +1,18 @@
const suffixes = ["M", "B", "T", "P", "E", "Z", "Y"];
const numberFormats = [0, 1, 2, 3].map(
(digits) =>
new Intl.NumberFormat("en-US", {
maximumFractionDigits: digits,
minimumFractionDigits: digits,
}),
);
/**
* @param {number} value
* @param {number} digits
*/
function formatNumber(value, digits) {
return value.toLocaleString("en-US", {
maximumFractionDigits: digits,
minimumFractionDigits: digits,
});
return numberFormats[digits].format(value);
}
/** @param {number} value */
+84 -37
View File
@@ -14,19 +14,37 @@ const dateFormat = new Intl.DateTimeFormat("en-US", {
const markerRadiusPx = 4;
/** @param {SVGSVGElement} svg */
function getMarkerRadiusInViewBox(svg) {
const width = svg.getBoundingClientRect().width;
/** @param {number} width */
function getMarkerRadiusInViewBox(width) {
return width ? (markerRadiusPx * VIEWBOX_WIDTH) / width : markerRadiusPx;
}
/**
* @param {ScrubberSeries} series
* @param {number} ratio
* @param {number} step
*/
function getPointAtRatio(series, ratio) {
return series.points[Math.round(ratio * (series.points.length - 1))];
function getPointAtStep(series, step) {
return series.points[step];
}
/**
* @param {ReturnType<typeof getPointAtStep>[]} points
* @param {number} y
*/
function getClosestPointIndex(points, y) {
let closestIndex = 0;
let closestDistance = Infinity;
for (const [index, point] of points.entries()) {
const distance = Math.abs(point.y - y);
if (distance < closestDistance) {
closestIndex = index;
closestDistance = distance;
}
}
return closestIndex;
}
/**
@@ -40,7 +58,7 @@ function updateTime(time, date) {
/**
* @param {Readout} readout
* @param {ReturnType<typeof getPointAtRatio>[]} points
* @param {ReturnType<typeof getPointAtStep>[]} points
*/
function updateReadout(readout, points) {
updateTime(readout.time, points[0].date);
@@ -57,6 +75,7 @@ function updateReadout(readout, points) {
*/
export function createScrubber(svg, readout, highlight) {
const group = createSvgElement("g");
const shade = createSvgElement("rect");
const guide = createSvgElement("line");
/** @type {ScrubberSeries[]} */
let series = [];
@@ -64,53 +83,83 @@ export function createScrubber(svg, readout, highlight) {
let markers = [];
let height = 0;
let stepCount = 0;
let currentStep = -1;
let currentPoints = getPointsAtStep(0);
let rect = svg.getBoundingClientRect();
group.dataset.scrubber = "root";
shade.dataset.scrubber = "shade";
guide.dataset.scrubber = "guide";
group.append(guide);
group.append(shade, guide);
svg.append(group);
function measure() {
rect = svg.getBoundingClientRect();
}
/** @param {number} step */
function getPointsAtStep(step) {
return series.map((item) => getPointAtStep(item, step));
}
/**
* @param {number} ratio
* @param {number} [y]
* @param {boolean} [scrubbing]
*/
function update(ratio, scrubbing = true) {
function update(ratio, y, scrubbing = true) {
if (!series.length) return;
const nextRatio = clamp(ratio, 0, 1);
const points = series.map((item) => getPointAtRatio(item, nextRatio));
const x = points[0].x.toFixed(2);
const nextStep = Math.round(clamp(ratio, 0, 1) * stepCount);
svg.dataset.index = Math.round(nextRatio * stepCount).toString();
guide.setAttribute("x1", x);
guide.setAttribute("x2", x);
guide.setAttribute("y1", "0");
guide.setAttribute("y2", height.toString());
updateReadout(readout, points);
if (nextStep !== currentStep) {
currentStep = nextStep;
currentPoints = getPointsAtStep(nextStep);
markers.forEach((marker, index) => {
const point = points[index];
const x = currentPoints[0].x;
const xText = x.toFixed(2);
marker.setAttribute("cx", point.x.toFixed(2));
marker.setAttribute("cy", point.y.toFixed(2));
});
svg.dataset.index = nextStep.toString();
shade.setAttribute("x", xText);
shade.setAttribute("y", "0");
shade.setAttribute("width", (VIEWBOX_WIDTH - x).toFixed(2));
shade.setAttribute("height", height.toString());
guide.setAttribute("x1", xText);
guide.setAttribute("x2", xText);
guide.setAttribute("y1", "0");
guide.setAttribute("y2", height.toString());
updateReadout(readout, currentPoints);
markers.forEach((marker, index) => {
const point = currentPoints[index];
marker.setAttribute("cx", point.x.toFixed(2));
marker.setAttribute("cy", point.y.toFixed(2));
});
}
if (scrubbing) {
svg.dataset.scrubbing = "true";
} else {
delete svg.dataset.scrubbing;
}
if (y !== undefined) {
highlight.preview(getClosestPointIndex(currentPoints, y));
}
}
function hide() {
update(1, false);
update(1, undefined, false);
}
function clear() {
series = [];
markers = [];
currentStep = -1;
currentPoints = [];
highlight.clearPreview();
group.replaceChildren(guide);
group.replaceChildren(shade, guide);
delete svg.dataset.index;
delete svg.dataset.scrubbing;
}
@@ -122,8 +171,10 @@ export function createScrubber(svg, readout, highlight) {
function setSeries(nextSeries, nextHeight) {
series = nextSeries;
height = nextHeight;
currentStep = -1;
stepCount = Math.max(...series.map(({ points }) => points.length - 1));
const radius = getMarkerRadiusInViewBox(svg);
measure();
const radius = getMarkerRadiusInViewBox(rect.width);
markers = series.map(({ color }, index) => {
const marker = createSvgElement("circle");
@@ -136,23 +187,19 @@ export function createScrubber(svg, readout, highlight) {
return marker;
});
group.replaceChildren(guide, ...markers);
update(1, false);
group.replaceChildren(shade, guide, ...markers);
update(1, undefined, false);
}
/** @param {PointerEvent} event */
function updateFromPointer(event) {
const { left, width } = svg.getBoundingClientRect();
const x = ((event.clientX - left) / width) * VIEWBOX_WIDTH;
const index = Number(
/** @type {SVGElement} */ (event.target).dataset.series,
);
const x = ((event.clientX - rect.left) / rect.width) * VIEWBOX_WIDTH;
const y = ((event.clientY - rect.top) / rect.height) * height;
if (Number.isInteger(index)) highlight.preview(index);
else highlight.clearPreview();
update(x / VIEWBOX_WIDTH);
update(x / VIEWBOX_WIDTH, y);
}
svg.addEventListener("pointerenter", measure);
svg.addEventListener("pointermove", updateFromPointer);
svg.addEventListener("pointerleave", () => {
highlight.clearPreview();
@@ -15,6 +15,11 @@ main.learn {
vector-effect: non-scaling-stroke;
}
[data-scrubber="shade"] {
fill: var(--black);
fill-opacity: 0.5;
}
[data-scrubber="marker"] {
fill: var(--black);
stroke: var(--color, var(--orange));
+6 -3
View File
@@ -82,13 +82,16 @@ export function createStackedSeries(loadedSeries, height, order, scale) {
if (value < 0) negative = end;
else positive = end;
const y0 = scaleY(start, bounds, height, scale);
const y1 = scaleY(end, bounds, height, scale);
plottedSeries[seriesIndex].points.push({
date,
value,
x,
y: scaleY(end, bounds, height, scale),
y0: scaleY(start, bounds, height, scale),
y1: scaleY(end, bounds, height, scale),
y: y1,
y0,
y1,
});
}
+6 -13
View File
@@ -1,20 +1,13 @@
import { createChartSetting } from "./setting.js";
export const viewTypes = /** @type {const} */ ({
line: "line",
area: "area",
stacked: "stacked",
bar: "bar",
dots: "dots",
});
const views = /** @type {const} */ ([
{ value: viewTypes.line, label: "Line" },
{ value: viewTypes.area, label: "Area" },
{ value: viewTypes.stacked, label: "Stack" },
{ value: viewTypes.bar, label: "Bars" },
{ value: viewTypes.dots, label: "Dots" },
{ value: "line", label: "Line" },
{ value: "area", label: "Area" },
{ value: "stacked", label: "Stack" },
{ value: "bar", label: "Bars" },
{ value: "dots", label: "Dots" },
]);
const defaultView = viewTypes.stacked;
const defaultView = "stacked";
const setting = createChartSetting({
storageKey: "view",
legend: "View",
@@ -1,6 +1,5 @@
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";
@@ -11,7 +10,7 @@ export const capitalizationSection = {
chart: {
title: "Capitalization",
unit: units.usd,
defaultType: viewTypes.line,
defaultType: /** @type {const} */ ("line"),
series: capitalizationSeries,
},
children: [
+1 -2
View File
@@ -14,7 +14,6 @@ import {
supplyProfitabilitySeries,
} from "../supply.js";
import { units } from "../charts/units.js";
import { viewTypes } from "../charts/views.js";
export const supplySection = {
title: "Supply",
@@ -43,7 +42,7 @@ export const supplySection = {
chart: {
title: "Exposed supply",
unit: units.btc,
defaultType: viewTypes.line,
defaultType: /** @type {const} */ ("line"),
series: exposedSupplySeries,
},
children: [