From 4d7212fb63a92b711485f405a2ec81b9b8f79711 Mon Sep 17 00:00:00 2001 From: nym21 Date: Mon, 6 Jul 2026 18:05:31 +0200 Subject: [PATCH] website_next: part 6 --- website_next/explore/block/fee-chart.js | 30 ++----- website_next/explore/block/fee-rates.js | 87 +++++++++++++++++++ website_next/explore/block/index.js | 26 ++++-- website_next/explore/block/preview/fees.js | 47 ++++------ website_next/explore/block/preview/index.js | 25 +++--- website_next/explore/block/preview/legend.js | 17 ++-- website_next/explore/block/receipt/style.css | 1 + website_next/explore/block/style.css | 7 ++ website_next/explore/block/transactions.js | 61 +++++-------- .../explore/block/transactions/style.css | 87 +++++-------------- website_next/heatmap/capacity.js | 30 ++----- website_next/heatmap/index.js | 15 ++-- website_next/heatmap/style.css | 1 + 13 files changed, 217 insertions(+), 217 deletions(-) create mode 100644 website_next/explore/block/fee-rates.js diff --git a/website_next/explore/block/fee-chart.js b/website_next/explore/block/fee-chart.js index 0d748f82a..21a8bb01f 100644 --- a/website_next/explore/block/fee-chart.js +++ b/website_next/explore/block/fee-chart.js @@ -3,30 +3,10 @@ import { createChartPoint, createChartPoints } from "../../chart/points.js"; import { createLinearXScale } from "../../chart/x.js"; import { createValueYScale } from "../../chart/y.js"; import { formatFeeRate } from "../../utils/fee-rate.js"; +import { FEE_RATE_PERCENTILES, FEE_RATE_STOPS } from "./fee-rates.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)"; +const FEE_AVERAGE_COLOR = "var(--white)"; /** @param {number} value */ function scaleFeeRate(value) { @@ -38,7 +18,7 @@ function scaleFeeRate(value) { * @returns {ChartSample[]} */ function createPercentileSamples(values) { - return values.map((y, index) => ({ x: FEE_PERCENTILE_X[index], y })); + return values.map((y, index) => ({ x: FEE_RATE_PERCENTILES[index], y })); } /** @@ -82,9 +62,9 @@ function createAverageSample(samples, averageRate) { function createEntries(percentileSamples, averageRate) { return [ ...percentileSamples.map((sample, index) => ({ - label: FEE_PERCENTILE_LABELS[index], + label: FEE_RATE_STOPS[index].label, sample, - color: FEE_PERCENTILE_COLORS[index], + color: FEE_RATE_STOPS[index].color, priority: 0, })), { diff --git a/website_next/explore/block/fee-rates.js b/website_next/explore/block/fee-rates.js new file mode 100644 index 000000000..6f4d8233c --- /dev/null +++ b/website_next/explore/block/fee-rates.js @@ -0,0 +1,87 @@ +export const FEE_RATE_PERCENTILES = /** @type {const} */ ([ + 0, + 10, + 25, + 50, + 75, + 90, + 100, +]); + +export const FEE_RATE_STOPS = /** @type {const} */ ([ + { label: "min", color: "var(--emerald)" }, + { label: "10%", color: "var(--green)" }, + { label: "25%", color: "var(--lime)" }, + { label: "50%", color: "var(--yellow)" }, + { label: "75%", color: "var(--amber)" }, + { label: "90%", color: "var(--orange)" }, + { label: "max", color: "var(--red)" }, +]); + +/** + * @param {number[]} values + * @param {number} percentile + */ +function percentileValue(values, percentile) { + return values[Math.round((percentile / 100) * (values.length - 1))]; +} + +/** @param {number[]} feeRates */ +export function createFeeRateRange(feeRates) { + const values = feeRates.toSorted((a, b) => a - b); + + return FEE_RATE_PERCENTILES.map((percentile) => { + return percentileValue(values, percentile); + }); +} + +/** + * @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 + */ +export function getFeeRateColor(feeRate, ranges) { + if (feeRate <= ranges[0]) return FEE_RATE_STOPS[0].color; + + for (let index = 1; index < ranges.length; index += 1) { + if (feeRate > ranges[index]) continue; + + const previousRate = ranges[index - 1]; + const nextRate = ranges[index]; + const span = nextRate - previousRate; + const ratio = span ? (feeRate - previousRate) / span : 0; + const previousShare = ((1 - ratio) * 100).toFixed(2); + const nextShare = (ratio * 100).toFixed(2); + + return `color-mix(in oklch, ${FEE_RATE_STOPS[index - 1].color} ${previousShare}%, ${FEE_RATE_STOPS[index].color} ${nextShare}%)`; + } + + return FEE_RATE_STOPS[FEE_RATE_STOPS.length - 1].color; +} diff --git a/website_next/explore/block/index.js b/website_next/explore/block/index.js index 220523cac..a5522200c 100644 --- a/website_next/explore/block/index.js +++ b/website_next/explore/block/index.js @@ -10,6 +10,15 @@ import { createBlockReceipt } from "./receipt.js"; function noop() {} +/** @param {string} side */ +function createColumn(side) { + const column = document.createElement("div"); + + column.dataset.blockColumn = side; + + return column; +} + export function createBlockDetails() { const element = document.createElement("section"); const receipt = createBlockReceipt(); @@ -42,16 +51,21 @@ export function createBlockDetails() { clearContent(); const preview = createBlockPreviewPane(block); + const left = createColumn("main"); + const right = createColumn("side"); destroyPreview = preview.destroy; - appendPane(content, "mining", [createMinerPane(block)]); - appendPane(content, "difficulty", [createDifficultyPane(block)]); - appendPane(content, "rewards", [createRewardsPane(extras)]); - appendPane(content, "block", [createTransactionPane(block)]); - appendPane(content, "preview", [preview.element]); - appendPane(content, "fees", [ + appendPane(left, "preview", [ + createTransactionPane(block), + preview.element, + ]); + appendPane(right, "mining", [createMinerPane(block)]); + appendPane(right, "rewards", [createRewardsPane(extras)]); + appendPane(right, "difficulty", [createDifficultyPane(block)]); + appendPane(right, "fees", [ createFeeChart(extras.feeRange, extras.avgFeeRate), ]); + content.append(left, right); } return /** @type {const} */ ({ diff --git a/website_next/explore/block/preview/fees.js b/website_next/explore/block/preview/fees.js index e79f52e37..be6f6c50d 100644 --- a/website_next/explore/block/preview/fees.js +++ b/website_next/explore/block/preview/fees.js @@ -1,39 +1,22 @@ -export const FEE_BUCKETS = /** @type {const} */ ([ - { label: "min", color: "var(--cyan)" }, - { label: "10%", color: "var(--blue)" }, - { label: "25%", color: "var(--violet)" }, - { label: "50%", color: "var(--white)" }, - { label: "75%", color: "var(--yellow)" }, - { label: "90%", color: "var(--orange)" }, - { label: "max", color: "var(--red)" }, -]); - -/** - * @param {number} feeRate - * @param {number[]} ranges - */ -export function getFeeBucket(feeRate, ranges) { - for (let index = 0; index < ranges.length; index += 1) { - if (feeRate <= ranges[index]) return FEE_BUCKETS[index]; - } - - return FEE_BUCKETS[FEE_BUCKETS.length - 1]; -} +import { + createFeeRateRange, + FEE_RATE_STOPS, + getFeeRateStopByRank, +} from "../fee-rates.js"; /** * @param {BlockPreviewTransaction[]} transactions - * @param {number[]} ranges */ -export function countFees(transactions, ranges) { - const counts = new Map(); +export function createPreviewFeeRange(transactions) { + return createFeeRateRange(transactions.map(({ feeRate }) => feeRate)); +} - for (const transaction of transactions) { - const bucket = getFeeBucket(transaction.feeRate, ranges); - - counts.set(bucket.label, (counts.get(bucket.label) ?? 0) + 1); - } - - return counts; +/** + * @param {number} index + * @param {number} count + */ +export function getFeeBucket(index, count) { + return getFeeRateStopByRank(index, count); } /** @@ -51,3 +34,5 @@ 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/index.js b/website_next/explore/block/preview/index.js index 8f5b524d0..1e5a756e7 100644 --- a/website_next/explore/block/preview/index.js +++ b/website_next/explore/block/preview/index.js @@ -1,19 +1,22 @@ import { createHeatmap } from "../../../heatmap/index.js"; 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 { getFeeBucket, orderTransactions } from "./fees.js"; +import { createPreviewFeeRange, getFeeBucket, orderTransactions } from "./fees.js"; import { createPreviewLegend } from "./legend.js"; /** * @param {BlockPreviewTransaction} transaction * @param {number[]} ranges + * @param {number} index + * @param {number} count */ -function createPreviewItem(transaction, ranges) { - const bucket = getFeeBucket(transaction.feeRate, ranges); +function createPreviewItem(transaction, ranges, index, count) { + const bucket = getFeeBucket(index, count); return { - color: bucket.color, + color: getFeeRateColor(transaction.feeRate, ranges), group: bucket.label, weight: transaction.weight, title: [ @@ -27,29 +30,29 @@ function createPreviewItem(transaction, ranges) { /** * @param {HTMLElement} content - * @param {number[]} ranges * @param {BlockPreviewTransaction[]} transactions */ -function renderPreview(content, ranges, 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 items = ordered.map((transaction) => { - return createPreviewItem(transaction, ranges); + const ranges = createPreviewFeeRange(ordered); + const items = ordered.map((transaction, index) => { + return createPreviewItem(transaction, ranges, index, ordered.length); }); figure.dataset.blockPreviewFigure = ""; caption.dataset.blockPreviewLegend = ""; title.append("Fees"); - caption.append(title, createPreviewLegend(ordered, ranges)); + caption.append(title, createPreviewLegend(ranges)); figure.append( caption, createHeatmap(items, { origin: "bottom", shape: "square", capacity: MAX_BLOCK_WEIGHT, - columns: 100, + columns: 84, }), ); content.replaceChildren(figure); @@ -80,7 +83,7 @@ export function createBlockPreviewPane(block) { void loadBlockPreview(block) .then((transactions) => { if (!live) return; - renderPreview(content, block.extras.feeRange, transactions); + renderPreview(content, transactions); }) .catch((error) => { if (!live) return; diff --git a/website_next/explore/block/preview/legend.js b/website_next/explore/block/preview/legend.js index 0d5601d48..3e0c1388e 100644 --- a/website_next/explore/block/preview/legend.js +++ b/website_next/explore/block/preview/legend.js @@ -4,33 +4,26 @@ import { createLegendList, } from "../../../legend/index.js"; import { formatFeeRate } from "../../../utils/fee-rate.js"; -import { formatNumber } from "../format.js"; -import { countFees, FEE_BUCKETS } from "./fees.js"; +import { FEE_BUCKETS } from "./fees.js"; /** - * @param {BlockPreviewTransaction[]} transactions * @param {number[]} ranges */ -export function createPreviewLegend(transactions, ranges) { - const counts = countFees(transactions, 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 count = counts.get(bucket.label) ?? 0; const { button, value } = createLegendItem({ label: bucket.label, color: bucket.color, - ariaLabel: `${bucket.label} fee rate bucket`, - detail: `${formatFeeRate(ranges[index])} sat/vB`, + ariaLabel: `${bucket.label} fee rate percentile`, + detail: "sat/vB", }); - if (count === 0) button.dataset.muted = ""; - value.textContent = formatNumber(count); + value.textContent = formatFeeRate(ranges[index]); appendLegendListItem(list, button); } return list; } - -/** @typedef {import("./fees.js").BlockPreviewTransaction} BlockPreviewTransaction */ diff --git a/website_next/explore/block/receipt/style.css b/website_next/explore/block/receipt/style.css index c824ea991..580580737 100644 --- a/website_next/explore/block/receipt/style.css +++ b/website_next/explore/block/receipt/style.css @@ -20,6 +20,7 @@ dialog[data-block-receipt] { font-family: var(--font-mono); overscroll-behavior: contain; scrollbar-width: none; + translate: none; :is( [data-receipt-head] > h2, diff --git a/website_next/explore/block/style.css b/website_next/explore/block/style.css index 03d5a3378..ea80b5ded 100644 --- a/website_next/explore/block/style.css +++ b/website_next/explore/block/style.css @@ -103,6 +103,13 @@ gap: 2rem; } + [data-block-column] { + display: grid; + align-content: start; + gap: 2rem; + min-width: 0; + } + section { display: grid; align-content: start; diff --git a/website_next/explore/block/transactions.js b/website_next/explore/block/transactions.js index d498cc361..7f4c826b4 100644 --- a/website_next/explore/block/transactions.js +++ b/website_next/explore/block/transactions.js @@ -2,61 +2,40 @@ import { formatBlockFill, formatBytes, formatNumber } from "./format.js"; /** @typedef {import("../../modules/brk-client/index.js").BlockInfoV1} Block */ -/** - * @param {string} label - * @param {(string | Node)[]} values - */ -function createBlockRow(label, values) { - const row = document.createElement("div"); - const name = document.createElement("span"); - const data = document.createElement("strong"); - - row.dataset.blockRow = ""; - name.textContent = label; - data.append(...values); - row.append(name, data); - - return row; -} - /** * @param {string} label * @param {string | Node} value * @param {string} type */ -function createBlockBox(label, value, type) { - const box = document.createElement("div"); +function createBlockMetaItem(label, value, type) { + const item = document.createElement("div"); + const name = document.createElement("span"); + const data = document.createElement("strong"); - box.dataset.blockBox = type; - box.append(createBlockRow(label, [value])); + item.dataset.blockMetaItem = type; + name.textContent = label; + data.append(value); + item.append(name, data); - return box; + return item; } /** @param {Block} block */ export function createTransactionPane(block) { const { extras } = block; - const box = document.createElement("div"); - const transactions = document.createElement("div"); - const io = document.createElement("div"); + const meta = document.createElement("div"); - box.dataset.blockBox = ""; - transactions.dataset.blockBox = "tx"; - io.dataset.blockIo = ""; - io.append( - createBlockBox("Input", formatNumber(extras.totalInputs), "input"), - createBlockBox("Output", formatNumber(extras.totalOutputs), "output"), - ); - transactions.append( - createBlockRow("Tx", [formatNumber(block.txCount)]), - io, - ); - box.append( - createBlockRow("Block", [ + meta.dataset.blockMeta = ""; + meta.append( + createBlockMetaItem( + "Block", `${formatBytes(block.size)} ยท ${formatBlockFill(block.weight)}`, - ]), - transactions, + "block", + ), + createBlockMetaItem("Tx", formatNumber(block.txCount), "tx"), + createBlockMetaItem("Input", formatNumber(extras.totalInputs), "input"), + createBlockMetaItem("Output", formatNumber(extras.totalOutputs), "output"), ); - return box; + return meta; } diff --git a/website_next/explore/block/transactions/style.css b/website_next/explore/block/transactions/style.css index ce5811a6d..4f34893d6 100644 --- a/website_next/explore/block/transactions/style.css +++ b/website_next/explore/block/transactions/style.css @@ -1,93 +1,54 @@ -#block-details section[data-group="block"] { - --section-color: var(--cyan); - - [data-block-box] { +#block-details section[data-group="preview"] { + [data-block-meta] { display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 0.5rem; min-width: 0; - border: 1px solid color-mix(in oklch, var(--section-color) 35%, transparent); - border-radius: 0.25rem; - padding: 0.75rem; - - &[data-block-box="tx"] { - border-color: color-mix(in oklch, var(--orange) 35%, transparent); - } - - &[data-block-box="input"] { - border-color: color-mix(in oklch, var(--yellow) 55%, transparent); - } - - &[data-block-box="output"] { - border-color: color-mix(in oklch, var(--red) 55%, transparent); - } } - [data-block-row] { + [data-block-meta-item] { display: grid; - grid-template-columns: minmax(4.5rem, auto) minmax(0, 1fr); - gap: 0.75rem; - align-items: center; + gap: 0.125rem; min-width: 0; > span { - color: var(--section-color); + color: var(--block-meta-color); font-size: var(--font-size-xs); line-height: var(--line-height-xs); text-transform: uppercase; } strong { - display: flex; - gap: 0.5rem; - align-items: center; - justify-content: flex-end; min-width: 0; - color: var(--section-color); + overflow-wrap: anywhere; + color: var(--block-meta-color); font-size: var(--font-size-sm); font-weight: var(--font-weight-strong); line-height: var(--line-height-sm); - text-align: right; } } - [data-block-io] { - display: grid; - grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); - gap: 0.75rem; - min-width: 0; - - [data-block-box] { - align-content: center; - padding: 0.5rem; - } - - [data-block-row] { - grid-template-columns: auto auto; - justify-content: space-between; - gap: 0.5rem; - width: 100%; - } + [data-block-meta-item="block"] { + --block-meta-color: var(--cyan); } - [data-block-box="tx"] > [data-block-row] { - > span, - strong { - color: var(--orange); - } + [data-block-meta-item="tx"] { + --block-meta-color: var(--orange); } - :is([data-block-box="input"], [data-block-box="output"]) > [data-block-row] { - > span, - strong { - color: var(--block-box-color); - } + [data-block-meta-item="input"] { + --block-meta-color: var(--yellow); } - [data-block-box="input"] { - --block-box-color: var(--yellow); - } - - [data-block-box="output"] { - --block-box-color: var(--red); + [data-block-meta-item="output"] { + --block-meta-color: var(--red); + } +} + +@media (max-width: 48rem) { + #block-details section[data-group="preview"] { + [data-block-meta] { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } } } diff --git a/website_next/heatmap/capacity.js b/website_next/heatmap/capacity.js index 2f1ea7e3b..2b3717797 100644 --- a/website_next/heatmap/capacity.js +++ b/website_next/heatmap/capacity.js @@ -4,13 +4,12 @@ import { packCells } from "./pack.js"; * @param {number} weight * @param {number} capacity * @param {number} columns - * @param {"floor" | "round"} rounding */ -function weightToSpan(weight, capacity, columns, rounding) { +function weightToSpan(weight, capacity, columns) { const cellWeight = capacity / (columns * columns); const span = Math.sqrt(weight / cellWeight); - return Math.max(1, Math[rounding](span)); + return Math.max(1, Math.round(span)); } /** @@ -18,12 +17,11 @@ function weightToSpan(weight, capacity, columns, rounding) { * @param {readonly Cell[]} cells * @param {number} capacity * @param {number} columns - * @param {"floor" | "round"} rounding */ -function resolveCapacityCells(cells, capacity, columns, rounding) { +function resolveCapacityCells(cells, capacity, columns) { return cells.map((cell) => ({ ...cell, - span: weightToSpan(cell.weight ?? 0, capacity, columns, rounding), + span: weightToSpan(cell.weight ?? 0, capacity, columns), })); } @@ -32,24 +30,12 @@ function resolveCapacityCells(cells, capacity, columns, rounding) { * @param {readonly Cell[]} cells * @param {number} capacity * @param {number} columns - * @param {"floor" | "round"} rounding */ -function createSquareLayout(cells, capacity, columns, rounding) { - const resolvedCells = resolveCapacityCells(cells, capacity, columns, rounding); - const layouts = packCells(resolvedCells, columns, columns); +export function createSquareLayout(cells, capacity, columns) { + const resolvedCells = resolveCapacityCells(cells, capacity, columns); + const layouts = packCells(resolvedCells, columns); - return layouts === null ? null : { columns, resolvedCells, layouts }; -} - -/** - * @template {CapacityCell} Cell - * @param {readonly Cell[]} cells - * @param {number} capacity - * @param {number} columns - */ -export function findSquareLayout(cells, capacity, columns) { - return createSquareLayout(cells, capacity, columns, "round") ?? - createSquareLayout(cells, capacity, columns, "floor"); + return { columns, resolvedCells, layouts: /** @type {NonNullable} */ (layouts) }; } /** diff --git a/website_next/heatmap/index.js b/website_next/heatmap/index.js index 091440480..b08a8d26f 100644 --- a/website_next/heatmap/index.js +++ b/website_next/heatmap/index.js @@ -1,4 +1,4 @@ -import { findSquareLayout } from "./capacity.js"; +import { createSquareLayout } from "./capacity.js"; import { packCells } from "./pack.js"; /** @@ -53,26 +53,29 @@ function layoutHeatmap(map, cellMeasure, gapMeasure, cells, options) { let columns = Math.max(1, Math.floor((width + gap) / (baseCell + gap))); let resolvedCells = cells; let layouts = packCells(resolvedCells, columns); + let rows; if (options.shape === "square" && options.capacity !== undefined) { - const square = findSquareLayout( + const square = createSquareLayout( cells, options.capacity, options.columns ?? columns, ); - if (square === null) return; columns = square.columns; resolvedCells = square.resolvedCells; layouts = square.layouts; - cell = Math.max(1, Math.floor((width - gap * (columns - 1)) / columns)); + cell = Math.max(1, (width - gap * (columns - 1)) / columns); + rows = columns; } if (layouts === null) return; - const rows = Math.max(...layouts.map(({ rows }) => rows), 0); + rows ??= Math.max(...layouts.map(({ rows }) => rows), 0); const gridWidth = unitsToPixels(columns, cell, gap); - const offset = Math.max(0, (width - gridWidth) / 2); + const offset = options.shape === "square" + ? 0 + : Math.max(0, (width - gridWidth) / 2); map.style.setProperty("height", `${unitsToPixels(rows, cell, gap)}px`); diff --git a/website_next/heatmap/style.css b/website_next/heatmap/style.css index 1d1cc7d4c..b559005b0 100644 --- a/website_next/heatmap/style.css +++ b/website_next/heatmap/style.css @@ -4,6 +4,7 @@ position: relative; min-width: 0; + overflow: hidden; } [data-heatmap-cell] {