import { ChartLegendPosition, ChartsProps, ChartType } from './types' import { ResponsiveContainer } from 'recharts' import { DEFAULT_COLORS } from './utils' import ChartHeader from './ChartHeader' import ChartLegends from './ChartLegend' import { useRef, useState, useEffect, useCallback, useId, useMemo } from 'react' import { renderChart } from './renderChart' import { normalizeChartData, transformNestedData } from './ChartUtils' import Block from '../../components/Primitives/Block/Block' import { ChartTokensType } from './chart.tokens' import { FOUNDATION_THEME } from '../../tokens' import { useBreakpoints } from '../../hooks/useBreakPoints' import { BREAKPOINTS } from '../../breakpoints/breakPoints' import { Button, ButtonSize, ButtonSubType, ButtonType } from '../Button' import { ChevronDown } from 'lucide-react' import { useResponsiveTokens } from '../../hooks/useResponsiveTokens' import useScrollLock from '../../hooks/useScrollLock' import { toPixels } from '../../global-utils/GlobalUtils' import ChartsSkeleton from './ChartsSkeleton' const Charts: React.FC = ({ chartType = ChartType.LINE, data: dataProp, colors, slot1, slot2, slot3, legendPosition = ChartLegendPosition.TOP, chartHeaderSlot, stackedLegends = false, stackedLegendsData, showAllLegends = false, barsize, xAxis, yAxis, tooltip, funnelConfig, noData, height = 400, showHeader = true, showCollapseIcon = true, isExpanded: isExpandedProp, onExpandedChange, chartName = 'Chart', skeleton, legends, CustomizedDot, lineSeriesKeys, ...props }) => { const data = normalizeChartData(dataProp) const { breakPointLabel } = useBreakpoints(BREAKPOINTS) const isSmallScreen = breakPointLabel === 'sm' const chartTokens = useResponsiveTokens('CHARTS') const [isExpandedState, setIsExpandedState] = useState(true) const isExpanded = isExpandedProp !== undefined ? isExpandedProp : isExpandedState const setIsExpanded = useCallback( (value: boolean) => { if (isExpandedProp === undefined) { setIsExpandedState(value) } onExpandedChange?.(value) }, [isExpandedProp, onExpandedChange] ) const chartContainerRef = useRef(null!) const [hoveredKey, setHoveredKey] = useState(null) const [selectedKeys, setSelectedKeys] = useState([]) const [showLegend, setShowLegend] = useState(false) const [isFullscreen, setIsFullscreen] = useState(false) const legendContainerRef = useRef(null!) const [legendContainerHeight, setLegendContainerHeight] = useState(0) useScrollLock(isFullscreen) if ( colors && colors.length > 0 && !colors.every( (item) => item && typeof item === 'object' && 'color' in item ) ) { // If any object is missing 'color' key, add fallback color from DEFAULT_COLORS colors = colors.map((item, index) => { if (item && typeof item === 'object' && 'color' in item) { return item as { key: string; color: string } } const defaultColor = DEFAULT_COLORS[index % DEFAULT_COLORS.length] const itemKey = typeof item === 'object' && item && 'key' in item ? (item as { key: string }).key : String(index) return { key: itemKey, color: defaultColor.color, } }) } if (!colors || colors.length === 0) colors = DEFAULT_COLORS const flattenedData = transformNestedData(data, selectedKeys) const lineKeys = data.length > 0 ? Object.keys(data[0].data) : [] const mergedXAxis = { label: xAxis?.label, showLabel: xAxis?.showLabel ?? false, interval: xAxis?.interval, show: xAxis?.show ?? true, ...xAxis, } const mergedYAxis = { label: yAxis?.label, showLabel: yAxis?.showLabel ?? false, interval: yAxis?.interval, show: yAxis?.show ?? true, ...yAxis, } const baseId = useId() const chartId = `${baseId}-chart` const headerId = `${baseId}-header` const chartDescriptionId = `${baseId}-chart-description` const chartLabel = useMemo(() => { const parts: string[] = [] if (chartHeaderSlot) { if (typeof chartHeaderSlot === 'string') { parts.push(chartHeaderSlot) } else if ( chartHeaderSlot && typeof chartHeaderSlot === 'object' && 'props' in chartHeaderSlot && chartHeaderSlot.props && typeof chartHeaderSlot.props === 'object' && 'children' in chartHeaderSlot.props ) { const children = chartHeaderSlot.props.children if (typeof children === 'string') { parts.push(children) } } } if (xAxis?.label) { parts.push(`X-axis: ${xAxis.label}`) } if (yAxis?.label) { parts.push(`Y-axis: ${yAxis.label}`) } return parts.length > 0 ? `${chartName} - ${parts.join(', ')}` : chartName }, [chartHeaderSlot, xAxis?.label, yAxis?.label, chartName]) const chartDescription = useMemo(() => { const parts: string[] = [] if (chartType) { parts.push(`${chartType} chart`) } if (data.length > 0) { parts.push(`showing ${data.length} data points`) } if (lineKeys.length > 0) { parts.push(`with ${lineKeys.length} series: ${lineKeys.join(', ')}`) } return parts.join(', ') }, [chartType, data.length, lineKeys]) const isLargeScreen = () => { return window.innerWidth >= 1024 } const isIOSOrSafari = () => { const userAgent = navigator.userAgent const isIOS = /iPad|iPhone|iPod/.test(userAgent) return isIOS } const handleFullscreen = useCallback(async () => { try { if (isLargeScreen()) { console.log( 'Fullscreen only available for small screens (<=786px)' ) return } if ( isIOSOrSafari() || !document.documentElement.requestFullscreen ) { setIsFullscreen(true) if (screen.orientation && 'lock' in screen.orientation) { try { // Disabled eslint for this line because TypeScript doesn't have proper types for orientation lock yet // eslint-disable-next-line @typescript-eslint/no-explicit-any await (screen.orientation as any).lock('landscape') } catch (err) { console.log('Orientation lock not supported:', err) } } setTimeout(() => { window.scrollTo(0, 1) }, 100) return } if (!document.fullscreenElement) { await document.documentElement.requestFullscreen() // Disabled eslint for this line because TypeScript doesn't have proper types for orientation lock yet // eslint-disable-next-line @typescript-eslint/no-explicit-any if (screen.orientation && (screen.orientation as any).lock) { try { // Disabled eslint for this line because TypeScript doesn't have proper types for orientation lock yet // eslint-disable-next-line @typescript-eslint/no-explicit-any await (screen.orientation as any).lock('landscape') } catch (err) { console.log('Orientation lock not supported:', err) } } setIsFullscreen(true) } } catch (err) { console.error('Error entering fullscreen:', err) alert('Error entering fullscreen:' + err) setIsFullscreen(true) } }, []) const handleExitFullscreen = useCallback(async () => { try { if (document.fullscreenElement) { await document.exitFullscreen() } if (screen.orientation && screen.orientation.unlock) { screen.orientation.unlock() } setIsFullscreen(false) } catch (err) { console.error('Error exiting fullscreen:', err) setIsFullscreen(false) } }, []) useEffect(() => { const handleFullscreenChange = () => { if (!document.fullscreenElement) { setIsFullscreen(false) if (screen.orientation && screen.orientation.unlock) { screen.orientation.unlock() } } } document.addEventListener('fullscreenchange', handleFullscreenChange) document.addEventListener( 'webkitfullscreenchange', handleFullscreenChange ) document.addEventListener('mozfullscreenchange', handleFullscreenChange) document.addEventListener('MSFullscreenChange', handleFullscreenChange) return () => { document.removeEventListener( 'fullscreenchange', handleFullscreenChange ) document.removeEventListener( 'webkitfullscreenchange', handleFullscreenChange ) document.removeEventListener( 'mozfullscreenchange', handleFullscreenChange ) document.removeEventListener( 'MSFullscreenChange', handleFullscreenChange ) } }, []) useEffect(() => { if (legendContainerRef.current) { setLegendContainerHeight( legendContainerRef.current?.offsetHeight || 0 ) } }, [showLegend]) const renderFullscreenChart = () => ( {skeleton?.show ? ( ) : ( <> {' '} {showHeader && ( )} {/* Hidden description for screen readers */}
{chartDescription}
{showHorizontallyStackedLegends() ? isExpanded && ( { }
{renderChart({ chartTokens, CustomizedDot, flattenedData, chartType, hoveredKey, lineKeys, colors, setHoveredKey, data, selectedKeys, isSmallScreen: false, barsize, xAxis: mergedXAxis, yAxis: { ...mergedYAxis, label: isSmallScreen ? undefined : mergedYAxis.label, showLabel: isSmallScreen ? false : mergedYAxis.showLabel, }, tooltip, funnelConfig, noData, onKeyClick: handleLegendClick, lineSeriesKeys, height: typeof height === 'number' ? height : undefined, })}
) : isExpanded && (
{renderChart({ chartTokens, CustomizedDot, flattenedData, chartType, hoveredKey, lineKeys, colors, setHoveredKey, data, selectedKeys, isSmallScreen, barsize, xAxis: mergedXAxis, yAxis: { ...mergedYAxis, label: isSmallScreen ? undefined : mergedYAxis.label, showLabel: isSmallScreen ? false : mergedYAxis.showLabel, }, tooltip, funnelConfig, noData, onKeyClick: handleLegendClick, lineSeriesKeys, height: typeof height === 'number' ? height : undefined, })}
)} )}
) const handleLegendClick = (key: string) => { setSelectedKeys((prevActiveKeys) => { if (prevActiveKeys.includes(key)) { return prevActiveKeys.filter((k) => k !== key) } else { return [...prevActiveKeys, key] } }) } const handleLegendEnter = (key: string) => { if ( selectedKeys.length === 0 || selectedKeys.length === lineKeys.length ) { setHoveredKey(key) } } const handleLegendLeave = () => { setHoveredKey(null) } const showHorizontallyStackedLegends = () => { return !( chartType === ChartType.PIE && legendPosition === ChartLegendPosition.RIGHT ) } return ( <> {skeleton?.show ? ( ) : ( <> {showHeader && ( )} {/* Hidden description for screen readers */}
{chartDescription}
{showHorizontallyStackedLegends() ? isExpanded && ( {!isSmallScreen && ( )}
{renderChart({ chartTokens, CustomizedDot, chartName, flattenedData, chartType, hoveredKey, lineKeys, colors, setHoveredKey, data, selectedKeys, isSmallScreen, barsize, xAxis: mergedXAxis, yAxis: { ...mergedYAxis, label: isSmallScreen ? undefined : mergedYAxis.label, showLabel: isSmallScreen ? false : mergedYAxis.showLabel, }, tooltip, funnelConfig, noData, onKeyClick: handleLegendClick, lineSeriesKeys, height: typeof height === 'number' ? height : undefined, })}
{isSmallScreen && ( {(showLegend || !stackedLegends) && ( )} {stackedLegends && (