import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, ScatterChart, Scatter, ZAxis, Cell, } 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 prepareSeismicPoints( data: SeismicDto[], rangeStart: number, rangeEnd: number, ): { time: number; magnitude: number; place: string }[] { return data .filter(ev => ev.timestamp >= rangeStart && ev.timestamp <= rangeEnd) .map(ev => ({ time: ev.timestamp, magnitude: ev.magnitude, place: ev.place })); } /** * 기압 데이터: 관측소별 시간순 포인트 (버킷 없이 원본 사용) * connectNulls 대신, 데이터가 있는 포인트만 연결 */ function preparePressureByStation( data: PressureDto[], rangeStart: number, rangeEnd: number, ): Record { const byStation: Record = {}; for (const r of data) { if (r.timestamp < rangeStart || r.timestamp > rangeEnd) continue; if (!byStation[r.station]) byStation[r.station] = []; byStation[r.station].push({ time: r.timestamp, value: r.pressureHpa }); } // 시간순 정렬 for (const arr of Object.values(byStation)) { arr.sort((a, b) => a.time - b.time); } return byStation; } 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) }; }); } const STATION_COLORS: Record = { 'tehran': '#ef4444', 'isfahan': '#f97316', 'bandar-abbas': '#3b82f6', 'shiraz': '#22c55e', 'tabriz': '#a855f7', }; const MAGNITUDE_COLORS = ['#fbbf24', '#f97316', '#ef4444', '#dc2626', '#991b1b']; function getMagnitudeColor(mag: number): string { if (mag < 2.5) return MAGNITUDE_COLORS[0]; if (mag < 3.5) return MAGNITUDE_COLORS[1]; if (mag < 4.5) return MAGNITUDE_COLORS[2]; if (mag < 5.5) return MAGNITUDE_COLORS[3]; return MAGNITUDE_COLORS[4]; } 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 seismicPoints = useMemo( () => prepareSeismicPoints(seismicData, rangeStart, rangeEnd), [seismicData, rangeStart, rangeEnd], ); const pressureByStation = useMemo( () => preparePressureByStation(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 = { type: 'number' as const, domain: [rangeStart, rangeEnd] as [number, number], ticks, tickFormatter: formatTickTime, tick: { fontSize: 9, fill: '#888' }, }; // 지진 y축: 데이터가 있으면 0~max+1, 없으면 0~6 const maxMag = seismicPoints.length > 0 ? Math.max(...seismicPoints.map(p => p.magnitude)) : 0; const seismicYMax = Math.max(maxMag + 1, 6); return (

{t('sensor.title')}

{/* 지진파: Scatter (이벤트 점) */}

{t('sensor.seismicActivity')} {seismicPoints.length > 0 && ( ({seismicPoints.length} events) )}

{ if (name === 'M') return [`M${v.toFixed(1)}`, 'Magnitude']; return [v, name]; }} /> {seismicPoints.map((p, i) => ( ))}
{/* 기압: 관측소별 개별 라인 */}

{t('sensor.airPressureHpa')}

[`${v.toFixed(1)} hPa`, name]} /> {Object.entries(pressureByStation).map(([station, points]) => ( ))}

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

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

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

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