chart.jsx 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  1. "use client";
  2. import * as React from "react"
  3. import * as RechartsPrimitive from "recharts"
  4. import { cn } from "@/lib/utils"
  5. // Format: { THEME_NAME: CSS_SELECTOR }
  6. const THEMES = {
  7. light: "",
  8. dark: ".dark"
  9. }
  10. const ChartContext = React.createContext(null)
  11. function useChart() {
  12. const context = React.useContext(ChartContext)
  13. if (!context) {
  14. throw new Error("useChart must be used within a <ChartContainer />")
  15. }
  16. return context
  17. }
  18. const ChartContainer = React.forwardRef(({ id, className, children, config, ...props }, ref) => {
  19. const uniqueId = React.useId()
  20. const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
  21. return (
  22. (<ChartContext.Provider value={{ config }}>
  23. <div
  24. data-chart={chartId}
  25. ref={ref}
  26. className={cn(
  27. "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",
  28. className
  29. )}
  30. {...props}>
  31. <ChartStyle id={chartId} config={config} />
  32. <RechartsPrimitive.ResponsiveContainer>
  33. {children}
  34. </RechartsPrimitive.ResponsiveContainer>
  35. </div>
  36. </ChartContext.Provider>)
  37. );
  38. })
  39. ChartContainer.displayName = "Chart"
  40. const ChartStyle = ({
  41. id,
  42. config
  43. }) => {
  44. const colorConfig = Object.entries(config).filter(([, config]) => config.theme || config.color)
  45. if (!colorConfig.length) {
  46. return null
  47. }
  48. return (
  49. (<style
  50. dangerouslySetInnerHTML={{
  51. __html: Object.entries(THEMES)
  52. .map(([theme, prefix]) => `
  53. ${prefix} [data-chart=${id}] {
  54. ${colorConfig
  55. .map(([key, itemConfig]) => {
  56. const color =
  57. itemConfig.theme?.[theme] ||
  58. itemConfig.color
  59. return color ? ` --color-${key}: ${color};` : null
  60. })
  61. .join("\n")}
  62. }
  63. `)
  64. .join("\n"),
  65. }} />)
  66. );
  67. }
  68. const ChartTooltip = RechartsPrimitive.Tooltip
  69. const ChartTooltipContent = React.forwardRef((
  70. {
  71. active,
  72. payload,
  73. className,
  74. indicator = "dot",
  75. hideLabel = false,
  76. hideIndicator = false,
  77. label,
  78. labelFormatter,
  79. labelClassName,
  80. formatter,
  81. color,
  82. nameKey,
  83. labelKey,
  84. },
  85. ref
  86. ) => {
  87. const { config } = useChart()
  88. const tooltipLabel = React.useMemo(() => {
  89. if (hideLabel || !payload?.length) {
  90. return null
  91. }
  92. const [item] = payload
  93. const key = `${labelKey || item.dataKey || item.name || "value"}`
  94. const itemConfig = getPayloadConfigFromPayload(config, item, key)
  95. const value =
  96. !labelKey && typeof label === "string"
  97. ? config[label]?.label || label
  98. : itemConfig?.label
  99. if (labelFormatter) {
  100. return (
  101. (<div className={cn("font-medium", labelClassName)}>
  102. {labelFormatter(value, payload)}
  103. </div>)
  104. );
  105. }
  106. if (!value) {
  107. return null
  108. }
  109. return <div className={cn("font-medium", labelClassName)}>{value}</div>;
  110. }, [
  111. label,
  112. labelFormatter,
  113. payload,
  114. hideLabel,
  115. labelClassName,
  116. config,
  117. labelKey,
  118. ])
  119. if (!active || !payload?.length) {
  120. return null
  121. }
  122. const nestLabel = payload.length === 1 && indicator !== "dot"
  123. return (
  124. (<div
  125. ref={ref}
  126. className={cn(
  127. "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",
  128. className
  129. )}>
  130. {!nestLabel ? tooltipLabel : null}
  131. <div className="grid gap-1.5">
  132. {payload.map((item, index) => {
  133. const key = `${nameKey || item.name || item.dataKey || "value"}`
  134. const itemConfig = getPayloadConfigFromPayload(config, item, key)
  135. const indicatorColor = color || item.payload.fill || item.color
  136. return (
  137. (<div
  138. key={item.dataKey}
  139. className={cn(
  140. "flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
  141. indicator === "dot" && "items-center"
  142. )}>
  143. {formatter && item?.value !== undefined && item.name ? (
  144. formatter(item.value, item.name, item, index, item.payload)
  145. ) : (
  146. <>
  147. {itemConfig?.icon ? (
  148. <itemConfig.icon />
  149. ) : (
  150. !hideIndicator && (
  151. <div
  152. className={cn("shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]", {
  153. "h-2.5 w-2.5": indicator === "dot",
  154. "w-1": indicator === "line",
  155. "w-0 border-[1.5px] border-dashed bg-transparent":
  156. indicator === "dashed",
  157. "my-0.5": nestLabel && indicator === "dashed",
  158. })}
  159. style={
  160. {
  161. "--color-bg": indicatorColor,
  162. "--color-border": indicatorColor
  163. }
  164. } />
  165. )
  166. )}
  167. <div
  168. className={cn(
  169. "flex flex-1 justify-between leading-none",
  170. nestLabel ? "items-end" : "items-center"
  171. )}>
  172. <div className="grid gap-1.5">
  173. {nestLabel ? tooltipLabel : null}
  174. <span className="text-muted-foreground">
  175. {itemConfig?.label || item.name}
  176. </span>
  177. </div>
  178. {item.value && (
  179. <span className="font-mono font-medium tabular-nums text-foreground">
  180. {item.value.toLocaleString()}
  181. </span>
  182. )}
  183. </div>
  184. </>
  185. )}
  186. </div>)
  187. );
  188. })}
  189. </div>
  190. </div>)
  191. );
  192. })
  193. ChartTooltipContent.displayName = "ChartTooltip"
  194. const ChartLegend = RechartsPrimitive.Legend
  195. const ChartLegendContent = React.forwardRef((
  196. { className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
  197. ref
  198. ) => {
  199. const { config } = useChart()
  200. if (!payload?.length) {
  201. return null
  202. }
  203. return (
  204. (<div
  205. ref={ref}
  206. className={cn(
  207. "flex items-center justify-center gap-4",
  208. verticalAlign === "top" ? "pb-3" : "pt-3",
  209. className
  210. )}>
  211. {payload.map((item) => {
  212. const key = `${nameKey || item.dataKey || "value"}`
  213. const itemConfig = getPayloadConfigFromPayload(config, item, key)
  214. return (
  215. (<div
  216. key={item.value}
  217. className={cn(
  218. "flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground"
  219. )}>
  220. {itemConfig?.icon && !hideIcon ? (
  221. <itemConfig.icon />
  222. ) : (
  223. <div
  224. className="h-2 w-2 shrink-0 rounded-[2px]"
  225. style={{
  226. backgroundColor: item.color,
  227. }} />
  228. )}
  229. {itemConfig?.label}
  230. </div>)
  231. );
  232. })}
  233. </div>)
  234. );
  235. })
  236. ChartLegendContent.displayName = "ChartLegend"
  237. // Helper to extract item config from a payload.
  238. function getPayloadConfigFromPayload(
  239. config,
  240. payload,
  241. key
  242. ) {
  243. if (typeof payload !== "object" || payload === null) {
  244. return undefined
  245. }
  246. const payloadPayload =
  247. "payload" in payload &&
  248. typeof payload.payload === "object" &&
  249. payload.payload !== null
  250. ? payload.payload
  251. : undefined
  252. let configLabelKey = key
  253. if (
  254. key in payload &&
  255. typeof payload[key] === "string"
  256. ) {
  257. configLabelKey = payload[key]
  258. } else if (
  259. payloadPayload &&
  260. key in payloadPayload &&
  261. typeof payloadPayload[key] === "string"
  262. ) {
  263. configLabelKey = payloadPayload[key]
  264. }
  265. return configLabelKey in config
  266. ? config[configLabelKey]
  267. : config[key];
  268. }
  269. export {
  270. ChartContainer,
  271. ChartTooltip,
  272. ChartTooltipContent,
  273. ChartLegend,
  274. ChartLegendContent,
  275. ChartStyle,
  276. }