Files
shadcn-admin-template/src/components/ui/chart.tsx
T
2026-07-25 15:42:10 +08:00

211 lines
5.0 KiB
TypeScript

import * as React from 'react'
import { useId } from 'react'
import * as RechartsChart from 'recharts'
import { cn } from '@/lib/utils'
export interface ChartConfig {
[key: string]: {
label?: string
icon?: React.ComponentType
color?: string
}
}
type ChartContextProps = {
config: ChartConfig
}
const THEMES = { light: '', dark: '.dark' } as const
const CHART_INITIAL_DIMENSION = { width: 1, height: 1 }
const ChartContext = React.createContext<ChartContextProps | null>(null)
export function useChart() {
const context = React.useContext(ChartContext)
if (!context) {
throw new Error('useChart must be used within a <ChartContainer>')
}
return context
}
export function ChartContainer({
id,
config,
className,
children,
...props
}: React.ComponentProps<'div'> & {
config: ChartConfig
children: React.ComponentProps<
typeof RechartsChart.ResponsiveContainer
>['children']
}) {
const uniqueId = useId()
const chartId = `chart-${id || uniqueId.replace(/:/g, '')}`
const containerRef = React.useRef<HTMLDivElement>(null)
const [isReady, setIsReady] = React.useState(false)
React.useLayoutEffect(() => {
const node = containerRef.current
if (!node) return
const updateSize = () => {
const rect = node.getBoundingClientRect()
const hasSize = rect.width > 0 && rect.height > 0
setIsReady((current) => (current === hasSize ? current : hasSize))
}
updateSize()
if (typeof ResizeObserver === 'undefined') return
const observer = new ResizeObserver(updateSize)
observer.observe(node)
return () => observer.disconnect()
}, [])
return (
<ChartContext.Provider value={{ config }}>
<div
ref={containerRef}
data-chart={chartId}
className={cn(
'chart flex aspect-video min-h-0 min-w-0 justify-center text-xs',
className
)}
{...props}
>
<ChartStyle id={chartId} config={config} />
{isReady ? (
<RechartsChart.ResponsiveContainer
width='100%'
height='100%'
minWidth={0}
initialDimension={CHART_INITIAL_DIMENSION}
>
{children}
</RechartsChart.ResponsiveContainer>
) : (
<div className='h-full w-full' />
)}
</div>
</ChartContext.Provider>
)
}
export function ChartStyle({
id,
config,
}: {
id: string
config: ChartConfig
}) {
const colorConfig = Object.entries(config).filter(
([, itemConfig]) => itemConfig.color
)
if (!colorConfig.length) return null
return (
<style
dangerouslySetInnerHTML={{
__html: Object.entries(THEMES)
.map(
([, prefix]) => `
${prefix} [data-chart=${id}] {
${colorConfig
.map(([key, itemConfig]) => ` --color-${key}: ${itemConfig.color};`)
.join('\n')}
}
`
)
.join('\n'),
}}
/>
)
}
export const ChartTooltip = RechartsChart.Tooltip
export function ChartTooltipContent({ active, payload, label }: any) {
const { config } = useChart()
if (!active || !payload?.length) return null
return (
<div className='min-w-32 rounded-md border bg-background/95 p-2.5 text-xs shadow-lg backdrop-blur'>
{label ? (
<div className='mb-2 font-medium text-foreground'>{label}</div>
) : null}
{payload.map((item: any) => (
<div
key={item.dataKey}
className='flex items-center justify-between gap-4 py-0.5'
>
<span className='flex items-center gap-2 text-muted-foreground'>
<span
className='h-2 w-2 rounded-full'
style={{ backgroundColor: item.color || item.fill }}
/>
{config[item.dataKey]?.label || item.name || item.dataKey}
</span>
<span className='font-medium text-foreground tabular-nums'>
{typeof item.value === 'number'
? item.value.toLocaleString('en-US')
: item.value}
</span>
</div>
))}
</div>
)
}
export function ChartLegendContent({ payload }: any) {
const { config } = useChart()
if (!payload?.length) return null
const orderedPayload = [...payload].sort(
(left: any, right: any) =>
Object.keys(config).indexOf(left.value) -
Object.keys(config).indexOf(right.value)
)
return (
<div className='flex flex-wrap items-center justify-end gap-4 text-xs text-muted-foreground'>
{orderedPayload.map((item: any) => (
<div key={item.value} className='flex items-center gap-1.5'>
<span
className='h-2 w-2 rounded-full'
style={{ backgroundColor: item.color }}
/>
<span>{config[item.value]?.label || item.value}</span>
</div>
))}
</div>
)
}
// Re-export Recharts components
export {
Area,
AreaChart,
Bar,
BarChart,
CartesianGrid,
Cell,
Legend,
Pie,
PieChart,
Radar,
RadarChart,
ResponsiveContainer,
Sector,
Tooltip,
XAxis,
YAxis,
} from 'recharts'