mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-07-10 02:28:13 -07:00
161 lines
4.2 KiB
JavaScript
161 lines
4.2 KiB
JavaScript
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
|
|
*/
|