mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-07-04 15:53:40 -07:00
192 lines
4.6 KiB
JavaScript
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
|
|
*/
|