乐趣区

关于g2plot:g2-4x-实现月份回归趋势线

我的项目需要

须要在一个折线图上减少一条回归趋势线。看了看文档发现目前在用的 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>

# 组件成果

遇到的坑

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 函数,上面代码中趋势线 trendYY 进行了同步。

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 对趋势线的利用。

退出移动版