@@ -31,6 +31,7 @@ import TooltipPortal from "~/components/primitives/TooltipPortal";
3131import { cn } from "~/utils/cn" ;
3232import { AgentStatusIcon , type AgentTone } from "./agent-badges" ;
3333import { AgentCard , AgentCardBody , AgentCardHeader } from "./agent-card" ;
34+ import { barTimesMs , condense , hotBarCount } from "./report-spark" ;
3435
3536/** Both cards' severity type (`Severity` / `ReportSeverity`) resolves to this. */
3637export type ReportSeverityKey = "ok" | "warn" | "crit" ;
@@ -461,20 +462,7 @@ const SPARK_WIDTH_CLASS = "w-[6.5rem]";
461462/** The chart's own width; the trailing peak label uses the column's remainder. */
462463const SPARK_WIDTH = 72 ;
463464
464- /** How many bars a series is condensed to, so each bar stays hoverable. */
465- const MAX_BARS = 18 ;
466-
467- /** Average adjacent points down so each bar is wide enough to read and hover. */
468- function condense ( points : number [ ] , maxBars : number ) : number [ ] {
469- if ( points . length <= maxBars ) return points ;
470- const perBar = points . length / maxBars ;
471- return Array . from ( { length : maxBars } , ( _ , i ) => {
472- const slice = points . slice ( Math . floor ( i * perBar ) , Math . max ( Math . floor ( ( i + 1 ) * perBar ) , 1 ) ) ;
473- return slice . reduce ( ( sum , v ) => sum + v , 0 ) / Math . max ( slice . length , 1 ) ;
474- } ) ;
475- }
476-
477- type ReportSparkDatum = { count : number ; date : Date ; hot : boolean } ;
465+ type ReportSparkDatum = { count : number ; date : Date | null ; hot : boolean } ;
478466
479467function ReportSparkTooltip ( {
480468 active,
@@ -486,9 +474,11 @@ function ReportSparkTooltip({
486474 return (
487475 < TooltipPortal active = { active } >
488476 < div className = "rounded-sm border border-grid-bright bg-background-dimmed px-3 py-2" >
489- < Header3 className = "border-b border-b-border-bright pb-2" >
490- { formatDateTime ( entry . date , "UTC" , [ ] , false , true ) }
491- </ Header3 >
477+ { entry . date ? (
478+ < Header3 className = "border-b border-b-border-bright pb-2" >
479+ { formatDateTime ( entry . date , "UTC" , [ ] , false , true ) }
480+ </ Header3 >
481+ ) : null }
492482 < div className = "mt-2 text-xs tabular-nums text-text-bright" > { formatPoint ( entry . count ) } </ div >
493483 { entry . hot ? < div className = "mt-1 text-xs text-warning" > in the anomaly window</ div > : null }
494484 </ div >
@@ -511,6 +501,8 @@ export function ReportSparkline({
511501 * matching trailing bars paint at full strength.
512502 */
513503 anomalyMinutes,
504+ /** When the series ends, from the report's `generatedAt`. Turns a bar into a time. */
505+ seriesEndMs,
514506 /** The metric's own formatter, used by the tooltip and the peak label. */
515507 formatPoint,
516508 label,
@@ -520,26 +512,20 @@ export function ReportSparkline({
520512 severity : ReportSeverityKey ;
521513 windowMinutes : number ;
522514 anomalyMinutes ?: number ;
515+ seriesEndMs : number | null ;
523516 formatPoint : ( value : number ) => string ;
524517 label : string ;
525518 className ?: string ;
526519} ) {
527- const bars = condense ( points , MAX_BARS ) ;
528- const windowMs = windowMinutes * 60_000 ;
529- // The view model carries buckets, not timestamps, so synthesise them from now
530- // backwards.
531- const barIntervalMs = bars . length > 0 ? windowMs / bars . length : windowMs ;
532- const startMs = Date . now ( ) - windowMs ;
533-
534- const minutesPerBar = bars . length > 0 ? windowMinutes / bars . length : 0 ;
535- const hotBars =
536- anomalyMinutes && minutesPerBar > 0
537- ? Math . min ( bars . length , Math . max ( 1 , Math . round ( anomalyMinutes / minutesPerBar ) ) )
538- : 0 ;
520+ const bars = condense ( points ) ;
521+ // The view model carries buckets, not timestamps, so spread them back from the
522+ // series' end. Never from the renderer's clock: see `report-spark.ts`.
523+ const times = barTimesMs ( bars . length , windowMinutes , seriesEndMs ) ;
524+ const hotBars = hotBarCount ( bars . length , windowMinutes , anomalyMinutes ) ;
539525
540526 const data : ReportSparkDatum [ ] = bars . map ( ( count , i ) => ( {
541527 count,
542- date : new Date ( startMs + i * barIntervalMs ) ,
528+ date : times [ i ] === null ? null : new Date ( times [ i ] ! ) ,
543529 hot : i >= bars . length - hotBars ,
544530 } ) ) ;
545531
@@ -627,6 +613,7 @@ export function ReportMetricRow({
627613 series,
628614 windowMinutes,
629615 anomalyMinutes,
616+ seriesEndMs,
630617 formatPoint,
631618} : {
632619 label : string ;
@@ -639,6 +626,7 @@ export function ReportMetricRow({
639626 series ?: number [ ] ;
640627 windowMinutes : number ;
641628 anomalyMinutes ?: number ;
629+ seriesEndMs : number | null ;
642630 formatPoint : ( value : number ) => string ;
643631} ) {
644632 const deltaClass =
@@ -671,6 +659,7 @@ export function ReportMetricRow({
671659 severity = { severity }
672660 windowMinutes = { windowMinutes }
673661 anomalyMinutes = { anomalyMinutes }
662+ seriesEndMs = { seriesEndMs }
674663 formatPoint = { formatPoint }
675664 label = { label }
676665 className = { SPARK_CELL_CLASS }
0 commit comments