diff --git a/src/components/time-series-chart/chart.tsx b/src/components/time-series-chart/chart.tsx index 480613a2..0242bb19 100644 --- a/src/components/time-series-chart/chart.tsx +++ b/src/components/time-series-chart/chart.tsx @@ -38,7 +38,7 @@ export function Chart({ const W = 1000; const H = 360; const padL = 60; - const padR = 96; + const padR = 130; const padT = 16; const padB = 36; const innerW = W - padL - padR; diff --git a/src/components/time-series-chart/series.tsx b/src/components/time-series-chart/series.tsx index afe17328..67e9f386 100644 --- a/src/components/time-series-chart/series.tsx +++ b/src/components/time-series-chart/series.tsx @@ -21,75 +21,99 @@ type SeriesPathsProps = { // now speak for themselves — SeriesPaths renders a natural break wherever // the underlying values are null. +// Max number of end-of-line labels rendered. The legend below shows all +// providers regardless — this cap prevents right-side label clutter when +// dozens of lines share the same Y range. +const MAX_INLINE_LABELS = 10; +// Minimum vertical gap (SVG units) between consecutive placed labels so +// name + value lines don't overlap. +const LABEL_GAP = 27; + export function SeriesPaths({ drawn, unit }: SeriesPathsProps) { + // Limit labels to top-N non-excluded lines (drawn is pre-sorted by value). + const labeledSlugs = new Set( + drawn.filter((d) => !d.excluded).slice(0, MAX_INLINE_LABELS).map((d) => d.slug), + ); + + // Collision deflection: sort labeled lines by their natural Y position, + // then push each label down until it clears the previous one. + const labelY: Record = {}; + const toPlace = drawn + .filter((d) => labeledSlugs.has(d.slug)) + .map((d) => ({ slug: d.slug, natural: d.lastY })) + .sort((a, b) => a.natural - b.natural); + let prevBottom = -Infinity; + for (const item of toPlace) { + const y = Math.max(item.natural, prevBottom); + labelY[item.slug] = y; + prevBottom = y + LABEL_GAP; + } + return ( <> - {drawn.map((d) => ( - - - { + const showLabel = labeledSlugs.has(d.slug); + const ly = labelY[d.slug] ?? d.lastY; + return ( + - {/* Live pulse halo. animated outward */} - - - - - {/* Trailing tail dot */} - - {/* End-of-line label */} - - {d.name} - - - {fmtUnit(d.last, unit)} - - - ))} + + + {/* Live pulse halo */} + + + + + {/* Trailing tail dot */} + + {/* End-of-line label — only for top-N, collision-deflected */} + {showLabel && ( + <> + + {d.name} + + + {fmtUnit(d.last, unit)} + + + )} + + ); + })} ); }