import { Bar, BarChart, CartesianGrid, Cell, LabelList, Tooltip, XAxis, YAxis, } from 'recharts' import { ChartType, FunnelConfig, NewNestedDataPoint, TooltipContentProps, TooltipConfig, XAxisConfig, YAxisConfig, } from './types' import { formatNumber } from './ChartUtils' import { CustomTooltip } from './CustomTooltip' import { DEFAULT_COLORS } from './utils' import { FOUNDATION_THEME } from '../../tokens' const FUNNEL_MARGIN = 10 const FUNNEL_LABEL_OFFSET = 8 const FUNNEL_LABEL_MARGIN = 180 const FUNNEL_COMPACT_LABEL_MARGIN = 136 const FUNNEL_NO_LABEL_MARGIN = 30 const FUNNEL_AXIS_WIDTH = 110 const FUNNEL_COMPACT_AXIS_WIDTH = 90 export type FunnelStage = { name: string value: number color: string dropOffPercentage: number seriesKey: string } export const calculateFunnelDropOff = ( value: number, baseValue: number ): number => { if ( !Number.isFinite(value) || !Number.isFinite(baseValue) || baseValue <= 0 ) { return 0 } return ((baseValue - value) / baseValue) * 100 } export const getFunnelStages = ( data: NewNestedDataPoint[], selectedKeys: string[], colors: { key: string; color: string }[], percentageBase: FunnelConfig['percentageBase'] = 'previous' ): FunnelStage[] => { const seriesKey = selectedKeys.find((key) => data.some((dataPoint) => dataPoint.data[key]) ) || Object.keys(data[0]?.data || {})[0] if (!seriesKey) return [] const values = data.map((dataPoint) => { const value = dataPoint.data[seriesKey]?.primary.val return typeof value === 'number' && Number.isFinite(value) ? value : 0 }) const firstValue = values[0] ?? 0 return data.map((dataPoint, index) => { const value = values[index] const baseValue = percentageBase === 'first' ? firstValue : index === 0 ? value : values[index - 1] const stageColor = colors.find((color) => color.key === dataPoint.name)?.color || colors[index % colors.length]?.color || DEFAULT_COLORS[index % DEFAULT_COLORS.length].color return { name: dataPoint.name, value, color: stageColor, dropOffPercentage: calculateFunnelDropOff(value, baseValue), seriesKey, } }) } const formatFunnelPercentage = (percentage: number): string => { const rounded = Math.round(percentage * 10) / 10 return `${Number.isInteger(rounded) ? rounded : rounded.toFixed(1)}%` } type FunnelViewBox = { x?: number y?: number width?: number height?: number } const FunnelLabel = ({ index, viewBox, stages, isSmallScreen, }: { index?: number viewBox?: FunnelViewBox stages: FunnelStage[] isSmallScreen: boolean }) => { const stage = index === undefined ? undefined : stages[index] if (!stage || !viewBox) return null const x = (viewBox.x || 0) + (viewBox.width || 0) + FUNNEL_LABEL_OFFSET const y = (viewBox.y || 0) + (viewBox.height || 0) / 2 return ( {`${formatNumber(stage.value)} ยท ${formatFunnelPercentage(stage.dropOffPercentage)} drop-off`} ) } export type FunnelChartProps = { data: NewNestedDataPoint[] selectedKeys: string[] colors: { key: string; color: string }[] funnelConfig?: FunnelConfig barsize?: number tooltip?: TooltipConfig xAxis?: XAxisConfig yAxis?: YAxisConfig isSmallScreen?: boolean hoveredKey: string | null setHoveredKey: (key: string | null) => void } export const FunnelChart = ({ data, selectedKeys, colors, funnelConfig, barsize, tooltip, xAxis, yAxis, isSmallScreen = false, hoveredKey, setHoveredKey, }: FunnelChartProps) => { const stages = getFunnelStages( data, selectedKeys, colors, funnelConfig?.percentageBase ) const showLabels = funnelConfig?.showLabels ?? true if (stages.length === 0) return null return ( setHoveredKey(null)} > { const mergedProps: TooltipContentProps = { ...props, originalData: data, chartType: ChartType.FUNNEL, selectedKeys, xAxis, yAxis, } return tooltip?.content ? ( tooltip.content(mergedProps) ) : ( ) }} /> {stages.map((stage) => ( ))} {showLabels && ( { const viewBox = props.viewBox && 'x' in props.viewBox ? props.viewBox : undefined return ( ) }} /> )} ) } export default FunnelChart