fix(echarts): Resolve interaction issues on charts with timelines (#21185)
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { memo, useEffect, useMemo, useRef, useState } from 'react'
|
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import ReactEcharts from 'echarts-for-react'
|
import ReactEcharts from 'echarts-for-react'
|
||||||
import SyntaxHighlighter from 'react-syntax-highlighter'
|
import SyntaxHighlighter from 'react-syntax-highlighter'
|
||||||
import {
|
import {
|
||||||
@@ -62,6 +62,17 @@ const getCorrectCapitalizationLanguageName = (language: string) => {
|
|||||||
// visit https://reactjs.org/docs/error-decoder.html?invariant=185 for the full message
|
// visit https://reactjs.org/docs/error-decoder.html?invariant=185 for the full message
|
||||||
// or use the non-minified dev environment for full errors and additional helpful warnings.
|
// or use the non-minified dev environment for full errors and additional helpful warnings.
|
||||||
|
|
||||||
|
// Define ECharts event parameter types
|
||||||
|
interface EChartsEventParams {
|
||||||
|
type: string;
|
||||||
|
seriesIndex?: number;
|
||||||
|
dataIndex?: number;
|
||||||
|
name?: string;
|
||||||
|
value?: any;
|
||||||
|
currentIndex?: number; // Added for timeline events
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
const CodeBlock: any = memo(({ inline, className, children = '', ...props }: any) => {
|
const CodeBlock: any = memo(({ inline, className, children = '', ...props }: any) => {
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const [isSVG, setIsSVG] = useState(true)
|
const [isSVG, setIsSVG] = useState(true)
|
||||||
@@ -70,6 +81,11 @@ const CodeBlock: any = memo(({ inline, className, children = '', ...props }: any
|
|||||||
const echartsRef = useRef<any>(null)
|
const echartsRef = useRef<any>(null)
|
||||||
const contentRef = useRef<string>('')
|
const contentRef = useRef<string>('')
|
||||||
const processedRef = useRef<boolean>(false) // Track if content was successfully processed
|
const processedRef = useRef<boolean>(false) // Track if content was successfully processed
|
||||||
|
const instanceIdRef = useRef<string>(`chart-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`) // Unique ID for logging
|
||||||
|
const isInitialRenderRef = useRef<boolean>(true) // Track if this is initial render
|
||||||
|
const chartInstanceRef = useRef<any>(null) // Direct reference to ECharts instance
|
||||||
|
const resizeTimerRef = useRef<NodeJS.Timeout | null>(null) // For debounce handling
|
||||||
|
const finishedEventCountRef = useRef<number>(0) // Track finished event trigger count
|
||||||
const match = /language-(\w+)/.exec(className || '')
|
const match = /language-(\w+)/.exec(className || '')
|
||||||
const language = match?.[1]
|
const language = match?.[1]
|
||||||
const languageShowName = getCorrectCapitalizationLanguageName(language || '')
|
const languageShowName = getCorrectCapitalizationLanguageName(language || '')
|
||||||
@@ -85,36 +101,64 @@ const CodeBlock: any = memo(({ inline, className, children = '', ...props }: any
|
|||||||
width: 'auto',
|
width: 'auto',
|
||||||
}) as any, [])
|
}) as any, [])
|
||||||
|
|
||||||
const echartsOnEvents = useMemo(() => ({
|
// Debounce resize operations
|
||||||
finished: () => {
|
const debouncedResize = useCallback(() => {
|
||||||
const instance = echartsRef.current?.getEchartsInstance?.()
|
if (resizeTimerRef.current)
|
||||||
if (instance)
|
clearTimeout(resizeTimerRef.current)
|
||||||
instance.resize()
|
|
||||||
|
resizeTimerRef.current = setTimeout(() => {
|
||||||
|
if (chartInstanceRef.current)
|
||||||
|
chartInstanceRef.current.resize()
|
||||||
|
resizeTimerRef.current = null
|
||||||
|
}, 200)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Handle ECharts instance initialization
|
||||||
|
const handleChartReady = useCallback((instance: any) => {
|
||||||
|
chartInstanceRef.current = instance
|
||||||
|
|
||||||
|
// Force resize to ensure timeline displays correctly
|
||||||
|
setTimeout(() => {
|
||||||
|
if (chartInstanceRef.current)
|
||||||
|
chartInstanceRef.current.resize()
|
||||||
|
}, 200)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Store event handlers in useMemo to avoid recreating them
|
||||||
|
const echartsEvents = useMemo(() => ({
|
||||||
|
finished: (params: EChartsEventParams) => {
|
||||||
|
// Limit finished event frequency to avoid infinite loops
|
||||||
|
finishedEventCountRef.current++
|
||||||
|
if (finishedEventCountRef.current > 3) {
|
||||||
|
// Stop processing after 3 times to avoid infinite loops
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chartInstanceRef.current) {
|
||||||
|
// Use debounced resize
|
||||||
|
debouncedResize()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}), [echartsRef]) // echartsRef is stable, so this effectively runs once.
|
}), [debouncedResize])
|
||||||
|
|
||||||
// Handle container resize for echarts
|
// Handle container resize for echarts
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (language !== 'echarts' || !echartsRef.current) return
|
if (language !== 'echarts' || !chartInstanceRef.current) return
|
||||||
|
|
||||||
const handleResize = () => {
|
const handleResize = () => {
|
||||||
// This gets the echarts instance from the component
|
if (chartInstanceRef.current)
|
||||||
const instance = echartsRef.current?.getEchartsInstance?.()
|
// Use debounced resize
|
||||||
if (instance)
|
debouncedResize()
|
||||||
instance.resize()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('resize', handleResize)
|
window.addEventListener('resize', handleResize)
|
||||||
|
|
||||||
// Also manually trigger resize after a short delay to ensure proper sizing
|
|
||||||
const resizeTimer = setTimeout(handleResize, 200)
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('resize', handleResize)
|
window.removeEventListener('resize', handleResize)
|
||||||
clearTimeout(resizeTimer)
|
if (resizeTimerRef.current)
|
||||||
|
clearTimeout(resizeTimerRef.current)
|
||||||
}
|
}
|
||||||
}, [language, echartsRef.current])
|
}, [language, debouncedResize])
|
||||||
|
|
||||||
// Process chart data when content changes
|
// Process chart data when content changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Only process echarts content
|
// Only process echarts content
|
||||||
@@ -222,6 +266,7 @@ const CodeBlock: any = memo(({ inline, className, children = '', ...props }: any
|
|||||||
}
|
}
|
||||||
}, [language, children])
|
}, [language, children])
|
||||||
|
|
||||||
|
// Cache rendered content to avoid unnecessary re-renders
|
||||||
const renderCodeContent = useMemo(() => {
|
const renderCodeContent = useMemo(() => {
|
||||||
const content = String(children).replace(/\n$/, '')
|
const content = String(children).replace(/\n$/, '')
|
||||||
switch (language) {
|
switch (language) {
|
||||||
@@ -274,6 +319,9 @@ const CodeBlock: any = memo(({ inline, className, children = '', ...props }: any
|
|||||||
|
|
||||||
// Success state: show the chart
|
// Success state: show the chart
|
||||||
if (chartState === 'success' && finalChartOption) {
|
if (chartState === 'success' && finalChartOption) {
|
||||||
|
// Reset finished event counter
|
||||||
|
finishedEventCountRef.current = 0
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
minWidth: '300px',
|
minWidth: '300px',
|
||||||
@@ -286,13 +334,20 @@ const CodeBlock: any = memo(({ inline, className, children = '', ...props }: any
|
|||||||
}}>
|
}}>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<ReactEcharts
|
<ReactEcharts
|
||||||
ref={echartsRef}
|
ref={(e) => {
|
||||||
|
if (e && isInitialRenderRef.current) {
|
||||||
|
echartsRef.current = e
|
||||||
|
isInitialRenderRef.current = false
|
||||||
|
}
|
||||||
|
}}
|
||||||
option={finalChartOption}
|
option={finalChartOption}
|
||||||
style={echartsStyle}
|
style={echartsStyle}
|
||||||
theme={isDarkMode ? 'dark' : undefined}
|
theme={isDarkMode ? 'dark' : undefined}
|
||||||
opts={echartsOpts}
|
opts={echartsOpts}
|
||||||
notMerge={true}
|
notMerge={false}
|
||||||
onEvents={echartsOnEvents}
|
lazyUpdate={false}
|
||||||
|
onEvents={echartsEvents}
|
||||||
|
onChartReady={handleChartReady}
|
||||||
/>
|
/>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
@@ -363,7 +418,7 @@ const CodeBlock: any = memo(({ inline, className, children = '', ...props }: any
|
|||||||
</SyntaxHighlighter>
|
</SyntaxHighlighter>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}, [children, language, isSVG, finalChartOption, props, theme, match, chartState, isDarkMode, echartsStyle, echartsOpts, echartsOnEvents])
|
}, [children, language, isSVG, finalChartOption, props, theme, match, chartState, isDarkMode, echartsStyle, echartsOpts, handleChartReady, echartsEvents])
|
||||||
|
|
||||||
if (inline || !match)
|
if (inline || !match)
|
||||||
return <code {...props} className={className}>{children}</code>
|
return <code {...props} className={className}>{children}</code>
|
||||||
|
Reference in New Issue
Block a user