import { useColor } from '@/hooks/useColor'; import { useEffect, useState } from 'react'; import { LayoutChangeEvent, View, ViewStyle } from 'react-native'; import Animated, { useAnimatedProps, useSharedValue, withDelay, withSpring, withTiming, } from 'react-native-reanimated'; import Svg, { Circle, G, Line, Text as SvgText } from 'react-native-svg'; // Animated SVG Components const AnimatedCircle = Animated.createAnimatedComponent(Circle); interface ChartConfig { width?: number; height?: number; padding?: number; showGrid?: boolean; showLabels?: boolean; animated?: boolean; duration?: number; } interface BubbleChartDataPoint { x: number; y: number; size: number; label?: string; color?: string; } type Props = { data: BubbleChartDataPoint[]; config?: ChartConfig; style?: ViewStyle; }; export const BubbleChart = ({ data, config = {}, style }: Props) => { const [containerWidth, setContainerWidth] = useState(300); const { height = 200, padding = 20, showGrid = true, showLabels = 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 maxX = Math.max(...data.map((d) => d.x)); const minX = Math.min(...data.map((d) => d.x)); const maxY = Math.max(...data.map((d) => d.y)); const minY = Math.min(...data.map((d) => d.y)); const maxSize = Math.max(...data.map((d) => d.size)); const xRange = maxX - minX || 1; const yRange = maxY - minY || 1; const innerChartWidth = chartWidth - padding * 2; const chartHeight = height - padding * 2; const colors = [ primaryColor, useColor('blue'), useColor('green'), useColor('orange'), useColor('purple'), useColor('pink'), ]; // Convert data to screen coordinates const bubbles = data.map((point, index) => ({ x: padding + ((point.x - minX) / xRange) * innerChartWidth, y: padding + ((maxY - point.y) / yRange) * chartHeight, radius: (point.size / maxSize) * 20 + 5, // Scale bubble size color: point.color || colors[index % colors.length], label: point.label, })); return ( {/* Grid lines */} {showGrid && ( {[0, 0.25, 0.5, 0.75, 1].map((ratio, index) => ( ))} )} {/* Bubbles */} {bubbles.map((bubble, index) => { const bubbleAnimatedProps = useAnimatedProps(() => ({ opacity: animationProgress.value * 0.7, r: withDelay( index * 100, withSpring(animationProgress.value * bubble.radius) ), })); return ( {showLabels && bubble.label && ( {bubble.label} )} ); })} {/* Axis labels */} {/* X-axis labels */} {[minX, (minX + maxX) / 2, maxX].map((value, index) => ( {Math.round(value)} ))} {/* Y-axis labels */} {[maxY, (minY + maxY) / 2, minY].map((value, index) => ( {Math.round(value)} ))} ); };