import React, { createContext, useContext, useEffect, useImperativeHandle, useMemo, } from 'react'; import type { BarChartItemDataInterface, BarChartRef, BarChartRefSelectedIndexPropsType, BarChartRootPropsInterface, } from './interfaces'; import { Easing, useAnimatedRef, useScrollViewOffset, useSharedValue, withTiming, type SharedValue, } from 'react-native-reanimated'; import { matchFont, type SkFont } from '@shopify/react-native-skia'; import { BAR_CHART_PADDING_LEFT, BAR_CHART_PADDING_RIGHT } from './constants'; import { calculateNiceScale } from './utils'; interface BarChartContextInterface { // Data data: BarChartItemDataInterface[]; maxValue: number; yAxisTicks: number[]; // Loading State isLoading?: boolean; skeletonData: BarChartItemDataInterface[]; // Dimensions height: number; width: number; // Container width /** Visible width */ canvasWidth: number; /** Total width of the scrollable content */ contentWidth: number; // Layout Metrics /** The Y coordinate where the bars start (the visual floor) */ /** The Y coordinate where the bars start (the visual floor) */ graphBottom: number; /** The maximum pixel height a bar can reach */ maxBarHeight: number; /** The height reserved for the X-Axis labels */ xAxisHeight: number; // Layout Props barWidth: number; spacing: number; verticalScaleFactor: number; isScrollable: boolean; // Styles & Skia barColor: string; activeBorderColor: string; activeBorderWidth: number; font: SkFont | null; showXAxis: boolean; // Interaction selectable: boolean; selectedIndex: SharedValue; // Animation (Master controller) entryProgress: SharedValue; // Ref for ScrollView (Native) scrollViewRef: React.RefObject; scrollX: SharedValue; // YAxis logic hasYAxis: boolean; yAxisWidth: number; onBarPress?: (item: BarChartItemDataInterface | null, index: number) => void; } const LABEL_PADDING_TOP = 4; const APPROX_FONT_HEIGHT = 18; export const BarChartContext = createContext< BarChartContextInterface | undefined >(undefined); const BarChartProvider: React.FC< BarChartRootPropsInterface & { ref?: React.Ref; } > = ({ data, isLoading = false, width = 300, height = 300, barWidth = 32, spacing = 12, barColor = '#E0E0E0', activeBorderColor = '#333333', activeBorderWidth = 2, scrollToTheEnd = false, isScrollable = false, selectable = false, font: propFont, yAxisTicksCount = 5, showXAxis = false, verticalScaleFactor = 0.8, ref, onBarPress, children, }) => { // 1. Font Loading (Fallback to System Font if needed) const systemFont = matchFont({ fontFamily: 'sans-serif', fontSize: 10 }); const font = propFont ?? systemFont; // 2. Shared Values & Refs const selectedIndex = useSharedValue(-1); const entryProgress = useSharedValue(0); const scrollViewRef = useAnimatedRef(); const scrollX = useScrollViewOffset(scrollViewRef); useEffect(() => { if (!isLoading) { entryProgress.value = 0; entryProgress.value = withTiming(1, { duration: 1200, easing: Easing.out(Easing.exp), }); if (scrollToTheEnd && isScrollable) { // Delay for ScrollView calculation setTimeout(() => { scrollViewRef.current?.scrollToEnd({ animated: true }); }, 100); } else { setTimeout(() => { scrollViewRef.current?.scrollTo({ x: 0, animated: true }); }, 100); } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [data, isLoading, scrollToTheEnd, isScrollable]); // 3. Layout Calculations const xAxisHeight = showXAxis ? LABEL_PADDING_TOP + APPROX_FONT_HEIGHT : 0; const graphBottom = height - xAxisHeight; const maxBarHeight = graphBottom * verticalScaleFactor; const skeletonData = useMemo(() => { return Array.from({ length: 6 }).map((_, index) => ({ label: '---', value: 100, id: `skeleton-${index}`, })); }, []); const layoutData = isLoading ? skeletonData : data; // Detect YAxis in children const { hasYAxis, yAxisWidth } = useMemo(() => { let found = false; let widthFound = 50; // Default width; const childrenArray = React.Children.toArray(children); for (const child of childrenArray) { if (React.isValidElement(child)) { // Check the type.displayName to find Y-Axis component // @ts-ignore if (child.type?.displayName === 'BarChartYAxis') { found = true; widthFound = (child.props as any).width || 50; } } } return { hasYAxis: found, yAxisWidth: widthFound, }; }, [children]); const { maxValue, yAxisTicks } = useMemo(() => { let rawMax = 0; if (isLoading) { rawMax = 100; } else { rawMax = Math.max(...data.map((item) => item.value), 0); } const scale = calculateNiceScale(0, rawMax, yAxisTicksCount); return { maxValue: scale.max, yAxisTicks: scale.ticks, }; }, [data, isLoading, yAxisTicksCount]); const { canvasWidth, contentWidth, effectiveBarWidth } = useMemo(() => { const SIDE_PADDING = BAR_CHART_PADDING_LEFT + BAR_CHART_PADDING_RIGHT; const availableCanvasWidth = hasYAxis ? width - yAxisWidth : width; const numberOfItems = Math.max(layoutData.length, 1); const numberOfSpaces = Math.max(numberOfItems - 1, 0); const totalSpacing = spacing * numberOfSpaces; if (isScrollable && !isLoading) { // SCROLL MODE: Use fixed barWidth from props const totalBarsWidth = numberOfItems * barWidth; const totalContentWidth = totalBarsWidth + totalSpacing + SIDE_PADDING; return { contentWidth: totalContentWidth, canvasWidth: availableCanvasWidth, effectiveBarWidth: barWidth, }; } else { // FIT MODE: Calculate dynamic barWidth to fit container const availableSpaceForBars = availableCanvasWidth - totalSpacing - SIDE_PADDING; // Prevent division by zero and ensure min width of 1px; const dynamicWidth = Math.max( availableSpaceForBars / Math.max(numberOfItems, 1), 1 ); return { contentWidth: availableCanvasWidth, canvasWidth: availableCanvasWidth, effectiveBarWidth: dynamicWidth, // Override the prop }; } }, [ layoutData, isScrollable, barWidth, spacing, width, hasYAxis, yAxisWidth, isLoading, ]); // 4. Expose Ref Mathods useImperativeHandle(ref, () => ({ scrollToStart: (animated = true) => { scrollViewRef.current?.scrollTo({ x: 0, animated }); }, scrollToEnd: (animated = true) => { scrollViewRef.current?.scrollToEnd({ animated }); }, scrollToIndex: (index: number, animated = true) => { const safeIndex = Math.max(0, Math.min(index, data.length - 1)); const positionX = safeIndex * (barWidth + spacing); scrollViewRef.current?.scrollTo({ x: positionX, animated }); }, selectedIndex: ( index: number, options?: BarChartRefSelectedIndexPropsType ) => { // Update the SharedValue (triggers UI animations instantly) if (index >= -1 && index < data.length) { selectedIndex.value = index; // Scroll to the bar if (options?.scrollToBar && index !== -1 && isScrollable) { const itemFullWidth = barWidth + spacing; // Center logic: Item Pos - Half Screen + Half Item const itemX = BAR_CHART_PADDING_LEFT + index * itemFullWidth; const centerOffsetX = itemX - canvasWidth / 2 + barWidth / 2; // Ensure scroll don't past bounds const maxOffset = contentWidth - canvasWidth; const targetX = Math.max(0, Math.min(centerOffsetX, maxOffset)); scrollViewRef.current?.scrollTo({ x: targetX, animated: options.animatedScroll, }); } } }, })); return ( {children} ); }; export const useBarChart = () => { const context = useContext(BarChartContext); if (!context) { throw new Error('useBarChart must be used inside a '); } return context; }; export default BarChartProvider;