@@ -9,20 +9,13 @@ import {
99 YAxis ,
1010 Tooltip ,
1111 CartesianGrid ,
12- Legend ,
1312} from 'recharts' ;
1413import type { HopData } from '../../shared/types' ;
1514
1615interface OverviewChartsProps {
1716 hops : HopData [ ] ;
1817}
1918
20- const HOP_COLORS = [
21- '#06b6d4' , '#10b981' , '#f59e0b' , '#ef4444' , '#8b5cf6' ,
22- '#ec4899' , '#14b8a6' , '#f97316' , '#6366f1' , '#84cc16' ,
23- '#e879f9' , '#22d3ee' , '#fb923c' , '#a78bfa' , '#34d399' ,
24- ] ;
25-
2619const TOOLTIP_STYLE = {
2720 backgroundColor : 'rgba(10,10,15,0.95)' ,
2821 border : '1px solid rgba(255,255,255,0.1)' ,
@@ -32,75 +25,60 @@ const TOOLTIP_STYLE = {
3225} ;
3326
3427export default function OverviewCharts ( { hops } : OverviewChartsProps ) {
35- const visibleHops = useMemo (
36- ( ) => hops . filter ( ( h ) => h . ip && h . ip !== '???' ) ,
28+ const lastHop = useMemo (
29+ ( ) => [ ... hops ] . reverse ( ) . find ( ( h ) => h . ip && h . ip !== '???' ) ?? null ,
3730 [ hops ] ,
3831 ) ;
3932
40- // Build time-series data: each index is a sample number, each hop contributes a column
33+ // Build time-series data for the last hop (destination) only
4134 const { latencyData, jitterData, lossData } = useMemo ( ( ) => {
42- if ( visibleHops . length === 0 ) return { latencyData : [ ] , jitterData : [ ] , lossData : [ ] } ;
43-
44- const maxLen = Math . max ( ...visibleHops . map ( ( h ) => h . history . length ) ) ;
45- const sampleCount = Math . min ( maxLen , 60 ) ;
46- const latency : Record < string , number | null > [ ] = [ ] ;
47- const jitter : Record < string , number | null > [ ] = [ ] ;
48- const loss : Record < string , number > [ ] = [ ] ;
49-
50- for ( let i = 0 ; i < sampleCount ; i ++ ) {
51- const lRow : Record < string , number | null > = { idx : i } ;
52- const jRow : Record < string , number | null > = { idx : i } ;
53- const losRow : Record < string , number > = { idx : i } ;
35+ if ( ! lastHop ) return { latencyData : [ ] , jitterData : [ ] , lossData : [ ] } ;
5436
55- for ( const hop of visibleHops ) {
56- const offset = Math . max ( 0 , hop . history . length - sampleCount ) ;
57- const val = hop . history [ offset + i ] ?? null ;
58- const key = `hop${ hop . hopNumber } ` ;
59- lRow [ key ] = val ;
37+ const sampleCount = Math . min ( lastHop . history . length , 60 ) ;
38+ const offset = Math . max ( 0 , lastHop . history . length - sampleCount ) ;
6039
61- // Compute rolling jitter (stddev of last 5 values up to this point)
62- const start = Math . max ( 0 , offset + i - 4 ) ;
63- const end = offset + i + 1 ;
64- const window = hop . history . slice ( start , end ) . filter ( ( v ) : v is number => v !== null ) ;
65- if ( window . length > 1 ) {
66- const mean = window . reduce ( ( a , b ) => a + b , 0 ) / window . length ;
67- const variance = window . reduce ( ( s , v ) => s + ( v - mean ) ** 2 , 0 ) / window . length ;
68- jRow [ key ] = Math . round ( Math . sqrt ( variance ) * 10 ) / 10 ;
69- } else {
70- jRow [ key ] = 0 ;
71- }
40+ const latency : { idx : number ; latency : number | null } [ ] = [ ] ;
41+ const jitter : { idx : number ; jitter : number } [ ] = [ ] ;
42+ const loss : { idx : number ; loss : number } [ ] = [ ] ;
7243
73- // Rolling loss: count nulls in last 10 samples
74- const lossStart = Math . max ( 0 , offset + i - 9 ) ;
75- const lossEnd = offset + i + 1 ;
76- const lossWindow = hop . history . slice ( lossStart , lossEnd ) ;
77- const nullCount = lossWindow . filter ( ( v ) => v === null ) . length ;
78- losRow [ key ] = Math . round ( ( nullCount / lossWindow . length ) * 100 * 10 ) / 10 ;
44+ for ( let i = 0 ; i < sampleCount ; i ++ ) {
45+ const val = lastHop . history [ offset + i ] ?? null ;
46+ latency . push ( { idx : i , latency : val } ) ;
47+
48+ // Rolling jitter (stddev of last 5 values)
49+ const jStart = Math . max ( 0 , offset + i - 4 ) ;
50+ const jEnd = offset + i + 1 ;
51+ const window = lastHop . history . slice ( jStart , jEnd ) . filter ( ( v ) : v is number => v !== null ) ;
52+ let jVal = 0 ;
53+ if ( window . length > 1 ) {
54+ const mean = window . reduce ( ( a , b ) => a + b , 0 ) / window . length ;
55+ const variance = window . reduce ( ( s , v ) => s + ( v - mean ) ** 2 , 0 ) / window . length ;
56+ jVal = Math . round ( Math . sqrt ( variance ) * 10 ) / 10 ;
7957 }
80-
81- latency . push ( lRow ) ;
82- jitter . push ( jRow ) ;
83- loss . push ( losRow ) ;
58+ jitter . push ( { idx : i , jitter : jVal } ) ;
59+
60+ // Rolling loss: count nulls in last 50 samples
61+ const lStart = Math . max ( 0 , offset + i - 49 ) ;
62+ const lEnd = offset + i + 1 ;
63+ const lossWindow = lastHop . history . slice ( lStart , lEnd ) ;
64+ const nullCount = lossWindow . filter ( ( v ) => v === null ) . length ;
65+ loss . push ( { idx : i , loss : Math . round ( ( nullCount / lossWindow . length ) * 100 * 10 ) / 10 } ) ;
8466 }
8567
8668 return { latencyData : latency , jitterData : jitter , lossData : loss } ;
87- } , [ visibleHops ] ) ;
69+ } , [ lastHop ] ) ;
8870
89- if ( visibleHops . length === 0 || latencyData . length === 0 ) return null ;
71+ if ( ! lastHop || latencyData . length === 0 ) return null ;
9072
91- const hopKeys = visibleHops . map ( ( h ) => ( {
92- key : `hop${ h . hopNumber } ` ,
93- label : `#${ h . hopNumber } ` ,
94- color : HOP_COLORS [ ( h . hopNumber - 1 ) % HOP_COLORS . length ] ,
95- } ) ) ;
73+ const destLabel = lastHop . hostname || lastHop . ip || `Hop #${ lastHop . hopNumber } ` ;
9674
9775 return (
9876 < div className = "flex-shrink-0 px-4 pb-2" >
9977 < div className = "flex gap-3" >
10078 { /* Latency */ }
10179 < div className = "flex-1 glass-card p-3" style = { { height : 160 } } >
10280 < div className = "text-[10px] uppercase tracking-wider text-white/20 mb-1 px-1" >
103- Latency (ms)
81+ Latency — { destLabel }
10482 </ div >
10583 < ResponsiveContainer width = "100%" height = "85%" >
10684 < LineChart data = { latencyData } >
@@ -113,27 +91,28 @@ export default function OverviewCharts({ hops }: OverviewChartsProps) {
11391 tickLine = { false }
11492 label = { { value : 'ms' , angle : - 90 , position : 'insideLeft' , style : { fontSize : 9 , fill : 'rgba(255,255,255,0.2)' } } }
11593 />
116- < Tooltip contentStyle = { TOOLTIP_STYLE } labelFormatter = { ( ) => '' } />
117- { hopKeys . map ( ( { key, color } ) => (
118- < Line
119- key = { key }
120- type = "monotone"
121- dataKey = { key }
122- stroke = { color }
123- strokeWidth = { 1.5 }
124- dot = { false }
125- isAnimationActive = { false }
126- connectNulls
127- />
128- ) ) }
94+ < Tooltip
95+ contentStyle = { TOOLTIP_STYLE }
96+ labelFormatter = { ( ) => '' }
97+ formatter = { ( value : number ) => [ `${ value } ms` , 'Latency' ] }
98+ />
99+ < Line
100+ type = "monotone"
101+ dataKey = "latency"
102+ stroke = "#06b6d4"
103+ strokeWidth = { 1.5 }
104+ dot = { false }
105+ isAnimationActive = { false }
106+ connectNulls
107+ />
129108 </ LineChart >
130109 </ ResponsiveContainer >
131110 </ div >
132111
133112 { /* Jitter */ }
134113 < div className = "flex-1 glass-card p-3" style = { { height : 160 } } >
135114 < div className = "text-[10px] uppercase tracking-wider text-white/20 mb-1 px-1" >
136- Jitter (ms)
115+ Jitter — { destLabel }
137116 </ div >
138117 < ResponsiveContainer width = "100%" height = "85%" >
139118 < LineChart data = { jitterData } >
@@ -146,37 +125,36 @@ export default function OverviewCharts({ hops }: OverviewChartsProps) {
146125 tickLine = { false }
147126 label = { { value : 'ms' , angle : - 90 , position : 'insideLeft' , style : { fontSize : 9 , fill : 'rgba(255,255,255,0.2)' } } }
148127 />
149- < Tooltip contentStyle = { TOOLTIP_STYLE } labelFormatter = { ( ) => '' } />
150- { hopKeys . map ( ( { key, color } ) => (
151- < Line
152- key = { key }
153- type = "monotone"
154- dataKey = { key }
155- stroke = { color }
156- strokeWidth = { 1.5 }
157- dot = { false }
158- isAnimationActive = { false }
159- connectNulls
160- />
161- ) ) }
128+ < Tooltip
129+ contentStyle = { TOOLTIP_STYLE }
130+ labelFormatter = { ( ) => '' }
131+ formatter = { ( value : number ) => [ `${ value } ms` , 'Jitter' ] }
132+ />
133+ < Line
134+ type = "monotone"
135+ dataKey = "jitter"
136+ stroke = "#f59e0b"
137+ strokeWidth = { 1.5 }
138+ dot = { false }
139+ isAnimationActive = { false }
140+ connectNulls
141+ />
162142 </ LineChart >
163143 </ ResponsiveContainer >
164144 </ div >
165145
166146 { /* Packet Loss */ }
167147 < div className = "flex-1 glass-card p-3" style = { { height : 160 } } >
168148 < div className = "text-[10px] uppercase tracking-wider text-white/20 mb-1 px-1" >
169- Packet Loss (%)
149+ Packet Loss — { destLabel }
170150 </ div >
171151 < ResponsiveContainer width = "100%" height = "85%" >
172152 < AreaChart data = { lossData } >
173153 < defs >
174- { hopKeys . map ( ( { key, color } ) => (
175- < linearGradient key = { key } id = { `loss-${ key } ` } x1 = "0" y1 = "0" x2 = "0" y2 = "1" >
176- < stop offset = "0%" stopColor = { color } stopOpacity = { 0.3 } />
177- < stop offset = "100%" stopColor = { color } stopOpacity = { 0 } />
178- </ linearGradient >
179- ) ) }
154+ < linearGradient id = "loss-dest" x1 = "0" y1 = "0" x2 = "0" y2 = "1" >
155+ < stop offset = "0%" stopColor = "#ef4444" stopOpacity = { 0.3 } />
156+ < stop offset = "100%" stopColor = "#ef4444" stopOpacity = { 0 } />
157+ </ linearGradient >
180158 </ defs >
181159 < CartesianGrid strokeDasharray = "3 3" stroke = "rgba(255,255,255,0.04)" />
182160 < XAxis dataKey = "idx" hide />
@@ -188,20 +166,21 @@ export default function OverviewCharts({ hops }: OverviewChartsProps) {
188166 tickLine = { false }
189167 label = { { value : '%' , angle : - 90 , position : 'insideLeft' , style : { fontSize : 9 , fill : 'rgba(255,255,255,0.2)' } } }
190168 />
191- < Tooltip contentStyle = { TOOLTIP_STYLE } labelFormatter = { ( ) => '' } />
192- { hopKeys . map ( ( { key, color } ) => (
193- < Area
194- key = { key }
195- type = "monotone"
196- dataKey = { key }
197- stroke = { color }
198- strokeWidth = { 1.5 }
199- fill = { `url(#loss-${ key } )` }
200- dot = { false }
201- isAnimationActive = { false }
202- connectNulls
203- />
204- ) ) }
169+ < Tooltip
170+ contentStyle = { TOOLTIP_STYLE }
171+ labelFormatter = { ( ) => '' }
172+ formatter = { ( value : number ) => [ `${ value } %` , 'Loss' ] }
173+ />
174+ < Area
175+ type = "monotone"
176+ dataKey = "loss"
177+ stroke = "#ef4444"
178+ strokeWidth = { 1.5 }
179+ fill = "url(#loss-dest)"
180+ dot = { false }
181+ isAnimationActive = { false }
182+ connectNulls
183+ />
205184 </ AreaChart >
206185 </ ResponsiveContainer >
207186 </ div >
0 commit comments