react-native-chart-kit
Version:
Beautiful React Native charts for dashboards, reports, and data-rich mobile apps.
159 lines (158 loc) • 6.61 kB
JavaScript
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;
};