1 import * as React from "react"
2 import * as RechartsPrimitive from "recharts"
3
4 import { cn } from "@/lib/utils"
5
6 // Format: { THEME_NAME: CSS_SELECTOR }
7 const THEMES = { light: "", dark: ".dark" } as const
8
9 export type ChartConfig = {
10 [k in string]: {
11 label?: React.ReactNode
12 icon?: React.ComponentType
13 } & (
14 | { color?: string; theme?: never }
15 | { color?: never; theme: Record<keyof typeof THEMES, string> }
16 )
17 }
18
19 type ChartContextProps = {
20 config: ChartConfig
21 }
22
23 const ChartContext = React.createContext<ChartContextProps | null>(null)
24
25 function useChart() {
26 const context = React.useContext(ChartContext)
27
28 if (!context) {
29 throw new Error("useChart must be used within a <ChartContainer />")
30 }
31
32 return context
33 }
34
35 const ChartContainer = React.forwardRef<
36 HTMLDivElement,
37 React.ComponentProps<"div"> & {
38 config: ChartConfig
39 children: React.ComponentProps<
40 typeof RechartsPrimitive.ResponsiveContainer
41 >["children"]
42 }
43 >(({ id, className, children, config, ...props }, ref) => {
44 const uniqueId = React.useId()
45 const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
46
47 return (
48 <ChartContext.Provider value={{ config }}>
49 <div
50 data-chart={chartId}
51 ref={ref}
52 className={cn(
53 "flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
54 className
55 )}
56 {...props}
57 >
58 <ChartStyle id={chartId} config={config} />
59 <RechartsPrimitive.ResponsiveContainer>
60 {children}
61 </RechartsPrimitive.ResponsiveContainer>
62 </div>
63 </ChartContext.Provider>
64 )
65 })
66 ChartContainer.displayName = "Chart"
67
68 const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
69 const colorConfig = Object.entries(config).filter(
70 ([, config]) => config.theme || config.color
71 )
72
73 if (!colorConfig.length) {
74 return null
75 }
76
77 return (
78 <style
79 dangerouslySetInnerHTML={{
80 __html: Object.entries(THEMES)
81 .map(
82 ([theme, prefix]) => `
83 ${prefix} [data-chart=${id}] {
84 ${colorConfig
85 .map(([key, itemConfig]) => {
86 const color =
87 itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
88 itemConfig.color
89 return color ? ` --color-${key}: ${color};` : null
90 })
91 .join("\n")}
92 }
93 `
94 )
95 .join("\n"),
96 }}
97 />
98 )
99 }
100
101 const ChartTooltip = RechartsPrimitive.Tooltip
102
103 const ChartTooltipContent = React.forwardRef<
104 HTMLDivElement,
105 React.ComponentProps<"div"> & {
106 active?: boolean
107 payload?: any[]
108 label?: any
109 hideLabel?: boolean
110 hideIndicator?: boolean
111 indicator?: "line" | "dot" | "dashed"
112 nameKey?: string
113 labelKey?: string
114 labelFormatter?: any
115 labelClassName?: string
116 formatter?: any
117 color?: string
118 }
119 >(
120 (
121 {
122 active,
123 payload,
124 className,
125 indicator = "dot",
126 hideLabel = false,
127 hideIndicator = false,
128 label,
129 labelFormatter,
130 labelClassName,
131 formatter,
132 color,
133 nameKey,
134 labelKey,
135 },
136 ref
137 ) => {
138 const { config } = useChart()
139
140 const tooltipLabel = React.useMemo(() => {
141 if (hideLabel || !payload?.length) {
142 return null
143 }
144
145 const [item] = payload
146 const key = `${labelKey || item.dataKey || item.name || "value"}`
147 const itemConfig = getPayloadConfigFromPayload(config, item, key)
148 const value =
149 !labelKey && typeof label === "string"
150 ? config[label as keyof typeof config]?.label || label
151 : itemConfig?.label
152
153 if (labelFormatter) {
154 return (
155 <div className={cn("font-medium", labelClassName)}>
156 {labelFormatter(value, payload)}
157 </div>
158 )
159 }
160
161 if (!value) {
162 return null
163 }
164
165 return <div className={cn("font-medium", labelClassName)}>{value}</div>
166 }, [
167 label,
168 labelFormatter,
169 payload,
170 hideLabel,
171 labelClassName,
172 config,
173 labelKey,
174 ])
175
176 if (!active || !payload?.length) {
177 return null
178 }
179
180 const nestLabel = payload.length === 1 && indicator !== "dot"
181
182 return (
183 <div
184 ref={ref}
185 className={cn(
186 "grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
187 className
188 )}
189 >
190 {!nestLabel ? tooltipLabel : null}
191 <div className="grid gap-1.5">
192 {payload.map((item, index) => {
193 const key = `${nameKey || item.name || item.dataKey || "value"}`
194 const itemConfig = getPayloadConfigFromPayload(config, item, key)
195 const indicatorColor = color || item.payload?.fill || item.color
196
197 return (
198 <div
199 key={item.dataKey || index}
200 className={cn(
201 "flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
202 indicator === "dot" && "items-center"
203 )}
204 >
205 {formatter && item?.value !== undefined && item.name ? (
206 formatter(item.value, item.name, item, index, item.payload)
207 ) : (
208 <>
209 {itemConfig?.icon ? (
210 <itemConfig.icon />
211 ) : (
212 !hideIndicator && (
213 <div
214 className={cn(
215 "shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]",
216 {
217 "h-2.5 w-2.5": indicator === "dot",
218 "w-1": indicator === "line",
219 "w-0 border-[1.5px] border-dashed bg-transparent":
220 indicator === "dashed",
221 "my-0.5": nestLabel && indicator === "dashed",
222 }
223 )}
224 style={
225 {
226 "--color-bg": indicatorColor,
227 "--color-border": indicatorColor,
228 } as React.CSSProperties
229 }
230 />
231 )
232 )}
233 <div
234 className={cn(
235 "flex flex-1 justify-between leading-none",
236 nestLabel ? "items-end" : "items-center"
237 )}
238 >
239 <div className="grid gap-1.5">
240 {nestLabel ? tooltipLabel : null}
241 <span className="text-muted-foreground">
242 {itemConfig?.label || item.name}
243 </span>
244 </div>
245 {item.value && (
246 <span className="font-mono font-medium tabular-nums text-foreground">
247 {item.value.toLocaleString()}
248 </span>
249 )}
250 </div>
251 </>
252 )}
253 </div>
254 )
255 })}
256 </div>
257 </div>
258 )
259 }
260 )
261 ChartTooltipContent.displayName = "ChartTooltip"
262
263 const ChartLegend = RechartsPrimitive.Legend
264
265 const ChartLegendContent = React.forwardRef<
266 HTMLDivElement,
267 React.ComponentProps<"div"> & {
268 payload?: any[]
269 verticalAlign?: "top" | "middle" | "bottom"
270 hideIcon?: boolean
271 nameKey?: string
272 }
273 >(
274 (
275 { className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
276 ref
277 ) => {
278 const { config } = useChart()
279
280 if (!payload?.length) {
281 return null
282 }
283
284 return (
285 <div
286 ref={ref}
287 className={cn(
288 "flex items-center justify-center gap-4",
289 verticalAlign === "top" ? "pb-3" : "pt-3",
290 className
291 )}
292 >
293 {payload.map((item: any) => {
294 const key = `${nameKey || item.dataKey || "value"}`
295 const itemConfig = getPayloadConfigFromPayload(config, item, key)
296
297 return (
298 <div
299 key={item.value}
300 className={cn(
301 "flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground"
302 )}
303 >
304 {itemConfig?.icon && !hideIcon ? (
305 <itemConfig.icon />
306 ) : (
307 <div
308 className="h-2 w-2 shrink-0 rounded-[2px]"
309 style={{
310 backgroundColor: item.color,
311 }}
312 />
313 )}
314 {itemConfig?.label}
315 </div>
316 )
317 })}
318 </div>
319 )
320 }
321 )
322 ChartLegendContent.displayName = "ChartLegend"
323
324 // Helper to extract item config from a payload.
325 function getPayloadConfigFromPayload(
326 config: ChartConfig,
327 payload: unknown,
328 key: string
329 ) {
330 if (typeof payload !== "object" || payload === null) {
331 return undefined
332 }
333
334 const payloadPayload =
335 "payload" in payload &&
336 typeof payload.payload === "object" &&
337 payload.payload !== null
338 ? payload.payload
339 : undefined
340
341 let configLabelKey: string = key
342
343 if (
344 key in payload &&
345 typeof payload[key as keyof typeof payload] === "string"
346 ) {
347 configLabelKey = payload[key as keyof typeof payload] as string
348 } else if (
349 payloadPayload &&
350 key in payloadPayload &&
351 typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
352 ) {
353 configLabelKey = payloadPayload[
354 key as keyof typeof payloadPayload
355 ] as string
356 }
357
358 return configLabelKey in config
359 ? config[configLabelKey]
360 : config[key as keyof typeof config]
361 }
362
363 export {
364 ChartContainer,
365 ChartTooltip,
366 ChartTooltipContent,
367 ChartLegend,
368 ChartLegendContent,
369 ChartStyle,
370 }