• 设为首页
  • 收藏本站
  • 积分充值
  • VIP赞助
  • 手机版
  • 微博
  • 微信
    微信公众号 添加方式:
    1:搜索微信号(888888
    2:扫描左侧二维码
  • 快捷导航
    福建二哥 门户 查看主题

    使用canvas仿Echarts实现金字塔图的实例代码

    发布者: 浪子 | 发布时间: 2025-6-16 12:21| 查看数: 43| 评论数: 0|帖子模式

    前言

    最近公司项目都偏向于数字化大屏展示🥱,而这次发给我的项目原型中出现了一个金字塔图🤔️, 好巧不巧,由于我们的图表都是使用Echarts,而Echarts中又不支持金字塔图,作为一个喜欢造轮子的前端开发,虽然自身技术不咋滴,但喜欢攻克难题的精神还是有的😁, 不断地内卷,才是我们这些普通前端开发的核心竞争力😂,所以就有了仿Echarts实现金字塔图的想法。
    不多说先上效果


    项目地址:(https://github.com/SHDjason/Pyramid.git)
    正文

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

    初始化canvas基本信息 并实现大小自适应
    1. <template>
    2.   <div id="canvas-warpper">
    3.     <div id="canvas-tooltip"></div>
    4.   </div>
    5. </template>
    复制代码
    先创建 canvas画布
    1.       // 创建canvas元素
    2.       this.canvas = document.createElement('canvas')
    3.       // 把canvas元素节点添加在el元素下
    4.       el.appendChild(this.canvas)
    5.       this.canvasWidth = el.offsetWidth
    6.       this.canvasHeight = el.offsetHeight
    7.       // 将canvas元素设置与父元素同宽
    8.       this.canvas.setAttribute('width', this.canvasWidth)
    9.       // 将canvas元素设置与父元素同高
    10.       this.canvas.setAttribute('height', this.canvasHeight)
    复制代码
    获取画布中心点 方便后面做自适应和定点
    1. this.canvasCenter = [
    2.         Math.round((this.canvasWidth - this.integration.distance[0] * 2) / 2) + this.integration.distance[0],
    3.         Math.round((this.canvasHeight - this.integration.distance[1] * 2) / 2) + this.integration.distance[1]
    4.       ]
    复制代码
    监听传来的数据 并计算数据占比
    刚好在这编写 数据排序(sort)的传入配置
    1.   watch: {
    2.     data: {
    3.       immediate: true,
    4.       deep: true,
    5.       handler(newValue) {
    6.         // 数据总量
    7.         let totalData = 0
    8.         newValue.forEach(element => {
    9.           totalData = totalData + Number(element.value)
    10.         })
    11.         this.dataInfo = newValue.map(item => {
    12.           const accounted = (item.value / totalData) * 100
    13.           return { ...item, accounted, title: this.integration.title }
    14.         })
    15.         if (this.integration.sort === 'max') {
    16.           this.dataInfo.sort((a, b) => {
    17.             return a.value - b.value
    18.           })
    19.         } else if (this.integration.sort === 'min') {
    20.           this.dataInfo.sort((a, b) => {
    21.             return b.value - a.value
    22.           })
    23.         }
    24.       }
    25.     }
    26.   },
    复制代码
    下面可以确定金字塔4个基本点的位置了
    这几个基本点的位置决定在后面金字塔展示的形状 可以根据自己的审美进行微调
    1. if (this.canvas.getContext) {
    2.         this.ctx = this.canvas.getContext('2d')
    3.         // 金字塔基本点位置
    4.         this.point.top = [this.canvasCenter[0] - this.canvasWidth / 13, this.integration.distance[1]]
    5.         this.point.left = [
    6.           this.integration.distance[0] * 1.5,
    7.           this.canvasHeight - this.integration.distance[1] - this.canvasHeight / 5
    8.         ]
    9.         this.point.right = [
    10.           this.canvasWidth - this.integration.distance[0] * 1.9,
    11.           this.canvasHeight - this.integration.distance[1] - this.canvasHeight / 5
    12.         ]
    13.         this.point.bottom = [
    14.           this.canvasCenter[0] - this.canvasWidth / 13,
    15.           this.canvasHeight - this.integration.distance[1]
    16.         ]
    17.         this.point.shadow = [
    18.           this.integration.distance[0] - this.canvasCenter[0] / 5,
    19.           this.canvasHeight / 1.2 - this.integration.distance[1]
    20.         ]
    21.         for (const key in this.point) {
    22.           this.point[key][0] = this.point[key][0] + this.integration.offset[0]
    23.           this.point[key][1] = this.point[key][1] + this.integration.offset[1]
    24.         }
    25.       } else {
    26.         throw 'canvas下未找到 getContext方法'
    27.       }
    复制代码
    完整代码
    1.       let el = document.getElementById('canvas-warpper')      // 创建canvas元素
    2.       this.canvas = document.createElement('canvas')
    3.       // 把canvas元素节点添加在el元素下
    4.       el.appendChild(this.canvas)
    5.       this.canvasWidth = el.offsetWidth
    6.       this.canvasHeight = el.offsetHeight
    7.       // 将canvas元素设置与父元素同宽
    8.       this.canvas.setAttribute('width', this.canvasWidth)
    9.       // 将canvas元素设置与父元素同高
    10.       this.canvas.setAttribute('height', this.canvasHeight)      this.canvasCenter = [
    11.         Math.round((this.canvasWidth - this.integration.distance[0] * 2) / 2) + this.integration.distance[0],
    12.         Math.round((this.canvasHeight - this.integration.distance[1] * 2) / 2) + this.integration.distance[1]
    13.       ]      if (this.canvas.getContext) {
    14.         this.ctx = this.canvas.getContext('2d')
    15.         // 金字塔基本点位置
    16.         this.point.top = [this.canvasCenter[0] - this.canvasWidth / 13, this.integration.distance[1]]
    17.         this.point.left = [
    18.           this.integration.distance[0] * 1.5,
    19.           this.canvasHeight - this.integration.distance[1] - this.canvasHeight / 5
    20.         ]
    21.         this.point.right = [
    22.           this.canvasWidth - this.integration.distance[0] * 1.9,
    23.           this.canvasHeight - this.integration.distance[1] - this.canvasHeight / 5
    24.         ]
    25.         this.point.bottom = [
    26.           this.canvasCenter[0] - this.canvasWidth / 13,
    27.           this.canvasHeight - this.integration.distance[1]
    28.         ]
    29.         this.point.shadow = [
    30.           this.integration.distance[0] - this.canvasCenter[0] / 5,
    31.           this.canvasHeight / 1.2 - this.integration.distance[1]
    32.         ]
    33.         for (const key in this.point) {
    34.           this.point[key][0] = this.point[key][0] + this.integration.offset[0]
    35.           this.point[key][1] = this.point[key][1] + this.integration.offset[1]
    36.         }
    37.       } else {
    38.         throw 'canvas下未找到 getContext方法'
    39.       }      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)    },
    复制代码
    计算金字塔每条边的角度

    为了后面给每个数据定点 但是 唉~ 奈何数学太差 所以我就想到了一个方法 :
    每条数据的定点范围肯定都是在 四个基本点的连线上。那我把每个基本点连线的角度求出来 ,到时候 在进行角度翻转到垂直后 再求每个条数据所占当前基本点连线的占比不就行了?
    1. /**
    2.    * @description: 求3点之间角度
    3.    * @return {*} 点 a 的角度
    4.    * @author: 舒冬冬
    5.    */
    6.   angle(a, b, c) {
    7.       const A = { X: a[0], Y: a[1] }
    8.       const B = { X: b[0], Y: b[1] }
    9.       const C = { X: c[0], Y: c[1] }
    10.       const AB = Math.sqrt(Math.pow(A.X - B.X, 2) + Math.pow(A.Y - B.Y, 2))
    11.       const AC = Math.sqrt(Math.pow(A.X - C.X, 2) + Math.pow(A.Y - C.Y, 2))
    12.       const BC = Math.sqrt(Math.pow(B.X - C.X, 2) + Math.pow(B.Y - C.Y, 2))
    13.       const cosA = (Math.pow(AB, 2) + Math.pow(AC, 2) - Math.pow(BC, 2)) / (2 * AB * AC)
    14.       const angleA = Math.round((Math.acos(cosA) * 180) / Math.PI)
    15.       return angleA
    16.     }  
    复制代码
    计算各个数据点的位置
    接下来就是确定每条数据的 绘画范围了
    我们先把金字塔左边和有右边旋转垂直后的点的位置确定下来
    1. /**
    2.      * @description: 根据A点旋转指定角度后B点的坐标位置
    3.      * @param {*} ptSrc 圆上某点(初始点);
    4.      * @param {*} ptRotationCenter 圆心点
    5.      * @param {*} angle 旋转角度°  -- [angle * M_PI / 180]:将角度换算为弧度
    6.      * 【注意】angle 逆时针为正,顺时针为负
    7.      * @return {*}
    8.      * @author: 舒冬冬
    9.      */
    10.     rotatePoint(ptSrc, ptRotationCenter, angle) {
    11.       const a = ptRotationCenter[0]
    12.       const b = ptRotationCenter[1]
    13.       const x0 = ptSrc[0]
    14.       const y0 = ptSrc[1]
    15.       const rx = a + (x0 - a) * Math.cos((angle * Math.PI) / 180) - (y0 - b) * Math.sin((angle * Math.PI) / 180)
    16.       const ry = b + (x0 - a) * Math.sin((angle * Math.PI) / 180) + (y0 - b) * Math.cos((angle * Math.PI) / 180)
    17.       const point = [rx, ry]
    18.       return point
    19.     },
    复制代码
    1. const LP = this.rotatePoint(this.point.left, this.point.top, this.topAngle.LTB * -1)
    2.       const RP = this.rotatePoint(this.point.right, this.point.top, this.topAngle.RTB)
    复制代码
    LP 为 TL 的边 逆时针旋转 LTB 角度后的 点的位置
    RP 为 TR 的边 顺时针旋转 RTB 角度后的 点的位置

    这样就可以确定 每个数据点在 三条边上的各自所占长度了 完整代码
    每个点的长度计算思路, 以在TL边上点为例:
    拿到 LP (逆时针旋转 LTB角度后的位置)长度,根据数据所占总数据占比 求出该条数据的长度 再把角度转回去还原该边 就能拿到该条数据再 TL 边的上的位置信息。
    1. const vertical = [ this.point.top[0], (LP[1] - this.point.top[1]) * (item.accounted / 100) + this.point.top[1] ]
    复制代码
    1.   /**
    2.     * @description: 计算数据的点位置
    3.     * @param {*} val 点占比
    4.     * @return {*}
    5.     * @author: 舒冬冬
    6.     */
    7.    calculationPointPosition(val) {
    8.      const LP = this.rotatePoint(this.point.left, this.point.top, this.topAngle.LTB * -1)
    9.      const RP = this.rotatePoint(this.point.right, this.point.top, this.topAngle.RTB)
    10.      let temporary = {
    11.        left: [
    12.          [0, 0],
    13.          [0, 0],
    14.          [0, 0]
    15.        ],
    16.        right: [
    17.          [0, 0],
    18.          [0, 0],
    19.          [0, 0]
    20.        ],
    21.        middle: [
    22.          [0, 0],
    23.          [0, 0],
    24.          [0, 0]
    25.        ]
    26.      }

    27.      
    28.      const dataInfo = val.map((item, index) => {
    29.        if (index === 0) {
    30.          for (const key in temporary) {
    31.            if (key === 'left') {
    32.              // 垂直后点的位置
    33.              // 垂直后点点距离
    34.              const vertical = [
    35.                this.point.top[0],
    36.                (LP[1] - this.point.top[1]) * (item.accounted / 100) + this.point.top[1]
    37.              ]
    38.              // 还原后点的位置
    39.              temporary.left = [this.point.top, this.rotatePoint(vertical, this.point.top, this.topAngle.LTB), vertical]
    40.            } else if (key === 'right') {
    41.              // 垂直后点点距离
    42.              const vertical = [
    43.                this.point.top[0],
    44.                (RP[1] - this.point.top[1]) * (item.accounted / 100) + this.point.top[1]
    45.              ]
    46.              // 还原后点的位置
    47.              temporary.right = [
    48.                this.point.top,
    49.                this.rotatePoint(vertical, this.point.top, this.topAngle.RTB * -1),
    50.                vertical
    51.              ]
    52.            } else if (key === 'middle') {
    53.              // 垂直后点点距离
    54.              temporary.middle = [
    55.                this.point.top,
    56.                [
    57.                  this.point.top[0],
    58.                  (this.point.bottom[1] - this.point.top[1]) * (item.accounted / 100) + this.point.top[1]
    59.                ],
    60.                [
    61.                  this.point.top[0],
    62.                  (this.point.bottom[1] - this.point.top[1]) * (item.accounted / 100) + this.point.top[1]
    63.                ]
    64.              ]
    65.            }
    66.          }
    67.        } else {
    68.          for (const key in temporary) {
    69.            const vertical = JSON.parse(JSON.stringify(temporary[key][2]))
    70.            if (key === 'left') {
    71.              // 垂直后点点距离
    72.              const vertical1 = [this.point.top[0], vertical[1] + (LP[1] - this.point.top[1]) * (item.accounted / 100)]
    73.              // 还原后点的位置
    74.              temporary.left = [
    75.                this.point.top,
    76.                this.rotatePoint(vertical1, this.point.top, this.topAngle.LTB),
    77.                vertical1
    78.              ]
    79.            } else if (key === 'right') {
    80.              // 垂直后点点距离
    81.              const vertical1 = [this.point.top[0], vertical[1] + (RP[1] - this.point.top[1]) * (item.accounted / 100)]
    82.              // 还原后点的位置
    83.              temporary.right = [
    84.                this.point.top,
    85.                this.rotatePoint(vertical1, this.point.top, this.topAngle.RTB * -1),
    86.                vertical1
    87.              ]
    88.            } else if (key === 'middle') {
    89.              temporary.middle = [
    90.                this.point.top,
    91.                [this.point.top[0], (this.point.bottom[1] - this.point.top[1]) * (item.accounted / 100) + vertical[1]],
    92.                [this.point.top[0], (this.point.bottom[1] - this.point.top[1]) * (item.accounted / 100) + vertical[1]]
    93.              ]
    94.            }
    95.          }
    96.        }

    97.        return { ...item, temporary: JSON.parse(JSON.stringify(temporary)) }
    98.      })
    99.      this.dataInfo = dataInfo
    100.    },
    复制代码
    这样就拿到了每个数据在每一条边上所占长度的点位。
    绘画

    数据图层绘画
    我们虽然拿到了每个数据在每一条边上所占长度的点位。 那怎么获取这条数据在该边上的所在的线段长度呢?
    很简单 因为 第一条数据的在该边长度的第二个点的位置就是第二条数据的第一个点的位置
    现在就可以进行下一步。
    数据 图层的绘画了
    1.    /**
    2.     * @description: 数据图层绘画
    3.     * @param {*}
    4.     * @return {*}
    5.     * @author: 舒冬冬
    6.     */
    7.    paintDataInfo() {
    8.      // let data = JSON.parse(JSON.stringify(this.dataInfo))
    9.      // data.reverse()
    10.      var index = -1
    11.      this.dataInfo = this.dataInfo.map(item => {
    12.        index++
    13.        if (this.integration.color.length === index) {
    14.          index = 0
    15.        }
    16.        return { ...item, color: this.integration.color[index] }
    17.      })
    18.      this.dataInfo = this.dataInfo.map((item, index) => {
    19.        let drawingPoint = []
    20.        this.ctx.fillStyle = item.color
    21.        this.ctx.beginPath()
    22.        let point1, point2, point3, point4, point5, point6
    23.        if (index === 0) {
    24.          [point1, point2, point3, point4, point5, point6] = [
    25.            item.temporary.left[0],
    26.            item.temporary.left[1],
    27.            item.temporary.middle[1],
    28.            item.temporary.right[1],
    29.            item.temporary.right[0],
    30.            item.temporary.middle[0]
    31.          ]
    32.        } else {
    33.          [point1, point2, point3, point4, point5, point6] = [
    34.            this.dataInfo[index - 1].temporary.left[1],
    35.            item.temporary.left[1],
    36.            item.temporary.middle[1],
    37.            item.temporary.right[1],
    38.            this.dataInfo[index - 1].temporary.right[1],
    39.            this.dataInfo[index - 1].temporary.middle[1]
    40.          ]
    41.        }
    42.        this.ctx.moveTo(...point1)
    43.        this.ctx.lineTo(...point2)
    44.        this.ctx.lineTo(...point3)
    45.        this.ctx.lineTo(...point4)
    46.        this.ctx.lineTo(...point5)
    47.        this.ctx.lineTo(...point6)
    48.        drawingPoint = [point1, point2, point3, point4, point5, point6]
    49.        if (this.integration.infoStyle.stroke) {
    50.          this.ctx.shadowOffsetX = 0
    51.          this.ctx.shadowOffsetY = 0
    52.          this.ctx.shadowBlur = 2
    53.          this.ctx.shadowColor = this.integration.infoStyle.strokeColor
    54.        }
    55.        this.ctx.fill()
    56.        return { ...item, drawingPoint }
    57.      })
    58.    }
    复制代码
    以上就基本完成 金字塔图的核心内容了。
    但是还是不够, 想要达到Echarts的简单的功能,单单有图是不行的
    文字的绘画

    字体绘画就比较简单了, 我们拥有每一个数据的点的位置,把每个数据点的 F C 两个点的长度 除2 的点的位置设为起点就行了
    1. /**
    2.     * @description: 绘画字体
    3.     * 此方法请在 paintDataInfo() 执行后使用
    4.     * @param {*}
    5.     * @return {*}
    6.     * @author: 舒冬冬
    7.     */
    8.    paintingText(lData) {
    9.      this.ctx.shadowColor = 'rgba(90,90,90,0)'
    10.      const color = this.integration.infoStyle.color ? this.integration.infoStyle.color : '#fff'
    11.      const width = this.integration.infoStyle.width ? this.integration.infoStyle.width : 0
    12.      const dotSize = this.integration.infoStyle.dotSize ? this.integration.infoStyle.dotSize : 4
    13.      const offset = this.integration.infoStyle.offset ? this.integration.infoStyle.offset : [0, 0]
    14.      let text = ''
    15.      this.ctx.strokeStyle = color
    16.      this.ctx.fillStyle = color
    17.      this.dataInfo.forEach((item, index) => {
    18.        if (item.drawingPoint) {
    19.          let line = [
    20.            [0, 0],
    21.            [0, 0]
    22.          ]
    23.          this.ctx.font = `normal lighter ${
    24.            this.integration.infoStyle.size ? this.integration.infoStyle.size : 14
    25.          }px sans-serif `

    26.          this.ctx.beginPath()
    27.          if (lData && index + 1 === lData.l) {
    28.            line = [
    29.              [
    30.                lData.obj.drawingPoint[2][0],
    31.                (lData.obj.drawingPoint[2][1] - lData.obj.drawingPoint[5][1]) / 2 + lData.obj.drawingPoint[5][1]
    32.              ],
    33.              [
    34.                lData.obj.drawingPoint[2][0] + lData.obj.drawingPoint[2][0] / 2 + width,
    35.                (lData.obj.drawingPoint[2][1] - lData.obj.drawingPoint[5][1]) / 2 + lData.obj.drawingPoint[5][1]
    36.              ]
    37.            ]

    38.            this.ctx.font = `normal lighter ${
    39.              this.integration.infoStyle.size ? this.integration.infoStyle.size + 2 : 16
    40.            }px sans-serif `
    41.            text =
    42.              this.integration.fontFormatter(item) !== 'default'
    43.                ? this.integration.fontFormatter(item)
    44.                : lData.obj.value + ' ---- ' + lData.obj.name
    45.            this.ctx.setLineDash([0, 0])
    46.            this.ctx.strokeText(
    47.              text,
    48.              line[1][0] + offset[0],
    49.              line[1][1] + (this.integration.infoStyle.size ? this.integration.infoStyle.size + 2 : 14) / 3 + offset[1]
    50.            )
    51.          } else {
    52.            line = [
    53.              [
    54.                item.drawingPoint[2][0],
    55.                (item.drawingPoint[2][1] - item.drawingPoint[5][1]) / 2 + item.drawingPoint[5][1]
    56.              ],
    57.              [
    58.                item.drawingPoint[2][0] + item.drawingPoint[2][0] / 2 + width,
    59.                (item.drawingPoint[2][1] - item.drawingPoint[5][1]) / 2 + item.drawingPoint[5][1]
    60.              ]
    61.            ]
    62.            text =
    63.              this.integration.fontFormatter(item) !== 'default'
    64.                ? this.integration.fontFormatter(item)
    65.                : item.value + ' ----- ' + item.name
    66.            this.ctx.setLineDash([0, 0])
    67.            this.ctx.strokeText(
    68.              text,
    69.              line[1][0] + offset[0],
    70.              line[1][1] + (this.integration.infoStyle.size ? this.integration.infoStyle.size + 2 : 16) / 3 + offset[1]
    71.            )
    72.          }
    73.          this.ctx.setLineDash(this.integration.infoStyle.setLineDash)
    74.          this.ctx.moveTo(...line[0])
    75.          this.ctx.lineTo(...line[1])
    76.          this.ctx.stroke()
    77.          this.ctx.arc(...line[0], dotSize, 0, 360, false)
    78.          this.ctx.fill() //画实心圆
    79.        } else {
    80.          throw '未找到 drawingPoint 属性'
    81.        }
    82.      })
    83.    },
    复制代码
    高亮图层

    高亮图层无非就是监听鼠标移入位置,并且判断鼠标移入位置是否存在图层内,在哪个图层内,然后重新绘画当前图层
    1.   /**
    2.     * @description: 鼠标事件注册
    3.     * @param {*}
    4.     * @return {*}
    5.     * @author: 舒冬冬
    6.     */
    7.    eventRegistered() {
    8.      const canvasWarpper = document.getElementById('canvas-warpper')
    9.      //注册事件
    10.      canvasWarpper.addEventListener('mousedown', this.doMouseDown, false)
    11.      canvasWarpper.addEventListener('mouseup', this.doMouseUp, false)
    12.      canvasWarpper.addEventListener('mousemove', this.doMouseMove, false)
    13.      // //注册事件
    14.      // this.canvas.addEventListener('mousedown', this.doMouseDown, false)
    15.      // this.canvas.addEventListener('mouseup', this.doMouseUp, false)
    16.      // this.canvas.addEventListener('mousemove', this.doMouseMove, false)
    17.    },
    18.       /**
    19.     * @description: 鼠标移动
    20.     * @param {*} e
    21.     * @return {*}
    22.     * @author: 舒冬冬
    23.     */
    24.    // eslint-disable-next-line no-unused-vars
    25.    doMouseMove(e) {
    26.      const x = e.pageX
    27.      const y = e.pageY
    28.      this.highlightCurrentRegion(this.determineDataMouse(this.getLocation(x, y)))
    29.      if (this.integration.tooltip.show) {
    30.        this.showTooltip(this.determineDataMouse(this.getLocation(x, y)), this.getLocation(x, y))
    31.      }
    32.    },
    33. /**
    34.     * @description: 判断鼠标在哪层位置上
    35.     * @param {*}
    36.     * @return {*}
    37.     * @author: 舒冬冬
    38.     */
    39.    determineDataMouse(mouseLocation) {
    40.      let req = false
    41.      for (let index = 0; index < this.dataInfo.length; index++) {
    42.        if (this.insidePolygon(this.dataInfo[index].drawingPoint, mouseLocation)) {
    43.          return (req = { l: index + 1, obj: this.dataInfo[index] })
    44.        }
    45.      }
    46.      return req
    47.    },
    48. /**
    49.     * @description: 高亮某一层级
    50.     * @param {*} lData 层级数据
    51.     * @return {*}
    52.     * @author: 舒冬冬
    53.     */
    54.    highlightCurrentRegion(lData) {
    55.      // const width = this.canvas.width;
    56.      // this.canvas.width = width;

    57.      this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
    58.      if (!lData) {
    59.        this.paintDataInfo()
    60.        this.ctx.shadowColor = 'rgba(90,90,90,0)'
    61.        this.paintingBody()
    62.        this.paintingText()
    63.        return
    64.      }
    65.      this.paintDataInfo()
    66.      this.ctx.shadowColor = 'rgba(90,90,90,0)'
    67.      this.paintingBody()
    68.      this.ctx.fillStyle = lData.obj.color
    69.      //  this.ctx.scale(1.05, 1.05)
    70.      this.ctx.beginPath()
    71.      this.ctx.moveTo(lData.obj.drawingPoint[0][0], lData.obj.drawingPoint[0][1])
    72.      this.ctx.lineTo(lData.obj.drawingPoint[1][0], lData.obj.drawingPoint[1][1])
    73.      this.ctx.lineTo(lData.obj.drawingPoint[2][0], lData.obj.drawingPoint[2][1])
    74.      this.ctx.lineTo(lData.obj.drawingPoint[3][0], lData.obj.drawingPoint[3][1])
    75.      this.ctx.lineTo(lData.obj.drawingPoint[4][0], lData.obj.drawingPoint[4][1])
    76.      this.ctx.lineTo(lData.obj.drawingPoint[5][0], lData.obj.drawingPoint[5][1])
    77.      this.ctx.shadowOffsetX = 0
    78.      this.ctx.shadowOffsetY = 0
    79.      this.ctx.shadowBlur = 10
    80.      this.ctx.shadowColor = this.integration.infoStyle.highlightedColor
    81.      this.ctx.fill()
    82.      // 阴影绘制
    83.      this.ctx.beginPath()
    84.      this.ctx.moveTo(lData.obj.drawingPoint[0][0], lData.obj.drawingPoint[0][1])
    85.      this.ctx.lineTo(lData.obj.drawingPoint[1][0], lData.obj.drawingPoint[1][1])
    86.      this.ctx.lineTo(lData.obj.drawingPoint[2][0], lData.obj.drawingPoint[2][1])
    87.      this.ctx.lineTo(lData.obj.drawingPoint[5][0], lData.obj.drawingPoint[5][1])
    88.      this.ctx.fillStyle = 'rgba(120,120,120,.15)'
    89.      this.ctx.fill()
    90.      this.paintingText(lData)
    91.    }
    复制代码
    显示tooltip位置

    可以先定义 tooltip 的渲染模板

    然后在代码上进行渲染
    1. showTooltip(lData, coordinates) {
    2.      let canvasWarpper = document.getElementById('canvas-warpper')
    3.      let canvasTooltip = document.getElementById('canvas-tooltip')
    4.      if (lData) {
    5.        canvasTooltip.style.zIndex = this.integration.tooltip.z
    6.        canvasTooltip.style.transition =
    7.          ' opacity 0.2s cubic-bezier(0.23, 1, 0.32, 1) 0s, visibility 0.2s cubic-bezier(0.23, 1, 0.32, 1) 0s,transform 0.15s'
    8.        let html = JSON.parse(JSON.stringify(this.tooltipDiv))
    9.        if (this.integration.tooltip.formatter) {
    10.          html = this.integration.tooltip.formatter(lData)
    11.        } else {
    12.          const searchVal = [
    13.            ['$[title]$', lData.obj.title],
    14.            ['$[name]$', lData.obj.name],
    15.            ['$[val]$', lData.obj.value],
    16.            ['$[color]$', lData.obj.color],
    17.            ['$[fontSize]$', this.integration.tooltip.fontSize],
    18.            ['$[backgroundColor]$', this.integration.tooltip.backgroundColor],
    19.            ['$[fontColor]$', this.integration.tooltip.fontColor]
    20.          ]
    21.          searchVal.forEach(el => {
    22.            html = html.replaceAll(...el)
    23.          })
    24.        }
    25.        canvasTooltip.innerHTML = html
    26.        canvasWarpper.style.cursor = 'pointer'
    27.        canvasTooltip.style.visibility = 'visible'
    28.        canvasTooltip.style.opacity = 1
    29.        let [x, y] = coordinates
    30.        x = x + 20
    31.        y = y + 20
    32.        // 画布高度
    33.        // canvasHeight: 0,
    34.        // 画布宽度
    35.        // canvasWidth: 0,
    36.        // 判断是否超出框架内容
    37.        if (x + canvasTooltip.clientWidth > this.canvasWidth) {
    38.          x = x - canvasTooltip.clientWidth - 40
    39.        }
    40.        if (y + canvasTooltip.clientHeight > this.canvasHeight) {
    41.          y = y - canvasTooltip.clientHeight - 40
    42.        }
    43.        canvasTooltip.style.transform = `translate3d(${x}px, ${y}px, 0px)`
    44.      } else {
    45.        canvasWarpper.style.cursor = 'default'
    46.        canvasTooltip.style.visibility = 'hidden'
    47.        canvasTooltip.style.opacity = 0
    48.      }
    49.    },
    复制代码
    而一些其他的配置功能呢也是比较简单的操作了,主要是太懒了😂,
    直接上完整源码吧! 源码上注释也比较全,不是很清楚的可以评论,我看到会回复的!
    完整源码
    1. <template>
    2.   <div id="canvas-warpper">
    3.     <div id="canvas-tooltip"></div>
    4.   </div>
    5. </template><script>export default {  name: 'Pyramid',  props: {    options: {      type: Object,      default: () => {        return {          title: '',          // 主体离边框距离          distance: [0, 0],          // 主体偏移值 (x,y)          offset: [0, 0],          // 排序(max , min)优先          sort: '',          // 颜色          color: ['#80FFA5', '#00DDFF', '#37A2FF', '#FF0087', '#FFBF00'],          // 格式化字体输出          fontFormatter: () => {            return 'default'          },          // tooltip信息配置          tooltip: {            show: true, // 是否显示            fontColor: '#000', //  字体内部颜色            fontSize: 14, // 字体大小            backgroundColor: '#fff', // tooltip背景            formatter: null, // 回调方法            z: 999999 // tooltip z-index层级          },          // 样式          infoStyle: {            stroke: false, // 是否描边            strokeColor: '#fff', //描边颜色            size: null, // 字体大小            color: null, //颜色            highlightedColor: '#fff', // 高亮颜色            setLineDash: [0, 0], // 虚线值            width: -10, // 设置多少 就会在基础上加上设置的值            offset: [0, 0], // 字体x,y的偏移度            dotSize: 4 //点大小          }        }      }    },    // 渲染数据    data: {      type: Array,      default: () => {        return [          { name: 'name1', value: 11 },          { name: 'name2', value: 11 },          { name: 'name3', value: 11 },          { name: 'name4', value: 77 },          { name: 'name5', value: 55 },          { name: 'name6', value: 66 }        ]      }    }  },  watch: {
    6.     data: {
    7.       immediate: true,
    8.       deep: true,
    9.       handler(newValue) {
    10.         // 数据总量
    11.         let totalData = 0
    12.         newValue.forEach(element => {
    13.           totalData = totalData + Number(element.value)
    14.         })
    15.         this.dataInfo = newValue.map(item => {
    16.           const accounted = (item.value / totalData) * 100
    17.           return { ...item, accounted, title: this.integration.title }
    18.         })
    19.         if (this.integration.sort === 'max') {
    20.           this.dataInfo.sort((a, b) => {
    21.             return a.value - b.value
    22.           })
    23.         } else if (this.integration.sort === 'min') {
    24.           this.dataInfo.sort((a, b) => {
    25.             return b.value - a.value
    26.           })
    27.         }
    28.       }
    29.     }
    30.   },  computed: {    integration() {      return {        title: this.options.title ? this.options.title : '',        // 主体离边框距离        distance: this.options.distance ? this.options.distance : [0, 0],        // 主体偏移值 (x,y)        offset: this.options.offset ? this.options.offset : [0, 0],        // 排序(max , min)优先        sort: this.options.sort ? this.options.sort : '',        // 颜色        color: this.options.color ? this.options.color : ['#80FFA5', '#00DDFF', '#37A2FF', '#FF0087', '#FFBF00'],        // 格式化字体输出        fontFormatter: this.options.fontFormatter          ? this.options.fontFormatter          : () => {              return 'default'            },        // tooltip显示        tooltip: {          show: this.options.tooltip ? (this.options.tooltip.show ? this.options.tooltip.show : true) : true, // 是否显示          fontColor: this.options.tooltip            ? this.options.tooltip.fontColor              ? this.options.tooltip.fontColor              : '#000'            : '#000', //  字体内部颜色          fontSize: this.options.tooltip ? (this.options.tooltip.fontSize ? this.options.tooltip.fontSize : 14) : 14, // 字体大小          backgroundColor: this.options.tooltip            ? this.options.tooltip.backgroundColor              ? this.options.tooltip.backgroundColor              : '#fff'            : '#fff', // tooltip背景          formatter: this.options.tooltip            ? this.options.tooltip.formatter              ? this.options.tooltip.formatter              : null            : null, // 返回方法          z: this.options.tooltip ? (this.options.tooltip.z ? this.options.tooltip.z : 999999) : 999999 // tooltip z-index层级        },        // 样式        infoStyle: {          stroke: this.options.infoStyle            ? this.options.infoStyle.stroke              ? this.options.infoStyle.stroke              : false            : false, //是否描边          strokeColor: this.options.infoStyle            ? this.options.infoStyle.strokeColor              ? this.options.infoStyle.strokeColor              : '#fff'            : '#fff', // 描边颜色          size: this.options.infoStyle ? (this.options.infoStyle.size ? this.options.infoStyle.size : null) : null, // 字体大小          color: this.options.infoStyle ? (this.options.infoStyle.color ? this.options.infoStyle.color : null) : null, //颜色          width: this.options.infoStyle            ? this.options.infoStyle.width || this.options.infoStyle.width !== 0              ? this.options.infoStyle.width              : -10            : -10, // 设置多少 就会在基础上加上设置的值          offset: this.options.infoStyle            ? this.options.infoStyle.offset              ? this.options.infoStyle.offset              : [0, 0]            : [0, 0], // 字体x,y的偏移度          setLineDash: this.options.infoStyle            ? this.options.infoStyle.setLineDash              ? this.options.infoStyle.setLineDash              : [0, 0]            : [0, 0], //虚线值          highlightedColor: this.options.infoStyle            ? this.options.infoStyle.highlightedColor              ? this.options.infoStyle.highlightedColor              : '#fff'            : '#fff', //高亮颜色          dotSize: this.options.infoStyle            ? this.options.infoStyle.dotSize || this.options.infoStyle.dotSize !== 0              ? this.options.infoStyle.dotSize              : 4            : 4 //点大小        }      }    }  },  data() {    return {      // canvas 主体      canvas: null,      // 图像渲染内容      ctx: null,      // 画布高度      canvasHeight: 0,      // 画布宽度      canvasWidth: 0,      // 画布中心点 [x,y]      canvasCenter: [0, 0],      // 金字塔四个点位置      point: {        top: [0, 0],        left: [0, 0],        right: [0, 0],        bottom: [0, 0],        shadow: [0, 0]      },      // 数据信息      dataInfo: [],      // 金字塔顶端角度信息      topAngle: {        LTB: 0,        RTB: 0      },      // tooltip 模板      tooltipDiv: `<div  style="margin: 0px 0 0; line-height: 1;border-color: $[backgroundColor]$ ;background-color: $[backgroundColor]$;color: $[fontColor]$;    border-width: 1px;border-radius: 4px;padding: 10px;pointer-events: none;box-shadow: rgb(0 0 0 / 20%) 1px 2px 10px;border-style: solid;white-space: nowrap;">        <div style="margin: 0px 0 0; line-height: 1">          <div style="font-size: $[fontSize]$px; color: $[fontColor]$; font-weight: 400; line-height: 1"> $[title]$ </div>          <div style="margin: 10px 0 0; line-height: 1">            <div style="margin: 0px 0 0; line-height: 1">              <div style="margin: 0px 0 0; line-height: 1">                <span                  style="                    display: inline-block;                    margin-right: 4px;                    border-radius: 10px;                    width: 10px;                    height: 10px;                    background-color: $[color]$;                  "                ></span>                <span style="font-size: $[fontSize]$px; color: $[fontColor]$; font-weight: 400; margin-left: 2px">$[name]$</span>                <span style="float: right; margin-left: 20px; font-size: $[fontSize]$px; color: $[fontColor]$; font-weight: 900">$[val]$</span>                <div style="clear: both"></div>              </div>              <div style="clear: both"></div>            </div>            <div style="clear: both"></div>          </div>          <div style="clear: both"></div>        </div>        <div style="clear: both"></div>      </div>`    }  },  mounted() {    this.init()  },  methods: {    init() {      this.initCanvasBaseInfo()      this.paintDataInfo()      this.paintingText()      this.paintingBody()      this.eventRegistered()    },    /**     * @description: 初始化canvas基本信息     * @param {*}     * @return {*}     * @author: 舒冬冬     */    initCanvasBaseInfo() {      let el = document.getElementById('canvas-warpper')      // 创建canvas元素
    31.       this.canvas = document.createElement('canvas')
    32.       // 把canvas元素节点添加在el元素下
    33.       el.appendChild(this.canvas)
    34.       this.canvasWidth = el.offsetWidth
    35.       this.canvasHeight = el.offsetHeight
    36.       // 将canvas元素设置与父元素同宽
    37.       this.canvas.setAttribute('width', this.canvasWidth)
    38.       // 将canvas元素设置与父元素同高
    39.       this.canvas.setAttribute('height', this.canvasHeight)      this.canvasCenter = [
    40.         Math.round((this.canvasWidth - this.integration.distance[0] * 2) / 2) + this.integration.distance[0],
    41.         Math.round((this.canvasHeight - this.integration.distance[1] * 2) / 2) + this.integration.distance[1]
    42.       ]      if (this.canvas.getContext) {
    43.         this.ctx = this.canvas.getContext('2d')
    44.         // 金字塔基本点位置
    45.         this.point.top = [this.canvasCenter[0] - this.canvasWidth / 13, this.integration.distance[1]]
    46.         this.point.left = [
    47.           this.integration.distance[0] * 1.5,
    48.           this.canvasHeight - this.integration.distance[1] - this.canvasHeight / 5
    49.         ]
    50.         this.point.right = [
    51.           this.canvasWidth - this.integration.distance[0] * 1.9,
    52.           this.canvasHeight - this.integration.distance[1] - this.canvasHeight / 5
    53.         ]
    54.         this.point.bottom = [
    55.           this.canvasCenter[0] - this.canvasWidth / 13,
    56.           this.canvasHeight - this.integration.distance[1]
    57.         ]
    58.         this.point.shadow = [
    59.           this.integration.distance[0] - this.canvasCenter[0] / 5,
    60.           this.canvasHeight / 1.2 - this.integration.distance[1]
    61.         ]
    62.         for (const key in this.point) {
    63.           this.point[key][0] = this.point[key][0] + this.integration.offset[0]
    64.           this.point[key][1] = this.point[key][1] + this.integration.offset[1]
    65.         }
    66.       } else {
    67.         throw 'canvas下未找到 getContext方法'
    68.       }      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: 鼠标事件注册     * @param {*}     * @return {*}     * @author: 舒冬冬     */    eventRegistered() {      const canvasWarpper = document.getElementById('canvas-warpper')      //注册事件      canvasWarpper.addEventListener('mousedown', this.doMouseDown, false)      canvasWarpper.addEventListener('mouseup', this.doMouseUp, false)      canvasWarpper.addEventListener('mousemove', this.doMouseMove, false)      // //注册事件      // this.canvas.addEventListener('mousedown', this.doMouseDown, false)      // this.canvas.addEventListener('mouseup', this.doMouseUp, false)      // this.canvas.addEventListener('mousemove', this.doMouseMove, false)    },    /**     * @description: 鼠标按下     * @param {*} e     * @return {*}     * @author: 舒冬冬     */    // eslint-disable-next-line no-unused-vars    doMouseDown(e) {},    /**     * @description: 鼠标弹起     * @param {*} e     * @return {*}     * @author: 舒冬冬     */    // eslint-disable-next-line no-unused-vars    doMouseUp(e) {},    /**     * @description: 鼠标移动     * @param {*} e     * @return {*}     * @author: 舒冬冬     */    // eslint-disable-next-line no-unused-vars    doMouseMove(e) {      const x = e.pageX      const y = e.pageY      this.highlightCurrentRegion(this.determineDataMouse(this.getLocation(x, y)))      if (this.integration.tooltip.show) {        this.showTooltip(this.determineDataMouse(this.getLocation(x, y)), this.getLocation(x, y))      }    },    /**     *  @description 判断一个点是否在多边形内部     *  @param points 多边形坐标集合     *  @param testPoint 测试点坐标     *  @author: 舒冬冬     *  返回true为真,false为假     */    insidePolygon(points, testPoint) {      const x = testPoint[0],        y = testPoint[1]      let inside = false      for (let i = 0, j = points.length - 1; i < points.length; j = i++) {        const xi = points[i][0],          yi = points[i][1]        const xj = points[j][0],          yj = points[j][1]        const intersect = yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi        if (intersect) inside = !inside      }      return inside    },    /**     * @description: 获取当前鼠标坐标     * @param {*}     * @return {*}     * @author: 舒冬冬     */    getLocation(x, y) {      const bbox = this.canvas.getBoundingClientRect()      return [(x - bbox.left) * (this.canvas.width / bbox.width), (y - bbox.top) * (this.canvas.height / bbox.height)]    },    // ======================================算法==========================================    /**
    69.      * @description: 根据A点旋转指定角度后B点的坐标位置
    70.      * @param {*} ptSrc 圆上某点(初始点);
    71.      * @param {*} ptRotationCenter 圆心点
    72.      * @param {*} angle 旋转角度°  -- [angle * M_PI / 180]:将角度换算为弧度
    73.      * 【注意】angle 逆时针为正,顺时针为负
    74.      * @return {*}
    75.      * @author: 舒冬冬
    76.      */
    77.     rotatePoint(ptSrc, ptRotationCenter, angle) {
    78.       const a = ptRotationCenter[0]
    79.       const b = ptRotationCenter[1]
    80.       const x0 = ptSrc[0]
    81.       const y0 = ptSrc[1]
    82.       const rx = a + (x0 - a) * Math.cos((angle * Math.PI) / 180) - (y0 - b) * Math.sin((angle * Math.PI) / 180)
    83.       const ry = b + (x0 - a) * Math.sin((angle * Math.PI) / 180) + (y0 - b) * Math.cos((angle * Math.PI) / 180)
    84.       const point = [rx, ry]
    85.       return point
    86.     },    /**     * @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: 计算两点之间距离     * @return {*}     * @author: 舒冬冬     */    getDistanceBetweenTwoPoints(a, b) {      const A = a[0] - b[0]      const B = a[1] - b[1]      const result = Math.sqrt(Math.pow(A, 2) + Math.pow(B, 2))      return result    },    /**     * @description: 计算数据的点位置     * @param {*} val 点占比     * @return {*}     * @author: 舒冬冬     */    calculationPointPosition(val) {      const LP = this.rotatePoint(this.point.left, this.point.top, this.topAngle.LTB * -1)
    87.       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],                  (this.point.bottom[1] - this.point.top[1]) * (item.accounted / 100) + this.point.top[1]                ]              ]            }          }        } else {          for (const key in temporary) {            const vertical = JSON.parse(JSON.stringify(temporary[key][2]))            if (key === 'left') {              // 垂直后点点距离              const vertical1 = [this.point.top[0], vertical[1] + (LP[1] - this.point.top[1]) * (item.accounted / 100)]              // 还原后点的位置              temporary.left = [                this.point.top,                this.rotatePoint(vertical1, this.point.top, this.topAngle.LTB),                vertical1              ]            } else if (key === 'right') {              // 垂直后点点距离              const vertical1 = [this.point.top[0], vertical[1] + (RP[1] - this.point.top[1]) * (item.accounted / 100)]              // 还原后点的位置              temporary.right = [                this.point.top,                this.rotatePoint(vertical1, this.point.top, this.topAngle.RTB * -1),                vertical1              ]            } else if (key === 'middle') {              temporary.middle = [                this.point.top,                [this.point.top[0], (this.point.bottom[1] - this.point.top[1]) * (item.accounted / 100) + vertical[1]],                [this.point.top[0], (this.point.bottom[1] - this.point.top[1]) * (item.accounted / 100) + vertical[1]]              ]            }          }        }        return { ...item, temporary: JSON.parse(JSON.stringify(temporary)) }      })      this.dataInfo = dataInfo    },    /**     * @description: 判断鼠标在哪层位置上     * @param {*}     * @return {*}     * @author: 舒冬冬     */    determineDataMouse(mouseLocation) {      let req = false      for (let index = 0; index < this.dataInfo.length; index++) {        if (this.insidePolygon(this.dataInfo[index].drawingPoint, mouseLocation)) {          return (req = { l: index + 1, obj: this.dataInfo[index] })        }      }      return req    },    // ======================================绘图==========================================    /**     * @description: 绘画主体     * @param {*}     * @return {*}     * @author: 舒冬冬     */    paintingBody() {      // 左半边金字塔阴影      this.ctx.fillStyle = 'rgba(120,120,120,.15)'      this.ctx.beginPath()      this.ctx.moveTo(...this.point.top)      this.ctx.lineTo(...this.point.bottom)      this.ctx.lineTo(...this.point.left)      this.ctx.fill()      this.ctx.fill()    },    /**     * @description: 数据图层绘画     * @param {*}     * @return {*}     * @author: 舒冬冬     */    paintDataInfo() {      var index = -1      this.dataInfo = this.dataInfo.map(item => {        index++        if (this.integration.color.length === index) {          index = 0        }        return { ...item, color: this.integration.color[index] }      })      this.dataInfo = this.dataInfo.map((item, index) => {        let drawingPoint = []        this.ctx.fillStyle = item.color        this.ctx.beginPath()        let point1, point2, point3, point4, point5, point6        if (index === 0) {          [point1, point2, point3, point4, point5, point6] = [            item.temporary.left[0],            item.temporary.left[1],            item.temporary.middle[1],            item.temporary.right[1],            item.temporary.right[0],            item.temporary.middle[0]          ]        } else {          [point1, point2, point3, point4, point5, point6] = [            this.dataInfo[index - 1].temporary.left[1],            item.temporary.left[1],            item.temporary.middle[1],            item.temporary.right[1],            this.dataInfo[index - 1].temporary.right[1],            this.dataInfo[index - 1].temporary.middle[1]          ]        }        this.ctx.moveTo(...point1)        this.ctx.lineTo(...point2)        this.ctx.lineTo(...point3)        this.ctx.lineTo(...point4)        this.ctx.lineTo(...point5)        this.ctx.lineTo(...point6)        drawingPoint = [point1, point2, point3, point4, point5, point6]        if (this.integration.infoStyle.stroke) {          this.ctx.shadowOffsetX = 0          this.ctx.shadowOffsetY = 0          this.ctx.shadowBlur = 2          this.ctx.shadowColor = this.integration.infoStyle.strokeColor        }        this.ctx.fill()        return { ...item, drawingPoint }      })    },    /**     * @description: 绘画字体     * 此方法请在 paintDataInfo() 执行后使用     * @param {*}     * @return {*}     * @author: 舒冬冬     */    paintingText(lData) {      this.ctx.shadowColor = 'rgba(90,90,90,0)'      const color = this.integration.infoStyle.color ? this.integration.infoStyle.color : '#fff'      const width = this.integration.infoStyle.width ? this.integration.infoStyle.width : 0      const dotSize = this.integration.infoStyle.dotSize ? this.integration.infoStyle.dotSize : 4      const offset = this.integration.infoStyle.offset ? this.integration.infoStyle.offset : [0, 0]      let text = ''      this.ctx.strokeStyle = color      this.ctx.fillStyle = color      this.dataInfo.forEach((item, index) => {        if (item.drawingPoint) {          let line = [            [0, 0],            [0, 0]          ]          this.ctx.font = `normal lighter ${            this.integration.infoStyle.size ? this.integration.infoStyle.size : 14          }px sans-serif `          this.ctx.beginPath()          if (lData && index + 1 === lData.l) {            line = [              [                lData.obj.drawingPoint[2][0],                (lData.obj.drawingPoint[2][1] - lData.obj.drawingPoint[5][1]) / 2 + lData.obj.drawingPoint[5][1]              ],              [                lData.obj.drawingPoint[2][0] + lData.obj.drawingPoint[2][0] / 2 + width,                (lData.obj.drawingPoint[2][1] - lData.obj.drawingPoint[5][1]) / 2 + lData.obj.drawingPoint[5][1]              ]            ]            this.ctx.font = `normal lighter ${              this.integration.infoStyle.size ? this.integration.infoStyle.size + 2 : 16            }px sans-serif `            text =              this.integration.fontFormatter(item) !== 'default'                ? this.integration.fontFormatter(item)                : lData.obj.value + ' ---- ' + lData.obj.name            this.ctx.setLineDash([0, 0])            this.ctx.strokeText(              text,              line[1][0] + offset[0],              line[1][1] + (this.integration.infoStyle.size ? this.integration.infoStyle.size + 2 : 14) / 3 + offset[1]            )          } else {            line = [              [                item.drawingPoint[2][0],                (item.drawingPoint[2][1] - item.drawingPoint[5][1]) / 2 + item.drawingPoint[5][1]              ],              [                item.drawingPoint[2][0] + item.drawingPoint[2][0] / 2 + width,                (item.drawingPoint[2][1] - item.drawingPoint[5][1]) / 2 + item.drawingPoint[5][1]              ]            ]            text =              this.integration.fontFormatter(item) !== 'default'                ? this.integration.fontFormatter(item)                : item.value + ' ----- ' + item.name            this.ctx.setLineDash([0, 0])            this.ctx.strokeText(              text,              line[1][0] + offset[0],              line[1][1] + (this.integration.infoStyle.size ? this.integration.infoStyle.size + 2 : 16) / 3 + offset[1]            )          }          this.ctx.setLineDash(this.integration.infoStyle.setLineDash)          this.ctx.moveTo(...line[0])          this.ctx.lineTo(...line[1])          this.ctx.stroke()          this.ctx.arc(...line[0], dotSize, 0, 360, false)          this.ctx.fill() //画实心圆        } else {          throw '未找到 drawingPoint 属性'        }      })    },    /**     * @description: 显示tooltip位置     * @param {*} lData 当前层级     * @param {*} coordinates 鼠标位置     * @return {*}     * @author: 舒冬冬     */    showTooltip(lData, coordinates) {      let canvasWarpper = document.getElementById('canvas-warpper')      let canvasTooltip = document.getElementById('canvas-tooltip')      if (lData) {        canvasTooltip.style.zIndex = this.integration.tooltip.z        canvasTooltip.style.transition =          ' opacity 0.2s cubic-bezier(0.23, 1, 0.32, 1) 0s, visibility 0.2s cubic-bezier(0.23, 1, 0.32, 1) 0s,transform 0.15s'        let html = JSON.parse(JSON.stringify(this.tooltipDiv))        if (this.integration.tooltip.formatter) {          html = this.integration.tooltip.formatter(lData)        } else {          const searchVal = [            ['$[title]$', lData.obj.title],            ['$[name]$', lData.obj.name],            ['$[val]$', lData.obj.value],            ['$[color]$', lData.obj.color],            ['$[fontSize]$', this.integration.tooltip.fontSize],            ['$[backgroundColor]$', this.integration.tooltip.backgroundColor],            ['$[fontColor]$', this.integration.tooltip.fontColor]          ]          searchVal.forEach(el => {            html = html.replaceAll(...el)          })        }        canvasTooltip.innerHTML = html        canvasWarpper.style.cursor = 'pointer'        canvasTooltip.style.visibility = 'visible'        canvasTooltip.style.opacity = 1        let [x, y] = coordinates        x = x + 20        y = y + 20        // 画布高度        // canvasHeight: 0,        // 画布宽度        // canvasWidth: 0,        // 判断是否超出框架内容        if (x + canvasTooltip.clientWidth > this.canvasWidth) {          x = x - canvasTooltip.clientWidth - 40        }        if (y + canvasTooltip.clientHeight > this.canvasHeight) {          y = y - canvasTooltip.clientHeight - 40        }        canvasTooltip.style.transform = `translate3d(${x}px, ${y}px, 0px)`      } else {        canvasWarpper.style.cursor = 'default'        canvasTooltip.style.visibility = 'hidden'        canvasTooltip.style.opacity = 0      }    },    /**     * @description: 高亮某一层级     * @param {*} lData 层级数据     * @return {*}     * @author: 舒冬冬     */    highlightCurrentRegion(lData) {      this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)      if (!lData) {        this.paintDataInfo()        this.ctx.shadowColor = 'rgba(90,90,90,0)'        this.paintingBody()        this.paintingText()        return      }      this.paintDataInfo()      this.ctx.shadowColor = 'rgba(90,90,90,0)'      this.paintingBody()      this.ctx.fillStyle = lData.obj.color      //  this.ctx.scale(1.05, 1.05)      this.ctx.beginPath()      this.ctx.moveTo(lData.obj.drawingPoint[0][0], lData.obj.drawingPoint[0][1])      this.ctx.lineTo(lData.obj.drawingPoint[1][0], lData.obj.drawingPoint[1][1])      this.ctx.lineTo(lData.obj.drawingPoint[2][0], lData.obj.drawingPoint[2][1])      this.ctx.lineTo(lData.obj.drawingPoint[3][0], lData.obj.drawingPoint[3][1])      this.ctx.lineTo(lData.obj.drawingPoint[4][0], lData.obj.drawingPoint[4][1])      this.ctx.lineTo(lData.obj.drawingPoint[5][0], lData.obj.drawingPoint[5][1])      this.ctx.shadowOffsetX = 0      this.ctx.shadowOffsetY = 0      this.ctx.shadowBlur = 10      this.ctx.shadowColor = this.integration.infoStyle.highlightedColor      this.ctx.fill()      // 阴影绘制      this.ctx.beginPath()      this.ctx.moveTo(lData.obj.drawingPoint[0][0], lData.obj.drawingPoint[0][1])      this.ctx.lineTo(lData.obj.drawingPoint[1][0], lData.obj.drawingPoint[1][1])      this.ctx.lineTo(lData.obj.drawingPoint[2][0], lData.obj.drawingPoint[2][1])      this.ctx.lineTo(lData.obj.drawingPoint[5][0], lData.obj.drawingPoint[5][1])      this.ctx.fillStyle = 'rgba(120,120,120,.15)'      this.ctx.fill()      this.paintingText(lData)    }  }}</script>
    复制代码
    结尾

    项目地址:(https://github.com/SHDjason/Pyramid.git)
    到此这篇关于使用canvas仿Echarts实现金字塔图的实例代码的文章就介绍到这了,更多相关canvas仿Echarts金字塔图内容请搜索脚本之家以前的文章或继续浏览下面的相关文章,希望大家以后多多支持脚本之家!

    来源:https://www.jb51.net/html5/797068.html
    免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

    本帖子中包含更多资源

    您需要 登录 才可以下载或查看,没有账号?立即注册

    ×

    最新评论

    浏览过的版块

    QQ Archiver 手机版 小黑屋 福建二哥 ( 闽ICP备2022004717号|闽公网安备35052402000345号 )

    Powered by Discuz! X3.5 © 2001-2023

    快速回复 返回顶部 返回列表