ECharts 用户行为分析平台实战
📋 项目概述
构建用户行为分析系统,通过漏斗图、桑基图、热力图等可视化用户转化路径和行为模式。
核心功能
- ✅ 漏斗分析:转化率与流失率
- ✅ 路径分析:用户行为序列
- ✅ 留存分析:同期群分析
- ✅ 事件分析:行为分布统计
- ✅ 用户分群:RFM模型
💻 核心实现
1. 漏斗分析图
typescript
// components/Analytics/FunnelChart.tsx
import React, { useMemo } from 'react';
import * as echarts from 'echarts/core';
import { FunnelChart } from 'echarts/charts';
import { TooltipComponent, LegendComponent } from 'echarts/components';
import { CanvasRenderer } from 'echarts/renderers';
import BaseChart from '@/components/Chart/BaseChart';
echarts.use([FunnelChart, TooltipComponent, LegendComponent, CanvasRenderer]);
interface FunnelStep {
name: string;
value: number;
conversionRate?: number;
}
interface FunnelChartProps {
data: FunnelStep[];
title?: string;
}
const FunnelChartComponent: React.FC<FunnelChartProps> = ({ data, title }) => {
const option = useMemo(() => {
const maxValue = Math.max(...data.map(d => d.value));
return {
title: {
text: title || '转化漏斗',
left: 'center'
},
tooltip: {
trigger: 'item',
formatter: (params: any) => {
const step = data[params.dataIndex];
return `
<div style="padding: 8px;">
<div style="font-weight: bold; margin-bottom: 4px;">${step.name}</div>
<div>用户数: ${step.value.toLocaleString()}</div>
${step.conversionRate ? `<div>转化率: ${step.conversionRate.toFixed(2)}%</div>` : ''}
</div>
`;
}
},
legend: {
orient: 'vertical',
left: 'left',
data: data.map(d => d.name)
},
series: [
{
name: '转化漏斗',
type: 'funnel',
left: '20%',
top: 60,
bottom: 60,
width: '60%',
min: 0,
max: maxValue,
minSize: '0%',
maxSize: '100%',
sort: 'descending',
gap: 2,
label: {
show: true,
position: 'inside',
formatter: (params: any) => {
const step = data[params.dataIndex];
return `${step.name}\n${step.value.toLocaleString()}`;
}
},
itemStyle: {
borderColor: '#fff',
borderWidth: 1
},
emphasis: {
label: {
fontSize: 20
}
},
data: data.map((step, index) => ({
value: step.value,
name: step.name,
itemStyle: {
color: getColorByIndex(index)
}
})),
// 添加转化率标注
markLine: {
symbol: 'none',
label: {
show: true,
position: 'end',
formatter: (params: any) => {
const step = data[params.dataIndex];
return step.conversionRate ? `${step.conversionRate.toFixed(1)}%` : '';
}
},
lineStyle: {
color: '#999',
type: 'dashed'
},
data: data.map((step, i) => ({
yAxis: step.value
}))
}
}
]
};
}, [data, title]);
return <BaseChart option={option} height={500} />;
};
function getColorByIndex(index: number): string {
const colors = ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de'];
return colors[index % colors.length];
}
export default FunnelChartComponent;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
2. 用户路径桑基图
typescript
// components/Analytics/UserPathSankey.tsx
import React, { useMemo } from 'react';
import * as echarts from 'echarts/core';
import { SankeyChart } from 'echarts/charts';
import { TooltipComponent, TitleComponent } from 'echarts/components';
import { CanvasRenderer } from 'echarts/renderers';
import BaseChart from '@/components/Chart/BaseChart';
echarts.use([SankeyChart, TooltipComponent, TitleComponent, CanvasRenderer]);
interface PathNode {
name: string;
category?: number;
}
interface PathLink {
source: string;
target: string;
value: number;
}
interface UserPathSankeyProps {
nodes: PathNode[];
links: PathLink[];
title?: string;
}
const UserPathSankey: React.FC<UserPathSankeyProps> = ({ nodes, links, title }) => {
const option = useMemo(() => ({
title: {
text: title || '用户行为路径',
left: 'center'
},
tooltip: {
trigger: 'item',
triggerOn: 'mousemove',
formatter: (params: any) => {
if (params.seriesType === 'sankey') {
if (params.dataType === 'edge') {
return `
<div style="padding: 8px;">
<div>${params.data.source} → ${params.data.target}</div>
<div>用户数: ${params.data.value.toLocaleString()}</div>
</div>
`;
}
return `
<div style="padding: 8px;">
<div style="font-weight: bold;">${params.data.name}</div>
<div>用户数: ${params.value.toLocaleString()}</div>
</div>
`;
}
return '';
}
},
series: {
type: 'sankey',
layout: 'none',
emphasis: {
focus: 'adjacency'
},
data: nodes,
links: links,
levels: [
{
depth: 0,
itemStyle: {
color: '#5470c6'
},
lineStyle: {
color: 'source',
opacity: 0.6
}
},
{
depth: 1,
itemStyle: {
color: '#91cc75'
},
lineStyle: {
color: 'source',
opacity: 0.6
}
},
{
depth: 2,
itemStyle: {
color: '#fac858'
},
lineStyle: {
color: 'source',
opacity: 0.6
}
}
],
lineStyle: {
curveness: 0.5
}
}
}), [nodes, links, title]);
return <BaseChart option={option} height={600} />;
};
export default UserPathSankey;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
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
3. 留存分析热力图
typescript
// components/Analytics/RetentionHeatmap.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 RetentionData {
cohort: string;
period: string;
retention: number;
}
interface RetentionHeatmapProps {
data: RetentionData[];
cohorts: string[];
periods: string[];
}
const RetentionHeatmap: React.FC<RetentionHeatmapProps> = ({
data,
cohorts,
periods
}) => {
const option = useMemo(() => {
// 转换数据格式
const heatmapData = data.map(item => [
periods.indexOf(item.period),
cohorts.indexOf(item.cohort),
item.retention
]);
return {
tooltip: {
position: 'top',
formatter: (params: any) => {
const [periodIdx, cohortIdx, retention] = params.data;
return `
<div style="padding: 8px;">
<div>同期群: ${cohorts[cohortIdx]}</div>
<div>周期: ${periods[periodIdx]}</div>
<div>留存率: ${retention}%</div>
</div>
`;
}
},
grid: {
height: '70%',
top: '15%'
},
xAxis: {
type: 'category',
data: periods,
splitArea: {
show: true
},
axisLabel: {
rotate: 45
}
},
yAxis: {
type: 'category',
data: cohorts,
splitArea: {
show: true
}
},
visualMap: {
min: 0,
max: 100,
calculable: true,
orient: 'horizontal',
left: 'center',
bottom: '5%',
inRange: {
color: [
'#ffffff',
'#e6f7ff',
'#91d5ff',
'#1890ff',
'#096dd9'
]
}
},
series: [{
name: '留存率',
type: 'heatmap',
data: heatmapData,
label: {
show: true,
formatter: (params: any) => `${params.data[2]}%`
},
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}]
};
}, [data, cohorts, periods]);
return <BaseChart option={option} height={500} />;
};
export default RetentionHeatmap;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
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
4. RFM用户分群
typescript
// components/Analytics/RFMScatter.tsx
import React, { useMemo } from 'react';
import * as echarts from 'echarts/core';
import { ScatterChart } from 'echarts/charts';
import {
TooltipComponent,
GridComponent,
VisualMapComponent
} from 'echarts/components';
import { CanvasRenderer } from 'echarts/renderers';
import BaseChart from '@/components/Chart/BaseChart';
echarts.use([
ScatterChart,
TooltipComponent,
GridComponent,
VisualMapComponent,
CanvasRenderer
]);
interface RFMData {
userId: string;
recency: number; // 最近消费时间
frequency: number; // 消费频次
monetary: number; // 消费金额
segment: string; // 分群标签
}
interface RFMScatterProps {
data: RFMData[];
}
const RFMScatter: React.FC<RFMScatterProps> = ({ data }) => {
const option = useMemo(() => {
const segments = Array.from(new Set(data.map(d => d.segment)));
return {
tooltip: {
formatter: (params: any) => {
return `
<div style="padding: 8px;">
<div>用户ID: ${params.data[3]}</div>
<div>Recency: ${params.data[0]}天</div>
<div>Frequency: ${params.data[1]}次</div>
<div>Monetary: ¥${params.data[2]}</div>
<div>分群: ${params.data[4]}</div>
</div>
`;
}
},
grid: {
left: '10%',
right: '15%',
bottom: '10%'
},
xAxis: {
name: 'Recency (天)',
type: 'value',
scale: true,
splitLine: { show: false }
},
yAxis: {
name: 'Frequency (次)',
type: 'value',
scale: true,
splitLine: { show: false }
},
visualMap: {
type: 'piecewise',
categories: segments,
dimension: 4,
orient: 'vertical',
right: 10,
top: 'center'
},
series: segments.map(segment => ({
name: segment,
type: 'scatter',
data: data
.filter(d => d.segment === segment)
.map(d => [d.recency, d.frequency, d.monetary, d.userId, d.segment]),
symbolSize: (dataItem: any) => Math.sqrt(dataItem[2]) / 5,
emphasis: {
focus: 'series',
label: {
show: true,
formatter: (param: any) => param.data[3]
}
}
}))
};
}, [data]);
return <BaseChart option={option} height={600} />;
};
export default RFMScatter;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
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
💎 总结
本实战案例展示了用户行为分析的核心可视化方案:
- 漏斗分析:转化率可视化
- 路径分析:桑基图展示行为序列
- 留存分析:热力图呈现同期群
- 用户分群:RFM散点图
适用于产品分析、运营优化等场景。
