From 046c624abf13cb0b19704cd0bd269876e4b1b068 Mon Sep 17 00:00:00 2001 From: nym21 Date: Mon, 6 Jul 2026 18:52:51 +0200 Subject: [PATCH] website_next: part 7 --- website_next/explore/block/fee-rates.js | 28 ----- website_next/explore/block/preview/fees.js | 16 +-- website_next/explore/block/preview/filters.js | 113 ++++++++++++++++++ website_next/explore/block/preview/index.js | 62 +++++----- website_next/explore/block/preview/legend.js | 29 ----- website_next/explore/block/preview/style.css | 4 + .../explore/block/transactions/style.css | 38 ++---- website_next/heatmap/capacity.js | 28 ++++- website_next/heatmap/index.js | 1 + website_next/heatmap/style.css | 9 ++ 10 files changed, 201 insertions(+), 127 deletions(-) create mode 100644 website_next/explore/block/preview/filters.js delete mode 100644 website_next/explore/block/preview/legend.js diff --git a/website_next/explore/block/fee-rates.js b/website_next/explore/block/fee-rates.js index 6f4d8233c..396fbbf16 100644 --- a/website_next/explore/block/fee-rates.js +++ b/website_next/explore/block/fee-rates.js @@ -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 diff --git a/website_next/explore/block/preview/fees.js b/website_next/explore/block/preview/fees.js index be6f6c50d..b213ba32b 100644 --- a/website_next/explore/block/preview/fees.js +++ b/website_next/explore/block/preview/fees.js @@ -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 }; diff --git a/website_next/explore/block/preview/filters.js b/website_next/explore/block/preview/filters.js new file mode 100644 index 000000000..346d96556 --- /dev/null +++ b/website_next/explore/block/preview/filters.js @@ -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} + */ +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} + */ +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 */ diff --git a/website_next/explore/block/preview/index.js b/website_next/explore/block/preview/index.js index 1e5a756e7..b97475266 100644 --- a/website_next/explore/block/preview/index.js +++ b/website_next/explore/block/preview/index.js @@ -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, + }))); } /** diff --git a/website_next/explore/block/preview/legend.js b/website_next/explore/block/preview/legend.js deleted file mode 100644 index 3e0c1388e..000000000 --- a/website_next/explore/block/preview/legend.js +++ /dev/null @@ -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; -} diff --git a/website_next/explore/block/preview/style.css b/website_next/explore/block/preview/style.css index ff44677b7..20c3a43c9 100644 --- a/website_next/explore/block/preview/style.css +++ b/website_next/explore/block/preview/style.css @@ -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); diff --git a/website_next/explore/block/transactions/style.css b/website_next/explore/block/transactions/style.css index 4f34893d6..bd2ad7e26 100644 --- a/website_next/explore/block/transactions/style.css +++ b/website_next/explore/block/transactions/style.css @@ -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) { diff --git a/website_next/heatmap/capacity.js b/website_next/heatmap/capacity.js index 2b3717797..3414af308 100644 --- a/website_next/heatmap/capacity.js +++ b/website_next/heatmap/capacity.js @@ -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} */ (layouts) }; } diff --git a/website_next/heatmap/index.js b/website_next/heatmap/index.js index b08a8d26f..3e97837d7 100644 --- a/website_next/heatmap/index.js +++ b/website_next/heatmap/index.js @@ -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, diff --git a/website_next/heatmap/style.css b/website_next/heatmap/style.css index b559005b0..0e14553a4 100644 --- a/website_next/heatmap/style.css +++ b/website_next/heatmap/style.css @@ -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] {