fix(echarts): Resolve interaction issues on charts with timelines (#21185)

This commit is contained in:
sayThQ199
2025-06-18 20:22:33 +08:00
committed by GitHub
parent 2eae7503e1
commit 2df4699312

View File

@@ -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>