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