mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-07-18 06:28:11 -07:00
website_next: part 5
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
import { packCells } from "./pack.js";
|
||||
|
||||
/**
|
||||
* @param {number} weight
|
||||
* @param {number} capacity
|
||||
* @param {number} columns
|
||||
* @param {"floor" | "round"} rounding
|
||||
*/
|
||||
function weightToSpan(weight, capacity, columns, rounding) {
|
||||
const cellWeight = capacity / (columns * columns);
|
||||
const span = Math.sqrt(weight / cellWeight);
|
||||
|
||||
return Math.max(1, Math[rounding](span));
|
||||
}
|
||||
|
||||
/**
|
||||
* @template {CapacityCell} Cell
|
||||
* @param {readonly Cell[]} cells
|
||||
* @param {number} capacity
|
||||
* @param {number} columns
|
||||
* @param {"floor" | "round"} rounding
|
||||
*/
|
||||
function resolveCapacityCells(cells, capacity, columns, rounding) {
|
||||
return cells.map((cell) => ({
|
||||
...cell,
|
||||
span: weightToSpan(cell.weight ?? 0, capacity, columns, rounding),
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* @template {CapacityCell} Cell
|
||||
* @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);
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} CapacityCell
|
||||
* @property {number} span
|
||||
* @property {number | undefined} weight
|
||||
*/
|
||||
@@ -0,0 +1,160 @@
|
||||
import { findSquareLayout } from "./capacity.js";
|
||||
import { packCells } from "./pack.js";
|
||||
|
||||
/**
|
||||
* @param {HeatmapItem} item
|
||||
* @returns {HTMLSpanElement}
|
||||
*/
|
||||
function createCell(item) {
|
||||
const cell = document.createElement("span");
|
||||
|
||||
cell.dataset.heatmapCell = item.group;
|
||||
cell.style.setProperty("--color", item.color);
|
||||
cell.title = item.title;
|
||||
|
||||
return cell;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} parent
|
||||
* @param {string} width
|
||||
*/
|
||||
function createMeasure(parent, width) {
|
||||
const measure = document.createElement("i");
|
||||
|
||||
measure.dataset.heatmapMeasure = "";
|
||||
measure.style.setProperty("width", width);
|
||||
parent.append(measure);
|
||||
|
||||
return measure;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} count
|
||||
* @param {number} cell
|
||||
* @param {number} gap
|
||||
*/
|
||||
function unitsToPixels(count, cell, gap) {
|
||||
return count * cell + Math.max(0, count - 1) * gap;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} map
|
||||
* @param {HTMLElement} cellMeasure
|
||||
* @param {HTMLElement} gapMeasure
|
||||
* @param {readonly HeatmapCell[]} cells
|
||||
* @param {HeatmapOptions} options
|
||||
*/
|
||||
function layoutHeatmap(map, cellMeasure, gapMeasure, cells, options) {
|
||||
const baseCell = cellMeasure.getBoundingClientRect().width;
|
||||
const gap = gapMeasure.getBoundingClientRect().width - baseCell;
|
||||
const width = map.getBoundingClientRect().width;
|
||||
let cell = baseCell;
|
||||
let columns = Math.max(1, Math.floor((width + gap) / (baseCell + gap)));
|
||||
let resolvedCells = cells;
|
||||
let layouts = packCells(resolvedCells, columns);
|
||||
|
||||
if (options.shape === "square" && options.capacity !== undefined) {
|
||||
const square = findSquareLayout(
|
||||
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));
|
||||
}
|
||||
|
||||
if (layouts === null) return;
|
||||
|
||||
const rows = Math.max(...layouts.map(({ rows }) => rows), 0);
|
||||
const gridWidth = unitsToPixels(columns, cell, gap);
|
||||
const offset = Math.max(0, (width - gridWidth) / 2);
|
||||
|
||||
map.style.setProperty("height", `${unitsToPixels(rows, cell, gap)}px`);
|
||||
|
||||
layouts.forEach((layout, index) => {
|
||||
const node = resolvedCells[index].node;
|
||||
|
||||
node.style.setProperty("--x", `${offset + layout.x * (cell + gap)}px`);
|
||||
node.style.setProperty("--y", `${layout.y * (cell + gap)}px`);
|
||||
node.style.setProperty("--size", `${unitsToPixels(layout.span, cell, gap)}px`);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} map
|
||||
* @param {HTMLElement} cellMeasure
|
||||
* @param {HTMLElement} gapMeasure
|
||||
* @param {readonly HeatmapCell[]} cells
|
||||
* @param {HeatmapOptions} options
|
||||
*/
|
||||
function observeLayout(map, cellMeasure, gapMeasure, cells, options) {
|
||||
let frame = 0;
|
||||
const layout = () => {
|
||||
cancelAnimationFrame(frame);
|
||||
frame = requestAnimationFrame(() => {
|
||||
layoutHeatmap(map, cellMeasure, gapMeasure, cells, options);
|
||||
});
|
||||
};
|
||||
const observer = new ResizeObserver(layout);
|
||||
|
||||
observer.observe(map);
|
||||
map.addEventListener("heatmap:destroy", () => {
|
||||
cancelAnimationFrame(frame);
|
||||
observer.disconnect();
|
||||
}, { once: true });
|
||||
layout();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HeatmapItem[]} items
|
||||
* @param {HeatmapOptions} [options]
|
||||
*/
|
||||
export function createHeatmap(items, options = {}) {
|
||||
const map = document.createElement("div");
|
||||
const cells = items.map((item) => ({
|
||||
node: createCell(item),
|
||||
span: Math.max(1, Math.round(item.span ?? 1)),
|
||||
weight: item.weight,
|
||||
}));
|
||||
|
||||
map.dataset.heatmap = options.origin ?? "";
|
||||
map.append(...cells.map(({ node }) => node));
|
||||
observeLayout(
|
||||
map,
|
||||
createMeasure(map, "var(--heatmap-cell-size)"),
|
||||
createMeasure(map, "calc(var(--heatmap-cell-size) + var(--heatmap-gap))"),
|
||||
cells,
|
||||
options,
|
||||
);
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} HeatmapItem
|
||||
* @property {string} color
|
||||
* @property {string} group
|
||||
* @property {string} title
|
||||
* @property {number} [span]
|
||||
* @property {number} [weight]
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} HeatmapOptions
|
||||
* @property {"bottom"} [origin]
|
||||
* @property {"square"} [shape]
|
||||
* @property {number} [capacity]
|
||||
* @property {number} [columns]
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} HeatmapCell
|
||||
* @property {HTMLSpanElement} node
|
||||
* @property {number} span
|
||||
* @property {number | undefined} weight
|
||||
*/
|
||||
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* @param {readonly PackCell[]} cells
|
||||
* @param {number} columns
|
||||
* @param {number} [rows]
|
||||
* @returns {PackLayout[] | null}
|
||||
*/
|
||||
export function packCells(cells, columns, rows) {
|
||||
const occupied = /** @type {boolean[][]} */ ([]);
|
||||
const layouts = [];
|
||||
let usedRows = 0;
|
||||
|
||||
for (const cell of cells) {
|
||||
const span = Math.min(cell.span, columns);
|
||||
const position = findPosition(occupied, columns, span, rows);
|
||||
|
||||
if (position === null) return null;
|
||||
|
||||
fillCells(occupied, position.x, position.y, span);
|
||||
usedRows = Math.max(usedRows, position.y + span);
|
||||
|
||||
layouts.push({ ...position, span, rows: rows ?? usedRows });
|
||||
}
|
||||
|
||||
return layouts;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean[][]} occupied
|
||||
* @param {number} columns
|
||||
* @param {number} span
|
||||
* @param {number} [rows]
|
||||
* @returns {{ x: number, y: number } | null}
|
||||
*/
|
||||
function findPosition(occupied, columns, span, rows) {
|
||||
const lastRow = rows === undefined ? Infinity : rows - span;
|
||||
|
||||
for (let y = 0; y <= lastRow; y += 1) {
|
||||
for (let x = 0; x <= columns - span; x += 1) {
|
||||
if (canPlace(occupied, x, y, span)) return { x, y };
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean[][]} occupied
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @param {number} span
|
||||
*/
|
||||
function canPlace(occupied, x, y, span) {
|
||||
for (let row = y; row < y + span; row += 1) {
|
||||
for (let column = x; column < x + span; column += 1) {
|
||||
if (occupied[row]?.[column]) return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean[][]} occupied
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @param {number} span
|
||||
*/
|
||||
function fillCells(occupied, x, y, span) {
|
||||
for (let row = y; row < y + span; row += 1) {
|
||||
occupied[row] ??= [];
|
||||
|
||||
for (let column = x; column < x + span; column += 1) {
|
||||
occupied[row][column] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} PackCell
|
||||
* @property {number} span
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} PackLayout
|
||||
* @property {number} x
|
||||
* @property {number} y
|
||||
* @property {number} span
|
||||
* @property {number} rows
|
||||
*/
|
||||
@@ -0,0 +1,30 @@
|
||||
[data-heatmap] {
|
||||
--heatmap-cell-size: 0.4rem;
|
||||
--heatmap-gap: 2px;
|
||||
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
[data-heatmap-cell] {
|
||||
position: absolute;
|
||||
left: var(--x);
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
background: var(--color);
|
||||
}
|
||||
|
||||
[data-heatmap="bottom"] > [data-heatmap-cell] {
|
||||
bottom: var(--y);
|
||||
}
|
||||
|
||||
[data-heatmap=""] > [data-heatmap-cell] {
|
||||
top: var(--y);
|
||||
}
|
||||
|
||||
[data-heatmap-measure] {
|
||||
position: absolute;
|
||||
height: 0;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
Reference in New Issue
Block a user