Files
brk/website_next/explore/block/fee-chart.js
T
2026-07-04 13:27:57 +02:00

187 lines
4.6 KiB
JavaScript

import { createXyChart } from "../../chart/xy/index.js";
import { createChartPoint, createChartPoints } from "../../chart/points.js";
import { createLinearXScale } from "../../chart/x.js";
import { createValueYScale } from "../../chart/y.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 FEE_PERCENTILE_X = /** @type {const} */ ([0, 10, 25, 50, 75, 90, 100]);
const VIEWBOX_HEIGHT = 180;
const FEE_AVERAGE_COLOR = "var(--green)";
/** @param {number} value */
function scaleFeeRate(value) {
return Math.log10(value + 1);
}
/**
* @param {readonly number[]} values
* @returns {ChartSample[]}
*/
function createPercentileSamples(values) {
return values.map((y, index) => ({ x: FEE_PERCENTILE_X[index], y }));
}
/**
* @param {ChartSample[]} samples
* @param {number} averageRate
*/
function createAverageSample(samples, averageRate) {
const scaledValues = samples.map(({ y }) => scaleFeeRate(y));
const scaledAverage = scaleFeeRate(averageRate);
if (scaledAverage <= scaledValues[0]) {
return { x: samples[0].x, y: averageRate };
}
for (let index = 1; index < scaledValues.length; index += 1) {
if (scaledAverage > scaledValues[index]) continue;
const previousValue = scaledValues[index - 1];
const nextValue = scaledValues[index];
const previousSample = samples[index - 1];
const nextSample = samples[index];
const span = nextValue - previousValue;
const ratio = span ? (scaledAverage - previousValue) / span : 0;
const previousX = /** @type {number} */ (previousSample.x);
const nextX = /** @type {number} */ (nextSample.x);
return {
x: previousX + (nextX - previousX) * ratio,
y: averageRate,
};
}
return { x: samples[samples.length - 1].x, y: averageRate };
}
/**
* @param {ChartSample[]} percentileSamples
* @param {number} averageRate
* @returns {FeeEntry[]}
*/
function createEntries(percentileSamples, averageRate) {
return [
...percentileSamples.map((sample, index) => ({
label: FEE_PERCENTILE_LABELS[index],
sample,
color: FEE_PERCENTILE_COLORS[index],
priority: 0,
})),
{
label: "avg",
sample: createAverageSample(percentileSamples, averageRate),
color: FEE_AVERAGE_COLOR,
priority: 1,
},
].sort((a, b) => a.sample.y - b.sample.y || 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 {ChartSample[]} percentileSamples
* @param {FeeEntry[]} entries
* @param {ChartFrame} frame
* @returns {XyPlottedSeries[]}
*/
function plotSeries(percentileSamples, entries, frame) {
const scaleX = createLinearXScale(
frame,
percentileSamples.map(({ x }) => /** @type {number} */ (x)),
);
const scalePlotY = createValueYScale(
frame,
entries.map(({ sample }) => sample.y),
scaleFeeRate,
);
const percentilePoints = createChartPoints(
percentileSamples,
scaleX,
scalePlotY,
);
return [
{ points: percentilePoints },
...entries.map((entry) => {
return {
points: [createChartPoint(entry.sample, 0, scaleX, scalePlotY)],
readout: entry.sample.y,
};
}),
];
}
/**
* @param {number[]} values
* @param {number} averageRate
* @param {(value: number) => string} formatRate
*/
export function createFeeChart(values, averageRate, formatRate) {
const percentileSamples = createPercentileSamples(values);
const entries = createEntries(percentileSamples, averageRate);
const figure = createXyChart({
title: "Fees",
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(percentileSamples, entries, frame),
marker: false,
});
figure.dataset.feeChart = "";
return figure;
}
/**
* @typedef {Object} FeeEntry
* @property {string} label
* @property {ChartSample} sample
* @property {string} color
* @property {number} priority
*/