first commit
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
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'
|
||||
Reference in New Issue
Block a user