ECharts 在线教育数据分析平台实战
📋 项目概述
构建在线教育学习分析系统,可视化展示学生学习进度、课程质量、教学效果等关键教育数据。
核心功能
- ✅ 学习进度:课程完成情况追踪
- ✅ 成绩分析:考试成绩分布与趋势
- ✅ 行为分析:学习时长与活跃度
- ✅ 课程评估:教学质量反馈
- ✅ 知识图谱:知识点掌握关系
💻 核心实现
1. 学生学习进度追踪
typescript
// components/Education/LearningProgress.tsx
import React, { useMemo } from 'react';
import * as echarts from 'echarts/core';
import { LineChart, BarChart, PictorialBarChart } from 'echarts/charts';
import {
GridComponent,
TooltipComponent,
LegendComponent,
ProgressComponent
} from 'echarts/components';
import { CanvasRenderer } from 'echarts/renderers';
import BaseChart from '@/components/Chart/BaseChart';
echarts.use([
LineChart,
BarChart,
PictorialBarChart,
GridComponent,
TooltipComponent,
LegendComponent,
ProgressComponent,
CanvasRenderer
]);
interface CourseProgress {
courseId: string;
courseName: string;
totalLessons: number;
completedLessons: number;
progress: number; // 0-100
studyTime: number; // 分钟
lastStudyTime: string;
}
interface LearningProgressProps {
courses: CourseProgress[];
studentId: string;
}
const LearningProgress: React.FC<LearningProgressProps> = ({ courses, studentId }) => {
// 总体进度仪表盘
const overallOption = useMemo(() => {
const avgProgress = courses.reduce((sum, c) => sum + c.progress, 0) / courses.length;
return {
series: [
{
type: 'gauge',
startAngle: 90,
endAngle: -270,
pointer: { show: false },
progress: {
show: true,
overlap: false,
roundCap: true,
clip: false,
itemStyle: {
borderWidth: 1,
borderColor: '#464646'
}
},
axisLine: {
lineStyle: {
width: 40,
color: [[1, '#e0e6f1']]
}
},
splitLine: { show: false },
axisTick: { show: false },
axisLabel: { show: false },
data: [{
value: avgProgress,
name: '总体进度',
title: {
offsetCenter: ['0%', '0%'],
fontSize: 16,
color: '#666'
},
detail: {
valueAnimation: true,
offsetCenter: ['0%', '10%'],
formatter: (value: number) => {
return `{value|${value.toFixed(0)}%}{unit|完成}`;
},
rich: {
value: {
fontSize: 36,
fontWeight: 'bold',
color: '#5470c6'
},
unit: {
fontSize: 16,
color: '#999',
padding: [0, 0, 0, 4]
}
}
}
}],
anchor: {
show: true,
showAbove: true,
size: 25,
itemStyle: { borderWidth: 10 }
},
title: { show: false },
detail: { show: false }
}
]
};
}, [courses]);
// 各课程进度条形图
const courseOption = useMemo(() => ({
title: {
text: '各课程学习进度',
left: 'center'
},
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
formatter: (params: any) => {
const data = params[0];
const course = courses[data.dataIndex];
return `
<div style="padding: 8px;">
<div style="font-weight: bold; margin-bottom: 4px;">${course.courseName}</div>
<div>进度: ${course.progress}%</div>
<div>课时: ${course.completedLessons}/${course.totalLessons}</div>
<div>学习时长: ${course.studyTime}分钟</div>
<div>最近学习: ${course.lastStudyTime}</div>
</div>
`;
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'value',
max: 100,
axisLabel: { formatter: '{value}%' }
},
yAxis: {
type: 'category',
data: courses.map(c => c.courseName),
inverse: true
},
series: [{
type: 'bar',
data: courses.map(course => ({
value: course.progress,
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
{ offset: 0, color: '#5470c6' },
{ offset: 1, color: '#91cc75' }
]),
borderRadius: [0, 10, 10, 0]
}
})),
label: {
show: true,
position: 'right',
formatter: '{c}%'
},
barWidth: '60%'
}]
}), [courses]);
return (
<div style={{ display: 'grid', gridTemplateColumns: '1fr 2fr', gap: '20px' }}>
<BaseChart option={overallOption} height={300} />
<BaseChart option={courseOption} height={300} />
</div>
);
};
export default LearningProgress;1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
2. 成绩分析雷达图
typescript
// components/Education/PerformanceRadar.tsx
import React, { useMemo } from 'react';
import * as echarts from 'echarts/core';
import { RadarChart } from 'echarts/charts';
import {
TooltipComponent,
LegendComponent,
TitleComponent
} from 'echarts/components';
import { CanvasRenderer } from 'echarts/renderers';
import BaseChart from '@/components/Chart/BaseChart';
echarts.use([
RadarChart,
TooltipComponent,
LegendComponent,
TitleComponent,
CanvasRenderer
]);
interface SubjectScore {
subject: string;
score: number;
average: number;
max: number;
}
interface PerformanceRadarProps {
scores: SubjectScore[];
studentName: string;
classAverage?: boolean;
}
const PerformanceRadar: React.FC<PerformanceRadarProps> = ({
scores,
studentName,
classAverage = true
}) => {
const option = useMemo(() => {
const indicators = scores.map(score => ({
name: score.subject,
max: score.max || 100
}));
const series: any[] = [
{
name: `${studentName}的成绩`,
type: 'radar',
data: [
{
value: scores.map(s => s.score),
name: studentName,
itemStyle: { color: '#5470c6' },
areaStyle: { opacity: 0.3 }
}
]
}
];
if (classAverage) {
series.push({
name: '班级平均',
type: 'radar',
data: [
{
value: scores.map(s => s.average),
name: '班级平均',
itemStyle: { color: '#91cc75' },
areaStyle: { opacity: 0.2 },
lineStyle: { type: 'dashed' }
}
]
});
}
return {
title: {
text: '学科能力分析',
left: 'center'
},
tooltip: {
trigger: 'item'
},
legend: {
orient: 'vertical',
left: 'left',
data: series.map(s => s.name)
},
radar: {
indicator: indicators,
shape: 'polygon',
splitNumber: 5,
axisName: {
color: '#666'
},
splitLine: {
lineStyle: {
color: ['#ddd', '#eee', '#f5f5f5', '#f9f9f9']
}
},
splitArea: {
show: true,
areaStyle: {
color: ['rgba(250,250,250,0.3)', 'rgba(245,245,245,0.3)']
}
},
axisLine: {
lineStyle: {
color: '#ddd'
}
}
},
series
};
}, [scores, studentName, classAverage]);
return <BaseChart option={option} height={500} />;
};
export default PerformanceRadar;1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
3. 学习行为热力图
typescript
// components/Education/StudyBehaviorHeatmap.tsx
import React, { useMemo } from 'react';
import * as echarts from 'echarts/core';
import { HeatmapChart } from 'echarts/charts';
import {
TooltipComponent,
GridComponent,
AxisPointerComponent
} from 'echarts/components';
import { CanvasRenderer } from 'echarts/renderers';
import BaseChart from '@/components/Chart/BaseChart';
echarts.use([
HeatmapChart,
TooltipComponent,
GridComponent,
AxisPointerComponent,
CanvasRenderer
]);
interface StudyRecord {
date: string;
hour: number;
duration: number; // 学习时长(分钟)
}
interface StudyBehaviorHeatmapProps {
records: StudyRecord[];
period: string;
}
const StudyBehaviorHeatmap: React.FC<StudyBehaviorHeatmapProps> = ({ records, period }) => {
const option = useMemo(() => {
// 生成小时标签
const hours = Array.from({ length: 24 }, (_, i) => `${i}:00`);
// 生成日期列表
const dates = Array.from(new Set(records.map(r => r.date))).sort();
// 转换数据格式
const heatmapData = records.map(record => [
hours.indexOf(`${record.hour}:00`),
dates.indexOf(record.date),
record.duration
]);
return {
title: {
text: `学习时段分布 (${period})`,
subtext: '颜色深浅表示学习时长',
left: 'center'
},
tooltip: {
position: 'top',
formatter: (params: any) => {
const [hourIdx, dateIdx, duration] = params.data;
return `
<div style="padding: 8px;">
<div>日期: ${dates[dateIdx]}</div>
<div>时段: ${hours[hourIdx]}</div>
<div>学习时长: ${duration}分钟</div>
</div>
`;
}
},
grid: {
height: '70%',
top: '15%'
},
xAxis: {
type: 'category',
data: hours,
splitArea: { show: true },
axisLabel: { rotate: 45 }
},
yAxis: {
type: 'category',
data: dates,
splitArea: { show: true }
},
visualMap: {
min: 0,
max: 120,
calculable: true,
orient: 'horizontal',
left: 'center',
bottom: '5%',
inRange: {
color: [
'#ffffff',
'#e6f7ff',
'#91d5ff',
'#1890ff',
'#096dd9',
'#0050b3'
]
}
},
series: [{
type: 'heatmap',
data: heatmapData,
label: {
show: false
},
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}]
};
}, [records, period]);
return <BaseChart option={option} height={500} />;
};
export default StudyBehaviorHeatmap;1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
4. 知识图谱
typescript
// components/Education/KnowledgeGraph.tsx
import React, { useMemo } from 'react';
import * as echarts from 'echarts/core';
import { GraphChart } from 'echarts/charts';
import {
TooltipComponent,
LegendComponent,
TitleComponent
} from 'echarts/components';
import { CanvasRenderer } from 'echarts/renderers';
import BaseChart from '@/components/Chart/BaseChart';
echarts.use([
GraphChart,
TooltipComponent,
LegendComponent,
TitleComponent,
CanvasRenderer
]);
interface KnowledgePoint {
id: string;
name: string;
category: string;
mastery: number; // 掌握程度 0-100
prerequisites?: string[];
}
interface KnowledgeGraphProps {
knowledgePoints: KnowledgePoint[];
}
const KnowledgeGraph: React.FC<KnowledgeGraphProps> = ({ knowledgePoints }) => {
const option = useMemo(() => {
const categories = Array.from(new Set(knowledgePoints.map(kp => kp.category)));
const nodes = knowledgePoints.map(kp => ({
id: kp.id,
name: kp.name,
value: kp.mastery,
category: categories.indexOf(kp.category),
symbolSize: Math.sqrt(kp.mastery) * 3,
itemStyle: {
color: getMasteryColor(kp.mastery)
},
label: {
show: true,
formatter: '{b}'
}
}));
const links: Array<{ source: string; target: string }> = [];
knowledgePoints.forEach(kp => {
if (kp.prerequisites) {
kp.prerequisites.forEach(prereq => {
links.push({
source: prereq,
target: kp.id
});
});
}
});
return {
title: {
text: '知识点掌握图谱',
subtext: '节点大小表示掌握程度,连线表示依赖关系',
left: 'center'
},
tooltip: {
formatter: (params: any) => {
if (params.dataType === 'node') {
const kp = knowledgePoints.find(k => k.id === params.data.id);
return `
<div style="padding: 8px;">
<div style="font-weight: bold;">${params.data.name}</div>
<div>分类: ${kp?.category}</div>
<div>掌握程度: ${params.data.value}%</div>
</div>
`;
}
return '';
}
},
legend: {
data: categories,
orient: 'vertical',
right: 10,
top: 'center'
},
animationDuration: 1500,
animationEasingUpdate: 'quinticInOut',
series: [{
type: 'graph',
layout: 'force',
data: nodes,
links,
categories: categories.map(name => ({ name })),
roam: true,
force: {
repulsion: 200,
gravity: 0.1,
edgeLength: 100
},
lineStyle: {
color: 'source',
curveness: 0.3
},
emphasis: {
focus: 'adjacency'
}
}]
};
}, [knowledgePoints]);
return <BaseChart option={option} height={600} />;
};
function getMasteryColor(mastery: number): string {
if (mastery >= 80) return '#52c41a';
if (mastery >= 60) return '#faad14';
return '#ff4d4f';
}
export default KnowledgeGraph;1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
💎 总结
本实战案例展示了在线教育数据分析的核心可视化方案:
- 学习进度:课程完成度追踪
- 成绩分析:多维度能力雷达图
- 行为分析:学习时段热力图
- 知识图谱:知识点依赖关系
适用于在线学习平台、教育管理系统等场景。
