modified the skeleton MUI
parent
e47161acd1
commit
40d8046617
|
|
@ -0,0 +1,34 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Box, Skeleton } from '@mui/material';
|
||||||
|
|
||||||
|
const ChartSkeleton = ({ count = 1 }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{Array.from({ length: count }).map((_, index) => (
|
||||||
|
<Box
|
||||||
|
key={index}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
borderRadius: '8px',
|
||||||
|
padding: '20px',
|
||||||
|
mb: 3,
|
||||||
|
height: '400px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
|
||||||
|
<Skeleton variant="text" width="40%" height={30} />
|
||||||
|
<Skeleton variant="text" width="20%" height={30} />
|
||||||
|
</Box>
|
||||||
|
<Skeleton variant="rectangular" width="100%" height="80%" />
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 2, gap: 2 }}>
|
||||||
|
{[1, 2, 3, 4].map((i) => (
|
||||||
|
<Skeleton key={i} variant="rounded" width={80} height={36} />
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ChartSkeleton;
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import React, { useState, useRef, useEffect } from 'react';
|
import React, { useState, useRef, useEffect } from 'react';
|
||||||
import { LineChart, XAxis, YAxis, CartesianGrid, Tooltip, Line, ResponsiveContainer, ReferenceArea } from 'recharts';
|
import { LineChart, XAxis, YAxis, CartesianGrid, Tooltip, Line, ResponsiveContainer, ReferenceArea } from 'recharts';
|
||||||
|
import { Skeleton } from '@mui/material';
|
||||||
import { HOUR, DAY } from './constants';
|
import { HOUR, DAY } from './constants';
|
||||||
|
|
||||||
const TIME_FORMATS = {
|
const TIME_FORMATS = {
|
||||||
LONG: 'dd.MM HH:mm', // Для диапазона > 24 часов
|
LONG: 'dd.MM HH:mm', // Для диапазона > 24 часов
|
||||||
MEDIUM: 'HH:mm', // Для диапазона > 1 часа
|
MEDIUM: 'HH:mm', // Для диапазона > 1 часа
|
||||||
|
|
@ -51,7 +53,6 @@ const LineChartComponent = ({
|
||||||
? Object.keys(displayData[0]).filter(k => !['timestamp', 'time', 'fullTime'].includes(k))
|
? Object.keys(displayData[0]).filter(k => !['timestamp', 'time', 'fullTime'].includes(k))
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
// Функция для определения оптимального формата времени в зависимости от диапазона
|
|
||||||
const getTimeFormat = () => {
|
const getTimeFormat = () => {
|
||||||
if (!data.length) return TIME_FORMATS.SHORT;
|
if (!data.length) return TIME_FORMATS.SHORT;
|
||||||
|
|
||||||
|
|
@ -77,7 +78,6 @@ const LineChartComponent = ({
|
||||||
const handleMouseDown = (e) => {
|
const handleMouseDown = (e) => {
|
||||||
if (!e) return;
|
if (!e) return;
|
||||||
|
|
||||||
// Получаем индекс точки по координатам
|
|
||||||
const activeIndex = e.activeTooltipIndex;
|
const activeIndex = e.activeTooltipIndex;
|
||||||
if (activeIndex === undefined || activeIndex < 0 || activeIndex >= data.length) return;
|
if (activeIndex === undefined || activeIndex < 0 || activeIndex >= data.length) return;
|
||||||
|
|
||||||
|
|
@ -113,7 +113,6 @@ const LineChartComponent = ({
|
||||||
const startIndex = Math.min(selectionArea.startIndex, selectionArea.endIndex);
|
const startIndex = Math.min(selectionArea.startIndex, selectionArea.endIndex);
|
||||||
const endIndex = Math.max(selectionArea.startIndex, selectionArea.endIndex);
|
const endIndex = Math.max(selectionArea.startIndex, selectionArea.endIndex);
|
||||||
|
|
||||||
// Нормализуем индексы к диапазону [0, 1] для родительского компонента
|
|
||||||
const normalizedStart = startIndex / (data.length - 1);
|
const normalizedStart = startIndex / (data.length - 1);
|
||||||
const normalizedEnd = endIndex / (data.length - 1);
|
const normalizedEnd = endIndex / (data.length - 1);
|
||||||
|
|
||||||
|
|
@ -152,7 +151,18 @@ const LineChartComponent = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!data.length) {
|
if (!data.length) {
|
||||||
return <div style={{ padding: '20px', textAlign: 'center' }}>Нет данных для отображения</div>;
|
return (
|
||||||
|
<Box sx={{
|
||||||
|
position: 'relative',
|
||||||
|
height: '400px',
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
borderRadius: '8px',
|
||||||
|
padding: '20px'
|
||||||
|
}}>
|
||||||
|
<Skeleton variant="text" width="60%" height={30} sx={{ mb: 2 }} />
|
||||||
|
<Skeleton variant="rectangular" width="100%" height="calc(100% - 50px)" />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,23 @@
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState, useCallback } from 'react';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import Skeleton from '@mui/material/Skeleton';
|
import Skeleton from '@mui/material/Skeleton';
|
||||||
|
|
||||||
const LazyChartBatchRenderer = ({ charts }) => {
|
const LazyChartBatchRenderer = ({ charts }) => {
|
||||||
const [visibleIndices, setVisibleIndices] = useState(new Set());
|
const [visibleIndices, setVisibleIndices] = useState(new Set());
|
||||||
const placeholderRefs = useRef([]);
|
const placeholderRefs = useRef([]);
|
||||||
|
const observerRef = useRef(null);
|
||||||
|
const cleanupTimeoutRef = useRef(null);
|
||||||
|
|
||||||
const ChartSkeleton = () => (
|
const ChartSkeleton = () => (
|
||||||
<Box sx={{ backgroundColor: '#fff', borderRadius: '8px', padding: '20px', marginBottom: '20px', position: 'relative', height: '500px' }}>
|
<Box sx={{
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
borderRadius: '8px',
|
||||||
|
padding: '20px',
|
||||||
|
marginBottom: '20px',
|
||||||
|
position: 'relative',
|
||||||
|
height: '400px',
|
||||||
|
overflow: 'hidden'
|
||||||
|
}}>
|
||||||
<Box sx={{ position: 'absolute', right: '20px', top: '20px' }}>
|
<Box sx={{ position: 'absolute', right: '20px', top: '20px' }}>
|
||||||
<Skeleton variant="circular" width={16} height={16} />
|
<Skeleton variant="circular" width={16} height={16} />
|
||||||
</Box>
|
</Box>
|
||||||
|
|
@ -15,46 +25,124 @@ const LazyChartBatchRenderer = ({ charts }) => {
|
||||||
<Skeleton variant="text" width="40%" height={30} />
|
<Skeleton variant="text" width="40%" height={30} />
|
||||||
<Skeleton variant="text" width="30%" height={30} />
|
<Skeleton variant="text" width="30%" height={30} />
|
||||||
</Box>
|
</Box>
|
||||||
<Skeleton variant="rectangular" width="100%" height={300} />
|
<Skeleton
|
||||||
|
variant="rectangular"
|
||||||
|
width="100%"
|
||||||
|
height="300px"
|
||||||
|
sx={{
|
||||||
|
transform: 'none',
|
||||||
|
animation: 'none'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 2, gap: 2 }}>
|
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 2, gap: 2 }}>
|
||||||
{[1, 2, 3, 4].map((_, i) => (
|
{[1, 2, 3, 4].map((_, i) => (
|
||||||
<Skeleton key={i} variant="rounded" width={80} height={36} />
|
<Skeleton
|
||||||
|
key={i}
|
||||||
|
variant="rounded"
|
||||||
|
width={80}
|
||||||
|
height={36}
|
||||||
|
sx={{
|
||||||
|
transform: 'none',
|
||||||
|
animation: 'none'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const observer = new IntersectionObserver(
|
const isElementFarFromViewport = useCallback((element) => {
|
||||||
(entries) => {
|
if (!element) return true;
|
||||||
setVisibleIndices((prev) => {
|
|
||||||
const updated = new Set(prev);
|
const rect = element.getBoundingClientRect();
|
||||||
entries.forEach((entry) => {
|
const buffer = window.innerHeight * 1.5;
|
||||||
const index = parseInt(entry.target.dataset.index, 10);
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
updated.add(index);
|
return rect.bottom < -buffer || rect.top > window.innerHeight + buffer;
|
||||||
} else {
|
}, []);
|
||||||
updated.delete(index);
|
|
||||||
|
|
||||||
|
const updateVisibleIndices = useCallback(() => {
|
||||||
|
const newVisibleIndices = new Set();
|
||||||
|
|
||||||
|
placeholderRefs.current.forEach((ref, index) => {
|
||||||
|
if (ref && !isElementFarFromViewport(ref)) {
|
||||||
|
newVisibleIndices.add(index);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return updated;
|
|
||||||
|
setVisibleIndices(prev => {
|
||||||
|
if (newVisibleIndices.size === prev.size &&
|
||||||
|
Array.from(newVisibleIndices).every(i => prev.has(i))) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
return newVisibleIndices;
|
||||||
|
});
|
||||||
|
}, [isElementFarFromViewport]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
observerRef.current = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
entries.forEach(entry => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
updateVisibleIndices();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
root: null,
|
root: null,
|
||||||
rootMargin: '200px',
|
rootMargin: '500px 0px',
|
||||||
threshold: 0.1,
|
threshold: 0.01
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
placeholderRefs.current.forEach((ref) => {
|
placeholderRefs.current.forEach(ref => {
|
||||||
if (ref) observer.observe(ref);
|
if (ref) observerRef.current.observe(ref);
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
const handleScroll = () => {
|
||||||
observer.disconnect();
|
if (cleanupTimeoutRef.current) {
|
||||||
|
clearTimeout(cleanupTimeoutRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanupTimeoutRef.current = setTimeout(() => {
|
||||||
|
updateVisibleIndices();
|
||||||
|
|
||||||
|
setVisibleIndices(prev => {
|
||||||
|
const updated = new Set(prev);
|
||||||
|
let changed = false;
|
||||||
|
|
||||||
|
placeholderRefs.current.forEach((ref, index) => {
|
||||||
|
if (ref && isElementFarFromViewport(ref) && prev.has(index)) {
|
||||||
|
updated.delete(index);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return changed ? updated : prev;
|
||||||
|
});
|
||||||
|
}, 150);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('scroll', handleScroll, { passive: true });
|
||||||
|
window.addEventListener('resize', updateVisibleIndices, { passive: true });
|
||||||
|
|
||||||
|
updateVisibleIndices();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (cleanupTimeoutRef.current) clearTimeout(cleanupTimeoutRef.current);
|
||||||
|
if (observerRef.current) observerRef.current.disconnect();
|
||||||
|
window.removeEventListener('scroll', handleScroll);
|
||||||
|
window.removeEventListener('resize', updateVisibleIndices);
|
||||||
|
};
|
||||||
|
}, [updateVisibleIndices]);
|
||||||
|
|
||||||
|
const shouldShowChart = (index) => {
|
||||||
|
return visibleIndices.has(index) ||
|
||||||
|
visibleIndices.has(index - 1) ||
|
||||||
|
visibleIndices.has(index + 1);
|
||||||
};
|
};
|
||||||
}, [charts]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -63,12 +151,17 @@ const LazyChartBatchRenderer = ({ charts }) => {
|
||||||
key={index}
|
key={index}
|
||||||
ref={(el) => (placeholderRefs.current[index] = el)}
|
ref={(el) => (placeholderRefs.current[index] = el)}
|
||||||
data-index={index}
|
data-index={index}
|
||||||
|
style={{
|
||||||
|
minHeight: '400px',
|
||||||
|
marginBottom: '20px',
|
||||||
|
transition: 'opacity 0.3s ease',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{visibleIndices.has(index) ? chart : <ChartSkeleton />}
|
{shouldShowChart(index) ? chart : <ChartSkeleton />}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default LazyChartBatchRenderer;
|
export default React.memo(LazyChartBatchRenderer);
|
||||||
Loading…
Reference in New Issue