mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-06-08 14:11:56 -07:00
heatmaps: part 6
This commit is contained in:
@@ -147,8 +147,6 @@ function initSelected() {
|
||||
break;
|
||||
}
|
||||
case "heatmap": {
|
||||
console.log("heatmap");
|
||||
|
||||
element = heatmapElement;
|
||||
|
||||
setHeatmapOption(option);
|
||||
@@ -156,8 +154,6 @@ function initSelected() {
|
||||
break;
|
||||
}
|
||||
case "chart": {
|
||||
console.log("chart");
|
||||
|
||||
element = chartElement;
|
||||
|
||||
if (firstTimeLoadingChart) {
|
||||
|
||||
@@ -1339,6 +1339,8 @@ export function createChart({ parent, brk, fitContent }) {
|
||||
|
||||
/** @type {Record<number, ReturnType<typeof createPersistedValue<"lin" | "log">>>} */
|
||||
const scalePersistedValues = {};
|
||||
/** @type {Record<number, HTMLElement | undefined>} */
|
||||
const scaleSelectorElements = {};
|
||||
|
||||
/**
|
||||
* @param {number} paneIndex
|
||||
@@ -1366,8 +1368,7 @@ export function createChart({ parent, brk, fitContent }) {
|
||||
const td = tr?.querySelector("td:last-child");
|
||||
if (!td) return;
|
||||
|
||||
// Remove previous if any
|
||||
td.querySelector(":scope > .field")?.remove();
|
||||
scaleSelectorElements[paneIndex]?.remove();
|
||||
|
||||
/** @type {HTMLTableCellElement} */ (td).style.position = "relative";
|
||||
|
||||
@@ -1381,6 +1382,7 @@ export function createChart({ parent, brk, fitContent }) {
|
||||
},
|
||||
toTitle: (c) => (c === "lin" ? "Linear scale" : "Logarithmic scale"),
|
||||
});
|
||||
scaleSelectorElements[paneIndex] = radios;
|
||||
td.append(radios);
|
||||
}
|
||||
|
||||
@@ -1586,7 +1588,7 @@ export function createChart({ parent, brk, fitContent }) {
|
||||
choices,
|
||||
groups,
|
||||
id: "index",
|
||||
}),
|
||||
}).element,
|
||||
);
|
||||
|
||||
for (const preset of getRangePresets()) {
|
||||
@@ -1649,7 +1651,7 @@ export function createChart({ parent, brk, fitContent }) {
|
||||
blueprints.panes[paneIndex].unit = unit;
|
||||
blueprints.rebuildPane(paneIndex);
|
||||
},
|
||||
}),
|
||||
}).element,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -179,12 +179,20 @@ export function createLabeledInput({
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @typedef {Object} Select
|
||||
* @property {HTMLElement} element
|
||||
* @property {() => T} get
|
||||
* @property {(choice: T) => void} set
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {Object} args
|
||||
* @param {T} args.initialValue
|
||||
* @param {string} [args.id]
|
||||
* @param {string} [args.label]
|
||||
* @param {string} [args.legend]
|
||||
* @param {readonly T[]} args.choices
|
||||
* @param {(value: T) => void} [args.onChange]
|
||||
* @param {(choice: T) => string} [args.toKey]
|
||||
@@ -193,6 +201,7 @@ export function createLabeledInput({
|
||||
*/
|
||||
export function createRadios({
|
||||
id,
|
||||
legend,
|
||||
choices,
|
||||
initialValue,
|
||||
onChange,
|
||||
@@ -200,8 +209,7 @@ export function createRadios({
|
||||
toLabel = /** @type {(choice: T) => string} */ ((c) => String(c)),
|
||||
toTitle,
|
||||
}) {
|
||||
const field = window.document.createElement("div");
|
||||
field.classList.add("field");
|
||||
const field = window.document.createElement("fieldset");
|
||||
|
||||
const initialKey = toKey(initialValue);
|
||||
|
||||
@@ -214,6 +222,12 @@ export function createRadios({
|
||||
span.textContent = toLabel(choices[0]);
|
||||
field.append(span);
|
||||
} else {
|
||||
if (legend) {
|
||||
const legendElement = window.document.createElement("legend");
|
||||
legendElement.textContent = legend;
|
||||
field.append(legendElement);
|
||||
}
|
||||
|
||||
const fieldId = id ?? "";
|
||||
choices.forEach((choice) => {
|
||||
const choiceKey = toKey(choice);
|
||||
@@ -232,10 +246,10 @@ export function createRadios({
|
||||
field.append(label);
|
||||
});
|
||||
|
||||
field.addEventListener("change", (event) => {
|
||||
// @ts-ignore
|
||||
onChange?.(fromKey(event.target.value));
|
||||
});
|
||||
field.addEventListener("change", (event) => {
|
||||
if (!(event.target instanceof HTMLInputElement)) return;
|
||||
onChange?.(fromKey(event.target.value));
|
||||
});
|
||||
}
|
||||
|
||||
return field;
|
||||
@@ -253,6 +267,7 @@ export function createRadios({
|
||||
* @param {(choice: T) => string} [args.toLabel]
|
||||
* @param {boolean} [args.sorted]
|
||||
* @param {{ label: string, items: T[] }[]} [args.groups]
|
||||
* @returns {Select<T>}
|
||||
*/
|
||||
export function createSelect({
|
||||
id,
|
||||
@@ -278,7 +293,11 @@ export function createSelect({
|
||||
if (choices.length === 1) {
|
||||
const span = window.document.createElement("span");
|
||||
span.textContent = toLabel(choices[0]);
|
||||
return span;
|
||||
return {
|
||||
element: span,
|
||||
get: () => initialValue,
|
||||
set: () => {},
|
||||
};
|
||||
}
|
||||
|
||||
const field = window.document.createElement("label");
|
||||
@@ -330,13 +349,19 @@ export function createSelect({
|
||||
}
|
||||
|
||||
field.addEventListener("click", (e) => {
|
||||
if (e.target !== select) {
|
||||
if (e.target !== select && "showPicker" in select) {
|
||||
e.preventDefault();
|
||||
select.showPicker();
|
||||
}
|
||||
});
|
||||
|
||||
return field;
|
||||
return {
|
||||
element: field,
|
||||
get: () => fromKey(select.value),
|
||||
set: (choice) => {
|
||||
select.value = toKey(choice);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/** @import { PartialHeatmapOption } from "../../scripts/options/types.js" */
|
||||
/** @import { HeatmapPoints } from "./types.js" */
|
||||
|
||||
import { createAverageCells } from "./cells.js";
|
||||
import { createAverageGrid } from "./grid.js";
|
||||
import { INFERNO_LUT, intensityColor } from "./lut.js";
|
||||
import { GENESIS_DATE, todayISODate } from "./time.js";
|
||||
|
||||
@@ -17,7 +17,7 @@ export const demoHeatmapOption = {
|
||||
points: {
|
||||
fetch: fetchDemoPoints,
|
||||
},
|
||||
grid: createAverageCells({ yStart: 0, yEnd: 1, nativeRows: ROWS }),
|
||||
grid: createAverageGrid({ yStart: 0, yEnd: 1, nativeRows: ROWS }),
|
||||
color: intensityColor({ light: INFERNO_LUT, dark: INFERNO_LUT }),
|
||||
};
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ function clamp(value, min, max) {
|
||||
* @param {number} [args.nativeRows]
|
||||
* @returns {HeatmapGridFactory}
|
||||
*/
|
||||
export function createAverageCells({
|
||||
export function createAverageGrid({
|
||||
yStart,
|
||||
yEnd,
|
||||
minCellSize = 1,
|
||||
@@ -99,9 +99,12 @@ function loadRange() {
|
||||
|
||||
rebuildGrid();
|
||||
|
||||
const missing = currentDates.flatMap((date, dateIndex) =>
|
||||
pointsByDate.has(date) ? [] : [{ date, dateIndex }],
|
||||
);
|
||||
/** @type {{ date: string, dateIndex: number }[]} */
|
||||
const missing = [];
|
||||
for (let dateIndex = 0; dateIndex < currentDates.length; dateIndex++) {
|
||||
const date = currentDates[dateIndex];
|
||||
if (!pointsByDate.has(date)) missing.push({ date, dateIndex });
|
||||
}
|
||||
let completed = currentDates.length - missing.length;
|
||||
let failed = 0;
|
||||
updateStatus(completed, currentDates.length, failed);
|
||||
@@ -114,17 +117,17 @@ function loadRange() {
|
||||
}).map(async () => {
|
||||
let index = nextMissingIndex();
|
||||
while (index !== undefined) {
|
||||
const date = missing[index];
|
||||
const entry = missing[index];
|
||||
try {
|
||||
const points = await option.points.fetch(date.date, controller.signal);
|
||||
const points = await option.points.fetch(entry.date, controller.signal);
|
||||
if (isCurrentLoad(option, controller, generation)) {
|
||||
pointsByDate.set(date.date, points);
|
||||
addDateToGrid(date.dateIndex, points);
|
||||
pointsByDate.set(entry.date, points);
|
||||
addDateToGrid(entry.dateIndex, points);
|
||||
}
|
||||
} catch (error) {
|
||||
if (controller.signal.aborted) return;
|
||||
failed += 1;
|
||||
console.error(`Failed to fetch heatmap points for ${date.date}`, error);
|
||||
console.error(`Failed to fetch heatmap points for ${entry.date}`, error);
|
||||
} finally {
|
||||
if (isCurrentLoad(option, controller, generation)) {
|
||||
completed += 1;
|
||||
@@ -244,10 +247,8 @@ function updateStatus(completed, total, failed) {
|
||||
|
||||
function createRangeControls() {
|
||||
const fieldset = document.createElement("fieldset");
|
||||
fieldset.classList.add("heatmap-controls");
|
||||
|
||||
statusElement = document.createElement("small");
|
||||
statusElement.classList.add("heatmap-status");
|
||||
|
||||
const currentYear = new Date().getUTCFullYear();
|
||||
const fromChoices = createFromChoices(currentYear);
|
||||
@@ -263,12 +264,12 @@ function createRangeControls() {
|
||||
onChange(choice) {
|
||||
fromChoice = choice;
|
||||
if (fromChoice.date > toChoice.date) {
|
||||
toChoice = findFirstToChoiceOnOrAfter(toChoices, fromChoice.date);
|
||||
setSelectChoice(toSelect, toChoice);
|
||||
toChoice = findMatchingChoice(toChoices, fromChoice);
|
||||
toSelect.set(toChoice);
|
||||
}
|
||||
setRange(fromChoice.date, toChoice.date);
|
||||
},
|
||||
toKey: rangeChoiceKey,
|
||||
toKey: rangeChoiceLabel,
|
||||
toLabel: rangeChoiceLabel,
|
||||
});
|
||||
const toSelect = createSelect({
|
||||
@@ -279,16 +280,16 @@ function createRangeControls() {
|
||||
onChange(choice) {
|
||||
toChoice = choice;
|
||||
if (fromChoice.date > toChoice.date) {
|
||||
fromChoice = findLastFromChoiceOnOrBefore(fromChoices, toChoice.date);
|
||||
setSelectChoice(fromSelect, fromChoice);
|
||||
fromChoice = findMatchingChoice(fromChoices, toChoice);
|
||||
fromSelect.set(fromChoice);
|
||||
}
|
||||
setRange(fromChoice.date, toChoice.date);
|
||||
},
|
||||
toKey: rangeChoiceKey,
|
||||
toKey: rangeChoiceLabel,
|
||||
toLabel: rangeChoiceLabel,
|
||||
});
|
||||
|
||||
fieldset.append(fromSelect, toSelect, statusElement);
|
||||
fieldset.append(fromSelect.element, toSelect.element, statusElement);
|
||||
|
||||
return fieldset;
|
||||
}
|
||||
@@ -320,13 +321,6 @@ function createToChoices(currentYear) {
|
||||
return choices;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {RangeChoice} choice
|
||||
*/
|
||||
function rangeChoiceKey(choice) {
|
||||
return choice.label;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {RangeChoice} choice
|
||||
*/
|
||||
@@ -336,27 +330,10 @@ function rangeChoiceLabel(choice) {
|
||||
|
||||
/**
|
||||
* @param {readonly RangeChoice[]} choices
|
||||
* @param {string} date
|
||||
* @param {RangeChoice} selected
|
||||
*/
|
||||
function findFirstToChoiceOnOrAfter(choices, date) {
|
||||
return choices.find((choice) => choice.date >= date) ?? choices[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {readonly RangeChoice[]} choices
|
||||
* @param {string} date
|
||||
*/
|
||||
function findLastFromChoiceOnOrBefore(choices, date) {
|
||||
return choices.findLast((choice) => choice.date <= date) ?? choices[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} control
|
||||
* @param {RangeChoice} choice
|
||||
*/
|
||||
function setSelectChoice(control, choice) {
|
||||
const select = control.querySelector("select");
|
||||
if (select) select.value = rangeChoiceKey(choice);
|
||||
function findMatchingChoice(choices, selected) {
|
||||
return choices.find((choice) => choice.label === selected.label) ?? choices[0];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -144,7 +144,7 @@
|
||||
margin-right: var(--negative-main-padding);
|
||||
margin-left: var(--negative-main-padding);
|
||||
|
||||
:is(div.field, label:has(> select)) {
|
||||
:is(fieldset:has(> label > input[type="radio"]), label:has(> select)) {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
gap: 0.375rem;
|
||||
@@ -178,7 +178,7 @@
|
||||
&:nth-child(2) {
|
||||
position: relative;
|
||||
|
||||
> .field {
|
||||
> fieldset:has(> label > input[type="radio"]) {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
@@ -212,7 +212,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
td:last-child > .field {
|
||||
td:last-child > fieldset:has(> label > input[type="radio"]) {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
z-index: 50;
|
||||
@@ -222,7 +222,7 @@
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
tr:not(:last-child) > td:last-child > .field {
|
||||
tr:not(:last-child) > td:last-child > fieldset:has(> label > input[type="radio"]) {
|
||||
top: 0;
|
||||
right: 0;
|
||||
gap: 0.375rem;
|
||||
|
||||
@@ -38,11 +38,14 @@
|
||||
}
|
||||
|
||||
fieldset {
|
||||
border: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
min-inline-size: 0;
|
||||
padding: 0;
|
||||
|
||||
> div.field {
|
||||
&:has(> label > input[type="radio"]) {
|
||||
text-transform: lowercase;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
Reference in New Issue
Block a user