@@ -1,4 +1,5 @@
import { useCallback , useEffect , useMemo , useRef , useState } from 'react' ;
import { useCallback , useEffect , useMemo , useRef , useState } from 'react' ;
import type { EChartsOption } from 'echarts' ;
import { getHistoryGranularity , type HistoryAxisLabelFormat } from '../../utils/historyGranularity' ;
import { getHistoryGranularity , type HistoryAxisLabelFormat } from '../../utils/historyGranularity' ;
import type { DataZoomEventBatch , DataZoomState , HistoryZoomRange } from './chartTypes' ;
import type { DataZoomEventBatch , DataZoomState , HistoryZoomRange } from './chartTypes' ;
import { HistoryChartArea } from './HistoryChartArea' ;
import { HistoryChartArea } from './HistoryChartArea' ;
@@ -86,6 +87,39 @@ function isSameZoomRange(left: HistoryZoomRange, right: HistoryZoomRange): boole
return left . from === right . from && left . to === right . to && left . granulate === right . granulate ;
return left . from === right . from && left . to === right . to && left . granulate === right . granulate ;
}
}
/** Вычисляет, какую часть верхнеуровневого периода занимает текущий пользовательский zoom. */
function createDataZoomState ( baseRange : HistoryZoomRange , zoomRange : HistoryZoomRange ) : DataZoomState {
// Верхнеуровневый период берём из инпутов "С /По" и считаем е г о полными 0..100%.
const baseFromMs = new Date ( baseRange . from ) . getTime ( ) ;
const baseToMs = new Date ( baseRange . to ) . getTime ( ) ;
// Пользовательский zoom живёт внутри верхнеуровневого периода и должен быть выделен на нижней шкале.
const zoomFromMs = new Date ( zoomRange . from ) . getTime ( ) ;
const zoomToMs = new Date ( zoomRange . to ) . getTime ( ) ;
const baseSpanMs = baseToMs - baseFromMs ;
// Если даты некорректны, показываем весь период, чтобы не ломать управление графиком.
if (
! Number . isFinite ( baseFromMs ) ||
! Number . isFinite ( baseToMs ) ||
! Number . isFinite ( zoomFromMs ) ||
! Number . isFinite ( zoomToMs ) ||
baseSpanMs <= 0
) {
return { start : 0 , end : 100 } ;
}
// Переводим абсолютные даты zoom-а в проценты относительно верхнеуровневого периода.
const start = ( ( zoomFromMs - baseFromMs ) / baseSpanMs ) * 100 ;
const end = ( ( zoomToMs - baseFromMs ) / baseSpanMs ) * 100 ;
// Ограничиваем значения диапазоном 0..100, чтобы ECharts не получил выход за шкалу.
return {
start : Math.max ( 0 , Math . min ( 100 , start ) ) ,
end : Math.max ( 0 , Math . min ( 100 , end ) ) ,
} ;
}
export function HistoryChart ( {
export function HistoryChart ( {
edge ,
edge ,
from ,
from ,
@@ -96,11 +130,17 @@ export function HistoryChart({
to ,
to ,
tagLabels = { } ,
tagLabels = { } ,
} : HistoryChartProps ) {
} : HistoryChartProps ) {
const baseRange = useMemo (
( ) = > createInitialZoomRange ( { from , granulate , labelFormat , tickIntervalMs , to } ) ,
[ from , granulate , labelFormat , tickIntervalMs , to ] ,
) ;
const [ zoomRange , setZoomRange ] = useState ( ( ) = >
const [ zoomRange , setZoomRange ] = useState ( ( ) = >
createInitialZoomRange ( { from , granulate , labelFormat , tickIntervalMs , to } ) ,
createInitialZoomRange ( { from , granulate , labelFormat , tickIntervalMs , to } ) ,
) ;
) ;
const baseRangeRef = useRef ( baseRange ) ;
const zoomRangeRef = useRef ( zoomRange ) ;
const zoomRangeRef = useRef ( zoomRange ) ;
const zoomTimerRef = useRef < ReturnType < typeof setTimeout > | null > ( null ) ;
const zoomTimerRef = useRef < ReturnType < typeof setTimeout > | null > ( null ) ;
const lastReadyOptionRef = useRef < EChartsOption | null > ( null ) ;
const lines = useHistoryChartQueries ( {
const lines = useHistoryChartQueries ( {
edge ,
edge ,
from : zoomRange . from ,
from : zoomRange . from ,
@@ -112,12 +152,13 @@ export function HistoryChart({
const series = useHistoryChartSeries ( lines , zoomRange . granulate ) ;
const series = useHistoryChartSeries ( lines , zoomRange . granulate ) ;
const legendData = useMemo ( ( ) = > lines . map ( ( line ) = > line . label ) , [ lines ] ) ;
const legendData = useMemo ( ( ) = > lines . map ( ( line ) = > line . label ) , [ lines ] ) ;
const loading = lines . some ( ( line ) = > line . loading ) ;
const loading = lines . some ( ( line ) = > line . loading ) ;
const hasData = series . length > 0 ;
useEffect ( ( ) = > {
useEffect ( ( ) = > {
const nextRange = createInitialZoomRange ( { from , granulate , labelFormat , tickIntervalMs , to } ) ;
baseRangeRef . current = baseRange ;
zoomRangeRef . current = next Range;
zoomRangeRef . current = base Range;
setZoomRange ( next Range) ;
setZoomRange ( base Range) ;
} , [ from , granulate , labelFormat , tickIntervalMs , to ] ) ;
} , [ baseRange ] ) ;
useEffect ( ( ) = > {
useEffect ( ( ) = > {
zoomRangeRef . current = zoomRange ;
zoomRangeRef . current = zoomRange ;
@@ -135,17 +176,29 @@ export function HistoryChart({
const option = useMemo (
const option = useMemo (
( ) = >
( ) = >
createHistoryChartOptions ( {
createHistoryChartOptions ( {
dataZoomState : { start : 0 , end : 100 } ,
dataZoomState : createDataZoomState ( baseRange , zoomRange ) ,
from : zoom Range. from ,
from : base Range. from ,
to : zoom Range.to,
to : base Range.to,
labelFormat : zoomRange.labelFormat ,
labelFormat : zoomRange.labelFormat ,
legendData ,
legendData ,
series ,
series ,
tickIntervalMs : zoomRange.tickIntervalMs ,
tickIntervalMs : zoomRange.tickIntervalMs ,
} ) ,
} ) ,
[ legendData , series , zoomRange ] ,
[ baseRange , legendData, series , zoomRange ] ,
) ;
) ;
// В о время lazy-загрузки нового zoom-диапазона оставляем на экране последний готовый график.
// Так canvas не заменяется loader-ом и пользователь не теряет визуальный контекст.
const fallbackOption = loading && ! hasData ? lastReadyOptionRef.current : null ;
const displayOption = fallbackOption ? ? option ;
const displayHasData = hasData || Boolean ( fallbackOption ) ;
useEffect ( ( ) = > {
if ( hasData ) {
lastReadyOptionRef . current = option ;
}
} , [ hasData , option ] ) ;
const handleDataZoom = useCallback ( ( event : DataZoomEventBatch ) = > {
const handleDataZoom = useCallback ( ( event : DataZoomEventBatch ) = > {
const state = ( event . batch ? ? [ event ] ) . find ( isXAxisDataZoom ) ;
const state = ( event . batch ? ? [ event ] ) . find ( isXAxisDataZoom ) ;
@@ -153,8 +206,9 @@ export function HistoryChart({
return ;
return ;
}
}
const baseRange = baseRangeRef . current ;
const currentRange = zoomRangeRef . current ;
const currentRange = zoomRangeRef . current ;
const nextRange = getZoomRangeFromEvent ( state , current Range) ;
const nextRange = getZoomRangeFromEvent ( state , base Range) ;
if ( ! nextRange || isSameZoomRange ( nextRange , currentRange ) ) {
if ( ! nextRange || isSameZoomRange ( nextRange , currentRange ) ) {
return ;
return ;
@@ -172,10 +226,10 @@ export function HistoryChart({
return (
return (
< HistoryChartArea
< HistoryChartArea
hasData = { series . length > 0 }
hasData = { displayHasData }
hasSelection = { tags . length > 0 }
hasSelection = { tags . length > 0 }
loading = { loading }
loading = { loading }
option = { o ption}
option = { displayO ption}
onDataZoom = { handleDataZoom }
onDataZoom = { handleDataZoom }
/ >
/ >
) ;
) ;