import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, } from 'recharts'; import type { SeismicDto, PressureDto } from '../../services/sensorApi'; interface Props { seismicData: SeismicDto[]; pressureData: PressureDto[]; currentTime: number; historyMinutes: number; } const MINUTE = 60_000; const BUCKET_COUNT = 48; function formatTickTime(epoch: number): string { const d = new Date(epoch); const pad = (n: number) => String(n).padStart(2, '0'); return `${pad(d.getMonth() + 1)}/${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`; } function buildTicks(currentTime: number, historyMinutes: number): number[] { const interval = historyMinutes * MINUTE; const ticks: number[] = []; for (let i = 8; i >= 0; i--) { ticks.push(currentTime - i * interval); } return ticks; } function aggregateSeismic( data: SeismicDto[], rangeStart: number, rangeEnd: number, ): { time: number; value: number }[] { const bucketSize = (rangeEnd - rangeStart) / BUCKET_COUNT; const buckets = Array.from({ length: BUCKET_COUNT }, (_, i) => ({ time: rangeStart + (i + 0.5) * bucketSize, value: 0, })); for (const ev of data) { if (ev.timestamp < rangeStart || ev.timestamp > rangeEnd) continue; const idx = Math.min(Math.floor((ev.timestamp - rangeStart) / bucketSize), BUCKET_COUNT - 1); buckets[idx].value = Math.max(buckets[idx].value, ev.magnitude * 10); } return buckets; } function aggregatePressure( data: PressureDto[], rangeStart: number, rangeEnd: number, ): { time: number; value: number }[] { const bucketSize = (rangeEnd - rangeStart) / BUCKET_COUNT; const buckets = Array.from({ length: BUCKET_COUNT }, (_, i) => ({ time: rangeStart + (i + 0.5) * bucketSize, values: [] as number[], })); for (const r of data) { if (r.timestamp < rangeStart || r.timestamp > rangeEnd) continue; const idx = Math.min(Math.floor((r.timestamp - rangeStart) / bucketSize), BUCKET_COUNT - 1); buckets[idx].values.push(r.pressureHpa); } return buckets.map(b => ({ time: b.time, value: b.values.length > 0 ? b.values.reduce((a, c) => a + c, 0) / b.values.length : 0, })); } function generateDemoData( rangeStart: number, rangeEnd: number, baseValue: number, variance: number, ): { time: number; value: number }[] { const bucketSize = (rangeEnd - rangeStart) / BUCKET_COUNT; return Array.from({ length: BUCKET_COUNT }, (_, i) => { const t = rangeStart + (i + 0.5) * bucketSize; const seed = Math.sin(t / 100_000) * 10000; const noise = (seed - Math.floor(seed)) * 2 - 1; return { time: t, value: Math.max(0, baseValue + noise * variance) }; }); } export function SensorChart({ seismicData, pressureData, currentTime, historyMinutes }: Props) { const { t } = useTranslation(); const totalMinutes = historyMinutes * 8; const rangeStart = currentTime - totalMinutes * MINUTE; const rangeEnd = currentTime; const ticks = useMemo(() => buildTicks(currentTime, historyMinutes), [currentTime, historyMinutes]); const seismicChart = useMemo( () => aggregateSeismic(seismicData, rangeStart, rangeEnd), [seismicData, rangeStart, rangeEnd], ); const pressureChart = useMemo( () => aggregatePressure(pressureData, rangeStart, rangeEnd), [pressureData, rangeStart, rangeEnd], ); const noiseChart = useMemo( () => generateDemoData(rangeStart, rangeEnd, 45, 30), [rangeStart, rangeEnd], ); const radiationChart = useMemo( () => generateDemoData(rangeStart, rangeEnd, 0.08, 0.06), [rangeStart, rangeEnd], ); const commonXAxis = { dataKey: 'time' as const, type: 'number' as const, domain: [rangeStart, rangeEnd] as [number, number], ticks, tickFormatter: formatTickTime, tick: { fontSize: 9, fill: '#888' }, }; return (

{t('sensor.title')}

{t('sensor.seismicActivity')}

[v.toFixed(1), 'Magnitude×10']} />

{t('sensor.airPressureHpa')}

[v.toFixed(1), 'hPa']} />

{t('sensor.noiseLevelDb')}{' '} (DEMO)

[v.toFixed(1), 'dB']} />

{t('sensor.radiationUsv')}{' '} (DEMO)

[v.toFixed(3), 'μSv/h']} />
); }