mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-07-09 18:18:14 -07:00
website_next: part 7
This commit is contained in:
@@ -35,34 +35,6 @@ export function createFeeRateRange(feeRates) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} index
|
||||
* @param {number} count
|
||||
*/
|
||||
export function getFeeRateStopByRank(index, count) {
|
||||
const percentile = count === 1 ? 100 : 100 - (index / (count - 1)) * 100;
|
||||
|
||||
for (let stopIndex = 0; stopIndex < FEE_RATE_PERCENTILES.length; stopIndex += 1) {
|
||||
if (percentile <= FEE_RATE_PERCENTILES[stopIndex]) {
|
||||
return FEE_RATE_STOPS[stopIndex];
|
||||
}
|
||||
}
|
||||
|
||||
return FEE_RATE_STOPS[FEE_RATE_STOPS.length - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} feeRate
|
||||
* @param {number[]} ranges
|
||||
*/
|
||||
export function getFeeRateStop(feeRate, ranges) {
|
||||
for (let index = 0; index < ranges.length; index += 1) {
|
||||
if (feeRate <= ranges[index]) return FEE_RATE_STOPS[index];
|
||||
}
|
||||
|
||||
return FEE_RATE_STOPS[FEE_RATE_STOPS.length - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} feeRate
|
||||
* @param {number[]} ranges
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import {
|
||||
createFeeRateRange,
|
||||
FEE_RATE_STOPS,
|
||||
getFeeRateStopByRank,
|
||||
} from "../fee-rates.js";
|
||||
import { createFeeRateRange } from "../fee-rates.js";
|
||||
|
||||
/**
|
||||
* @param {BlockPreviewTransaction[]} transactions
|
||||
@@ -11,14 +7,6 @@ export function createPreviewFeeRange(transactions) {
|
||||
return createFeeRateRange(transactions.map(({ feeRate }) => feeRate));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} index
|
||||
* @param {number} count
|
||||
*/
|
||||
export function getFeeBucket(index, count) {
|
||||
return getFeeRateStopByRank(index, count);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {BlockPreviewTransaction[]} transactions
|
||||
*/
|
||||
@@ -34,5 +22,3 @@ export function orderTransactions(transactions) {
|
||||
* @property {number} weight
|
||||
* @property {number} feeRate
|
||||
*/
|
||||
|
||||
export { FEE_RATE_STOPS as FEE_BUCKETS };
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import {
|
||||
appendLegendListItem,
|
||||
createLegendItem,
|
||||
createLegendList,
|
||||
} from "../../../legend/index.js";
|
||||
import { formatNumber } from "../format.js";
|
||||
|
||||
const VERSION_FILTERS = /** @type {const} */ ([1, 2, 3]);
|
||||
|
||||
/** @param {number} version */
|
||||
export function getVersionKey(version) {
|
||||
return String(version);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {BlockPreviewTransaction[]} transactions
|
||||
* @returns {Map<number, number>}
|
||||
*/
|
||||
function countVersions(transactions) {
|
||||
const counts = new Map();
|
||||
|
||||
for (const transaction of transactions) {
|
||||
counts.set(transaction.version, (counts.get(transaction.version) ?? 0) + 1);
|
||||
}
|
||||
|
||||
return counts;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} heatmap
|
||||
* @returns {Map<string, HTMLElement[]>}
|
||||
*/
|
||||
function groupCells(heatmap) {
|
||||
const groups = new Map();
|
||||
const cells = /** @type {HTMLElement[]} */ ([
|
||||
...heatmap.querySelectorAll("[data-heatmap-cell]"),
|
||||
]);
|
||||
|
||||
for (const cell of cells) {
|
||||
const key = /** @type {string} */ (cell.dataset.heatmapCell);
|
||||
let group = groups.get(key);
|
||||
|
||||
if (group === undefined) {
|
||||
group = [];
|
||||
groups.set(key, group);
|
||||
}
|
||||
|
||||
group.push(cell);
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLButtonElement} button
|
||||
* @param {boolean} active
|
||||
*/
|
||||
function setActive(button, active) {
|
||||
button.setAttribute("aria-pressed", String(active));
|
||||
button.toggleAttribute("data-muted", !active);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLButtonElement} button
|
||||
*/
|
||||
function setPending(button) {
|
||||
button.disabled = true;
|
||||
button.setAttribute("aria-pressed", "false");
|
||||
button.removeAttribute("data-muted");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {BlockPreviewTransaction[]} transactions
|
||||
* @param {HTMLElement | null} heatmap
|
||||
* @param {Object} [options]
|
||||
* @param {boolean} [options.pending]
|
||||
*/
|
||||
export function createVersionFilters(transactions, heatmap, options = {}) {
|
||||
const list = createLegendList({ scroll: true });
|
||||
const counts = countVersions(transactions);
|
||||
const cells = heatmap === null ? new Map() : groupCells(heatmap);
|
||||
const pending = options.pending === true;
|
||||
|
||||
for (const version of VERSION_FILTERS) {
|
||||
const count = counts.get(version) ?? 0;
|
||||
const key = getVersionKey(version);
|
||||
const { button, value } = createLegendItem({
|
||||
label: `tx v${version}`,
|
||||
color: "var(--white)",
|
||||
ariaLabel: `Transaction version ${version}`,
|
||||
});
|
||||
|
||||
value.textContent = pending ? "..." : formatNumber(count);
|
||||
if (pending) {
|
||||
setPending(button);
|
||||
} else {
|
||||
button.addEventListener("click", () => {
|
||||
const active = button.getAttribute("aria-pressed") !== "true";
|
||||
|
||||
setActive(button, active);
|
||||
for (const cell of cells.get(key) ?? []) {
|
||||
cell.toggleAttribute("data-muted", !active);
|
||||
}
|
||||
});
|
||||
setActive(button, true);
|
||||
}
|
||||
appendLegendListItem(list, button);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/** @typedef {import("./fees.js").BlockPreviewTransaction} BlockPreviewTransaction */
|
||||
@@ -3,21 +3,17 @@ import { formatFeeRate } from "../../../utils/fee-rate.js";
|
||||
import { formatWeight, MAX_BLOCK_WEIGHT } from "../format.js";
|
||||
import { getFeeRateColor } from "../fee-rates.js";
|
||||
import { loadBlockPreview } from "./data.js";
|
||||
import { createPreviewFeeRange, getFeeBucket, orderTransactions } from "./fees.js";
|
||||
import { createPreviewLegend } from "./legend.js";
|
||||
import { createPreviewFeeRange, orderTransactions } from "./fees.js";
|
||||
import { createVersionFilters, getVersionKey } from "./filters.js";
|
||||
|
||||
/**
|
||||
* @param {BlockPreviewTransaction} transaction
|
||||
* @param {number[]} ranges
|
||||
* @param {number} index
|
||||
* @param {number} count
|
||||
*/
|
||||
function createPreviewItem(transaction, ranges, index, count) {
|
||||
const bucket = getFeeBucket(index, count);
|
||||
|
||||
function createPreviewItem(transaction, ranges) {
|
||||
return {
|
||||
color: getFeeRateColor(transaction.feeRate, ranges),
|
||||
group: bucket.label,
|
||||
group: getVersionKey(transaction.version),
|
||||
weight: transaction.weight,
|
||||
title: [
|
||||
transaction.txid,
|
||||
@@ -28,34 +24,42 @@ function createPreviewItem(transaction, ranges, index, count) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} body
|
||||
* @param {HTMLElement} filters
|
||||
*/
|
||||
function createFigure(body, filters) {
|
||||
const figure = document.createElement("figure");
|
||||
const caption = document.createElement("figcaption");
|
||||
const title = document.createElement("h5");
|
||||
|
||||
figure.dataset.blockPreviewFigure = "";
|
||||
caption.dataset.blockPreviewLegend = "";
|
||||
title.append("Filters");
|
||||
caption.append(title, filters);
|
||||
figure.append(caption, body);
|
||||
|
||||
return figure;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} content
|
||||
* @param {BlockPreviewTransaction[]} transactions
|
||||
*/
|
||||
function renderPreview(content, transactions) {
|
||||
const figure = document.createElement("figure");
|
||||
const caption = document.createElement("figcaption");
|
||||
const title = document.createElement("h5");
|
||||
const ordered = orderTransactions(transactions);
|
||||
const ranges = createPreviewFeeRange(ordered);
|
||||
const items = ordered.map((transaction, index) => {
|
||||
return createPreviewItem(transaction, ranges, index, ordered.length);
|
||||
const items = ordered.map((transaction) => {
|
||||
return createPreviewItem(transaction, ranges);
|
||||
});
|
||||
const heatmap = createHeatmap(items, {
|
||||
origin: "bottom",
|
||||
shape: "square",
|
||||
capacity: MAX_BLOCK_WEIGHT,
|
||||
columns: 84,
|
||||
});
|
||||
|
||||
figure.dataset.blockPreviewFigure = "";
|
||||
caption.dataset.blockPreviewLegend = "";
|
||||
title.append("Fees");
|
||||
caption.append(title, createPreviewLegend(ranges));
|
||||
figure.append(
|
||||
caption,
|
||||
createHeatmap(items, {
|
||||
origin: "bottom",
|
||||
shape: "square",
|
||||
capacity: MAX_BLOCK_WEIGHT,
|
||||
columns: 84,
|
||||
}),
|
||||
);
|
||||
content.replaceChildren(figure);
|
||||
content.replaceChildren(createFigure(heatmap, createVersionFilters(ordered, heatmap)));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -67,7 +71,9 @@ function renderStatus(content, status) {
|
||||
|
||||
p.dataset.blockPreviewStatus = status;
|
||||
p.textContent = status;
|
||||
content.replaceChildren(p);
|
||||
content.replaceChildren(createFigure(p, createVersionFilters([], null, {
|
||||
pending: true,
|
||||
})));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import {
|
||||
appendLegendListItem,
|
||||
createLegendItem,
|
||||
createLegendList,
|
||||
} from "../../../legend/index.js";
|
||||
import { formatFeeRate } from "../../../utils/fee-rate.js";
|
||||
import { FEE_BUCKETS } from "./fees.js";
|
||||
|
||||
/**
|
||||
* @param {number[]} ranges
|
||||
*/
|
||||
export function createPreviewLegend(ranges) {
|
||||
const list = createLegendList({ scroll: true });
|
||||
|
||||
for (let index = 0; index < FEE_BUCKETS.length; index += 1) {
|
||||
const bucket = FEE_BUCKETS[index];
|
||||
const { button, value } = createLegendItem({
|
||||
label: bucket.label,
|
||||
color: bucket.color,
|
||||
ariaLabel: `${bucket.label} fee rate percentile`,
|
||||
detail: "sat/vB",
|
||||
});
|
||||
|
||||
value.textContent = formatFeeRate(ranges[index]);
|
||||
appendLegendListItem(list, button);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
@@ -23,6 +23,10 @@
|
||||
}
|
||||
|
||||
[data-block-preview-status] {
|
||||
display: grid;
|
||||
aspect-ratio: 1;
|
||||
margin: 0;
|
||||
place-items: center;
|
||||
color: var(--gray);
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: var(--line-height-sm);
|
||||
|
||||
@@ -1,48 +1,36 @@
|
||||
#block-details section[data-group="preview"] {
|
||||
[data-block-meta] {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 0.5rem;
|
||||
grid-template-columns: minmax(0, 1fr) repeat(3, max-content);
|
||||
gap: 1.5rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
[data-block-meta-item] {
|
||||
display: grid;
|
||||
gap: 0.125rem;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
min-width: 0;
|
||||
|
||||
> span {
|
||||
color: var(--block-meta-color);
|
||||
font-size: var(--font-size-xs);
|
||||
line-height: var(--line-height-xs);
|
||||
flex: 0 0 auto;
|
||||
color: var(--gray);
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: var(--line-height-sm);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
strong {
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
color: var(--block-meta-color);
|
||||
color: var(--white);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-strong);
|
||||
line-height: var(--line-height-sm);
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
[data-block-meta-item="block"] {
|
||||
--block-meta-color: var(--cyan);
|
||||
}
|
||||
|
||||
[data-block-meta-item="tx"] {
|
||||
--block-meta-color: var(--orange);
|
||||
}
|
||||
|
||||
[data-block-meta-item="input"] {
|
||||
--block-meta-color: var(--yellow);
|
||||
}
|
||||
|
||||
[data-block-meta-item="output"] {
|
||||
--block-meta-color: var(--red);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 48rem) {
|
||||
|
||||
@@ -25,6 +25,31 @@ function resolveCapacityCells(cells, capacity, columns) {
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* @template {CapacityCell} Cell
|
||||
* @param {readonly Cell[]} cells
|
||||
* @param {number} capacity
|
||||
* @param {number} columns
|
||||
*/
|
||||
function fitCapacityCells(cells, capacity, columns) {
|
||||
let resolvedCells = resolveCapacityCells(cells, capacity, columns);
|
||||
let layouts = packCells(resolvedCells, columns, columns);
|
||||
|
||||
while (layouts === null) {
|
||||
const largest = Math.max(...resolvedCells.map(({ span }) => span));
|
||||
|
||||
if (largest <= 1) break;
|
||||
|
||||
resolvedCells = resolvedCells.map((cell) => ({
|
||||
...cell,
|
||||
span: cell.span === largest ? largest - 1 : cell.span,
|
||||
}));
|
||||
layouts = packCells(resolvedCells, columns, columns);
|
||||
}
|
||||
|
||||
return { resolvedCells, layouts };
|
||||
}
|
||||
|
||||
/**
|
||||
* @template {CapacityCell} Cell
|
||||
* @param {readonly Cell[]} cells
|
||||
@@ -32,8 +57,7 @@ function resolveCapacityCells(cells, capacity, columns) {
|
||||
* @param {number} columns
|
||||
*/
|
||||
export function createSquareLayout(cells, capacity, columns) {
|
||||
const resolvedCells = resolveCapacityCells(cells, capacity, columns);
|
||||
const layouts = packCells(resolvedCells, columns);
|
||||
const { resolvedCells, layouts } = fitCapacityCells(cells, capacity, columns);
|
||||
|
||||
return { columns, resolvedCells, layouts: /** @type {NonNullable<typeof layouts>} */ (layouts) };
|
||||
}
|
||||
|
||||
@@ -126,6 +126,7 @@ export function createHeatmap(items, options = {}) {
|
||||
}));
|
||||
|
||||
map.dataset.heatmap = options.origin ?? "";
|
||||
if (options.shape != null) map.dataset.heatmapShape = options.shape;
|
||||
map.append(...cells.map(({ node }) => node));
|
||||
observeLayout(
|
||||
map,
|
||||
|
||||
@@ -7,12 +7,21 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
[data-heatmap-shape="square"] {
|
||||
aspect-ratio: 1;
|
||||
}
|
||||
|
||||
[data-heatmap-cell] {
|
||||
position: absolute;
|
||||
left: var(--x);
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
background: var(--color);
|
||||
transition: opacity 150ms;
|
||||
|
||||
&[data-muted] {
|
||||
opacity: 0.12;
|
||||
}
|
||||
}
|
||||
|
||||
[data-heatmap="bottom"] > [data-heatmap-cell] {
|
||||
|
||||
Reference in New Issue
Block a user