fix: apply color blind mode to executive heatmaps

This commit is contained in:
Herbert Wolverson committed 2026-03-10 12:48:38 -05:00
1 parent 1b42cd3b5c
commit 518c568be9
3 files changed
+62 -15

No files matched your search

@@ -69,7 +69,7 @@ function qoqHeatmapRow(blocks, colorFn) {
(dlTotal === null || dlTotal === undefined) &&
(ulTotal === null || ulTotal === undefined);
if (allMissing) {
cells += `<div class="exec-heat-cell empty" title="No data"></div>`;
cells += `<div class="exec-heat-cell empty" title="No data" aria-label="Block ${i + 1}: no data"></div>`;
continue;
}
@@ -93,7 +93,7 @@ function qoqHeatmapRow(blocks, colorFn) {
// Top = upload, bottom = download.
cells += `
<div class="exec-heat-cell split" title="${title}">
<div class="exec-heat-cell split" title="${escapeAttr(title)}" aria-label="${escapeAttr(title)}">
<div class="exec-split-grid">
${part(ulTotal)}
${part(dlTotal)}
@@ -115,13 +115,14 @@ function qoqRow(labelText, badge, blocks, colorFn, labelHtml = null) {
`;
const labelTitle = escapeAttr(labelText);
const visibleLabel = labelHtml ?? escapeHtml(labelText);
const latestLabel = `${formatLatest(latestTop, "", 0)} / ${formatLatest(latestBottom, "", 0)}`;
return `
<div class="exec-heat-row">
<div class="exec-heat-row" role="listitem" aria-label="${escapeAttr(`${labelText} ${badge || ""} latest ${latestLabel}`)}">
<div class="exec-heat-label text-truncate" title="${labelTitle}">
<div class="fw-semibold text-truncate">${visibleLabel}</div>
${badge ? `<span class="badge bg-light text-secondary border">${badge}</span>` : ""}
</div>
<div class="exec-heat-cells">${qoqHeatmapRow(blocks, colorFn)}</div>
<div class="exec-heat-cells" role="img" aria-label="${escapeAttr(`${labelText} heatmap history`)}">${qoqHeatmapRow(blocks, colorFn)}</div>
<div class="text-muted small text-end exec-latest split">${formattedLatest}</div>
</div>
`;
@@ -139,13 +140,14 @@ function qoqHeatRow(label, badge, blocks, link = null) {
const redactClass =
badge === "Site" || badge === "Circuit" ? " redactable" : "";
const labelMarkup = link ? `<a href="${link}">${label}</a>` : label;
const latestLabel = `${formatLatest(latestTop, "", 0)} / ${formatLatest(latestBottom, "", 0)}`;
return `
<div class="exec-heat-row">
<div class="exec-heat-label text-truncate" title="${label}">
<div class="exec-heat-row" role="listitem" aria-label="${escapeAttr(`${label} ${badge || ""} latest ${latestLabel}`)}">
<div class="exec-heat-label text-truncate" title="${escapeAttr(label)}">
<div class="fw-semibold text-truncate${redactClass}">${labelMarkup}</div>
${badge ? `<span class="badge bg-light text-secondary border">${badge}</span>` : ""}
</div>
<div class="exec-heat-cells">${qoqHeatmapRow(blocks, colorByQoqScore)}</div>
<div class="exec-heat-cells" role="img" aria-label="${escapeAttr(`${label} heatmap history`)}">${qoqHeatmapRow(blocks, colorByQoqScore)}</div>
<div class="text-muted small text-end exec-latest split">${formattedLatest}</div>
</div>
`;
@@ -156,6 +158,12 @@ class ExecutiveHeatmapBase extends BaseDashlet {
super(slot);
this.size = 6;
this.lastData = null;
this._colorBlindListener = () => {
if (this.lastData) {
this.render();
}
};
this._colorBlindBound = false;
}
canBeSlowedDown() { return true; }
@@ -170,6 +178,10 @@ class ExecutiveHeatmapBase extends BaseDashlet {
}
setup() {
if (!this._colorBlindBound) {
window.addEventListener("colorBlindModeChanged", this._colorBlindListener);
this._colorBlindBound = true;
}
if (window.executiveHeatmapData) {
this.lastData = window.executiveHeatmapData;
this.render();
@@ -255,7 +267,7 @@ export class ExecutiveGlobalHeatmapDashlet extends ExecutiveHeatmapBase {
<div class="exec-section-title mb-0"><i class="fas fa-thermometer-half me-2 text-warning"></i>Global Heatmap</div>
<span class="text-muted small">15 minutes, newest on the right</span>
</div>
<div class="exec-heat-rows">${body}</div>
<div class="exec-heat-rows" role="list">${body}</div>
</div>
</div>
`;
@@ -361,7 +373,7 @@ class ExecutiveMetricHeatmapBase extends ExecutiveHeatmapBase {
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2 mb-2">
<div class="exec-section-title mb-0">${titleHtml}</div>
</div>
<div class="exec-heat-rows">${metricRows}</div>
<div class="exec-heat-rows" role="list">${metricRows}</div>
</div>
</div>
`;
@@ -1,7 +1,29 @@
import {lerpGreenToRedViaOrange} from "../helpers/scaling";
import {isColorBlindMode} from "../helpers/colorblind";
export const MAX_HEATMAP_ROWS = 10;
function lerpViridis(t) {
const stops = [
[68, 1, 84],
[59, 82, 139],
[33, 145, 140],
[94, 201, 98],
[253, 231, 37],
];
if (t <= 0) return "#440154";
if (t >= 1) return "#FDE725";
const idx = t * (stops.length - 1);
const i = Math.floor(idx);
const frac = idx - i;
const c0 = stops[i];
const c1 = stops[i + 1];
const r = Math.round(c0[0] + frac * (c1[0] - c0[0]));
const g = Math.round(c0[1] + frac * (c1[1] - c0[1]));
const b = Math.round(c0[2] + frac * (c1[2] - c0[2]));
return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
}
export function formatCount(value) {
if (value === undefined || value === null) return "—";
const num = Number(value);
@@ -14,7 +36,12 @@ export function clampPercent(value) {
return Math.min(100, Math.max(0, num));
}
export const colorByCapacity = (pct) => lerpGreenToRedViaOrange(100 - clampPercent(pct), 100);
export const colorByCapacity = (pct) => {
const clamped = clampPercent(pct);
return isColorBlindMode()
? lerpViridis(clamped / 100)
: lerpGreenToRedViaOrange(100 - clamped, 100);
};
export function isIpLike(name) {
if (!name) return false;
@@ -243,7 +270,7 @@ function heatmapRowSplit(topValues, bottomValues, colorFn, formatValue) {
(top === null || top === undefined) &&
(bottom === null || bottom === undefined);
if (allMissing) {
cells += `<div class="exec-heat-cell empty" title="No data"></div>`;
cells += `<div class="exec-heat-cell empty" title="No data" aria-label="Block ${i + 1}: no data"></div>`;
continue;
}
@@ -269,7 +296,7 @@ function heatmapRowSplit(topValues, bottomValues, colorFn, formatValue) {
// Top = upload, bottom = download.
cells += `
<div class="exec-heat-cell split" title="${title}">
<div class="exec-heat-cell split" title="${escapeHeatAttr(title)}" aria-label="${escapeHeatAttr(title)}">
<div class="exec-split-grid">
${part(top)}
${part(bottom)}
@@ -290,13 +317,14 @@ function splitHeatRow(label, badge, topValues, bottomValues, colorFn, formatValu
const redactClass =
badge === "Site" || badge === "Circuit" ? " redactable" : "";
const labelMarkup = link ? `<a href="${link}">${label}</a>` : label;
const latestLabel = `${formatValue(latestTop)} / ${formatValue(latestBottom)}`;
return `
<div class="exec-heat-row">
<div class="exec-heat-label text-truncate" title="${label}">
<div class="exec-heat-row" role="listitem" aria-label="${escapeHeatAttr(`${label} ${badge || ""} latest ${latestLabel}`)}">
<div class="exec-heat-label text-truncate" title="${escapeHeatAttr(label)}">
<div class="fw-semibold text-truncate${redactClass}">${labelMarkup}</div>
${badge ? `<span class="badge bg-light text-secondary border">${badge}</span>` : ""}
</div>
<div class="exec-heat-cells">${heatmapRowSplit(topValues, bottomValues, colorFn, formatValue)}</div>
<div class="exec-heat-cells" role="img" aria-label="${escapeHeatAttr(`${label} heatmap history`)}">${heatmapRowSplit(topValues, bottomValues, colorFn, formatValue)}</div>
<div class="text-muted small text-end exec-latest split">${formattedLatest}</div>
</div>
`;
@@ -136,7 +136,9 @@ function renderHeatmapTable(targetId, metricKey) {
if (!cfg) return;
const target = document.getElementById(targetId);
if (!target) return;
let lastData = null;
const renderRows = (data) => {
lastData = data;
getSiteIdMap().then((siteIdMap) => {
const activeTarget = document.getElementById(targetId);
if (!activeTarget) return;
@@ -182,6 +184,11 @@ function renderHeatmapTable(targetId, metricKey) {
`;
});
};
window.addEventListener("colorBlindModeChanged", () => {
if (lastData) {
renderRows(lastData);
}
});
listenExecutiveHeatmaps(renderRows);
// initial placeholder
target.innerHTML = `<div class="text-muted small">Waiting for heatmap data…</div>`;