Files
chinese-family-tree-2/components/dashboard/statistics-charts.tsx
T
freedakgmail 3375a79ed4 0.0.5.1
2025-11-23 15:56:24 +08:00

353 lines
9.0 KiB
TypeScript

"use client"
import { useEffect, useRef } from "react"
import * as echarts from "echarts"
import type { FamilyMember } from "@/types/family"
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
interface StatisticsChartsProps {
members: FamilyMember[]
}
export function StatisticsCharts({ members }: StatisticsChartsProps) {
const genderChartRef = useRef<HTMLDivElement>(null)
const generationChartRef = useRef<HTMLDivElement>(null)
const ageDistributionRef = useRef<HTMLDivElement>(null)
const birthYearChartRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if (!genderChartRef.current || members.length === 0) return
// 性别分布饼图
const genderChart = echarts.init(genderChartRef.current)
const genderData = {
male: members.filter(m => m.gender === 'MALE').length,
female: members.filter(m => m.gender === 'FEMALE').length,
other: members.filter(m => m.gender === 'OTHER').length,
}
genderChart.setOption({
title: {
text: '性别分布',
left: 'center',
top: 10,
textStyle: {
fontSize: 14,
fontWeight: 'normal'
}
},
tooltip: {
trigger: 'item',
formatter: '{b}: {c} 人 ({d}%)'
},
legend: {
bottom: 10,
left: 'center'
},
series: [
{
type: 'pie',
radius: ['40%', '70%'],
center: ['50%', '50%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: false
},
emphasis: {
label: {
show: true,
fontSize: 16,
fontWeight: 'bold'
}
},
data: [
{ value: genderData.male, name: '男性', itemStyle: { color: '#3b82f6' } },
{ value: genderData.female, name: '女性', itemStyle: { color: '#ec4899' } },
...(genderData.other > 0 ? [{ value: genderData.other, name: '其他', itemStyle: { color: '#8b5cf6' } }] : [])
]
}
]
})
return () => {
genderChart.dispose()
}
}, [members])
useEffect(() => {
if (!generationChartRef.current || members.length === 0) return
// 世代分布柱状图
const generationChart = echarts.init(generationChartRef.current)
const generationData = new Map<number, number>()
members.forEach(m => {
const gen = m.generation || 0
generationData.set(gen, (generationData.get(gen) || 0) + 1)
})
const sortedGenerations = Array.from(generationData.entries()).sort((a, b) => a[0] - b[0])
generationChart.setOption({
title: {
text: '世代分布',
left: 'center',
top: 10,
textStyle: {
fontSize: 14,
fontWeight: 'normal'
}
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
},
formatter: '{b}世: {c} 人'
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
top: '15%',
containLabel: true
},
xAxis: {
type: 'category',
data: sortedGenerations.map(([gen]) => `${gen}`),
axisLabel: {
rotate: 45
}
},
yAxis: {
type: 'value',
name: '人数'
},
series: [
{
type: 'bar',
data: sortedGenerations.map(([, count]) => count),
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#83bff6' },
{ offset: 0.5, color: '#188df0' },
{ offset: 1, color: '#188df0' }
])
},
emphasis: {
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#2378f7' },
{ offset: 0.7, color: '#2378f7' },
{ offset: 1, color: '#83bff6' }
])
}
}
}
]
})
return () => {
generationChart.dispose()
}
}, [members])
useEffect(() => {
if (!ageDistributionRef.current || members.length === 0) return
// 年龄分布图
const ageChart = echarts.init(ageDistributionRef.current)
const currentYear = new Date().getFullYear()
const ageGroups = {
'0-18': 0,
'19-35': 0,
'36-50': 0,
'51-65': 0,
'66+': 0,
'已故': 0
}
members.forEach(m => {
if (m.deathDate) {
ageGroups['已故']++
} else if (m.birthDate) {
const age = currentYear - new Date(m.birthDate).getFullYear()
if (age <= 18) ageGroups['0-18']++
else if (age <= 35) ageGroups['19-35']++
else if (age <= 50) ageGroups['36-50']++
else if (age <= 65) ageGroups['51-65']++
else ageGroups['66+']++
}
})
ageChart.setOption({
title: {
text: '年龄分布',
left: 'center',
top: 10,
textStyle: {
fontSize: 14,
fontWeight: 'normal'
}
},
tooltip: {
trigger: 'item',
formatter: '{b}: {c} 人 ({d}%)'
},
legend: {
bottom: 10,
left: 'center'
},
series: [
{
type: 'pie',
radius: '60%',
center: ['50%', '50%'],
data: Object.entries(ageGroups)
.filter(([, count]) => count > 0)
.map(([name, value]) => ({ name, value })),
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}
]
})
return () => {
ageChart.dispose()
}
}, [members])
useEffect(() => {
if (!birthYearChartRef.current || members.length === 0) return
// 出生年份趋势图
const birthYearChart = echarts.init(birthYearChartRef.current)
const birthYearData = new Map<number, number>()
members.forEach(m => {
if (m.birthDate) {
const year = new Date(m.birthDate).getFullYear()
if (year >= 1900) { // 只统计1900年以后的
const decade = Math.floor(year / 10) * 10
birthYearData.set(decade, (birthYearData.get(decade) || 0) + 1)
}
}
})
const sortedYears = Array.from(birthYearData.entries()).sort((a, b) => a[0] - b[0])
birthYearChart.setOption({
title: {
text: '出生年代分布',
left: 'center',
top: 10,
textStyle: {
fontSize: 14,
fontWeight: 'normal'
}
},
tooltip: {
trigger: 'axis',
formatter: '{b}年代: {c} 人'
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
top: '15%',
containLabel: true
},
xAxis: {
type: 'category',
data: sortedYears.map(([year]) => `${year}s`),
axisLabel: {
rotate: 45
}
},
yAxis: {
type: 'value',
name: '人数'
},
series: [
{
type: 'line',
data: sortedYears.map(([, count]) => count),
smooth: true,
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(99, 102, 241, 0.5)' },
{ offset: 1, color: 'rgba(99, 102, 241, 0.1)' }
])
},
lineStyle: {
color: '#6366f1',
width: 2
},
itemStyle: {
color: '#6366f1'
}
}
]
})
return () => {
birthYearChart.dispose()
}
}, [members])
return (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div ref={genderChartRef} className="h-[300px]" />
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div ref={generationChartRef} className="h-[300px]" />
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div ref={ageDistributionRef} className="h-[300px]" />
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div ref={birthYearChartRef} className="h-[300px]" />
</CardContent>
</Card>
</div>
)
}