| 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 { | ||||
| // 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) | ||||
| 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 || '') | ||||
| width: 'auto', | width: 'auto', | ||||
| }) as any, []) | }) as any, []) | ||||
| const echartsOnEvents = useMemo(() => ({ | |||||
| finished: () => { | |||||
| const instance = echartsRef.current?.getEchartsInstance?.() | |||||
| if (instance) | |||||
| instance.resize() | |||||
| // Debounce resize operations | |||||
| const debouncedResize = useCallback(() => { | |||||
| if (resizeTimerRef.current) | |||||
| clearTimeout(resizeTimerRef.current) | |||||
| 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 | |||||
| const instance = echartsRef.current?.getEchartsInstance?.() | |||||
| if (instance) | |||||
| instance.resize() | |||||
| if (chartInstanceRef.current) | |||||
| // Use debounced resize | |||||
| debouncedResize() | |||||
| } | } | ||||
| 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 | ||||
| } | } | ||||
| }, [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) { | ||||
| // 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', | ||||
| }}> | }}> | ||||
| <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} | |||||
| onEvents={echartsOnEvents} | |||||
| notMerge={false} | |||||
| lazyUpdate={false} | |||||
| onEvents={echartsEvents} | |||||
| onChartReady={handleChartReady} | |||||
| /> | /> | ||||
| </ErrorBoundary> | </ErrorBoundary> | ||||
| </div> | </div> | ||||
| </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> |