Skip to content

Commit 44b955e

Browse files
dbfxclaude
andcommitted
feat: simplify charts to show destination-only and rolling 50-ping loss window
Overview charts (latency, jitter, packet loss) now show only the last hop (destination) instead of all hops. Loss monitor calculates loss % over the last 50 pings instead of all-time, and the loss graph shows a single averaged line across all targets. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c8f0add commit 44b955e

File tree

3 files changed

+116
-152
lines changed

3 files changed

+116
-152
lines changed

src/renderer/components/LossMonitorView.tsx

Lines changed: 25 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import {
1717
Line,
1818
} from 'react-simple-maps';
1919
import type { TargetStats } from '../hooks/useLossMonitor';
20-
import Sparkline from './Sparkline';
2120

2221
const GEO_URL = 'https://cdn.jsdelivr.net/npm/world-atlas@2/land-110m.json';
2322

@@ -56,24 +55,24 @@ export default function LossMonitorView({ targets, isRunning, onStart, onStop }:
5655

5756
const maxLen = Math.max(...targets.map((t) => t.lossHistory.length));
5857
const sampleCount = Math.min(maxLen, 120);
59-
const data: Record<string, number>[] = [];
58+
const data: { idx: number; avg: number }[] = [];
6059

6160
for (let i = 0; i < sampleCount; i++) {
62-
const row: Record<string, number> = { idx: i };
61+
let sum = 0;
62+
let count = 0;
6363
for (const t of targets) {
6464
const offset = Math.max(0, t.lossHistory.length - sampleCount);
65-
row[t.id] = t.lossHistory[offset + i] ?? 0;
65+
const val = t.lossHistory[offset + i];
66+
if (val !== undefined) {
67+
sum += val;
68+
count++;
69+
}
6670
}
67-
data.push(row);
71+
data.push({ idx: i, avg: count > 0 ? Math.round((sum / count) * 10) / 10 : 0 });
6872
}
6973
return data;
7074
}, [targets]);
7175

72-
const targetMap = useMemo(() => {
73-
const m = new Map<string, TargetStats>();
74-
for (const t of targets) m.set(t.id, t);
75-
return m;
76-
}, [targets]);
7776

7877
return (
7978
<div className="h-full flex flex-col">
@@ -160,27 +159,17 @@ export default function LossMonitorView({ targets, isRunning, onStart, onStop }:
160159
<div className="glass-card p-3 h-full">
161160
<div className="flex items-center justify-between mb-1 px-1">
162161
<div className="text-[10px] uppercase tracking-wider text-white/20">
163-
Packet Loss Over Time
164-
</div>
165-
<div className="flex gap-3">
166-
{targets.map((t) => (
167-
<div key={t.id} className="flex items-center gap-1 text-[10px] text-white/30">
168-
<span className="w-2 h-2 rounded-full" style={{ backgroundColor: t.color }} />
169-
{t.name}
170-
</div>
171-
))}
162+
Avg Packet Loss Over Time (last 50 pings)
172163
</div>
173164
</div>
174165
{chartData.length > 0 ? (
175166
<ResponsiveContainer width="100%" height="85%">
176167
<AreaChart data={chartData}>
177168
<defs>
178-
{targets.map((t) => (
179-
<linearGradient key={t.id} id={`loss-fill-${t.id}`} x1="0" y1="0" x2="0" y2="1">
180-
<stop offset="0%" stopColor={t.color} stopOpacity={0.25} />
181-
<stop offset="100%" stopColor={t.color} stopOpacity={0} />
182-
</linearGradient>
183-
))}
169+
<linearGradient id="loss-fill-avg" x1="0" y1="0" x2="0" y2="1">
170+
<stop offset="0%" stopColor="#ef4444" stopOpacity={0.3} />
171+
<stop offset="100%" stopColor="#ef4444" stopOpacity={0} />
172+
</linearGradient>
184173
</defs>
185174
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.04)" />
186175
<XAxis dataKey="idx" hide />
@@ -196,24 +185,18 @@ export default function LossMonitorView({ targets, isRunning, onStart, onStop }:
196185
<Tooltip
197186
contentStyle={TOOLTIP_STYLE}
198187
labelFormatter={() => ''}
199-
formatter={(value: number, name: string) => {
200-
const t = targetMap.get(name);
201-
return [`${value}%`, t?.name || name];
202-
}}
188+
formatter={(value: number) => [`${value}%`, 'Avg Loss']}
189+
/>
190+
<Area
191+
type="monotone"
192+
dataKey="avg"
193+
stroke="#ef4444"
194+
strokeWidth={2}
195+
fill="url(#loss-fill-avg)"
196+
dot={false}
197+
isAnimationActive={false}
198+
connectNulls
203199
/>
204-
{targets.map((t) => (
205-
<Area
206-
key={t.id}
207-
type="monotone"
208-
dataKey={t.id}
209-
stroke={t.color}
210-
strokeWidth={2}
211-
fill={`url(#loss-fill-${t.id})`}
212-
dot={false}
213-
isAnimationActive={false}
214-
connectNulls
215-
/>
216-
))}
217200
</AreaChart>
218201
</ResponsiveContainer>
219202
) : (

src/renderer/components/OverviewCharts.tsx

Lines changed: 83 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,13 @@ import {
99
YAxis,
1010
Tooltip,
1111
CartesianGrid,
12-
Legend,
1312
} from 'recharts';
1413
import type { HopData } from '../../shared/types';
1514

1615
interface 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-
2619
const 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

3427
export 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

Comments
 (0)