mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-07-10 02:28:13 -07:00
website_next: part 6
This commit is contained in:
@@ -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,
|
||||
})),
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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} */ ({
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<typeof layouts>} */ (layouts) };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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`);
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
[data-heatmap-cell] {
|
||||
|
||||
Reference in New Issue
Block a user