import { useColor } from '@/hooks/useColor'; import { useEffect, useState } from 'react'; import { LayoutChangeEvent, View, ViewStyle } from 'react-native'; import Animated, { useAnimatedProps, useSharedValue, withTiming, } from 'react-native-reanimated'; import Svg, { G, Line, Rect, Text as SvgText } from 'react-native-svg'; // Animated SVG Components const AnimatedRect = Animated.createAnimatedComponent(Rect); interface ChartConfig { width?: number; height?: number; padding?: number; showGrid?: boolean; showLabels?: boolean; animated?: boolean; duration?: number; } export interface StackedBarDataPoint { label: string; values: number[]; } type Props = { data: StackedBarDataPoint[]; colors?: string[]; config?: ChartConfig; style?: ViewStyle; categories?: string[]; horizontal?: boolean; }; export const StackedBarChart = ({ data, colors = [], config = {}, style, categories = [], horizontal = false, }: Props) => { const [containerWidth, setContainerWidth] = useState(300); const { height = 200, padding = 20, showLabels = true, showGrid = true, animated = true, duration = 800, } = config; const chartWidth = containerWidth || config.width || 300; const primaryColor = useColor('primary'); const mutedColor = useColor('mutedForeground'); const animationProgress = useSharedValue(0); const handleLayout = (event: LayoutChangeEvent) => { const { width: measuredWidth } = event.nativeEvent.layout; if (measuredWidth > 0) { setContainerWidth(measuredWidth); } }; useEffect(() => { if (animated) { animationProgress.value = withTiming(1, { duration }); } else { animationProgress.value = 1; } }, [data, animated, duration]); if (!data.length) return null; const maxValue = Math.max( ...data.map((d) => d.values.reduce((sum, val) => sum + val, 0)) ); const seriesCount = data[0]?.values.length || 0; const innerChartWidth = chartWidth - padding * 2; const chartHeight = height - padding * 2; // Default colors if not provided const defaultColors = [ '#8884d8', '#82ca9d', '#ffc658', '#ff7300', '#00ff00', '#0088fe', primaryColor, ]; const seriesColors = colors.length >= seriesCount ? colors : [...colors, ...defaultColors].slice(0, seriesCount); if (horizontal) { // Horizontal stacked bars const barHeight = (chartHeight / data.length) * 0.8; const barSpacing = (chartHeight / data.length) * 0.2; return ( {/* Grid lines */} {showGrid && ( {[0, 0.25, 0.5, 0.75, 1].map((ratio, index) => ( ))} )} {data.map((item, itemIndex) => { let cumulativeWidth = 0; const y = padding + itemIndex * (barHeight + barSpacing) + barSpacing / 2; return ( {item.values.map((value, valueIndex) => { const segmentWidth = (value / maxValue) * innerChartWidth; const x = padding + cumulativeWidth; const segmentAnimatedProps = useAnimatedProps(() => ({ width: animationProgress.value * segmentWidth, })); cumulativeWidth += segmentWidth; return ( ); })} {/* Bar labels */} {showLabels && ( {item.label} )} ); })} {/* Legend */} {categories.length > 0 && ( {categories.map((category, index) => ( {category} ))} )} ); } // Vertical stacked bars const barWidth = (innerChartWidth / data.length) * 0.8; const barSpacing = (innerChartWidth / data.length) * 0.2; return ( {/* Grid lines */} {showGrid && ( {[0, 0.25, 0.5, 0.75, 1].map((ratio, index) => ( ))} )} {data.map((item, itemIndex) => { let cumulativeHeight = 0; const x = padding + itemIndex * (barWidth + barSpacing) + barSpacing / 2; const totalValue = item.values.reduce((sum, val) => sum + val, 0); return ( {item.values.map((value, valueIndex) => { const segmentHeight = (value / maxValue) * chartHeight; const y = height - padding - cumulativeHeight - segmentHeight; const segmentAnimatedProps = useAnimatedProps(() => ({ height: animationProgress.value * segmentHeight, y: height - padding - cumulativeHeight - animationProgress.value * segmentHeight, })); cumulativeHeight += segmentHeight; return ( ); })} {/* Bar labels */} {showLabels && ( {item.label} )} ); })} {/* Legend */} {categories.length > 0 && ( {categories.map((category, index) => ( {category} ))} )} ); };