@@ -115,3 +115,258 @@ ${colorConfig
115
115
/>
116
116
) ;
117
117
} ;
118
+
119
+ export const ChartTooltip = RechartsPrimitive . Tooltip ;
120
+
121
+ export const ChartTooltipContent = React . forwardRef <
122
+ HTMLDivElement ,
123
+ React . ComponentProps < typeof RechartsPrimitive . Tooltip > &
124
+ React . ComponentProps < "div" > & {
125
+ hideLabel ?: boolean ;
126
+ hideIndicator ?: boolean ;
127
+ indicator ?: "line" | "dot" | "dashed" ;
128
+ nameKey ?: string ;
129
+ labelKey ?: string ;
130
+ }
131
+ > (
132
+ (
133
+ {
134
+ active,
135
+ payload,
136
+ className,
137
+ indicator = "dot" ,
138
+ hideLabel = false ,
139
+ hideIndicator = false ,
140
+ label,
141
+ labelFormatter,
142
+ labelClassName,
143
+ formatter,
144
+ color,
145
+ nameKey,
146
+ labelKey,
147
+ } ,
148
+ ref ,
149
+ ) => {
150
+ const { config } = useChart ( ) ;
151
+
152
+ const tooltipLabel = React . useMemo ( ( ) => {
153
+ if ( hideLabel || ! payload ?. length ) {
154
+ return null ;
155
+ }
156
+
157
+ const [ item ] = payload ;
158
+ const key = `${ labelKey || item . dataKey || item . name || "value" } ` ;
159
+ const itemConfig = getPayloadConfigFromPayload ( config , item , key ) ;
160
+ const value =
161
+ ! labelKey && typeof label === "string"
162
+ ? config [ label as keyof typeof config ] ?. label || label
163
+ : itemConfig ?. label ;
164
+
165
+ if ( labelFormatter ) {
166
+ return (
167
+ < div className = { cn ( "font-medium" , labelClassName ) } >
168
+ { labelFormatter ( value , payload ) }
169
+ </ div >
170
+ ) ;
171
+ }
172
+
173
+ if ( ! value ) {
174
+ return null ;
175
+ }
176
+
177
+ return < div className = { cn ( "font-medium" , labelClassName ) } > { value } </ div > ;
178
+ } , [
179
+ label ,
180
+ labelFormatter ,
181
+ payload ,
182
+ hideLabel ,
183
+ labelClassName ,
184
+ config ,
185
+ labelKey ,
186
+ ] ) ;
187
+
188
+ if ( ! active || ! payload ?. length ) {
189
+ return null ;
190
+ }
191
+
192
+ const nestLabel = payload . length === 1 && indicator !== "dot" ;
193
+
194
+ return (
195
+ < div
196
+ ref = { ref }
197
+ className = { cn (
198
+ "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" ,
199
+ className ,
200
+ ) }
201
+ >
202
+ { ! nestLabel ? tooltipLabel : null }
203
+ < div className = "grid gap-1.5" >
204
+ { payload . map ( ( item , index ) => {
205
+ const key = `${ nameKey || item . name || item . dataKey || "value" } ` ;
206
+ const itemConfig = getPayloadConfigFromPayload ( config , item , key ) ;
207
+ const indicatorColor = color || item . payload . fill || item . color ;
208
+
209
+ return (
210
+ < div
211
+ key = { item . dataKey }
212
+ className = { cn (
213
+ "flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground" ,
214
+ indicator === "dot" && "items-center" ,
215
+ ) }
216
+ >
217
+ { formatter && item ?. value !== undefined && item . name ? (
218
+ formatter ( item . value , item . name , item , index , item . payload )
219
+ ) : (
220
+ < >
221
+ { itemConfig ?. icon ? (
222
+ < itemConfig . icon />
223
+ ) : (
224
+ ! hideIndicator && (
225
+ < div
226
+ className = { cn (
227
+ "shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]" ,
228
+ {
229
+ "h-2.5 w-2.5" : indicator === "dot" ,
230
+ "w-1" : indicator === "line" ,
231
+ "w-0 border-[1.5px] border-dashed bg-transparent" :
232
+ indicator === "dashed" ,
233
+ "my-0.5" : nestLabel && indicator === "dashed" ,
234
+ } ,
235
+ ) }
236
+ style = {
237
+ {
238
+ "--color-bg" : indicatorColor ,
239
+ "--color-border" : indicatorColor ,
240
+ } as React . CSSProperties
241
+ }
242
+ />
243
+ )
244
+ ) }
245
+ < div
246
+ className = { cn (
247
+ "flex flex-1 justify-between leading-none" ,
248
+ nestLabel ? "items-end" : "items-center" ,
249
+ ) }
250
+ >
251
+ < div className = "grid gap-1.5" >
252
+ { nestLabel ? tooltipLabel : null }
253
+ < span className = "text-muted-foreground" >
254
+ { itemConfig ?. label || item . name }
255
+ </ span >
256
+ </ div >
257
+ { item . value && (
258
+ < span className = "font-mono font-medium tabular-nums text-foreground" >
259
+ { item . value . toLocaleString ( ) }
260
+ </ span >
261
+ ) }
262
+ </ div >
263
+ </ >
264
+ ) }
265
+ </ div >
266
+ ) ;
267
+ } ) }
268
+ </ div >
269
+ </ div >
270
+ ) ;
271
+ } ,
272
+ ) ;
273
+ ChartTooltipContent . displayName = "ChartTooltip" ;
274
+
275
+ export const ChartLegend = RechartsPrimitive . Legend ;
276
+
277
+ export const ChartLegendContent = React . forwardRef <
278
+ HTMLDivElement ,
279
+ React . ComponentProps < "div" > &
280
+ Pick < RechartsPrimitive . LegendProps , "payload" | "verticalAlign" > & {
281
+ hideIcon ?: boolean ;
282
+ nameKey ?: string ;
283
+ }
284
+ > (
285
+ (
286
+ { className, hideIcon = false , payload, verticalAlign = "bottom" , nameKey } ,
287
+ ref ,
288
+ ) => {
289
+ const { config } = useChart ( ) ;
290
+
291
+ if ( ! payload ?. length ) {
292
+ return null ;
293
+ }
294
+
295
+ return (
296
+ < div
297
+ ref = { ref }
298
+ className = { cn (
299
+ "flex items-center justify-center gap-4" ,
300
+ verticalAlign === "top" ? "pb-3" : "pt-3" ,
301
+ className ,
302
+ ) }
303
+ >
304
+ { payload . map ( ( item ) => {
305
+ const key = `${ nameKey || item . dataKey || "value" } ` ;
306
+ const itemConfig = getPayloadConfigFromPayload ( config , item , key ) ;
307
+
308
+ return (
309
+ < div
310
+ key = { item . value }
311
+ className = { cn (
312
+ "flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground" ,
313
+ ) }
314
+ >
315
+ { itemConfig ?. icon && ! hideIcon ? (
316
+ < itemConfig . icon />
317
+ ) : (
318
+ < div
319
+ className = "h-2 w-2 shrink-0 rounded-[2px]"
320
+ style = { {
321
+ backgroundColor : item . color ,
322
+ } }
323
+ />
324
+ ) }
325
+ { itemConfig ?. label }
326
+ </ div >
327
+ ) ;
328
+ } ) }
329
+ </ div >
330
+ ) ;
331
+ } ,
332
+ ) ;
333
+ ChartLegendContent . displayName = "ChartLegend" ;
334
+
335
+ // Helper to extract item config from a payload.
336
+ function getPayloadConfigFromPayload (
337
+ config : ChartConfig ,
338
+ payload : unknown ,
339
+ key : string ,
340
+ ) {
341
+ if ( typeof payload !== "object" || payload === null ) {
342
+ return undefined ;
343
+ }
344
+
345
+ const payloadPayload =
346
+ "payload" in payload &&
347
+ typeof payload . payload === "object" &&
348
+ payload . payload !== null
349
+ ? payload . payload
350
+ : undefined ;
351
+
352
+ let configLabelKey : string = key ;
353
+
354
+ if (
355
+ key in payload &&
356
+ typeof payload [ key as keyof typeof payload ] === "string"
357
+ ) {
358
+ configLabelKey = payload [ key as keyof typeof payload ] as string ;
359
+ } else if (
360
+ payloadPayload &&
361
+ key in payloadPayload &&
362
+ typeof payloadPayload [ key as keyof typeof payloadPayload ] === "string"
363
+ ) {
364
+ configLabelKey = payloadPayload [
365
+ key as keyof typeof payloadPayload
366
+ ] as string ;
367
+ }
368
+
369
+ return configLabelKey in config
370
+ ? config [ configLabelKey ]
371
+ : config [ key as keyof typeof config ] ;
372
+ }
0 commit comments