diff --git a/website/scripts/main.js b/website/scripts/main.js index a8d96768d..049385581 100644 --- a/website/scripts/main.js +++ b/website/scripts/main.js @@ -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) { diff --git a/website/scripts/utils/chart/index.js b/website/scripts/utils/chart/index.js index e20f9882d..feca0ac13 100644 --- a/website/scripts/utils/chart/index.js +++ b/website/scripts/utils/chart/index.js @@ -1339,6 +1339,8 @@ export function createChart({ parent, brk, fitContent }) { /** @type {Record>>} */ const scalePersistedValues = {}; + /** @type {Record} */ + 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, ); }); diff --git a/website/scripts/utils/dom.js b/website/scripts/utils/dom.js index 881deecf6..3b5b1e110 100644 --- a/website/scripts/utils/dom.js +++ b/website/scripts/utils/dom.js @@ -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} */ 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); + }, + }; } /** diff --git a/website/src/heatmap/demo.js b/website/src/heatmap/demo.js index 15dc83edb..a5e109256 100644 --- a/website/src/heatmap/demo.js +++ b/website/src/heatmap/demo.js @@ -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 }), }; diff --git a/website/src/heatmap/cells.js b/website/src/heatmap/grid.js similarity index 99% rename from website/src/heatmap/cells.js rename to website/src/heatmap/grid.js index 6c311df2c..31df46e18 100644 --- a/website/src/heatmap/cells.js +++ b/website/src/heatmap/grid.js @@ -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, diff --git a/website/src/heatmap/index.js b/website/src/heatmap/index.js index 431ea4805..f8070fbdb 100644 --- a/website/src/heatmap/index.js +++ b/website/src/heatmap/index.js @@ -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]; } /** diff --git a/website/styles/chart.css b/website/styles/chart.css index 56830dfb9..4d3dc0eca 100644 --- a/website/styles/chart.css +++ b/website/styles/chart.css @@ -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; diff --git a/website/styles/components.css b/website/styles/components.css index 5028f829c..127f8e1b3 100644 --- a/website/styles/components.css +++ b/website/styles/components.css @@ -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;