114 lines
4.1 KiB
TypeScript
114 lines
4.1 KiB
TypeScript
import { useState, useEffect, useMemo } from 'react';
|
|
import Plot from 'react-plotly.js';
|
|
|
|
interface SeismicSectionProps {
|
|
nodes: any[];
|
|
currentTime: number;
|
|
channel: string;
|
|
visible: boolean;
|
|
onClose: () => void;
|
|
}
|
|
|
|
const API_BASE = '/seismic/api';
|
|
|
|
function SeismicSection({ nodes, currentTime, channel, visible, onClose }: SeismicSectionProps) {
|
|
const [data, setData] = useState<any>(null);
|
|
const [viewMode, setViewMode] = useState<'day' | 'global'>('day');
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (!visible) return;
|
|
setLoading(true);
|
|
|
|
const url = viewMode === 'global'
|
|
? `${API_BASE}/global-history?channel=${channel}`
|
|
: `${API_BASE}/rms-timeline?date=${new Date(currentTime * 1000).toISOString().split('T')[0]}&channel=${channel}`;
|
|
|
|
fetch(url)
|
|
.then(r => r.json())
|
|
.then(d => {
|
|
setData(d.nodes);
|
|
setLoading(false);
|
|
})
|
|
.catch(() => setLoading(false));
|
|
}, [visible, viewMode, channel, currentTime]);
|
|
|
|
useEffect(() => {
|
|
if (!visible) return;
|
|
|
|
const handleEscape = (e: KeyboardEvent) => {
|
|
if (e.key === 'Escape') onClose();
|
|
};
|
|
|
|
document.addEventListener('keydown', handleEscape);
|
|
return () => document.removeEventListener('keydown', handleEscape);
|
|
}, [visible, onClose]);
|
|
|
|
const plotData = useMemo(() => {
|
|
if (!data) return [];
|
|
const traces: any[] = [];
|
|
const nodeIds = Object.keys(data).sort();
|
|
|
|
nodeIds.forEach((id, index) => {
|
|
const points = data[id];
|
|
if (!points || points.length === 0) return;
|
|
|
|
const maxRms = Math.max(...points.map((p: any) => p.rms)) || 1;
|
|
|
|
traces.push({
|
|
x: points.map((p: any) => new Date(p.ts * 1000)),
|
|
y: points.map((p: any) => (p.rms / maxRms) * 0.9 + index),
|
|
type: 'scatter',
|
|
mode: 'lines',
|
|
name: `Node ${id}`,
|
|
line: { color: '#4ade80', width: 1 },
|
|
fill: 'tonexty',
|
|
showlegend: false
|
|
});
|
|
});
|
|
return traces;
|
|
}, [data]);
|
|
|
|
if (!visible) return null;
|
|
|
|
return (
|
|
<div style={{ position: 'fixed', top: '0', left: '0', right: '0', bottom: '0', background: '#0f172a', zIndex: 3000, display: 'flex', flexDirection: 'column', overflowBehavior: 'contain' }}>
|
|
<div style={{ padding: '15px 20px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', background: '#1e293b' }}>
|
|
<div>
|
|
<h2 style={{ margin: 0, display: 'inline', marginRight: '20px' }}>Vue Sismique Globale</h2>
|
|
<div style={{ display: 'inline-flex', gap: '5px', background: '#0f172a', padding: '3px', borderRadius: '4px' }}>
|
|
<button onClick={() => setViewMode('day')} style={{ background: viewMode === 'day' ? '#3b82f6' : 'transparent', color: '#fff', border: 'none', padding: '5px 15px', cursor: 'pointer' }}>Journée</button>
|
|
<button onClick={() => setViewMode('global')} style={{ background: viewMode === 'global' ? '#3b82f6' : 'transparent', color: '#fff', border: 'none', padding: '5px 15px', cursor: 'pointer' }}>Toute la période (12j)</button>
|
|
</div>
|
|
</div>
|
|
<button onClick={onClose} style={{ background: '#ef4444', color: '#fff', border: 'none', padding: '8px 20px', borderRadius: '4px', cursor: 'pointer' }}>Fermer</button>
|
|
</div>
|
|
|
|
<div style={{ flex: 1, padding: '10px' }}>
|
|
{loading ? <div style={{ textAlign: 'center', marginTop: '20%' }}>Fusion des données…</div> : (
|
|
<Plot
|
|
data={plotData}
|
|
layout={{
|
|
autosize: true,
|
|
paper_bgcolor: 'rgba(0,0,0,0)',
|
|
plot_bgcolor: 'rgba(0,0,0,0)',
|
|
margin: { t: 10, b: 50, l: 60, r: 20 },
|
|
font: { color: '#94a3b8' },
|
|
xaxis: { gridcolor: '#1e293b', title: 'Temps' },
|
|
yaxis: {
|
|
gridcolor: '#1e293b',
|
|
tickvals: plotData.map((_, i) => i),
|
|
ticktext: Object.keys(data || {}).sort().map(id => `b${id}`)
|
|
}
|
|
}}
|
|
config={{ responsive: true }}
|
|
style={{ width: '100%', height: '100%' }}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default SeismicSection;
|