229 lines
8.4 KiB
JavaScript
Executable File
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); |