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); const AnimatedLine = Animated.createAnimatedComponent(Line); interface ChartConfig { width?: number; height?: number; padding?: number; showGrid?: boolean; showLabels?: boolean; animated?: boolean; duration?: number; } interface CandlestickDataPoint { date: string; open: number; high: number; low: number; close: number; } type Props = { data: CandlestickDataPoint[]; config?: ChartConfig; style?: ViewStyle; }; export const CandlestickChart = ({ data, config = {}, style }: Props) => { const [containerWidth, setContainerWidth] = useState(300); const { height = 200, padding = 20, showGrid = true, showLabels = true, animated = true, duration = 800, } = config; // Use measured width or fallback to config width or default const chartWidth = containerWidth || config.width || 300; const bullishColor = useColor('green'); const bearishColor = useColor('red'); 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 allValues = data.flatMap((d) => [d.open, d.high, d.low, d.close]); const maxValue = Math.max(...allValues); const minValue = Math.min(...allValues); const valueRange = maxValue - minValue || 1; const innerChartWidth = chartWidth - padding * 2; const chartHeight = height - padding * 2; const candleWidth = (innerChartWidth / data.length) * 0.6; const candleSpacing = (innerChartWidth / data.length) * 0.4; return ( {/* Grid lines */} {showGrid && ( {[0, 0.25, 0.5, 0.75, 1].map((ratio, index) => ( ))} )} {data.map((item, index) => { const isBullish = item.close >= item.open; const color = isBullish ? bullishColor : bearishColor; const x = padding + index * (candleWidth + candleSpacing) + candleSpacing / 2; const highY = padding + ((maxValue - item.high) / valueRange) * chartHeight; const lowY = padding + ((maxValue - item.low) / valueRange) * chartHeight; const openY = padding + ((maxValue - item.open) / valueRange) * chartHeight; const closeY = padding + ((maxValue - item.close) / valueRange) * chartHeight; const bodyTop = Math.min(openY, closeY); const bodyHeight = Math.abs(closeY - openY) || 1; const wickAnimatedProps = useAnimatedProps(() => ({ y1: highY, y2: lowY, opacity: animationProgress.value, })); const bodyAnimatedProps = useAnimatedProps(() => ({ height: animationProgress.value * bodyHeight, y: bodyTop, opacity: animationProgress.value, })); return ( {/* High-Low wick */} {/* Open-Close body */} {showLabels && index % Math.max(1, Math.floor(data.length / 5)) === 0 && ( {item.date} )} ); })} ); };