From 8a8e3f60eccae7c5217a810160b5629fad5ee097 Mon Sep 17 00:00:00 2001 From: Hugh Kaznowski Date: Sun, 6 Sep 2026 01:44:05 +0100 Subject: [PATCH] Squash chart bars/legend into top-N + Other for many-series panels PanelChart's BarChart and PieChart rendered one bar/slice and legend row per PromqlSeries with no bound, so panels with many series wrapped the 8-color palette (repeating colors) and produced unreadable, unbounded legends. Add squashRows(), which sorts by value descending and collapses series beyond COLORS.length - 1 into a single "Other" bucket (sum of the rest), applied to both bar and pie chart data before render. Panels at or below the threshold are unaffected. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o --- webapp/src/components/PanelChart.tsx | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/webapp/src/components/PanelChart.tsx b/webapp/src/components/PanelChart.tsx index 78b34ab..315f227 100644 --- a/webapp/src/components/PanelChart.tsx +++ b/webapp/src/components/PanelChart.tsx @@ -25,6 +25,22 @@ export function seriesLabel(labels: Record): string { // Fixed, small categorical palette to avoid a colour-scale dependency; cycles if there are more series than colours. const COLORS = ['#38bdf8', '#f472b6', '#a78bfa', '#4ade80', '#fbbf24', '#fb7185', '#2dd4bf', '#f97316'] +// One less than the palette size: above this many series, keep the top MAX_KEPT_SERIES (by value) and squash the +// rest into a single "Other" bucket, so the squashed range never exceeds COLORS.length and never repeats a colour. +const MAX_KEPT_SERIES = COLORS.length - 1 + +// Sorts rows by value descending and, if there are more than MAX_KEPT_SERIES, keeps the top MAX_KEPT_SERIES and +// collapses the remainder into a single "Other" row summing their values. Below the threshold, rows are returned +// unchanged so panels with few series keep their existing bar/slice order and colour assignment. +function squashRows(rows: { name: string; value: number }[]): { name: string; value: number }[] { + if (rows.length <= MAX_KEPT_SERIES) return rows + const sorted = [...rows].sort((a, b) => b.value - a.value) + const kept = sorted.slice(0, MAX_KEPT_SERIES) + const rest = sorted.slice(MAX_KEPT_SERIES) + const otherValue = rest.reduce((sum, r) => sum + r.value, 0) + return [...kept, { name: `Other (${rest.length})`, value: otherValue }] +} + // Merges series into one row-per-timestamp dataset (recharts wants a single array keyed by series name); a timestamp missing from a series is left undefined there, which recharts skips when drawing that line. function toLineData(series: PromqlSeries[]): { rows: Record[]; keys: string[] } { const keys = series.map((s) => seriesLabel(s.labels)) @@ -101,7 +117,7 @@ export function PanelChart({ series, chartType }: { series: PromqlSeries[]; char } if (chartType === 'bar') { - const rows = series.map((s) => ({ name: seriesLabel(s.labels), value: latestValue(s) })) + const rows = squashRows(series.map((s) => ({ name: seriesLabel(s.labels), value: latestValue(s) }))) return ( @@ -119,7 +135,7 @@ export function PanelChart({ series, chartType }: { series: PromqlSeries[]; char ) } - const rows = series.map((s) => ({ name: seriesLabel(s.labels), value: sumValue(s) })) + const rows = squashRows(series.map((s) => ({ name: seriesLabel(s.labels), value: sumValue(s) }))) const total = rows.reduce((sum, r) => sum + r.value, 0) if (total === 0) { return
No non-zero values in range.