import { useEffect, useRef, useState } from 'react'; import { View, Text, Animated, Easing } from 'react-native'; import Svg, { Path, Circle } from 'react-native-svg'; import { useColors } from '../../lib/theme'; export type HalfDonutSegment = { value: number; color: string }; type Props = { segments: HalfDonutSegment[]; centerValue: number | string; centerLabel: string; width?: number; }; const W_DEFAULT = 220; const H_DEFAULT = 130; const R = 90; const STROKE = 18; function polar(cx: number, cy: number, r: number, angleDeg: number) { const rad = (angleDeg * Math.PI) / 180; return { x: cx + r * Math.cos(rad), y: cy + r * Math.sin(rad) }; } function arcPath(cx: number, cy: number, r: number, startDeg: number, endDeg: number) { const start = polar(cx, cy, r, startDeg); const end = polar(cx, cy, r, endDeg); const largeArc = endDeg - startDeg > 180 ? 1 : 0; return `M ${start.x} ${start.y} A ${r} ${r} 0 ${largeArc} 1 ${end.x} ${end.y}`; } export function HalfDonut({ segments, centerValue, centerLabel, width = W_DEFAULT }: Props) { const colors = useColors(); const scale = width / W_DEFAULT; const W = width; const H = Math.round(H_DEFAULT * scale); const cx = W / 2; const cy = H - Math.round(8 * scale); const r = Math.round(R * scale); const stroke = Math.round(STROKE * scale); const total = Math.max(1, segments.reduce((s, x) => s + x.value, 0)); let cumAngle = 180; const arcs = segments.map((seg) => { const startAngle = cumAngle; const endAngle = cumAngle + 180 * (seg.value / total); cumAngle = endAngle; return { ...seg, startAngle, endAngle }; }); const animProgress = useRef(new Animated.Value(0)).current; const [progress, setProgress] = useState(0); const animKey = typeof centerValue === 'number' ? centerValue : centerValue; useEffect(() => { animProgress.setValue(0); const l = animProgress.addListener(({ value }) => setProgress(value)); Animated.timing(animProgress, { toValue: 1, duration: 1100, easing: Easing.out(Easing.cubic), useNativeDriver: false, }).start(); return () => animProgress.removeListener(l); }, [animKey, animProgress]); const isEmpty = typeof centerValue === 'number' ? centerValue === 0 : centerValue === '0'; return ( {arcs.map((a, i) => { const animatedEnd = a.startAngle + (a.endAngle - a.startAngle) * progress; if (animatedEnd <= a.startAngle + 0.5) return null; return ( ); })} {isEmpty && ( )} {centerValue} {centerLabel} ); }