From 0552d85aa707aec8a319dafdcf9e221149cc2e56 Mon Sep 17 00:00:00 2001 From: Jaret Burkett Date: Thu, 16 Jul 2026 16:06:05 -0600 Subject: [PATCH] Gice the loss graph more diverse colors --- ui/src/components/JobLossGraph.tsx | 49 +++++++++++++++--------------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/ui/src/components/JobLossGraph.tsx b/ui/src/components/JobLossGraph.tsx index 73f4713..7d5ec10 100644 --- a/ui/src/components/JobLossGraph.tsx +++ b/ui/src/components/JobLossGraph.tsx @@ -98,30 +98,19 @@ function emaWithNulls(ys: (number | null)[], alpha: number): (number | null)[] { return out; } -function hashToIndex(str: string, mod: number) { - let h = 2166136261; - for (let i = 0; i < str.length; i++) { - h ^= str.charCodeAt(i); - h = Math.imul(h, 16777619); - } - return Math.abs(h) % mod; -} - +// Hue order is deliberate: adjacent slots are maximally separated for +// colorblind viewers, validated against the gray-950 chart surface. const PALETTE = [ - 'rgba(96,165,250,1)', // blue-400 - 'rgba(52,211,153,1)', // emerald-400 - 'rgba(167,139,250,1)', // purple-400 - 'rgba(251,191,36,1)', // amber-400 - 'rgba(244,114,182,1)', // pink-400 - 'rgba(248,113,113,1)', // red-400 - 'rgba(34,211,238,1)', // cyan-400 - 'rgba(129,140,248,1)', // indigo-400 + 'rgba(57,135,229,1)', // blue + 'rgba(0,131,0,1)', // green + 'rgba(213,81,129,1)', // magenta + 'rgba(201,133,0,1)', // yellow + 'rgba(25,158,112,1)', // aqua + 'rgba(217,89,38,1)', // orange + 'rgba(144,133,233,1)', // violet + 'rgba(230,103,103,1)', // red ]; -function strokeForKey(key: string) { - return PALETTE[hashToIndex(key, PALETTE.length)]; -} - // Persisted, per-URL graph settings. Sliders + display toggles + which loss // series are visible. Zoom / highlighted window is intentionally NOT persisted. interface PersistedSettings { @@ -243,6 +232,18 @@ export default function JobLossGraph({ job }: Props) { const activeKeys = useMemo(() => lossKeys.filter(k => enabled[k] !== false), [lossKeys, enabled]); + // Assign palette slots by position in the (sorted) lossKeys list rather than + // by hashing the key name — hashing let different keys collide onto the same + // color. Keyed off lossKeys (not activeKeys) so toggling a series off never + // repaints the others. + const colorByKey = useMemo(() => { + const m: Record = {}; + lossKeys.forEach((k, i) => { + m[k] = PALETTE[i % PALETTE.length]; + }); + return m; + }, [lossKeys]); + // Build uPlot-aligned data + series configs. const built = useMemo(() => { const stride = Math.max(1, plotStride | 0); @@ -305,7 +306,7 @@ export default function JobLossGraph({ job }: Props) { const smooth = emaWithNulls(raw, alpha); const fullSmooth = emaWithNulls(raw, fullAlpha); - const color = strokeForKey(key); + const color = colorByKey[key] ?? PALETTE[0]; const colorDull = dulledColor(color); const colArrays: (number | null)[][] = []; @@ -395,7 +396,7 @@ export default function JobLossGraph({ job }: Props) { } return { data: data as uPlot.AlignedData, seriesConfigs, scales, axes, yClip, sparseFlags }; - }, [series, activeKeys, smoothing, plotStride, windowSize, useLogScale, showTrend, clipOutliers]); + }, [series, activeKeys, colorByKey, smoothing, plotStride, windowSize, useLogScale, showTrend, clipOutliers]); // Layout wrapper we measure for sizing — uPlot collapses its own mount node // to width:min-content, so we can't read sizes off it. @@ -614,7 +615,7 @@ export default function JobLossGraph({ job }: Props) { aria-pressed={enabled[k] !== false} title={k} > - + {k} ))}