// ColumnChart/index.tsx // This is the main entry point for the ColumnChart component in the RN chart library. // It composes the full column chart, handling layout, axis configuration, empty state, and rendering grouped/stacked columns. // The component is highly configurable and uses subcomponents for axes, content, and frame. import React, { useMemo, useState, useCallback, useEffect } from 'react'; import type { ViewStyle, LayoutChangeEvent } from 'react-native'; import ChartFrame from '../shared/ChartFrame'; import type { DataValue, HeaderConfig, Series, StyleConfig, XAxisConfig, YAxisConfig, } from '../types'; import ColumnChartContent from './ColumnChartContent'; import { createNiceScale } from '../shared/niceNumbers'; import { StyledColumnChartWrapper } from './StyledColumnChart'; import useCustomColor from '../shared/hooks/useCustomColor'; /** * Props for the ColumnChart component. * * @property data - The data to be displayed in the chart. Each item represents a series. * @property yAxisConfig - Optional configuration for the Y axis. * @property xAxisConfig - Optional configuration for the X axis. * @property style - Additional style for the root View. Follows the same convention as other components (e.g., Button). * @property testID - Testing id of the component. Passed to the root View and used as a suffix for inner elements. * @property headerConfig - Optional header configuration (title, actions). * @property emptyText - Optional text to display when data is empty. * @property onBarPress - Called when a bar (column segment) is pressed. Receives info about the bar. */ export interface ColumnChartProps { /** * The data to be displayed in the chart. Each item represents a series. */ data: Array>>; /** * Optional configuration for the Y axis. * Note: minValue is omitted and always set to zero internally by the component. */ yAxisConfig?: Omit; /** * Optional configuration for the X axis. */ xAxisConfig?: XAxisConfig; /** * Additional style for the root View. */ style?: ViewStyle; /** * Testing id of the component. */ testID?: string; /** * Header configuration for the chart. */ headerConfig?: HeaderConfig; /** * Text to display when the chart has no data (empty state). */ emptyText?: string; /** * Called when a bar (column segment) is pressed. Receives info about the bar. */ onBarPress?: (info: { value: number | undefined; xLabel: string; seriesLabel: string; seriesIndex: number; xIndex: number; }) => void; /** * * styleConfig use to custom the style of the chart. * * styleConfig must be an object: * * color?: use to custom the legend colors. */ styleConfig?: StyleConfig; } /** * ColumnChart component for rendering grouped/stacked column charts. * Handles layout, axis configuration, empty state, and data validation. * Uses ChartFrame for layout and axes, and ColumnChartContent for rendering columns. * * @param data - Array of series, each with a label and array of values. * @param yAxisConfig - Optional Y axis configuration. * @param xAxisConfig - Optional X axis configuration. * @param style - Optional style for the chart container. * @param testID - Optional test ID for testing. * @param headerConfig - Optional header configuration (title, actions). * @param emptyText - Optional text to display when data is empty. * @param onBarPress - Called when a bar (column segment) is pressed. Receives info about the bar. * * Example usage: * */ const ColumnChart = ({ data, yAxisConfig = {}, xAxisConfig = {}, style, testID, headerConfig, emptyText, onBarPress, styleConfig, }: ColumnChartProps) => { const colorScale = useCustomColor({ data, seriesConfig: styleConfig?.series, }); const xLabels = useMemo(() => { return xAxisConfig.labels && xAxisConfig.labels.length > 0 ? xAxisConfig.labels : data[0]?.data.map((_, index) => index.toString()) || []; }, [data[0]?.data.length, xAxisConfig.labels?.length]); useEffect(() => { // Validation: xLabels length must match each series' data length, only if labels are explicitly provided if (xAxisConfig.labels && xAxisConfig.labels.length > 0) { data.forEach((series) => { if (series.data.length !== xLabels.length) { throw new Error( `xAxisConfig.labels length (${xLabels.length}) does not match data length (${series.data.length}) in series label: ${series.label}` ); } }); } }, [data, xAxisConfig]); useEffect(() => { // Assert that the chart does not support negative values data.forEach((series) => { series.data.forEach((value, idx) => { if (typeof value === 'number' && value < 0) { throw new Error( `Negative values are not supported in ColumnChart. Found value ${value} in series "${series.label}" at index ${idx}.` ); } }); }); }, [data]); const [dimensions, setDimensions] = useState<{ width: number; height: number; } | null>(null); const onLayout = useCallback((event: LayoutChangeEvent) => { const { width, height } = event.nativeEvent.layout; if (width > 0 && height > 0) { setDimensions({ width, height }); } }, []); // Calculate stacked maxY const yMax = useMemo( () => Math.max( ...xLabels.map((_, xIdx) => data.reduce((sum, series) => sum + (series.data[xIdx] ?? 0), 0) ) ), [data, xLabels] ); const niceValues = useMemo(() => { const maxDataValue = yMax; const minDataValue = 0; return createNiceScale(minDataValue, maxDataValue); }, [data]); const yAxisStep = yAxisConfig?.step ?? niceValues.tickSpacing; const yAxisInterval = yAxisConfig?.tick?.interval ?? yAxisStep; const calculatedYAxisConfig = useMemo(() => { return { ...yAxisConfig, maxValue: yAxisConfig?.maxValue ?? niceValues.niceMax, minValue: 0, step: yAxisConfig?.step ?? niceValues.tickSpacing, tick: { interval: yAxisInterval, }, }; }, [yAxisConfig, niceValues, yAxisInterval]); const calculatedXAxisConfig = useMemo(() => { return { ...xAxisConfig, labels: xLabels, }; }, [xAxisConfig, xLabels]); return ( {dimensions && ( ( )} /> )} ); }; export default ColumnChart;