import ReactECharts from 'echarts-for-react'
import type { TrendPoint } from '../types'
import { EXAM_TYPE_LABELS } from '../types'
interface Props {
points: TrendPoint[]
subjectName: string
threshold: number
}
const COLORS = {
up: '#52c41a',
down: '#ff4d4f',
flat: '#8c8c8c',
volatile: '#fa8c16',
}
export default function TrendChart({ points, subjectName, threshold }: Props) {
if (points.length === 0) {
return
暂无成绩数据
}
const dates = points.map((p) => p.exam_date)
const values = points.map((p) => p.ratio_percent)
const lineSeries = points.slice(1).map((point, i) => {
let color = COLORS.flat
if (point.direction === 'up') color = COLORS.up
if (point.direction === 'down') color = COLORS.down
return {
type: 'line' as const,
data: dates.map((_, idx) => (idx === i || idx === i + 1 ? values[idx] : null)),
connectNulls: false,
showSymbol: false,
lineStyle: { width: 3, color },
tooltip: { show: false },
silent: true,
}
})
const markPoints = points
.map((point, i) => ({ point, i }))
.filter(({ point }) => point.is_volatile)
.map(({ i }) => ({
coord: [dates[i], values[i]],
symbol: 'circle',
symbolSize: 18,
itemStyle: {
color: COLORS.volatile,
borderColor: '#fff',
borderWidth: 2,
},
label: { show: false },
}))
const option = {
title: {
text: `${subjectName} 成绩占比趋势`,
left: 'center',
textStyle: { fontSize: 16 },
},
tooltip: {
trigger: 'axis',
formatter: (params: Array<{ dataIndex: number; value: number }>) => {
const idx = params[0]?.dataIndex ?? 0
const p = points[idx]
if (!p) return ''
const typeLabel = EXAM_TYPE_LABELS[p.exam_type]
let html = `${p.exam_date} (${typeLabel})
占比: ${p.ratio_percent}%`
if (p.title) html += `
${p.title}`
if (p.delta_percent !== null) {
const sign = p.delta_percent > 0 ? '+' : ''
html += `
较上次: ${sign}${p.delta_percent}%`
if (p.is_volatile) html += ' [大幅波动]'
}
return html
},
},
grid: { left: 50, right: 30, top: 60, bottom: 50 },
xAxis: {
type: 'category',
data: dates,
axisLabel: { rotate: 30 },
},
yAxis: {
type: 'value',
name: '占比 (%)',
min: 0,
max: 100,
},
series: [
{
type: 'line',
data: values,
symbol: 'circle',
symbolSize: (_val: number, params: { dataIndex: number }) =>
points[params.dataIndex]?.is_volatile ? 14 : 8,
itemStyle: {
color: (params: { dataIndex: number }) => {
const p = points[params.dataIndex]
if (p?.is_volatile) return COLORS.volatile
if (p?.direction === 'up') return COLORS.up
if (p?.direction === 'down') return COLORS.down
return '#1677ff'
},
},
lineStyle: { opacity: 0 },
markPoint: markPoints.length ? { data: markPoints } : undefined,
z: 10,
},
...lineSeries,
],
legend: {
bottom: 0,
data: [
{ name: '上升', itemStyle: { color: COLORS.up } },
{ name: '下降', itemStyle: { color: COLORS.down } },
{ name: '大幅波动', itemStyle: { color: COLORS.volatile } },
],
},
}
return (
波动阈值: {(threshold * 100).toFixed(0)}%,超过此变化幅度将高亮显示
)
}