Files
Checkmate/client/src/Utils/DataUtils.ts
T

137 lines
4.1 KiB
TypeScript
Raw Normal View History

2026-01-23 23:54:58 +00:00
// Interpolate color between three theme colors over 0-100 range.
// 0-50 => start (success.light), 50-75 => mid (warning.light), 75-100 => end (error.light)
export const getResponseColor = (
ms: number,
colors: {
start: string | undefined;
mid: string | undefined;
end: string | undefined;
}
): string => {
// New ranges with open high end:
// 0–300ms: interpolate start (good) -> mid (warning)
// 300–600ms: interpolate mid (warning) -> end (error)
// >600ms: solid end (error)
// MUI default success/warning/error.main values, used only when the caller
// fails to pass theme colors (defensive — keeps the gradient meaningful).
2026-01-23 23:54:58 +00:00
const safe = { ...colors };
if (!safe.start) safe.start = "#4caf50";
if (!safe.mid) safe.mid = "#ff9800";
if (!safe.end) safe.end = "#f44336";
2026-01-23 23:54:58 +00:00
const toHex = (c: number) => c.toString(16).padStart(2, "0");
const clamp = (n: number) => Math.min(255, Math.max(0, Math.round(n)));
const rgbToHex = (r: number, g: number, b: number) =>
`#${toHex(clamp(r))}${toHex(clamp(g))}${toHex(clamp(b))}`;
const normalizeToHex = (value: string) => {
const v = value.trim();
if (v.startsWith("#")) {
const h = v.slice(1);
const full =
h.length === 3
? h
.split("")
.map((c) => c + c)
.join("")
: h.substring(0, 6);
return `#${full.toLowerCase()}`;
}
const m = v
.replace(/\s+/g, "")
.match(/^rgba?\((\d{1,3}),(\d{1,3}),(\d{1,3})(?:,(0|0?\.\d+|1))?\)$/i);
if (m) {
const r = parseInt(m[1], 10);
const g = parseInt(m[2], 10);
const b = parseInt(m[3], 10);
return rgbToHex(r, g, b);
}
// Fallback neutral
return "#7f7f7f";
};
const parseHex = (hex: string) => {
const h = hex.replace("#", "");
const full =
h.length === 3
? h
.split("")
.map((c) => c + c)
.join("")
: h;
const n = parseInt(full, 16);
return { r: (n >> 16) & 255, g: (n >> 8) & 255, b: n & 255 };
};
const mix = (a: number, b: number, t: number) => Math.round(a + (b - a) * t);
const v = Math.max(0, ms);
if (v <= 300) {
const t = v / 300; // 0..1 from start->mid
const s = parseHex(normalizeToHex(safe.start));
const m = parseHex(normalizeToHex(safe.mid));
const r = mix(s.r, m.r, t);
const g = mix(s.g, m.g, t);
const b = mix(s.b, m.b, t);
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
}
if (v <= 600) {
const t = (v - 300) / 300; // 0..1 from mid->end
const m = parseHex(normalizeToHex(safe.mid));
const e = parseHex(normalizeToHex(safe.end));
const r = mix(m.r, e.r, t);
const g = mix(m.g, e.g, t);
const b = mix(m.b, e.b, t);
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
}
return normalizeToHex(safe.end);
};
2026-07-31 11:36:10 -07:00
2026-08-17 13:54:14 -07:00
export const MIN_HEIGHT_PCT = 8;
2026-07-31 11:36:10 -07:00
const CAP_PERCENTILE = 95;
const percentile = (sortedValues: number[], p: number): number => {
const index = (p / 100) * (sortedValues.length - 1);
const lower = Math.floor(index);
const upper = lower + 1;
const weight = index % 1;
if (upper >= sortedValues.length) return sortedValues[lower];
return sortedValues[lower] * (1 - weight) + sortedValues[upper] * weight;
};
export const computeBarHeights = (
checks: { responseTime?: number | null }[]
): number[] => {
const values = checks.map((check) => {
return typeof check.responseTime === "number" && Number.isFinite(check.responseTime)
? Math.max(0, check.responseTime)
: 0;
});
const sorted = values.slice().sort((a, b) => a - b);
const cap = percentile(sorted, CAP_PERCENTILE);
if (cap <= 0) return values.map(() => MIN_HEIGHT_PCT);
return values.map((value) => {
return Math.max(MIN_HEIGHT_PCT, Math.min(100, (value / cap) * 100));
});
};
2026-08-17 13:54:14 -07:00
export const computeDayBarHeights = (
buckets: { date: string; avgResponseTime: number | null }[]
): Map<string, number> => {
const values = buckets.map((bucket) =>
typeof bucket.avgResponseTime === "number" && Number.isFinite(bucket.avgResponseTime)
? Math.max(0, bucket.avgResponseTime)
: 0
);
const sorted = values.slice().sort((a, b) => a - b);
const cap = percentile(sorted, CAP_PERCENTILE);
return new Map(
buckets.map((bucket, i) => [
bucket.date,
cap <= 0
? MIN_HEIGHT_PCT
: Math.max(MIN_HEIGHT_PCT, Math.min(100, (values[i] / cap) * 100)),
])
);
};