使用canvas仿Echarts实现金字塔图

  • Post author:
  • Post category:其他




前言

最近公司项目都偏向于数字化大屏展示🥱,而这次发给我的项目原型中出现了一个金字塔图🤔️, 好巧不巧,由于我们的图表都是使用

Echarts

,而

Echarts

中又不支持金字塔图,作为一个喜欢造轮子的前端开发,虽然自身技术不咋滴,但喜欢攻克难题的精神还是有的😁, 不断地内卷,才是我们这些普通前端开发的核心竞争力😂,所以就有了仿Echarts实现金字塔图的想法。


不多说先上效果

ScreenFlow.gif


项目地址:

(https://github.com/SHDjason/Pyramid.git)



正文

目前

demo

是基于

vue2.x

框架

项目实现可传入配置有:主体图位置(distance)、主体图偏移度(offset)、数据排序(sort)、图颜色(color)、数据文本回调(fontFormatter)、tooltip配置(tooltip)、数据展示样式配置(infoStyle)等

image.png



初始化canvas基本信息 并实现大小自适应

<template>
  <div id="canvas-warpper">
    <div id="canvas-tooltip"></div>
  </div>
</template>



先创建 canvas画布

      // 创建canvas元素
      this.canvas = document.createElement('canvas')
      // 把canvas元素节点添加在el元素下
      el.appendChild(this.canvas)
      this.canvasWidth = el.offsetWidth
      this.canvasHeight = el.offsetHeight
      // 将canvas元素设置与父元素同宽
      this.canvas.setAttribute('width', this.canvasWidth)
      // 将canvas元素设置与父元素同高
      this.canvas.setAttribute('height', this.canvasHeight)



获取画布中心点 方便后面做自适应和定点

 this.canvasCenter = [
        Math.round((this.canvasWidth - this.integration.distance[0] * 2) / 2) + this.integration.distance[0],
        Math.round((this.canvasHeight - this.integration.distance[1] * 2) / 2) + this.integration.distance[1]
      ]



监听传来的数据 并计算数据占比

刚好在这编写 数据排序(sort)的传入配置

  watch: {
    data: {
      immediate: true,
      deep: true,
      handler(newValue) {
        // 数据总量
        let totalData = 0
        newValue.forEach(element => {
          totalData = totalData + Number(element.value)
        })
        this.dataInfo = newValue.map(item => {
          const accounted = (item.value / totalData) * 100
          return { ...item, accounted, title: this.integration.title }
        })
        if (this.integration.sort === 'max') {
          this.dataInfo.sort((a, b) => {
            return a.value - b.value
          })
        } else if (this.integration.sort === 'min') {
          this.dataInfo.sort((a, b) => {
            return b.value - a.value
          })
        }
      }
    }
  },



下面可以确定金字塔4个基本点的位置了

这几个基本点的位置决定在后面金字塔展示的形状 可以根据自己的审美进行微调

 if (this.canvas.getContext) {
        this.ctx = this.canvas.getContext('2d')
        // 金字塔基本点位置
        this.point.top = [this.canvasCenter[0] - this.canvasWidth / 13, this.integration.distance[1]]
        this.point.left = [
          this.integration.distance[0] * 1.5,
          this.canvasHeight - this.integration.distance[1] - this.canvasHeight / 5
        ]
        this.point.right = [
          this.canvasWidth - this.integration.distance[0] * 1.9,
          this.canvasHeight - this.integration.distance[1] - this.canvasHeight / 5
        ]
        this.point.bottom = [
          this.canvasCenter[0] - this.canvasWidth / 13,
          this.canvasHeight - this.integration.distance[1]
        ]
        this.point.shadow = [
          this.integration.distance[0] - this.canvasCenter[0] / 5,
          this.canvasHeight / 1.2 - this.integration.distance[1]
        ]
        for (const key in this.point) {
          this.point[key][0] = this.point[key][0] + this.integration.offset[0]
          this.point[key][1] = this.point[key][1] + this.integration.offset[1]
        }
      } else {
        throw 'canvas下未找到 getContext方法'
      }
  • 完整代码
      let el = document.getElementById('canvas-warpper')
      // 创建canvas元素
      this.canvas = document.createElement('canvas')
      // 把canvas元素节点添加在el元素下
      el.appendChild(this.canvas)
      this.canvasWidth = el.offsetWidth
      this.canvasHeight = el.offsetHeight
      // 将canvas元素设置与父元素同宽
      this.canvas.setAttribute('width', this.canvasWidth)
      // 将canvas元素设置与父元素同高
      this.canvas.setAttribute('height', this.canvasHeight)
      this.canvasCenter = [
        Math.round((this.canvasWidth - this.integration.distance[0] * 2) / 2) + this.integration.distance[0],
        Math.round((this.canvasHeight - this.integration.distance[1] * 2) / 2) + this.integration.distance[1]
      ]
      if (this.canvas.getContext) {
        this.ctx = this.canvas.getContext('2d')
        // 金字塔基本点位置
        this.point.top = [this.canvasCenter[0] - this.canvasWidth / 13, this.integration.distance[1]]
        this.point.left = [
          this.integration.distance[0] * 1.5,
          this.canvasHeight - this.integration.distance[1] - this.canvasHeight / 5
        ]
        this.point.right = [
          this.canvasWidth - this.integration.distance[0] * 1.9,
          this.canvasHeight - this.integration.distance[1] - this.canvasHeight / 5
        ]
        this.point.bottom = [
          this.canvasCenter[0] - this.canvasWidth / 13,
          this.canvasHeight - this.integration.distance[1]
        ]
        this.point.shadow = [
          this.integration.distance[0] - this.canvasCenter[0] / 5,
          this.canvasHeight / 1.2 - this.integration.distance[1]
        ]
        for (const key in this.point) {
          this.point[key][0] = this.point[key][0] + this.integration.offset[0]
          this.point[key][1] = this.point[key][1] + this.integration.offset[1]
        }
      } else {
        throw 'canvas下未找到 getContext方法'
      }
      this.topAngle.LTB = this.angle(this.point.top, this.point.left, this.point.bottom)
      this.topAngle.RTB = this.angle(this.point.top, this.point.right, this.point.bottom)
      // 计算各数据点位置
      this.calculationPointPosition(this.dataInfo)
    },



