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