Files
brk/website_next/explore/block/fee-chart.js
T
2026-07-03 21:06:32 +02:00

192 lines
4.6 KiB
JavaScript

import { createXyChart } from "../../chart/xy/index.js";
import { getPlotHeight, insetPlotY } from "../../chart/viewbox.js";
export const FEE_PERCENTILE_LABELS = /** @type {const} */ ([
"min",
"10%",
"25%",
"50%",
"75%",
"90%",
"max",
]);
const FEE_PERCENTILE_COLORS = /** @type {const} */ ([
"var(--cyan)",
"var(--blue)",
"var(--violet)",
"var(--white)",
"var(--yellow)",
"var(--orange)",
"var(--red)",
]);
const VIEWBOX_HEIGHT = 180;
const FEE_AVERAGE_COLOR = "var(--green)";
/** @param {number} value */
function scaleFeeRate(value) {
return Math.log10(value + 1);
}
/**
* @param {number[]} values
* @param {number} averageRate
* @returns {FeeEntry[]}
*/
function createEntries(values, averageRate) {
return [
...values.map((value, index) => ({
label: FEE_PERCENTILE_LABELS[index],
value,
color: FEE_PERCENTILE_COLORS[index],
pointIndex: index,
priority: 0,
})),
{
label: "avg",
value: averageRate,
color: FEE_AVERAGE_COLOR,
pointIndex: null,
priority: 1,
},
].sort((a, b) => a.value - b.value || a.priority - b.priority);
}
/**
* @param {FeeEntry[]} entries
* @returns {XySeries[]}
*/
function createSeries(entries) {
return [
{
label: "range",
color: () => "var(--gray)",
kind: /** @type {const} */ ("line"),
hidden: true,
},
...entries.map((entry) => ({
label: entry.label,
color: () => entry.color,
kind: /** @type {const} */ ("point"),
})),
];
}
/**
* @param {readonly number[]} values
* @param {ChartFrame} frame
* @returns {{ x: number, y: number, value: number }[]}
*/
function createPoints(values, frame) {
const scaledValues = values.map(scaleFeeRate);
const min = Math.min(...scaledValues);
const max = Math.max(...scaledValues);
const span = max - min;
const plotHeight = getPlotHeight(frame);
const xScale = frame.width / (scaledValues.length - 1);
return scaledValues.map((value, index) => ({
x: xScale * index,
y: span
? insetPlotY(frame, (1 - (value - min) / span) * plotHeight)
: insetPlotY(frame, plotHeight / 2),
value: values[index],
}));
}
/**
* @param {number[]} values
* @param {{ x: number, y: number, value: number }[]} points
* @param {number} target
*/
function interpolatePoint(values, points, target) {
const scaledValues = values.map(scaleFeeRate);
const scaledTarget = scaleFeeRate(target);
if (scaledTarget <= scaledValues[0]) {
return { ...points[0], value: target };
}
for (let index = 1; index < scaledValues.length; index += 1) {
if (scaledTarget > scaledValues[index]) continue;
const previousValue = scaledValues[index - 1];
const nextValue = scaledValues[index];
const previousPoint = points[index - 1];
const nextPoint = points[index];
const span = nextValue - previousValue;
const ratio = span ? (scaledTarget - previousValue) / span : 0;
return {
x: previousPoint.x + (nextPoint.x - previousPoint.x) * ratio,
y: previousPoint.y + (nextPoint.y - previousPoint.y) * ratio,
value: target,
};
}
return { ...points[points.length - 1], value: target };
}
/**
* @param {number[]} values
* @param {FeeEntry[]} entries
* @param {ChartFrame} frame
* @returns {XyPlottedSeries[]}
*/
function plotSeries(values, entries, frame) {
const points = createPoints(values, frame);
return [
{ points },
...entries.map((entry) => {
const point =
entry.pointIndex === null
? interpolatePoint(values, points, entry.value)
: points[entry.pointIndex];
return {
points: [point],
value: entry.value,
};
}),
];
}
/**
* @param {number[]} values
* @param {number} averageRate
* @param {(value: number) => string} formatRate
*/
export function createFeeChart(values, averageRate, formatRate) {
const entries = createEntries(values, averageRate);
const figure = createXyChart({
title: "Percentiles",
unit: {
id: "sat/vB",
name: "satoshis per virtual byte",
format: formatRate,
},
ariaLabel: `Fee rate percentiles from ${formatRate(
values[0],
)} to ${formatRate(values[values.length - 1])} sat/vB`,
fallbackHeight: VIEWBOX_HEIGHT,
series: createSeries(entries),
plot: (frame) => plotSeries(values, entries, frame),
marker: false,
});
figure.dataset.feeChart = "";
return figure;
}
/**
* @typedef {Object} FeeEntry
* @property {string} label
* @property {number} value
* @property {string} color
* @property {number | null} pointIndex
* @property {number} priority
*/