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, { Defs, G, Line, LinearGradient, Path, Stop, Text as SvgText, } from 'react-native-svg'; // Animated SVG Components const AnimatedPath = Animated.createAnimatedComponent(Path); interface ChartConfig { width?: number; height?: number; padding?: number; showGrid?: boolean; showLabels?: boolean; animated?: boolean; duration?: number; } export interface StackedAreaDataPoint { x: number; y: number[]; label?: string; } type Props = { data: StackedAreaDataPoint[]; colors?: string[]; config?: ChartConfig; style?: ViewStyle; categories?: string[]; }; // Utility function to create smooth path const createSmoothPath = (points: { x: number; y: number }[]): string => { if (points.length === 0) return ''; let path = `M${points[0].x},${points[0].y}`; for (let i = 1; i < points.length; i++) { const prevPoint = points[i - 1]; const currentPoint = points[i]; const cpx = (prevPoint.x + currentPoint.x) / 2; const cpy = prevPoint.y; path += ` Q${cpx},${cpy} ${currentPoint.x},${currentPoint.y}`; } return path; }; const createAreaPath = ( topPoints: { x: number; y: number }[], bottomPoints: { x: number; y: number }[] ): string => { if (topPoints.length === 0 || bottomPoints.length === 0) return ''; // Create the top curve const topPath = createSmoothPath(topPoints); // Create the bottom curve (reversed order for proper path closure) const reversedBottomPoints = [...bottomPoints].reverse(); // Start the area path with the top curve let areaPath = topPath; // Add line to the last bottom point areaPath += ` L${reversedBottomPoints[0].x},${reversedBottomPoints[0].y}`; // Add the bottom curve if (reversedBottomPoints.length > 1) { for (let i = 1; i < reversedBottomPoints.length; i++) { const prevPoint = reversedBottomPoints[i - 1]; const currentPoint = reversedBottomPoints[i]; const cpx = (prevPoint.x + currentPoint.x) / 2; const cpy = prevPoint.y; areaPath += ` Q${cpx},${cpy} ${currentPoint.x},${currentPoint.y}`; } } // Close the path areaPath += ' Z'; return areaPath; }; export const StackedAreaChart = ({ data, colors = [], config = {}, style, categories = [], }: Props) => { const [containerWidth, setContainerWidth] = useState(300); const { height = 200, padding = 20, showGrid = true, showLabels = true, animated = true, duration = 1000, } = 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; // Calculate stacked totals and max value const stackedData = data.map((point) => { const cumulative = point.y.reduce((acc, val, idx) => { acc.push((acc[acc.length - 1] || 0) + val); return acc; }, [] as number[]); return { ...point, cumulative }; }); const maxValue = Math.max( ...stackedData.map((d) => Math.max(...d.cumulative)) ); const seriesCount = data[0]?.y.length || 0; const innerChartWidth = chartWidth - padding * 2; const chartHeight = height - padding * 2; // Default colors if not provided const defaultColors = [ primaryColor, '#8884d8', '#82ca9d', '#ffc658', '#ff7300', '#00ff00', '#0088fe', ]; const seriesColors = colors.length >= seriesCount ? colors : [...colors, ...defaultColors].slice(0, seriesCount); return ( {seriesColors.map((color, index) => ( ))} {/* Grid lines */} {showGrid && ( {[0, 0.25, 0.5, 0.75, 1].map((ratio, index) => ( ))} )} {/* Stacked areas */} {Array.from({ length: seriesCount }, (_, seriesIndex) => { const topPoints = stackedData.map((point, pointIndex) => ({ x: padding + (pointIndex / (data.length - 1)) * innerChartWidth, y: padding + ((maxValue - point.cumulative[seriesIndex]) / maxValue) * chartHeight, })); // All areas extend from x-axis (y=0) to their cumulative value const bottomPoints = stackedData.map((point, pointIndex) => ({ x: padding + (pointIndex / (data.length - 1)) * innerChartWidth, y: height - padding, // Always extend to x-axis (y=0 in data terms) })); const areaPath = createAreaPath(topPoints, bottomPoints); const areaAnimatedProps = useAnimatedProps(() => ({ opacity: animationProgress.value * (seriesIndex === 0 ? 1 : 0.7), // Make upper areas slightly transparent })); return ( ); })} {/* Labels */} {showLabels && ( {data.map((point, index) => ( {point.label || point.x.toString()} ))} )} {/* Legend */} {categories.length > 0 && ( {categories.map((category, index) => ( {category} ))} )} ); };