mirror of
https://github.com/LibreQoE/LibreQoS.git
synced 2026-11-02 08:10:02 -06:00
fix: apply color blind mode to executive heatmaps
This commit is contained in:
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>`;
|
||||
|
||||
Reference in new issue
Block a user