美文网首页
g2 4.x 实现月份回归趋势线

g2 4.x 实现月份回归趋势线

作者: VioletJack | 来源:发表于2023-02-15 10:00 被阅读0次

项目需求

在一个折线图上增加回归趋势线,而 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 函数,下面代码中趋势线 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 轴信息。

image.png

解决方案也很简单,先全部乘以 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 对趋势线的应用。

相关文章

  • g2 4.x 实现月份回归趋势线

    项目需求 在一个折线图上增加回归趋势线,而 g2plot 没有相关的支持,于是用来可以支持趋势线的 g2 来做。 ...

  • 「动手学深度学习」线性回归

    1. 主要内容 线性回归的基本要素 线性回归模型从零开始的实现 线性回归模型使用PyTorch的简洁实现 2. 线...

  • 两个tf模型合并

    下面要实现的功能是:g1和g2并联,placeholder输入x是3.0, g1实现系y=3*x,g2实现y+3,...

  • 2019-05-21承景胜;什么是趋势线?趋势线有什么作用?

    承景胜;什么是趋势线?趋势线有什么作用? 趋势线主要用于寻找价格波动的方向及拐点,趋势线分为上升趋势线与下降趋势线...

  • 直播笔记|画对趋势线,顺势而为!

    什么是趋势线 趋势线两种,一种是支撑趋势线,另一种是压力趋势线又称为阻力趋势线。趋势线由上下波动的所有趋势组成,并...

  • Excel中趋势线

    就是上面这个东西,它表示的是数据的一种趋势,可以用来分析预测问题,类似于回归分析。 添加趋势线后,我们可以显示值,...

  • 2020-08-13--线性回归01

    线性回归算法简介 解决回归问题 思想简单,容易实现 许多强大的非线性模型的基础 结果具有很好的 线性回归分为一元线...

  • 均线

    均线本质:交易成本线。 最简单的趋势线。 买点: ①股价站在趋势线上; ②均线趋势线向上; ③靠近均线时买入。 卖...

  • ⑩技术面-趋势和突破法成长股的买入点

    如何从技术面判断买入点:左侧和右侧 趋势线买入法 为什么成长股适合用趋势线? 股价跌破趋势线怎么办? 趋势线买入的...

  • 趋势有级别

    当一条趋势线在时间上涵盖数月之久,按道氏理论的分类便称为“主要趋势线”或“长期趋势线”。主要趋势是趋势的主要方向,...

网友评论

      本文标题:g2 4.x 实现月份回归趋势线

      本文链接:https://www.haomeiwen.com/subject/lgqskdtx.html