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