计算金字塔每条边的角度

为了后面给每个数据定点 但是 唉~ 奈何数学太差 所以我就想到了一个方法 :

每条数据的定点范围肯定都是在 四个基本点的连线上。那我把每个基本点连线的角度求出来 ,到时候 在进行角度翻转到垂直后 再求每个条数据所占当前基本点连线的占比不就行了?

 /**
   * @description: 求3点之间角度
   * @return {*} 点 a 的角度
   * @author: 舒冬冬
   */
  angle(a, b, c) {
      const A = { X: a[0], Y: a[1] }
      const B = { X: b[0], Y: b[1] }
      const C = { X: c[0], Y: c[1] }
      const AB = Math.sqrt(Math.pow(A.X - B.X, 2) + Math.pow(A.Y - B.Y, 2))
      const AC = Math.sqrt(Math.pow(A.X - C.X, 2) + Math.pow(A.Y - C.Y, 2))
      const BC = Math.sqrt(Math.pow(B.X - C.X, 2) + Math.pow(B.Y - C.Y, 2))
      const cosA = (Math.pow(AB, 2) + Math.pow(AC, 2) - Math.pow(BC, 2)) / (2 * AB * AC)
      const angleA = Math.round((Math.acos(cosA) * 180) / Math.PI)
      return angleA
    }
    
     



计算各个数据点的位置

  • 接下来就是确定每条数据的 绘画范围了

我们先把金字塔左边和有右边旋转垂直后的点的位置确定下来

/**
     * @description: 根据A点旋转指定角度后B点的坐标位置
     * @param {*} ptSrc 圆上某点(初始点);
     * @param {*} ptRotationCenter 圆心点
     * @param {*} angle 旋转角度°  -- [angle * M_PI / 180]:将角度换算为弧度
     * 【注意】angle 逆时针为正,顺时针为负
     * @return {*}
     * @author: 舒冬冬
     */
    rotatePoint(ptSrc, ptRotationCenter, angle) {
      const a = ptRotationCenter[0]
      const b = ptRotationCenter[1]
      const x0 = ptSrc[0]
      const y0 = ptSrc[1]
      const rx = a + (x0 - a) * Math.cos((angle * Math.PI) / 180) - (y0 - b) * Math.sin((angle * Math.PI) / 180)
      const ry = b + (x0 - a) * Math.sin((angle * Math.PI) / 180) + (y0 - b) * Math.cos((angle * Math.PI) / 180)
      const point = [rx, ry]
      return point
    },
const LP = this.rotatePoint(this.point.left, this.point.top, this.topAngle.LTB * -1)
      const RP = this.rotatePoint(this.point.right, this.point.top, this.topAngle.RTB)

LP 为 TL 的边 逆时针旋转 LTB 角度后的 点的位置

RP 为 TR 的边 顺时针旋转 RTB 角度后的 点的位置

image.png

  • 这样就可以确定 每个数据点在 三条边上的各自所占长度了 完整代码

    每个点的长度计算思路, 以在TL边上点为例:

    拿到 LP (逆时针旋转 LTB角度后的位置)长度,根据数据所占总数据占比 求出该条数据的长度 再把角度转回去还原该边 就能拿到该条数据再 TL 边的上的位置信息。


    const vertical = [ this.point.top[0], (LP[1] - this.point.top[1]) * (item.accounted / 100) + this.point.top[1] ]
  /**
    * @description: 计算数据的点位置
    * @param {*} val 点占比
    * @return {*}
    * @author: 舒冬冬
    */
   calculationPointPosition(val) {
     const LP = this.rotatePoint(this.point.left, this.point.top, this.topAngle.LTB * -1)
     const RP = this.rotatePoint(this.point.right, this.point.top, this.topAngle.RTB)
     let temporary = {
       left: [
         [0, 0],
         [0, 0],
         [0, 0]
       ],
       right: [
         [0, 0],
         [0, 0],
         [0, 0]
       ],
       middle: [
         [0, 0],
         [0, 0],
         [0, 0]
       ]
     }

     
     const dataInfo = val.map((item, index) => {
       if (index === 0) {
         for (const key in temporary) {
           if (key === 'left') {
             // 垂直后点的位置
             // 垂直后点点距离
             const vertical = [
               this.point.top[0],
               (LP[1] - this.point.top[1]) * (item.accounted / 100) + this.point.top[1]
             ]
             // 还原后点的位置
             temporary.left = [this.point.top, this.rotatePoint(vertical, this.point.top, this.topAngle.LTB), vertical]
           } else if (key === 'right') {
             // 垂直后点点距离
             const vertical = [
               this.point.top[0],
               (RP[1] - this.point.top[1]) * (item.accounted / 100) + this.point.top[1]
             ]
             // 还原后点的位置
             temporary.right = [
               this.point.top,
               this.rotatePoint(vertical, this.point.top, this.topAngle.RTB * -1),
               vertical
             ]
           } else if (key === 'middle') {
             // 垂直后点点距离
             temporary.middle = [
               this.point.top,
               [
                 this.point.top[0],
                 (this.point.bottom[1] - this.point.top[1]) * (item.accounted / 100) + this.point.top[1]
               ],
               [
                 this.point.top[0],
            



版权声明:本文为qq_40852667原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。