UNPKG

react-native-chart-kit

Version:

Beautiful React Native charts for dashboards, reports, and data-rich mobile apps.

142 lines (141 loc) 5.52 kB
import { useEffect, useMemo, useRef, useState } from "react"; import { buildLineChartYAxisLabels, resolveLineChartAxisLabelAnimationConfig } from "./axisLabels"; const axisLabelPositionThreshold = 0.5; const axisLabelOpacityThreshold = 0.01; const getAxisLabelSignature = (labels) => labels .map((label) => `${label.key}:${label.text}:${label.y.toFixed(2)}`) .join("|"); const interpolateValue = (from, to, progress) => from + (to - from) * progress; const easeAxisLabelTransition = (progress) => { const clampedProgress = Math.min(Math.max(progress, 0), 1); return 1 - Math.pow(1 - clampedProgress, 3); }; const getInterpolatedLabels = ({ from, progress, to }) => { const fromByKey = new Map(from.map((label) => [label.key, label])); const toByKey = new Map(to.map((label) => [label.key, label])); const orderedKeys = [ ...to.map((label) => label.key), ...from.map((label) => label.key).filter((key) => !toByKey.has(key)) ]; return orderedKeys .map((key) => { const fromLabel = fromByKey.get(key); const toLabel = toByKey.get(key); if (fromLabel && toLabel) { return { ...toLabel, y: interpolateValue(fromLabel.y, toLabel.y, progress), opacity: interpolateValue(fromLabel.opacity, toLabel.opacity, progress) }; } if (toLabel) { return { ...toLabel, opacity: progress }; } if (fromLabel) { return { ...fromLabel, opacity: interpolateValue(fromLabel.opacity, 0, progress) }; } return undefined; }) .filter((label) => label !== undefined && label.opacity > axisLabelOpacityThreshold); }; const hasMeaningfulAxisLabelDelta = ({ from, to }) => { if (from.length !== to.length) { return true; } return to.some((label, index) => { const previousLabel = from[index]; return (!previousLabel || previousLabel.key !== label.key || previousLabel.text !== label.text || Math.abs(previousLabel.y - label.y) > axisLabelPositionThreshold || Math.abs(previousLabel.opacity - label.opacity) > axisLabelOpacityThreshold); }); }; export const useAnimatedYAxisLabels = (labels, animation) => { const resolvedAnimation = useMemo(() => resolveLineChartAxisLabelAnimationConfig(animation), [animation]); const latestLabelsRef = useRef(labels); const hasInitialLabelsRef = useRef(false); const [animatedLabels, setAnimatedLabels] = useState(labels); useEffect(() => { let animationFrame = 0; const targetLabels = labels; const currentLabels = latestLabelsRef.current; const targetSignature = getAxisLabelSignature(targetLabels); const currentSignature = getAxisLabelSignature(currentLabels); if (!hasInitialLabelsRef.current) { hasInitialLabelsRef.current = true; latestLabelsRef.current = targetLabels; setAnimatedLabels(targetLabels); return () => { cancelAnimationFrame(animationFrame); }; } if (currentSignature === targetSignature || !hasMeaningfulAxisLabelDelta({ from: currentLabels, to: targetLabels })) { latestLabelsRef.current = targetLabels; return () => { cancelAnimationFrame(animationFrame); }; } if (!resolvedAnimation.enabled || resolvedAnimation.duration <= 0) { latestLabelsRef.current = targetLabels; animationFrame = requestAnimationFrame(() => { setAnimatedLabels(targetLabels); }); return () => { cancelAnimationFrame(animationFrame); }; } let startTime; const tick = (timestamp) => { startTime ?? (startTime = timestamp); const progress = Math.min((timestamp - startTime) / resolvedAnimation.duration, 1); const easedProgress = easeAxisLabelTransition(progress); const nextLabels = getInterpolatedLabels({ from: currentLabels, progress: easedProgress, to: targetLabels }); latestLabelsRef.current = nextLabels; setAnimatedLabels(nextLabels); if (progress < 1) { animationFrame = requestAnimationFrame(tick); } else { latestLabelsRef.current = targetLabels; setAnimatedLabels(targetLabels); } }; animationFrame = requestAnimationFrame(tick); return () => { cancelAnimationFrame(animationFrame); }; }, [ labels, resolvedAnimation.duration, resolvedAnimation.enabled, resolvedAnimation.strategy ]); return animatedLabels; }; export const useLineChartYAxisLabels = (model, animation) => { const labels = useMemo(() => buildLineChartYAxisLabels({ formatYLabel: model.formatYLabel, labelOffset: model.resolvedTheme.typography.axisLabelSize * 0.36, ticks: model.yTicks, yScale: model.yScale }), [ model.formatYLabel, model.resolvedTheme.typography.axisLabelSize, model.yScale, model.yTicks ]); return useAnimatedYAxisLabels(labels, animation); };