trust-module-frontend/src/Charts/Components/LineChartComponent.jsx

229 lines
8.4 KiB
JavaScript
Executable File

import React, { useState, useRef, useEffect } from 'react';
import { LineChart, XAxis, YAxis, CartesianGrid, Tooltip, Line, ResponsiveContainer, ReferenceArea } from 'recharts';
import { HOUR, DAY } from './constants';
const TIME_FORMATS = {
LONG: 'dd.MM HH:mm', // Для диапазона > 24 часов
MEDIUM: 'HH:mm', // Для диапазона > 1 часа
SHORT: 'HH:mm:ss' // Для коротких диапазонов
};
const LineChartComponent = ({
chartData,
metricName,
colors,
onRangeSelect,
filteredData
}) => {
const [selectionArea, setSelectionArea] = useState(null);
const [isSelecting, setIsSelecting] = useState(false);
const chartRef = useRef(null);
const allTimestamps = Object.values(chartData)
.flat()
.map(point => point.timestamp)
.filter((timestamp, index, self) => self.indexOf(timestamp) === index)
.sort((a, b) => a - b);
const data = allTimestamps.map(timestamp => {
const point = { timestamp };
const firstPoint = Object.values(chartData)
.flat()
.find(p => p.timestamp === timestamp);
if (firstPoint) {
point.time = firstPoint.time;
point.fullTime = firstPoint.fullTime;
}
Object.keys(chartData).forEach(key => {
const instanceData = chartData[key].find(p => p.timestamp === timestamp);
point[key] = instanceData ? instanceData.value : null;
});
return point;
});
const displayData = filteredData || data;
const instanceKeys = displayData.length
? Object.keys(displayData[0]).filter(k => !['timestamp', 'time', 'fullTime'].includes(k))
: [];
// Функция для определения оптимального формата времени в зависимости от диапазона
const getTimeFormat = () => {
if (!data.length) return TIME_FORMATS.SHORT;
const range = data[data.length - 1].timestamp - data[0].timestamp;
if (range > DAY) return TIME_FORMATS.LONG;
if (range > HOUR) return TIME_FORMATS.MEDIUM;
return TIME_FORMATS.SHORT;
};
useEffect(() => {
const handleSelectStart = (e) => {
if (isSelecting) {
e.preventDefault();
}
};
document.addEventListener('selectstart', handleSelectStart);
return () => document.removeEventListener('selectstart', handleSelectStart);
}, [isSelecting]);
const handleMouseDown = (e) => {
if (!e) return;
// Получаем индекс точки по координатам
const activeIndex = e.activeTooltipIndex;
if (activeIndex === undefined || activeIndex < 0 || activeIndex >= data.length) return;
setIsSelecting(true);
setSelectionArea({
start: data[activeIndex].timestamp,
end: null,
startIndex: activeIndex,
endIndex: null
});
};
const handleMouseMove = (e) => {
if (!isSelecting || !selectionArea?.start || !e) return;
const activeIndex = e.activeTooltipIndex;
if (activeIndex === undefined || activeIndex < 0 || activeIndex >= data.length) return;
setSelectionArea(prev => ({
...prev,
end: data[activeIndex].timestamp,
endIndex: activeIndex
}));
};
const handleMouseUp = () => {
if (!isSelecting || !selectionArea?.start || !selectionArea?.end) {
setIsSelecting(false);
setSelectionArea(null);
return;
}
const startIndex = Math.min(selectionArea.startIndex, selectionArea.endIndex);
const endIndex = Math.max(selectionArea.startIndex, selectionArea.endIndex);
// Нормализуем индексы к диапазону [0, 1] для родительского компонента
const normalizedStart = startIndex / (data.length - 1);
const normalizedEnd = endIndex / (data.length - 1);
onRangeSelect({
startIndex: normalizedStart,
endIndex: normalizedEnd
});
setIsSelecting(false);
setSelectionArea(null);
};
const CustomTooltip = ({ active, payload, label }) => {
if (active && payload && payload.length) {
const currentPoint = data.find(point => point.timestamp === label);
return (
<div style={{
backgroundColor: '#fff',
padding: '10px',
border: '1px solid #ccc',
borderRadius: '4px',
boxShadow: '0 2px 5px rgba(0,0,0,0.1)'
}}>
<p style={{ fontWeight: 'bold', marginBottom: '5px' }}>
{currentPoint?.fullTime || new Date(label).toLocaleString('ru-RU')}
</p>
{payload.map((item, index) => (
<p key={index} style={{ color: item.color }}>
{`Значение: ${item.value}`}
</p>
))}
</div>
);
}
return null;
};
if (!data.length) {
return <div style={{ padding: '20px', textAlign: 'center' }}>Нет данных для отображения</div>;
}
return (
<div style={{ position: 'relative', height: '400px' }}>
<ResponsiveContainer width="100%" height="100%">
<LineChart
data={displayData}
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
ref={chartRef}
>
<CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" />
<XAxis
dataKey="timestamp"
height={75}
tick={{ fontSize: 12, angle: -45, textAnchor: 'end' }}
interval={Math.max(1, Math.floor(data.length / 10))}
tickFormatter={(timestamp) => {
const date = new Date(timestamp);
const format = getTimeFormat();
if (format === 'dd.MM HH:mm') {
return date.toLocaleString('ru-RU', {
day: '2-digit',
month: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
} else if (format === 'HH:mm') {
return date.toLocaleString('ru-RU', {
hour: '2-digit',
minute: '2-digit'
});
} else {
return date.toLocaleString('ru-RU', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
}
}}
/>
<YAxis tick={{ fontSize: 12 }} />
<Tooltip content={<CustomTooltip />} />
{instanceKeys.map((instance, index) => (
<Line
key={instance}
type=""
dataKey={instance}
name={instance}
stroke={colors[index % colors.length]}
strokeWidth={2}
dot={false}
activeDot={{ r: 6 }}
/>
))}
{selectionArea?.start && selectionArea?.end && (
<ReferenceArea
x1={selectionArea.start}
x2={selectionArea.end}
strokeOpacity={0.3}
fill="#4a6baf"
/>
)}
</LineChart>
</ResponsiveContainer>
</div>
);
};
export default React.memo(LineChartComponent);