'use client' import * as React from 'react' import { Tooltip, Legend, ResponsiveContainer } from 'recharts' import { cn } from '@ciphera-net/ui' // ─── ChartConfig ──────────────────────────────────────────────────── export type ChartConfig = Record< string, { label?: React.ReactNode icon?: React.ComponentType color?: string theme?: { light: string; dark: string } } > // ─── ChartContext ─────────────────────────────────────────────────── type ChartContextProps = { config: ChartConfig } const ChartContext = React.createContext(null) function useChart() { const context = React.useContext(ChartContext) if (!context) { throw new Error('useChart must be used within a ') } return context } // ─── ChartContainer ──────────────────────────────────────────────── const ChartContainer = React.forwardRef< HTMLDivElement, React.ComponentProps<'div'> & { config: ChartConfig children: React.ComponentProps['children'] } >(({ id, className, children, config, ...props }, ref) => { const uniqueId = React.useId() const chartId = `chart-${id || uniqueId.replace(/:/g, '')}` // Build CSS variables from config const colorVars = React.useMemo(() => { const vars: Record = {} for (const [key, value] of Object.entries(config)) { if (value.color) { vars[`--color-${key}`] = value.color } } return vars }, [config]) return (
{children}
) }) ChartContainer.displayName = 'ChartContainer' // ─── ChartTooltip ────────────────────────────────────────────────── const ChartTooltip = Tooltip // ─── ChartTooltipContent ─────────────────────────────────────────── const ChartTooltipContent = React.forwardRef< HTMLDivElement, React.ComponentProps & React.ComponentProps<'div'> & { hideLabel?: boolean hideIndicator?: boolean indicator?: 'line' | 'dot' | 'dashed' nameKey?: string labelKey?: string labelFormatter?: (value: string, payload: Record[]) => React.ReactNode } >( ( { active, payload, className, indicator = 'dot', hideLabel = false, hideIndicator = false, label, labelFormatter, labelKey, nameKey, }, ref, ) => { const { config } = useChart() const tooltipLabel = React.useMemo(() => { if (hideLabel || !payload?.length) return null const item = payload[0] const key = `${labelKey || item?.dataKey || item?.name || 'value'}` const itemConfig = getPayloadConfigFromPayload(config, item, key) const value = !labelKey && typeof label === 'string' ? config[label as keyof typeof config]?.label || label : itemConfig?.label if (labelFormatter) { return labelFormatter( value as string, payload as Record[], ) } return value }, [label, labelFormatter, payload, hideLabel, config, labelKey]) if (!active || !payload?.length) return null const nestLabel = payload.length === 1 && indicator !== 'dot' return (
{!nestLabel ? tooltipLabel ? (
{tooltipLabel}
) : null : null}
{payload.map((item, index) => { const key = `${nameKey || item.name || item.dataKey || 'value'}` const itemConfig = getPayloadConfigFromPayload(config, item, key) const indicatorColor = item.fill || item.color return (
svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground', indicator === 'dot' && 'items-center', )} > {itemConfig?.icon ? ( ) : ( !hideIndicator && (
) )}
{nestLabel ? tooltipLabel : null} {itemConfig?.label || item.name}
{item.value != null && ( {typeof item.value === 'number' ? item.value.toLocaleString() : item.value} )}
) })}
) }, ) ChartTooltipContent.displayName = 'ChartTooltipContent' // ─── ChartLegend ─────────────────────────────────────────────────── const ChartLegend = Legend const ChartLegendContent = React.forwardRef< HTMLDivElement, React.ComponentProps<'div'> & Pick, 'payload' | 'verticalAlign'> & { hideIcon?: boolean nameKey?: string } >( ( { className, hideIcon = false, payload, verticalAlign = 'bottom', nameKey }, ref, ) => { const { config } = useChart() if (!payload?.length) return null return (
{payload.map((item) => { const key = `${nameKey || item.dataKey || 'value'}` const itemConfig = getPayloadConfigFromPayload(config, item, key) return (
{itemConfig?.icon && !hideIcon ? ( ) : (
)} {itemConfig?.label}
) })}
) }, ) ChartLegendContent.displayName = 'ChartLegendContent' // ─── Helpers ─────────────────────────────────────────────────────── function getPayloadConfigFromPayload( config: ChartConfig, payload: unknown, key: string, ) { if (typeof payload !== 'object' || payload === null) return undefined const payloadPayload = 'payload' in payload && typeof (payload as Record).payload === 'object' && (payload as Record).payload !== null ? ((payload as Record).payload as Record) : undefined let configLabelKey = key if ( key in config ) { configLabelKey = key } else if (payloadPayload) { const payloadKey = Object.keys(payloadPayload).find( (k) => payloadPayload[k] === key && k in config, ) if (payloadKey) configLabelKey = payloadKey } return configLabelKey in config ? config[configLabelKey] : config[key] } export { ChartContainer, ChartTooltip, ChartTooltipContent, ChartLegend, ChartLegendContent, ChartContext, useChart, }