import { AxisType, ChartType, CustomTooltipProps, NewNestedDataPoint, TooltipFormatter, XAxisConfig, YAxisConfig, } from './types' import { formatNumber, getAxisFormatter, createDateTimeFormatter, } from './ChartUtils' import { parseTimestamp } from './DateTimeFormatter' import Block from '../../components/Primitives/Block/Block' import Text from '../../components/Text/Text' import { ReactNode, useEffect, useRef } from 'react' import { Payload, ValueType, NameType, } from 'recharts/types/component/DefaultTooltipContent' import { FOUNDATION_THEME } from '../../tokens' interface AuxItem { label: string val: string | number type?: AxisType dateOnly?: boolean smart?: boolean timeZone?: string hour12?: boolean key?: string } const formatTooltipLabel = ( label: string | number, xAxis?: XAxisConfig ): string => { if (!xAxis) return String(label) if (xAxis.tickFormatter) { return xAxis.tickFormatter(label) } if (xAxis.type === AxisType.DATE_TIME) { const tooltipFormatter = createDateTimeFormatter({ useUTC: xAxis.useUTC ?? true, smartDateTimeFormat: false, showYear: true, dateOnly: false, timeOnly: false, formatString: xAxis.formatString, }) return tooltipFormatter(label) } if (xAxis.type) { return getAxisFormatter(xAxis)(label) } return String(label) } const formatTooltipValue = ( value: string | number, yAxis?: YAxisConfig ): string => { if (!yAxis) { return typeof value === 'number' ? formatNumber(value) : String(value) } if (yAxis.tickFormatter) { return yAxis.tickFormatter(value) } if (yAxis.type) { return getAxisFormatter(yAxis)(value) } return typeof value === 'number' ? formatNumber(value) : String(value) } const formatAuxTooltipValue = ( value: string | number, auxItem: AuxItem ): string => { if (auxItem.type) { return getAxisFormatter(auxItem)(value) } return typeof value === 'number' ? formatNumber(value) : String(value) } export const findDataPointByLabel = ( originalData: NewNestedDataPoint[], label: string | number ) => { const labelAsString = String(label) const directMatch = originalData.find((item) => item.name === labelAsString) if (directMatch) { return directMatch } const labelTimestamp = typeof label === 'number' ? label : parseTimestamp(label) if (labelTimestamp == null) { return undefined } return originalData.find((item) => { const itemTimestamp = parseTimestamp(item.name) return itemTimestamp !== null && itemTimestamp === labelTimestamp }) } const getDataIndex = ( originalData: NewNestedDataPoint[], label: string | number ): number => { const dataPoint = findDataPointByLabel(originalData, label) return dataPoint ? originalData.indexOf(dataPoint) : -1 } const formatTooltipHeader = ( label: string | number, xAxis: XAxisConfig | undefined, labelFormatter: CustomTooltipProps['labelFormatter'] ): ReactNode => { return labelFormatter ? labelFormatter(label) : formatTooltipLabel(label, xAxis) } const formatPrimaryTooltipValue = ({ value, seriesName, dataIndex, color, payload, yAxis, formatter, }: { value: string | number seriesName: string | number dataIndex: number color: string | undefined payload: unknown yAxis?: YAxisConfig formatter?: TooltipFormatter }): ReactNode => { if (!formatter) return formatTooltipValue(value, yAxis) return formatter({ seriesName, value, dataIndex, color: color || '#AD46FF', payload, }) } export const getRelevantData = ( originalData: NewNestedDataPoint[], label: string | number, hoveredKey: string ) => { const currentDataPoint = findDataPointByLabel(originalData, label) if ( !currentDataPoint || !currentDataPoint.data || !currentDataPoint.data[hoveredKey] ) { return null } return currentDataPoint.data[hoveredKey] } export const CustomTooltip = ({ active, payload, label, hoveredKey, originalData, setHoveredKey, chartType, selectedKeys, xAxis, yAxis, formatter, labelFormatter, }: CustomTooltipProps) => { if (!active || !payload || !payload.length) { return null } const getColor = (key: string) => { const payloadItem = payload.find( (item) => String(item.dataKey) === key || item.payload?.seriesKey === key ) return payloadItem?.color || payloadItem?.payload?.color || '#AD46FF' } return ( {chartType == ChartType.LINE && ( )} {(chartType === ChartType.BAR || chartType === ChartType.AREA || chartType === ChartType.LINE_BAR || chartType === ChartType.FUNNEL) && ( )} {chartType === ChartType.PIE && ( )} {chartType === ChartType.SCATTER && ( )} ) } const BarChartTooltip = ({ originalData, label, payload, getColor, xAxis, yAxis, formatter, labelFormatter, }: { originalData: NewNestedDataPoint[] label: string | number payload: Payload[] getColor: (key: string) => string | undefined xAxis?: XAxisConfig yAxis?: YAxisConfig formatter?: TooltipFormatter labelFormatter?: CustomTooltipProps['labelFormatter'] }) => { const relevantData = findDataPointByLabel(originalData, label)?.data const dataIndex = getDataIndex(originalData, label) // Collect all aux data from all DataPoints in relevantData, attaching the key to each aux item const auxData = relevantData ? Object.entries(relevantData).flatMap(([key, dataPoint]) => Array.isArray(dataPoint?.aux) && dataPoint.aux.length > 0 ? dataPoint.aux.map((auxItem: AuxItem) => ({ ...auxItem, key, })) : [] ) : undefined return ( <> {formatTooltipHeader(label, xAxis, labelFormatter)} {relevantData && Object.keys(relevantData) .filter((key) => key !== 'name') .map((key, index) => { const payloadItem = payload.find( (item) => String(item.dataKey) === key || item.payload?.seriesKey === key ) return ( {key} {formatPrimaryTooltipValue({ value: relevantData[key].primary .val, seriesName: payloadItem?.payload ?.seriesKey || payloadItem?.name || key, dataIndex, color: payloadItem?.color || getColor(key), payload: payloadItem?.payload || relevantData[key], yAxis, formatter, })} ) })} {auxData && auxData.length > 0 && ( {auxData.map((auxItem: AuxItem, index: number) => ( {auxData.length > 1 ? `${auxItem.label} (${auxItem.key})` : auxItem.label} {formatAuxTooltipValue(auxItem.val, auxItem)} ))} )} ) } const LineChartTooltip = ({ originalData, hoveredKey, label, getColor, active, payload, selectedKeys, setHoveredKey, xAxis, yAxis, formatter, labelFormatter, }: { originalData: NewNestedDataPoint[] hoveredKey: string | null label: string getColor: (key: string) => string | undefined active: boolean payload: Payload[] selectedKeys: string[] setHoveredKey: (key: string) => void xAxis?: XAxisConfig yAxis?: YAxisConfig formatter?: TooltipFormatter labelFormatter?: CustomTooltipProps['labelFormatter'] }) => { const updateScheduledRef = useRef(false) useEffect(() => { if (active && hoveredKey == null && !updateScheduledRef.current) { updateScheduledRef.current = true queueMicrotask(() => { updateScheduledRef.current = false if (selectedKeys.length > 0) { setHoveredKey(selectedKeys[0]) } else if (originalData.length > 0 && originalData[0].data) { setHoveredKey(Object.keys(originalData[0].data)[0]) } }) } else if (!active) { updateScheduledRef.current = false } }, [active, hoveredKey, selectedKeys, setHoveredKey]) if (!active || !payload || !payload.length || !hoveredKey || !label) { return null } const relevantData = getRelevantData(originalData, label, hoveredKey) if (!relevantData) { return null } return ( <> {hoveredKey} {formatTooltipHeader(label, xAxis, labelFormatter)} {relevantData.primary.label} {formatPrimaryTooltipValue({ value: relevantData.primary.val, seriesName: hoveredKey, dataIndex: getDataIndex(originalData, label), color: getColor(hoveredKey), payload: payload.find( (item) => String(item.dataKey) === hoveredKey )?.payload || relevantData, yAxis, formatter, })} {relevantData.aux && relevantData.aux.length > 0 && ( {relevantData.aux.map((auxItem: AuxItem, index: number) => ( {auxItem.label} {formatAuxTooltipValue(auxItem.val, auxItem)} ))} )} {relevantData.error && ( {relevantData.error.title} {relevantData.error.errorData?.map( ( errorItem: { label: string; value: string }, index: number ) => ( {errorItem.label} {errorItem.value} ) )} )} ) } const PieChartTooltip = ({ originalData, hoveredKey, active, payload, selectedKeys, setHoveredKey, xAxis, yAxis, label, formatter, labelFormatter, }: { originalData: NewNestedDataPoint[] hoveredKey: string | null active: boolean payload: Payload[] selectedKeys: string[] setHoveredKey: (key: string) => void label: string | number xAxis?: XAxisConfig yAxis?: YAxisConfig formatter?: TooltipFormatter labelFormatter?: CustomTooltipProps['labelFormatter'] }) => { const updateScheduledRef = useRef(false) useEffect(() => { if (active && hoveredKey == null && !updateScheduledRef.current) { updateScheduledRef.current = true queueMicrotask(() => { updateScheduledRef.current = false if (selectedKeys.length > 0) { setHoveredKey(selectedKeys[0]) } else if (originalData.length > 0 && originalData[0].data) { setHoveredKey(Object.keys(originalData[0].data)[0]) } }) } else if (!active) { updateScheduledRef.current = false } }, [active, hoveredKey, selectedKeys, setHoveredKey]) if (!active || !payload || !payload.length || !hoveredKey) { return null } let name = payload[0].name as string if (!name) name = Object.keys(originalData[0].data)[0] const data = originalData[0].data[name] return ( <> {name} {formatTooltipHeader( label || originalData[0].name, xAxis, labelFormatter )} {data.primary.label} {formatPrimaryTooltipValue({ value: data.primary.val, seriesName: name, dataIndex: getDataIndex( originalData, originalData[0].name ), color: payload[0].color || payload[0].payload.fill, payload: payload[0].payload, yAxis, formatter, })} {data.aux && data.aux.length > 0 && ( {data.aux.map((auxItem: AuxItem, index: number) => ( {auxItem.label} {formatAuxTooltipValue(auxItem.val, auxItem)} ))} )} ) } const ScatterChartTooltip = ({ originalData, hoveredKey, active, payload, selectedKeys, xAxis, yAxis, formatter, labelFormatter, }: { originalData: NewNestedDataPoint[] hoveredKey: string | null active: boolean payload: Payload[] selectedKeys: string[] xAxis?: XAxisConfig yAxis?: YAxisConfig formatter?: TooltipFormatter labelFormatter?: CustomTooltipProps['labelFormatter'] }) => { if (!active || !payload || !payload.length) { return null } // Get the scatter point data from payload const point = payload[0]?.payload if ( !point || typeof point.x === 'undefined' || typeof point.y === 'undefined' ) { return null } // Find the series key from the payload or use first available key const seriesKey = hoveredKey || (selectedKeys.length > 0 ? selectedKeys[0] : Object.keys(originalData[0]?.data || {})[0]) if (!seriesKey) { return null } return ( <> {seriesKey} {formatTooltipHeader( point.name || 'Data Point', xAxis, labelFormatter )} X: {formatTooltipValue(point.x, xAxis)} Y: {formatPrimaryTooltipValue({ value: point.y, seriesName: seriesKey, dataIndex: getDataIndex( originalData, point.name || '' ), color: point.fill, payload: point, yAxis, formatter, })} ) }