项目需求
在一个折线图上增加回归趋势线,而 g2plot 没有相关的支持,于是用来可以支持趋势线的 g2 来做。
项目中使用的 g2 版本是 "@antv/g2": "^4.1.26"
数据源
首先看下数据源,数据是某时间段内的月流水折线图。
export default [
{ legend: '流水', x: '2020-02', y: 9422524800 },
{ legend: '流水', x: '2020-03', y: 9384111400 },
{ legend: '流水', x: '2020-04', y: 9738050100 },
{ legend: '流水', x: '2020-05', y: 9131817000 },
{ legend: '流水', x: '2020-06', y: 8214020510 },
{ legend: '流水', x: '2020-07', y: 8846402001 },
{ legend: '流水', x: '2020-08', y: 8688620800 },
{ legend: '流水', x: '2020-09', y: 8394596700 },
{ legend: '流水', x: '2020-10', y: 7316115400 },
{ legend: '流水', x: '2020-11', y: 7511430100 },
{ legend: '流水', x: '2020-12', y: 7566870200 },
{ legend: '流水', x: '2021-01', y: 6587654400 },
{ legend: '流水', x: '2021-02', y: 5228583200 },
{ legend: '流水', x: '2021-03', y: 4103783800 },
{ legend: '流水', x: '2021-04', y: 3220705200 },
{ legend: '流水', x: '2021-05', y: 2503257600 },
{ legend: '流水', x: '2021-06', y: 2802926800 },
{ legend: '流水', x: '2021-07', y: 2491700010 },
{ legend: '流水', x: '2021-08', y: 3044604008 },
{ legend: '流水', x: '2021-09', y: 3025648200 },
{ legend: '流水', x: '2021-10', y: 5056173600 },
{ legend: '流水', x: '2021-11', y: 6031860100 },
{ legend: '流水', x: '2021-12', y: 6377486200 },
{ legend: '流水', x: '2022-01', y: 6308561500 },
{ legend: '流水', x: '2022-02', y: 7015006800 },
{ legend: '流水', x: '2022-03', y: 7213882100 },
{ legend: '流水', x: '2022-04', y: 8600740100 },
{ legend: '流水', x: '2022-05', y: 9593594010 },
{ legend: '流水', x: '2022-06', y: 6926563800 },
{ legend: '流水', x: '2022-07', y: 9748705600 },
{ legend: '流水', x: '2022-08', y: 9545976190 },
{ legend: '流水', x: '2022-09', y: 3650933130 },
{ legend: '流水', x: '2022-10', y: 4269464400 },
{ legend: '流水', x: '2022-11', y: 2650898700 },
{ legend: '流水', x: '2022-12', y: 2679398600 },
{ legend: '流水', x: '2023-01', y: 2946118600 }
]
组件
下面是组件,如有需要可直接使用。
<template>
<div ref="ChartContainer"></div>
</template>
<script>
import { Chart } from '@antv/g2'
import DataSet from '@antv/data-set'
import { formatData } from '@/utils/formatter'
export default {
name: 'LineChart',
components: {},
props: {
chartData: Array,
formatType: String,
showTrend: Boolean,
},
computed: {
// 每年一月份备注
annotations() {
const startOfYears = this.chartData
.map((item, index) => ({ ...item, index }))
.filter(item => item.x.endsWith('-01'))
return startOfYears.map(item => ({
type: 'line',
start: [`${item.index}`, 'start'],
end: [`${item.index}`, 'end'],
top: true,
style: {
stroke: '#606266',
lineWidth: 1,
lineDash: [4, 4]
}
}))
}
},
mounted() {
this.initChart()
this.renderChart()
},
beforeDestroy() {
this.chart = null
},
methods: {
initChart() {
this.chart = new Chart({
container: this.$refs.ChartContainer,
autoFit: true,
height: 300,
appendPadding: 10
})
},
renderChart() {
this.chart.clear()
const transData = this.chartData.map((item, index) => ({
...item,
index
}))
this.chart.scale({
trendX: {
range: [0, 1]
},
y: {
min: 0,
sync: true,
nice: true
},
trendY: {
min: 0,
sync: 'y',
nice: true
}
})
this.chart.tooltip({
showCrosshairs: true,
title: (title, datum) => {
return datum.x
},
customItems: items => {
return items.map(item => {
return {
...item,
name: item.data.legend,
value: formatData(item.data.y, this.formatType)
}
})
}
})
const view1 = this.chart.createView()
view1.axis('index', {
label: {
formatter: val => {
return transData[val].x
},
style: {
fill: '#000000'
}
}
})
view1.axis('y', {
label: {
formatter: val => {
return formatData(val, this.formatType)
},
style: {
fill: '#000000'
}
}
})
if (this.chartData.length > 0) {
view1.legend({
custom: true,
items: [
{
value: this.chartData[0].legend,
name: this.chartData[0].legend,
marker: { style: { fill: '#ed786c' } }
}
]
})
}
view1.data(transData)
view1
.line()
.position('index*y')
.color('#ed786c')
.shape('smooth')
this.annotations.forEach(option => {
view1.annotation().line(option)
})
if (this.showTrend) {
const ds = new DataSet()
const dv = ds.createView().source(transData)
dv.transform({
type: 'regression',
method: 'polynomial',
fields: ['index', 'y'],
bandwidth: 0.1,
as: ['trendX', 'trendY']
})
const view2 = this.chart.createView({
padding: [5, 0, 48, 72]
})
view2.axis(false)
view2.data(dv.rows)
view2
.line()
.position('trendX*trendY')
.style({
stroke: '#969696',
lineDash: [3, 3]
})
.tooltip(false)
}
this.chart.render()
}
},
watch: {
showTrend() {
if (this.chart) {
this.renderChart()
}
},
chartData() {
if (this.chart) {
this.renderChart()
}
}
}
}
</script>
组件效果
image.png遇到的坑
x 轴使用月份字符串导致浏览器卡死
一开始,我以为 DataSet 是识别日期的,于是 x 轴直接提供的 2022-04
这种字符串,结果……直接浏览器崩溃了。猜测是转换出的数据量太大导致。
于是去看了看示例,示例用的是年份 2022
,它使用 DataSet 的 map 转换将字符串转成了数字类型。
const dv = ds.createView().source(data);
dv.transform({
type: 'map',
callback: row => {
row.year = parseInt(row.year, 10);
return row;
}
}).transform({
type: 'regression',
method: 'polynomial',
fields: ['year', 'value'],
bandwidth: 0.1,
as: ['Year', 'Value']
});
如果我将示例里面的 map 部分去掉浏览器依旧卡死。所以我试着将 X 转为数组类型的值。
使用月份时间戳导致趋势线离谱
我试着将月份转成了两种数字格式,假如月份是 2022-02
,第一种我给他转成了 202202,第二种我则是使用了 1548381600 这种进位到秒的 unix 时间戳。
第一种转换的确出了数据,但是达不到预期。我想了想其实月份并不是递增的,比如 202212 后面并不是 202213 而是 202301 了。所以这种 X 轴算趋势线应该是不成立的。
第二种转换需要特别注意的一点是,由于 unix 时间戳有 10 位数,所以 bandwidth
不能再用 0.1 了,否则会转换出 100000+ 条数据出来,我给的间隔是 60*60*24
也就是一天。结果趋势线的确出来了,但是趋势线的值却是一堆负数。实测了一下 g2 示例中的转换趋势线,发现也是一堆负数。所以并不能将数据线和趋势线的 Y 轴同步。同步后就会出现数据线和趋势线相隔很远的情况。
我不清楚其中的计算方式,不过无法同步 Y 轴总让人难受。
两种方式都不满意后,突然灵光一闪。既然要连续的数,那么我不管月份,而是把这些 X 轴的点当作是 1 2 3 4 5 6 7 8 9 10
这种连续数字呢?不一定非得要安装月份数据来安排 X 轴啊。
// 记录下索引值
const transData = this.chartData.map((item, index) => ({
...item,
index
}))
// 将索引值当做 X 轴
dv.transform({
type: 'regression',
method: 'polynomial',
fields: ['index', 'y'],
bandwidth: 0.1,
as: ['trendX', 'trendY']
})
结果是可以的,不但趋势线的走势和预期的一致,而且趋势线 Y 轴数据也能和折线图 Y 轴数据对上,完美解决。
两个 chart view 数据不同步
需要正确配置 scale
函数,下面代码中趋势线 trendY
和 Y
进行了同步。
this.chart.scale({
trendX: {
range: [0, 1]
},
y: {
min: 0,
sync: true,
nice: true
},
trendY: {
min: 0,
sync: 'y',
nice: true
}
})
两个 chart view 不重合
由于 chart 是使用了两个 view 来进行重叠绘制,而两个图并没有完全对齐。
解决方案是,打开两个 view 的 axis 坐标,配置 padding 让两个图的 x 轴和 y 轴都重合。
Y 轴数值太小导致趋势线出现异常
在测试过程中,发现由一条条水平线组成的趋势线,这显然是有问题的。经测试发现是 transform 函数只保留两位小数,所以会出现 64,64,64,64,65,65,65,65,66,66,66,66
这种 y 轴信息。
解决方案也很简单,先全部乘以 10000,然后进行趋势线转换,转后再全部除以 10000 就可以了。
dv.transform({
type: 'map',
callback: row => {
row.y = Number(row.y) * 10000
return row
}
})
dv.transform({
type: 'regression',
method: 'polynomial',
fields: ['index', 'y'],
bandwidth: 0.1,
as: ['trendX', 'trendY']
})
const rows = dv.rows.map(row => ({
...row,
trendY: row.trendY / 10000
}))
试图清掉现有画布重新渲染
多次 render 画布会出现一些奇怪问题,所以每次数据更新都需要刷新画布。原本我使用的是 g2 提供的 clear 函数。
this.chart.clear()
但实际应用中 clear 函数无法清楚自定义的 legend 图例,不知道为什么,于是只能暴力破解了。
this.chart = null
this.$refs.ChartContainer.innerHTML = ''
// 然后再重新 new chart
this.chart = new Chart({
container: this.$refs.ChartContainer,
autoFit: true,
height: 300,
appendPadding: 10
})
最后
由于 g2 4.x 对于趋势线的描述很少,所以让我走了不少的弯路。去网上查资料呢也没找到太有用的,所以记录下,希望能帮到同样遇到问题的朋友吧。
g2 最新支持
就在发文的 2023/02/15 当天,g2 更新了官方文档及相关示例。相比于老版文档中很小一块儿讲趋势线不同,新版本有专门的示例来演示新版 g2 对趋势线的应用。
网友评论