UNPKG

react-native-chart-kit

Version:

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

159 lines (158 loc) 6.61 kB
import { useEffect, useRef, useState } from "react"; const defaultSelectionAnimationDuration = 180; const dimmedBarOpacity = 0.54; const defaultGridStrokeOpacity = 0.78; const selectedGridStrokeOpacity = 0; const selectedStrokeOpacity = 0; export const getSettledBarChartSelectionAnimationState = (selectedBarKey) => ({ fromKey: selectedBarKey, toKey: selectedBarKey, progress: 1 }); export const resolveBarChartSelectionAnimationConfig = (animation) => { if (animation === false) { return { enabled: false, duration: 0 }; } const config = typeof animation === "object" ? animation : {}; const duration = typeof config.duration === "number" && Number.isFinite(config.duration) ? Math.max(0, config.duration) : defaultSelectionAnimationDuration; return { enabled: true, duration }; }; const easeOutCubic = (progress) => { const clampedProgress = Math.min(Math.max(progress, 0), 1); return 1 - Math.pow(1 - clampedProgress, 3); }; const interpolate = (from, to, progress) => from + (to - from) * progress; const parseHexColor = (color) => { const normalized = color.trim(); const hex = normalized.startsWith("#") ? normalized.slice(1) : ""; if (!/^[\da-f]+$/i.test(hex)) { return undefined; } if (hex.length === 3 || hex.length === 4) { const [r, g, b] = hex.split(""); if (!r || !g || !b) { return undefined; } return { b: Number.parseInt(`${b}${b}`, 16), g: Number.parseInt(`${g}${g}`, 16), r: Number.parseInt(`${r}${r}`, 16) }; } if (hex.length === 6 || hex.length === 8) { return { b: Number.parseInt(hex.slice(4, 6), 16), g: Number.parseInt(hex.slice(2, 4), 16), r: Number.parseInt(hex.slice(0, 2), 16) }; } return undefined; }; const parseRgbColor = (color) => { const match = color .trim() .match(/^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)/i); if (!match) { return undefined; } return { b: Number(match[3]), g: Number(match[2]), r: Number(match[1]) }; }; const parseColor = (color) => parseHexColor(color) ?? parseRgbColor(color); const clampChannel = (channel) => Math.max(0, Math.min(255, Math.round(channel))); const toHexChannel = (channel) => clampChannel(channel).toString(16).padStart(2, "0"); const blendOpaqueColor = ({ backgroundColor, color, opacity }) => { const foreground = parseColor(color); const background = parseColor(backgroundColor); if (!foreground || !background) { return color; } const clampedOpacity = Math.max(0, Math.min(1, opacity)); return `#${toHexChannel(foreground.r * clampedOpacity + background.r * (1 - clampedOpacity))}${toHexChannel(foreground.g * clampedOpacity + background.g * (1 - clampedOpacity))}${toHexChannel(foreground.b * clampedOpacity + background.b * (1 - clampedOpacity))}`; }; const getOpacityForSelectionKey = ({ barKey, selectedKey }) => selectedKey === undefined || selectedKey === barKey ? 1 : dimmedBarOpacity; const getStrokeOpacityForSelectionKey = ({ barKey, selectedKey }) => (selectedKey === barKey ? selectedStrokeOpacity : 0); export const getAnimatedBarSelectionOpacity = ({ barKey, state }) => interpolate(getOpacityForSelectionKey({ barKey, selectedKey: state.fromKey }), getOpacityForSelectionKey({ barKey, selectedKey: state.toKey }), state.progress); export const getAnimatedBarSelectionFill = ({ backgroundColor, barKey, color, state }) => { const opacity = getAnimatedBarSelectionOpacity({ barKey, state }); return opacity >= 0.999 ? color : blendOpaqueColor({ backgroundColor, color, opacity }); }; export const getAnimatedBarSelectionStrokeOpacity = ({ barKey, state }) => interpolate(getStrokeOpacityForSelectionKey({ barKey, selectedKey: state.fromKey }), getStrokeOpacityForSelectionKey({ barKey, selectedKey: state.toKey }), state.progress); export const getAnimatedBarSelectionGridOpacity = ({ state }) => { if (state.fromKey !== undefined || state.toKey !== undefined) { return selectedGridStrokeOpacity; } return defaultGridStrokeOpacity; }; export const getBarChartSelectionGridOpacity = ({ selectedBarKey, state }) => { if (selectedBarKey !== undefined) { return selectedGridStrokeOpacity; } return getAnimatedBarSelectionGridOpacity({ state }); }; export const shouldRenderBarChartGridLines = ({ selectedBarKey, state }) => selectedBarKey === undefined && state.fromKey === undefined && state.toKey === undefined; export const useBarChartSelectionAnimation = ({ animation, selectedBarKey }) => { const config = resolveBarChartSelectionAnimationConfig(animation); const previousTargetRef = useRef(selectedBarKey); const [state, setState] = useState({ fromKey: selectedBarKey, toKey: selectedBarKey, progress: 1 }); useEffect(() => { const fromKey = previousTargetRef.current; if (fromKey === selectedBarKey) { return undefined; } previousTargetRef.current = selectedBarKey; if (!config.enabled || config.duration <= 0) { const animationFrame = requestAnimationFrame(() => { setState({ fromKey: selectedBarKey, toKey: selectedBarKey, progress: 1 }); }); return () => { cancelAnimationFrame(animationFrame); }; } let animationFrame = 0; let startTime; const tick = (timestamp) => { startTime ?? (startTime = timestamp); const rawProgress = Math.min((timestamp - startTime) / config.duration, 1); const progress = easeOutCubic(rawProgress); setState(rawProgress >= 1 ? getSettledBarChartSelectionAnimationState(selectedBarKey) : { fromKey, toKey: selectedBarKey, progress }); if (rawProgress < 1) { animationFrame = requestAnimationFrame(tick); } }; animationFrame = requestAnimationFrame((timestamp) => { setState({ fromKey, toKey: selectedBarKey, progress: 0 }); tick(timestamp); }); return () => { cancelAnimationFrame(animationFrame); }; }, [config.duration, config.enabled, selectedBarKey]); return state; };