243 lines
7.3 KiB
TypeScript
243 lines
7.3 KiB
TypeScript
"use client"
|
|
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/app/components/ui/card"
|
|
import { ChartContainer, ChartTooltip, ChartTooltipContent } from "@/app/components/ui/chart"
|
|
import {
|
|
LineChart,
|
|
Line,
|
|
AreaChart,
|
|
Area,
|
|
BarChart,
|
|
Bar,
|
|
PieChart,
|
|
Pie,
|
|
Cell,
|
|
XAxis,
|
|
YAxis,
|
|
CartesianGrid,
|
|
ResponsiveContainer,
|
|
Legend,
|
|
} from "recharts"
|
|
|
|
export interface ChartData {
|
|
[key: string]: any
|
|
}
|
|
|
|
export interface ChartConfig {
|
|
[key: string]: {
|
|
label: string
|
|
color: string
|
|
}
|
|
}
|
|
|
|
export interface BaseChartProps {
|
|
title?: string
|
|
description?: string
|
|
data: ChartData[]
|
|
config: ChartConfig
|
|
className?: string
|
|
height?: number
|
|
}
|
|
|
|
// 折线图组件
|
|
export function LineChartComponent({ title, description, data, config, className, height = 300 }: BaseChartProps) {
|
|
return (
|
|
<Card className={className}>
|
|
{(title || description) && (
|
|
<CardHeader>
|
|
{title && <CardTitle>{title}</CardTitle>}
|
|
{description && <CardDescription>{description}</CardDescription>}
|
|
</CardHeader>
|
|
)}
|
|
<CardContent>
|
|
<ChartContainer config={config} className={`h-[${height}px]`}>
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
<LineChart data={data} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}>
|
|
<CartesianGrid strokeDasharray="3 3" />
|
|
<XAxis dataKey="name" />
|
|
<YAxis />
|
|
<ChartTooltip content={<ChartTooltipContent />} />
|
|
<Legend />
|
|
{Object.entries(config).map(([key, { color }]) => (
|
|
<Line
|
|
key={key}
|
|
type="monotone"
|
|
dataKey={key}
|
|
stroke={color}
|
|
strokeWidth={2}
|
|
dot={{ fill: color, strokeWidth: 2, r: 4 }}
|
|
activeDot={{ r: 6 }}
|
|
/>
|
|
))}
|
|
</LineChart>
|
|
</ResponsiveContainer>
|
|
</ChartContainer>
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
}
|
|
|
|
// 面积图组件
|
|
export function AreaChartComponent({ title, description, data, config, className, height = 300 }: BaseChartProps) {
|
|
return (
|
|
<Card className={className}>
|
|
{(title || description) && (
|
|
<CardHeader>
|
|
{title && <CardTitle>{title}</CardTitle>}
|
|
{description && <CardDescription>{description}</CardDescription>}
|
|
</CardHeader>
|
|
)}
|
|
<CardContent>
|
|
<ChartContainer config={config} className={`h-[${height}px]`}>
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
<AreaChart data={data} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}>
|
|
<CartesianGrid strokeDasharray="3 3" />
|
|
<XAxis dataKey="name" />
|
|
<YAxis />
|
|
<ChartTooltip content={<ChartTooltipContent />} />
|
|
<Legend />
|
|
{Object.entries(config).map(([key, { color }]) => (
|
|
<Area
|
|
key={key}
|
|
type="monotone"
|
|
dataKey={key}
|
|
stackId="1"
|
|
stroke={color}
|
|
fill={color}
|
|
fillOpacity={0.6}
|
|
/>
|
|
))}
|
|
</AreaChart>
|
|
</ResponsiveContainer>
|
|
</ChartContainer>
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
}
|
|
|
|
// 柱状图组件
|
|
export function BarChartComponent({ title, description, data, config, className, height = 300 }: BaseChartProps) {
|
|
return (
|
|
<Card className={className}>
|
|
{(title || description) && (
|
|
<CardHeader>
|
|
{title && <CardTitle>{title}</CardTitle>}
|
|
{description && <CardDescription>{description}</CardDescription>}
|
|
</CardHeader>
|
|
)}
|
|
<CardContent>
|
|
<ChartContainer config={config} className={`h-[${height}px]`}>
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
<BarChart data={data} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}>
|
|
<CartesianGrid strokeDasharray="3 3" />
|
|
<XAxis dataKey="name" />
|
|
<YAxis />
|
|
<ChartTooltip content={<ChartTooltipContent />} />
|
|
<Legend />
|
|
{Object.entries(config).map(([key, { color }]) => (
|
|
<Bar key={key} dataKey={key} fill={color} radius={[4, 4, 0, 0]} />
|
|
))}
|
|
</BarChart>
|
|
</ResponsiveContainer>
|
|
</ChartContainer>
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
}
|
|
|
|
// 饼图组件
|
|
export function PieChartComponent({
|
|
title,
|
|
description,
|
|
data,
|
|
config,
|
|
className,
|
|
height = 300,
|
|
}: BaseChartProps & { dataKey?: string; nameKey?: string }) {
|
|
const COLORS = Object.values(config).map((item) => item.color)
|
|
|
|
return (
|
|
<Card className={className}>
|
|
{(title || description) && (
|
|
<CardHeader>
|
|
{title && <CardTitle>{title}</CardTitle>}
|
|
{description && <CardDescription>{description}</CardDescription>}
|
|
</CardHeader>
|
|
)}
|
|
<CardContent>
|
|
<ChartContainer config={config} className={`h-[${height}px]`}>
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
<PieChart>
|
|
<Pie
|
|
data={data}
|
|
cx="50%"
|
|
cy="50%"
|
|
labelLine={false}
|
|
label={({ name, percent }) => `${name} ${(percent * 100).toFixed(0)}%`}
|
|
outerRadius={80}
|
|
fill="#8884d8"
|
|
dataKey="value"
|
|
>
|
|
{data.map((entry, index) => (
|
|
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
|
|
))}
|
|
</Pie>
|
|
<ChartTooltip content={<ChartTooltipContent />} />
|
|
<Legend />
|
|
</PieChart>
|
|
</ResponsiveContainer>
|
|
</ChartContainer>
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
}
|
|
|
|
// 组合图表组件
|
|
export function ComboChartComponent({
|
|
title,
|
|
description,
|
|
data,
|
|
config,
|
|
className,
|
|
height = 300,
|
|
lineKeys = [],
|
|
barKeys = [],
|
|
}: BaseChartProps & { lineKeys?: string[]; barKeys?: string[] }) {
|
|
return (
|
|
<Card className={className}>
|
|
{(title || description) && (
|
|
<CardHeader>
|
|
{title && <CardTitle>{title}</CardTitle>}
|
|
{description && <CardDescription>{description}</CardDescription>}
|
|
</CardHeader>
|
|
)}
|
|
<CardContent>
|
|
<ChartContainer config={config} className={`h-[${height}px]`}>
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
<LineChart data={data} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}>
|
|
<CartesianGrid strokeDasharray="3 3" />
|
|
<XAxis dataKey="name" />
|
|
<YAxis />
|
|
<ChartTooltip content={<ChartTooltipContent />} />
|
|
<Legend />
|
|
{barKeys.map((key) => (
|
|
<Bar key={key} dataKey={key} fill={config[key]?.color} radius={[4, 4, 0, 0]} />
|
|
))}
|
|
{lineKeys.map((key) => (
|
|
<Line
|
|
key={key}
|
|
type="monotone"
|
|
dataKey={key}
|
|
stroke={config[key]?.color}
|
|
strokeWidth={2}
|
|
dot={{ fill: config[key]?.color, strokeWidth: 2, r: 4 }}
|
|
/>
|
|
))}
|
|
</LineChart>
|
|
</ResponsiveContainer>
|
|
</ChartContainer>
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
}
|