经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » JS/JS库/框架 » Echarts » 查看文章
使用canvas仿Echarts实现金字塔图的实例代码_html5
来源:jb51  时间:2021/11/9 17:52:20  对本文有异议

前言

最近公司项目都偏向于数字化大屏展示🥱,而这次发给我的项目原型中出现了一个金字塔图🤔️, 好巧不巧,由于我们的图表都是使用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基本信息 并实现大小自适应

  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')
  2. // 创建canvas元素
  3. this.canvas = document.createElement('canvas')
  4. // 把canvas元素节点添加在el元素下
  5. el.appendChild(this.canvas)
  6. this.canvasWidth = el.offsetWidth
  7. this.canvasHeight = el.offsetHeight
  8. // 将canvas元素设置与父元素同宽
  9. this.canvas.setAttribute('width', this.canvasWidth)
  10. // 将canvas元素设置与父元素同高
  11. this.canvas.setAttribute('height', this.canvasHeight)
  12. this.canvasCenter = [
  13. Math.round((this.canvasWidth - this.integration.distance[0] * 2) / 2) + this.integration.distance[0],
  14. Math.round((this.canvasHeight - this.integration.distance[1] * 2) / 2) + this.integration.distance[1]
  15. ]
  16. if (this.canvas.getContext) {
  17. this.ctx = this.canvas.getContext('2d')
  18. // 金字塔基本点位置
  19. this.point.top = [this.canvasCenter[0] - this.canvasWidth / 13, this.integration.distance[1]]
  20. this.point.left = [
  21. this.integration.distance[0] * 1.5,
  22. this.canvasHeight - this.integration.distance[1] - this.canvasHeight / 5
  23. ]
  24. this.point.right = [
  25. this.canvasWidth - this.integration.distance[0] * 1.9,
  26. this.canvasHeight - this.integration.distance[1] - this.canvasHeight / 5
  27. ]
  28. this.point.bottom = [
  29. this.canvasCenter[0] - this.canvasWidth / 13,
  30. this.canvasHeight - this.integration.distance[1]
  31. ]
  32. this.point.shadow = [
  33. this.integration.distance[0] - this.canvasCenter[0] / 5,
  34. this.canvasHeight / 1.2 - this.integration.distance[1]
  35. ]
  36. for (const key in this.point) {
  37. this.point[key][0] = this.point[key][0] + this.integration.offset[0]
  38. this.point[key][1] = this.point[key][1] + this.integration.offset[1]
  39. }
  40. } else {
  41. throw 'canvas下未找到 getContext方法'
  42. }
  43. this.topAngle.LTB = this.angle(this.point.top, this.point.left, this.point.bottom)
  44. this.topAngle.RTB = this.angle(this.point.top, this.point.right, this.point.bottom)
  45. // 计算各数据点位置
  46. this.calculationPointPosition(this.dataInfo)
  47. },

计算金字塔每条边的角度

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

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

  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 角度后的 点的位置

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] ]

  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.  
  98. return { ...item, temporary: JSON.parse(JSON.stringify(temporary)) }
  99. })
  100. this.dataInfo = dataInfo
  101. },

这样就拿到了每个数据在每一条边上所占长度的点位。

绘画

数据图层绘画

我们虽然拿到了每个数据在每一条边上所占长度的点位。 那怎么获取这条数据在该边上的所在的线段长度呢?
很简单 因为 第一条数据的在该边长度的第二个点的位置就是第二条数据的第一个点的位置
现在就可以进行下一步。
数据 图层的绘画了

  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 的点的位置设为起点就行了

image.png

  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.  
  27. this.ctx.beginPath()
  28. if (lData && index + 1 === lData.l) {
  29. line = [
  30. [
  31. lData.obj.drawingPoint[2][0],
  32. (lData.obj.drawingPoint[2][1] - lData.obj.drawingPoint[5][1]) / 2 + lData.obj.drawingPoint[5][1]
  33. ],
  34. [
  35. lData.obj.drawingPoint[2][0] + lData.obj.drawingPoint[2][0] / 2 + width,
  36. (lData.obj.drawingPoint[2][1] - lData.obj.drawingPoint[5][1]) / 2 + lData.obj.drawingPoint[5][1]
  37. ]
  38. ]
  39.  
  40. this.ctx.font = `normal lighter ${
  41. this.integration.infoStyle.size ? this.integration.infoStyle.size + 2 : 16
  42. }px sans-serif `
  43. text =
  44. this.integration.fontFormatter(item) !== 'default'
  45. ? this.integration.fontFormatter(item)
  46. : lData.obj.value + ' ---- ' + lData.obj.name
  47. this.ctx.setLineDash([0, 0])
  48. this.ctx.strokeText(
  49. text,
  50. line[1][0] + offset[0],
  51. line[1][1] + (this.integration.infoStyle.size ? this.integration.infoStyle.size + 2 : 14) / 3 + offset[1]
  52. )
  53. } else {
  54. line = [
  55. [
  56. item.drawingPoint[2][0],
  57. (item.drawingPoint[2][1] - item.drawingPoint[5][1]) / 2 + item.drawingPoint[5][1]
  58. ],
  59. [
  60. item.drawingPoint[2][0] + item.drawingPoint[2][0] / 2 + width,
  61. (item.drawingPoint[2][1] - item.drawingPoint[5][1]) / 2 + item.drawingPoint[5][1]
  62. ]
  63. ]
  64. text =
  65. this.integration.fontFormatter(item) !== 'default'
  66. ? this.integration.fontFormatter(item)
  67. : item.value + ' ----- ' + item.name
  68. this.ctx.setLineDash([0, 0])
  69. this.ctx.strokeText(
  70. text,
  71. line[1][0] + offset[0],
  72. line[1][1] + (this.integration.infoStyle.size ? this.integration.infoStyle.size + 2 : 16) / 3 + offset[1]
  73. )
  74. }
  75. this.ctx.setLineDash(this.integration.infoStyle.setLineDash)
  76. this.ctx.moveTo(...line[0])
  77. this.ctx.lineTo(...line[1])
  78. this.ctx.stroke()
  79. this.ctx.arc(...line[0], dotSize, 0, 360, false)
  80. this.ctx.fill() //画实心圆
  81. } else {
  82. throw '未找到 drawingPoint 属性'
  83. }
  84. })
  85. },

高亮图层

高亮图层无非就是监听鼠标移入位置,并且判断鼠标移入位置是否存在图层内,在哪个图层内,然后重新绘画当前图层

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

显示tooltip位置

可以先定义 tooltip 的渲染模板

image.png

然后在代码上进行渲染

  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>
  6.  
  7. <script>
  8. export default {
  9. name: 'Pyramid',
  10. props: {
  11. options: {
  12. type: Object,
  13. default: () => {
  14. return {
  15. title: '',
  16. // 主体离边框距离
  17. distance: [0, 0],
  18. // 主体偏移值 (x,y)
  19. offset: [0, 0],
  20. // 排序(max , min)优先
  21. sort: '',
  22. // 颜色
  23. color: ['#80FFA5', '#00DDFF', '#37A2FF', '#FF0087', '#FFBF00'],
  24. // 格式化字体输出
  25. fontFormatter: () => {
  26. return 'default'
  27. },
  28. // tooltip信息配置
  29. tooltip: {
  30. show: true, // 是否显示
  31. fontColor: '#000', // 字体内部颜色
  32. fontSize: 14, // 字体大小
  33. backgroundColor: '#fff', // tooltip背景
  34. formatter: null, // 回调方法
  35. z: 999999 // tooltip z-index层级
  36. },
  37. // 样式
  38. infoStyle: {
  39. stroke: false, // 是否描边
  40. strokeColor: '#fff', //描边颜色
  41. size: null, // 字体大小
  42. color: null, //颜色
  43. highlightedColor: '#fff', // 高亮颜色
  44. setLineDash: [0, 0], // 虚线值
  45. width: -10, // 设置多少 就会在基础上加上设置的值
  46. offset: [0, 0], // 字体x,y的偏移度
  47. dotSize: 4 //点大小
  48. }
  49. }
  50. }
  51. },
  52.  
  53. // 渲染数据
  54. data: {
  55. type: Array,
  56. default: () => {
  57. return [
  58. { name: 'name1', value: 11 },
  59. { name: 'name2', value: 11 },
  60. { name: 'name3', value: 11 },
  61. { name: 'name4', value: 77 },
  62. { name: 'name5', value: 55 },
  63. { name: 'name6', value: 66 }
  64. ]
  65. }
  66. }
  67. },
  68. watch: {
  69. data: {
  70. immediate: true,
  71. deep: true,
  72. handler(newValue) {
  73. // 数据总量
  74. let totalData = 0
  75. newValue.forEach(element => {
  76. totalData = totalData + Number(element.value)
  77. })
  78. this.dataInfo = newValue.map(item => {
  79. const accounted = (item.value / totalData) * 100
  80. return { ...item, accounted, title: this.integration.title }
  81. })
  82. if (this.integration.sort === 'max') {
  83. this.dataInfo.sort((a, b) => {
  84. return a.value - b.value
  85. })
  86. } else if (this.integration.sort === 'min') {
  87. this.dataInfo.sort((a, b) => {
  88. return b.value - a.value
  89. })
  90. }
  91. }
  92. }
  93. },
  94. computed: {
  95. integration() {
  96. return {
  97. title: this.options.title ? this.options.title : '',
  98. // 主体离边框距离
  99. distance: this.options.distance ? this.options.distance : [0, 0],
  100. // 主体偏移值 (x,y)
  101. offset: this.options.offset ? this.options.offset : [0, 0],
  102. // 排序(max , min)优先
  103. sort: this.options.sort ? this.options.sort : '',
  104. // 颜色
  105. color: this.options.color ? this.options.color : ['#80FFA5', '#00DDFF', '#37A2FF', '#FF0087', '#FFBF00'],
  106. // 格式化字体输出
  107. fontFormatter: this.options.fontFormatter
  108. ? this.options.fontFormatter
  109. : () => {
  110. return 'default'
  111. },
  112. // tooltip显示
  113. tooltip: {
  114. show: this.options.tooltip ? (this.options.tooltip.show ? this.options.tooltip.show : true) : true, // 是否显示
  115. fontColor: this.options.tooltip
  116. ? this.options.tooltip.fontColor
  117. ? this.options.tooltip.fontColor
  118. : '#000'
  119. : '#000', // 字体内部颜色
  120. fontSize: this.options.tooltip ? (this.options.tooltip.fontSize ? this.options.tooltip.fontSize : 14) : 14, // 字体大小
  121. backgroundColor: this.options.tooltip
  122. ? this.options.tooltip.backgroundColor
  123. ? this.options.tooltip.backgroundColor
  124. : '#fff'
  125. : '#fff', // tooltip背景
  126. formatter: this.options.tooltip
  127. ? this.options.tooltip.formatter
  128. ? this.options.tooltip.formatter
  129. : null
  130. : null, // 返回方法
  131. z: this.options.tooltip ? (this.options.tooltip.z ? this.options.tooltip.z : 999999) : 999999 // tooltip z-index层级
  132. },
  133. // 样式
  134. infoStyle: {
  135. stroke: this.options.infoStyle
  136. ? this.options.infoStyle.stroke
  137. ? this.options.infoStyle.stroke
  138. : false
  139. : false, //是否描边
  140. strokeColor: this.options.infoStyle
  141. ? this.options.infoStyle.strokeColor
  142. ? this.options.infoStyle.strokeColor
  143. : '#fff'
  144. : '#fff', // 描边颜色
  145. size: this.options.infoStyle ? (this.options.infoStyle.size ? this.options.infoStyle.size : null) : null, // 字体大小
  146. color: this.options.infoStyle ? (this.options.infoStyle.color ? this.options.infoStyle.color : null) : null, //颜色
  147. width: this.options.infoStyle
  148. ? this.options.infoStyle.width || this.options.infoStyle.width !== 0
  149. ? this.options.infoStyle.width
  150. : -10
  151. : -10, // 设置多少 就会在基础上加上设置的值
  152. offset: this.options.infoStyle
  153. ? this.options.infoStyle.offset
  154. ? this.options.infoStyle.offset
  155. : [0, 0]
  156. : [0, 0], // 字体x,y的偏移度
  157. setLineDash: this.options.infoStyle
  158. ? this.options.infoStyle.setLineDash
  159. ? this.options.infoStyle.setLineDash
  160. : [0, 0]
  161. : [0, 0], //虚线值
  162. highlightedColor: this.options.infoStyle
  163. ? this.options.infoStyle.highlightedColor
  164. ? this.options.infoStyle.highlightedColor
  165. : '#fff'
  166. : '#fff', //高亮颜色
  167. dotSize: this.options.infoStyle
  168. ? this.options.infoStyle.dotSize || this.options.infoStyle.dotSize !== 0
  169. ? this.options.infoStyle.dotSize
  170. : 4
  171. : 4 //点大小
  172. }
  173. }
  174. }
  175. },
  176. data() {
  177. return {
  178. // canvas 主体
  179. canvas: null,
  180. // 图像渲染内容
  181. ctx: null,
  182. // 画布高度
  183. canvasHeight: 0,
  184. // 画布宽度
  185. canvasWidth: 0,
  186. // 画布中心点 [x,y]
  187. canvasCenter: [0, 0],
  188. // 金字塔四个点位置
  189. point: {
  190. top: [0, 0],
  191. left: [0, 0],
  192. right: [0, 0],
  193. bottom: [0, 0],
  194. shadow: [0, 0]
  195. },
  196. // 数据信息
  197. dataInfo: [],
  198. // 金字塔顶端角度信息
  199. topAngle: {
  200. LTB: 0,
  201. RTB: 0
  202. },
  203. // tooltip 模板
  204. tooltipDiv: `<div style="margin: 0px 0 0; line-height: 1;border-color: $[backgroundColor]$ ;background-color: $[backgroundColor]$;color: $[fontColor]$;
  205. 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;">
  206. <div style="margin: 0px 0 0; line-height: 1">
  207. <div style="font-size: $[fontSize]$px; color: $[fontColor]$; font-weight: 400; line-height: 1"> $[title]$ </div>
  208. <div style="margin: 10px 0 0; line-height: 1">
  209. <div style="margin: 0px 0 0; line-height: 1">
  210. <div style="margin: 0px 0 0; line-height: 1">
  211. <span
  212. style="
  213. display: inline-block;
  214. margin-right: 4px;
  215. border-radius: 10px;
  216. width: 10px;
  217. height: 10px;
  218. background-color: $[color]$;
  219. "
  220. ></span>
  221. <span style="font-size: $[fontSize]$px; color: $[fontColor]$; font-weight: 400; margin-left: 2px">$[name]$</span>
  222. <span style="float: right; margin-left: 20px; font-size: $[fontSize]$px; color: $[fontColor]$; font-weight: 900">$[val]$</span>
  223. <div style="clear: both"></div>
  224. </div>
  225. <div style="clear: both"></div>
  226. </div>
  227. <div style="clear: both"></div>
  228. </div>
  229. <div style="clear: both"></div>
  230. </div>
  231. <div style="clear: both"></div>
  232. </div>`
  233. }
  234. },
  235. mounted() {
  236. this.init()
  237. },
  238. methods: {
  239. init() {
  240. this.initCanvasBaseInfo()
  241. this.paintDataInfo()
  242. this.paintingText()
  243. this.paintingBody()
  244. this.eventRegistered()
  245. },
  246. /**
  247. * @description: 初始化canvas基本信息
  248. * @param {*}
  249. * @return {*}
  250. * @author: 舒冬冬
  251. */
  252. initCanvasBaseInfo() {
  253. let el = document.getElementById('canvas-warpper')
  254. // 创建canvas元素
  255. this.canvas = document.createElement('canvas')
  256. // 把canvas元素节点添加在el元素下
  257. el.appendChild(this.canvas)
  258. this.canvasWidth = el.offsetWidth
  259. this.canvasHeight = el.offsetHeight
  260. // 将canvas元素设置与父元素同宽
  261. this.canvas.setAttribute('width', this.canvasWidth)
  262. // 将canvas元素设置与父元素同高
  263. this.canvas.setAttribute('height', this.canvasHeight)
  264. this.canvasCenter = [
  265. Math.round((this.canvasWidth - this.integration.distance[0] * 2) / 2) + this.integration.distance[0],
  266. Math.round((this.canvasHeight - this.integration.distance[1] * 2) / 2) + this.integration.distance[1]
  267. ]
  268. if (this.canvas.getContext) {
  269. this.ctx = this.canvas.getContext('2d')
  270. // 金字塔基本点位置
  271. this.point.top = [this.canvasCenter[0] - this.canvasWidth / 13, this.integration.distance[1]]
  272. this.point.left = [
  273. this.integration.distance[0] * 1.5,
  274. this.canvasHeight - this.integration.distance[1] - this.canvasHeight / 5
  275. ]
  276. this.point.right = [
  277. this.canvasWidth - this.integration.distance[0] * 1.9,
  278. this.canvasHeight - this.integration.distance[1] - this.canvasHeight / 5
  279. ]
  280. this.point.bottom = [
  281. this.canvasCenter[0] - this.canvasWidth / 13,
  282. this.canvasHeight - this.integration.distance[1]
  283. ]
  284. this.point.shadow = [
  285. this.integration.distance[0] - this.canvasCenter[0] / 5,
  286. this.canvasHeight / 1.2 - this.integration.distance[1]
  287. ]
  288. for (const key in this.point) {
  289. this.point[key][0] = this.point[key][0] + this.integration.offset[0]
  290. this.point[key][1] = this.point[key][1] + this.integration.offset[1]
  291. }
  292. } else {
  293. throw 'canvas下未找到 getContext方法'
  294. }
  295. this.topAngle.LTB = this.angle(this.point.top, this.point.left, this.point.bottom)
  296. this.topAngle.RTB = this.angle(this.point.top, this.point.right, this.point.bottom)
  297. // 计算各数据点位置
  298. this.calculationPointPosition(this.dataInfo)
  299. },
  300. // ======================================事件==========================================
  301. /**
  302. * @description: 鼠标事件注册
  303. * @param {*}
  304. * @return {*}
  305. * @author: 舒冬冬
  306. */
  307. eventRegistered() {
  308. const canvasWarpper = document.getElementById('canvas-warpper')
  309. //注册事件
  310. canvasWarpper.addEventListener('mousedown', this.doMouseDown, false)
  311. canvasWarpper.addEventListener('mouseup', this.doMouseUp, false)
  312. canvasWarpper.addEventListener('mousemove', this.doMouseMove, false)
  313. // //注册事件
  314. // this.canvas.addEventListener('mousedown', this.doMouseDown, false)
  315. // this.canvas.addEventListener('mouseup', this.doMouseUp, false)
  316. // this.canvas.addEventListener('mousemove', this.doMouseMove, false)
  317. },
  318. /**
  319. * @description: 鼠标按下
  320. * @param {*} e
  321. * @return {*}
  322. * @author: 舒冬冬
  323. */
  324. // eslint-disable-next-line no-unused-vars
  325. doMouseDown(e) {},
  326. /**
  327. * @description: 鼠标弹起
  328. * @param {*} e
  329. * @return {*}
  330. * @author: 舒冬冬
  331. */
  332. // eslint-disable-next-line no-unused-vars
  333. doMouseUp(e) {},
  334. /**
  335. * @description: 鼠标移动
  336. * @param {*} e
  337. * @return {*}
  338. * @author: 舒冬冬
  339. */
  340. // eslint-disable-next-line no-unused-vars
  341. doMouseMove(e) {
  342. const x = e.pageX
  343. const y = e.pageY
  344. this.highlightCurrentRegion(this.determineDataMouse(this.getLocation(x, y)))
  345. if (this.integration.tooltip.show) {
  346. this.showTooltip(this.determineDataMouse(this.getLocation(x, y)), this.getLocation(x, y))
  347. }
  348. },
  349.  
  350. /**
  351. * @description 判断一个点是否在多边形内部
  352. * @param points 多边形坐标集合
  353. * @param testPoint 测试点坐标
  354. * @author: 舒冬冬
  355. * 返回true为真,false为假
  356. */
  357. insidePolygon(points, testPoint) {
  358. const x = testPoint[0],
  359. y = testPoint[1]
  360. let inside = false
  361. for (let i = 0, j = points.length - 1; i < points.length; j = i++) {
  362. const xi = points[i][0],
  363. yi = points[i][1]
  364. const xj = points[j][0],
  365. yj = points[j][1]
  366.  
  367. const intersect = yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi
  368. if (intersect) inside = !inside
  369. }
  370. return inside
  371. },
  372. /**
  373. * @description: 获取当前鼠标坐标
  374. * @param {*}
  375. * @return {*}
  376. * @author: 舒冬冬
  377. */
  378. getLocation(x, y) {
  379. const bbox = this.canvas.getBoundingClientRect()
  380. return [(x - bbox.left) * (this.canvas.width / bbox.width), (y - bbox.top) * (this.canvas.height / bbox.height)]
  381. },
  382. // ======================================算法==========================================
  383.  
  384. /**
  385. * @description: 根据A点旋转指定角度后B点的坐标位置
  386. * @param {*} ptSrc 圆上某点(初始点);
  387. * @param {*} ptRotationCenter 圆心点
  388. * @param {*} angle 旋转角度° -- [angle * M_PI / 180]:将角度换算为弧度
  389. * 【注意】angle 逆时针为正,顺时针为负
  390. * @return {*}
  391. * @author: 舒冬冬
  392. */
  393. rotatePoint(ptSrc, ptRotationCenter, angle) {
  394. const a = ptRotationCenter[0]
  395. const b = ptRotationCenter[1]
  396. const x0 = ptSrc[0]
  397. const y0 = ptSrc[1]
  398. const rx = a + (x0 - a) * Math.cos((angle * Math.PI) / 180) - (y0 - b) * Math.sin((angle * Math.PI) / 180)
  399. const ry = b + (x0 - a) * Math.sin((angle * Math.PI) / 180) + (y0 - b) * Math.cos((angle * Math.PI) / 180)
  400. const point = [rx, ry]
  401. return point
  402. },
  403.  
  404. /**
  405. * @description: 求3点之间角度
  406. * @return {*} 点 a 的角度
  407. * @author: 舒冬冬
  408. */
  409. angle(a, b, c) {
  410. const A = { X: a[0], Y: a[1] }
  411. const B = { X: b[0], Y: b[1] }
  412. const C = { X: c[0], Y: c[1] }
  413. const AB = Math.sqrt(Math.pow(A.X - B.X, 2) + Math.pow(A.Y - B.Y, 2))
  414. const AC = Math.sqrt(Math.pow(A.X - C.X, 2) + Math.pow(A.Y - C.Y, 2))
  415. const BC = Math.sqrt(Math.pow(B.X - C.X, 2) + Math.pow(B.Y - C.Y, 2))
  416. const cosA = (Math.pow(AB, 2) + Math.pow(AC, 2) - Math.pow(BC, 2)) / (2 * AB * AC)
  417. const angleA = Math.round((Math.acos(cosA) * 180) / Math.PI)
  418. return angleA
  419. },
  420. /**
  421. * @description: 计算两点之间距离
  422. * @return {*}
  423. * @author: 舒冬冬
  424. */
  425. getDistanceBetweenTwoPoints(a, b) {
  426. const A = a[0] - b[0]
  427. const B = a[1] - b[1]
  428. const result = Math.sqrt(Math.pow(A, 2) + Math.pow(B, 2))
  429. return result
  430. },
  431. /**
  432. * @description: 计算数据的点位置
  433. * @param {*} val 点占比
  434. * @return {*}
  435. * @author: 舒冬冬
  436. */
  437. calculationPointPosition(val) {
  438. const LP = this.rotatePoint(this.point.left, this.point.top, this.topAngle.LTB * -1)
  439. const RP = this.rotatePoint(this.point.right, this.point.top, this.topAngle.RTB)
  440. let temporary = {
  441. left: [
  442. [0, 0],
  443. [0, 0],
  444. [0, 0]
  445. ],
  446. right: [
  447. [0, 0],
  448. [0, 0],
  449. [0, 0]
  450. ],
  451. middle: [
  452. [0, 0],
  453. [0, 0],
  454. [0, 0]
  455. ]
  456. }
  457.  
  458. const dataInfo = val.map((item, index) => {
  459. if (index === 0) {
  460. for (const key in temporary) {
  461. if (key === 'left') {
  462. // 垂直后点的位置
  463. // 垂直后点点距离
  464. const vertical = [
  465. this.point.top[0],
  466. (LP[1] - this.point.top[1]) * (item.accounted / 100) + this.point.top[1]
  467. ]
  468. // 还原后点的位置
  469. temporary.left = [this.point.top, this.rotatePoint(vertical, this.point.top, this.topAngle.LTB), vertical]
  470. } else if (key === 'right') {
  471. // 垂直后点点距离
  472. const vertical = [
  473. this.point.top[0],
  474. (RP[1] - this.point.top[1]) * (item.accounted / 100) + this.point.top[1]
  475. ]
  476. // 还原后点的位置
  477. temporary.right = [
  478. this.point.top,
  479. this.rotatePoint(vertical, this.point.top, this.topAngle.RTB * -1),
  480. vertical
  481. ]
  482. } else if (key === 'middle') {
  483. // 垂直后点点距离
  484. temporary.middle = [
  485. this.point.top,
  486. [
  487. this.point.top[0],
  488. (this.point.bottom[1] - this.point.top[1]) * (item.accounted / 100) + this.point.top[1]
  489. ],
  490. [
  491. this.point.top[0],
  492. (this.point.bottom[1] - this.point.top[1]) * (item.accounted / 100) + this.point.top[1]
  493. ]
  494. ]
  495. }
  496. }
  497. } else {
  498. for (const key in temporary) {
  499. const vertical = JSON.parse(JSON.stringify(temporary[key][2]))
  500. if (key === 'left') {
  501. // 垂直后点点距离
  502. const vertical1 = [this.point.top[0], vertical[1] + (LP[1] - this.point.top[1]) * (item.accounted / 100)]
  503. // 还原后点的位置
  504. temporary.left = [
  505. this.point.top,
  506. this.rotatePoint(vertical1, this.point.top, this.topAngle.LTB),
  507. vertical1
  508. ]
  509. } else if (key === 'right') {
  510. // 垂直后点点距离
  511. const vertical1 = [this.point.top[0], vertical[1] + (RP[1] - this.point.top[1]) * (item.accounted / 100)]
  512. // 还原后点的位置
  513. temporary.right = [
  514. this.point.top,
  515. this.rotatePoint(vertical1, this.point.top, this.topAngle.RTB * -1),
  516. vertical1
  517. ]
  518. } else if (key === 'middle') {
  519. temporary.middle = [
  520. this.point.top,
  521. [this.point.top[0], (this.point.bottom[1] - this.point.top[1]) * (item.accounted / 100) + vertical[1]],
  522. [this.point.top[0], (this.point.bottom[1] - this.point.top[1]) * (item.accounted / 100) + vertical[1]]
  523. ]
  524. }
  525. }
  526. }
  527.  
  528. return { ...item, temporary: JSON.parse(JSON.stringify(temporary)) }
  529. })
  530. this.dataInfo = dataInfo
  531. },
  532. /**
  533. * @description: 判断鼠标在哪层位置上
  534. * @param {*}
  535. * @return {*}
  536. * @author: 舒冬冬
  537. */
  538. determineDataMouse(mouseLocation) {
  539. let req = false
  540. for (let index = 0; index < this.dataInfo.length; index++) {
  541. if (this.insidePolygon(this.dataInfo[index].drawingPoint, mouseLocation)) {
  542. return (req = { l: index + 1, obj: this.dataInfo[index] })
  543. }
  544. }
  545. return req
  546. },
  547. // ======================================绘图==========================================
  548. /**
  549. * @description: 绘画主体
  550. * @param {*}
  551. * @return {*}
  552. * @author: 舒冬冬
  553. */
  554. paintingBody() {
  555. // 左半边金字塔阴影
  556. this.ctx.fillStyle = 'rgba(120,120,120,.15)'
  557. this.ctx.beginPath()
  558. this.ctx.moveTo(...this.point.top)
  559. this.ctx.lineTo(...this.point.bottom)
  560. this.ctx.lineTo(...this.point.left)
  561. this.ctx.fill()
  562.  
  563. this.ctx.fill()
  564. },
  565. /**
  566. * @description: 数据图层绘画
  567. * @param {*}
  568. * @return {*}
  569. * @author: 舒冬冬
  570. */
  571. paintDataInfo() {
  572. var index = -1
  573. this.dataInfo = this.dataInfo.map(item => {
  574. index++
  575. if (this.integration.color.length === index) {
  576. index = 0
  577. }
  578. return { ...item, color: this.integration.color[index] }
  579. })
  580. this.dataInfo = this.dataInfo.map((item, index) => {
  581. let drawingPoint = []
  582. this.ctx.fillStyle = item.color
  583. this.ctx.beginPath()
  584. let point1, point2, point3, point4, point5, point6
  585. if (index === 0) {
  586. [point1, point2, point3, point4, point5, point6] = [
  587. item.temporary.left[0],
  588. item.temporary.left[1],
  589. item.temporary.middle[1],
  590. item.temporary.right[1],
  591. item.temporary.right[0],
  592. item.temporary.middle[0]
  593. ]
  594. } else {
  595. [point1, point2, point3, point4, point5, point6] = [
  596. this.dataInfo[index - 1].temporary.left[1],
  597. item.temporary.left[1],
  598. item.temporary.middle[1],
  599. item.temporary.right[1],
  600. this.dataInfo[index - 1].temporary.right[1],
  601. this.dataInfo[index - 1].temporary.middle[1]
  602. ]
  603. }
  604. this.ctx.moveTo(...point1)
  605. this.ctx.lineTo(...point2)
  606. this.ctx.lineTo(...point3)
  607. this.ctx.lineTo(...point4)
  608. this.ctx.lineTo(...point5)
  609. this.ctx.lineTo(...point6)
  610. drawingPoint = [point1, point2, point3, point4, point5, point6]
  611. if (this.integration.infoStyle.stroke) {
  612. this.ctx.shadowOffsetX = 0
  613. this.ctx.shadowOffsetY = 0
  614. this.ctx.shadowBlur = 2
  615. this.ctx.shadowColor = this.integration.infoStyle.strokeColor
  616. }
  617. this.ctx.fill()
  618. return { ...item, drawingPoint }
  619. })
  620. },
  621. /**
  622. * @description: 绘画字体
  623. * 此方法请在 paintDataInfo() 执行后使用
  624. * @param {*}
  625. * @return {*}
  626. * @author: 舒冬冬
  627. */
  628. paintingText(lData) {
  629. this.ctx.shadowColor = 'rgba(90,90,90,0)'
  630. const color = this.integration.infoStyle.color ? this.integration.infoStyle.color : '#fff'
  631. const width = this.integration.infoStyle.width ? this.integration.infoStyle.width : 0
  632. const dotSize = this.integration.infoStyle.dotSize ? this.integration.infoStyle.dotSize : 4
  633. const offset = this.integration.infoStyle.offset ? this.integration.infoStyle.offset : [0, 0]
  634. let text = ''
  635. this.ctx.strokeStyle = color
  636. this.ctx.fillStyle = color
  637. this.dataInfo.forEach((item, index) => {
  638. if (item.drawingPoint) {
  639. let line = [
  640. [0, 0],
  641. [0, 0]
  642. ]
  643. this.ctx.font = `normal lighter ${
  644. this.integration.infoStyle.size ? this.integration.infoStyle.size : 14
  645. }px sans-serif `
  646.  
  647. this.ctx.beginPath()
  648. if (lData && index + 1 === lData.l) {
  649. line = [
  650. [
  651. lData.obj.drawingPoint[2][0],
  652. (lData.obj.drawingPoint[2][1] - lData.obj.drawingPoint[5][1]) / 2 + lData.obj.drawingPoint[5][1]
  653. ],
  654. [
  655. lData.obj.drawingPoint[2][0] + lData.obj.drawingPoint[2][0] / 2 + width,
  656. (lData.obj.drawingPoint[2][1] - lData.obj.drawingPoint[5][1]) / 2 + lData.obj.drawingPoint[5][1]
  657. ]
  658. ]
  659.  
  660. this.ctx.font = `normal lighter ${
  661. this.integration.infoStyle.size ? this.integration.infoStyle.size + 2 : 16
  662. }px sans-serif `
  663. text =
  664. this.integration.fontFormatter(item) !== 'default'
  665. ? this.integration.fontFormatter(item)
  666. : lData.obj.value + ' ---- ' + lData.obj.name
  667. this.ctx.setLineDash([0, 0])
  668. this.ctx.strokeText(
  669. text,
  670. line[1][0] + offset[0],
  671. line[1][1] + (this.integration.infoStyle.size ? this.integration.infoStyle.size + 2 : 14) / 3 + offset[1]
  672. )
  673. } else {
  674. line = [
  675. [
  676. item.drawingPoint[2][0],
  677. (item.drawingPoint[2][1] - item.drawingPoint[5][1]) / 2 + item.drawingPoint[5][1]
  678. ],
  679. [
  680. item.drawingPoint[2][0] + item.drawingPoint[2][0] / 2 + width,
  681. (item.drawingPoint[2][1] - item.drawingPoint[5][1]) / 2 + item.drawingPoint[5][1]
  682. ]
  683. ]
  684. text =
  685. this.integration.fontFormatter(item) !== 'default'
  686. ? this.integration.fontFormatter(item)
  687. : item.value + ' ----- ' + item.name
  688. this.ctx.setLineDash([0, 0])
  689. this.ctx.strokeText(
  690. text,
  691. line[1][0] + offset[0],
  692. line[1][1] + (this.integration.infoStyle.size ? this.integration.infoStyle.size + 2 : 16) / 3 + offset[1]
  693. )
  694. }
  695. this.ctx.setLineDash(this.integration.infoStyle.setLineDash)
  696. this.ctx.moveTo(...line[0])
  697. this.ctx.lineTo(...line[1])
  698. this.ctx.stroke()
  699. this.ctx.arc(...line[0], dotSize, 0, 360, false)
  700. this.ctx.fill() //画实心圆
  701. } else {
  702. throw '未找到 drawingPoint 属性'
  703. }
  704. })
  705. },
  706. /**
  707. * @description: 显示tooltip位置
  708. * @param {*} lData 当前层级
  709. * @param {*} coordinates 鼠标位置
  710. * @return {*}
  711. * @author: 舒冬冬
  712. */
  713. showTooltip(lData, coordinates) {
  714. let canvasWarpper = document.getElementById('canvas-warpper')
  715. let canvasTooltip = document.getElementById('canvas-tooltip')
  716. if (lData) {
  717. canvasTooltip.style.zIndex = this.integration.tooltip.z
  718. canvasTooltip.style.transition =
  719. ' 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'
  720. let html = JSON.parse(JSON.stringify(this.tooltipDiv))
  721. if (this.integration.tooltip.formatter) {
  722. html = this.integration.tooltip.formatter(lData)
  723. } else {
  724. const searchVal = [
  725. ['$[title]$', lData.obj.title],
  726. ['$[name]$', lData.obj.name],
  727. ['$[val]$', lData.obj.value],
  728. ['$[color]$', lData.obj.color],
  729. ['$[fontSize]$', this.integration.tooltip.fontSize],
  730. ['$[backgroundColor]$', this.integration.tooltip.backgroundColor],
  731. ['$[fontColor]$', this.integration.tooltip.fontColor]
  732. ]
  733. searchVal.forEach(el => {
  734. html = html.replaceAll(...el)
  735. })
  736. }
  737. canvasTooltip.innerHTML = html
  738. canvasWarpper.style.cursor = 'pointer'
  739. canvasTooltip.style.visibility = 'visible'
  740. canvasTooltip.style.opacity = 1
  741. let [x, y] = coordinates
  742. x = x + 20
  743. y = y + 20
  744. // 画布高度
  745. // canvasHeight: 0,
  746. // 画布宽度
  747. // canvasWidth: 0,
  748. // 判断是否超出框架内容
  749. if (x + canvasTooltip.clientWidth > this.canvasWidth) {
  750. x = x - canvasTooltip.clientWidth - 40
  751. }
  752. if (y + canvasTooltip.clientHeight > this.canvasHeight) {
  753. y = y - canvasTooltip.clientHeight - 40
  754. }
  755. canvasTooltip.style.transform = `translate3d(${x}px, ${y}px, 0px)`
  756. } else {
  757. canvasWarpper.style.cursor = 'default'
  758. canvasTooltip.style.visibility = 'hidden'
  759. canvasTooltip.style.opacity = 0
  760. }
  761. },
  762. /**
  763. * @description: 高亮某一层级
  764. * @param {*} lData 层级数据
  765. * @return {*}
  766. * @author: 舒冬冬
  767. */
  768. highlightCurrentRegion(lData) {
  769.  
  770. this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
  771. if (!lData) {
  772. this.paintDataInfo()
  773. this.ctx.shadowColor = 'rgba(90,90,90,0)'
  774. this.paintingBody()
  775. this.paintingText()
  776. return
  777. }
  778. this.paintDataInfo()
  779. this.ctx.shadowColor = 'rgba(90,90,90,0)'
  780. this.paintingBody()
  781. this.ctx.fillStyle = lData.obj.color
  782. // this.ctx.scale(1.05, 1.05)
  783. this.ctx.beginPath()
  784. this.ctx.moveTo(lData.obj.drawingPoint[0][0], lData.obj.drawingPoint[0][1])
  785. this.ctx.lineTo(lData.obj.drawingPoint[1][0], lData.obj.drawingPoint[1][1])
  786. this.ctx.lineTo(lData.obj.drawingPoint[2][0], lData.obj.drawingPoint[2][1])
  787. this.ctx.lineTo(lData.obj.drawingPoint[3][0], lData.obj.drawingPoint[3][1])
  788. this.ctx.lineTo(lData.obj.drawingPoint[4][0], lData.obj.drawingPoint[4][1])
  789. this.ctx.lineTo(lData.obj.drawingPoint[5][0], lData.obj.drawingPoint[5][1])
  790. this.ctx.shadowOffsetX = 0
  791. this.ctx.shadowOffsetY = 0
  792. this.ctx.shadowBlur = 10
  793. this.ctx.shadowColor = this.integration.infoStyle.highlightedColor
  794. this.ctx.fill()
  795. // 阴影绘制
  796. this.ctx.beginPath()
  797. this.ctx.moveTo(lData.obj.drawingPoint[0][0], lData.obj.drawingPoint[0][1])
  798. this.ctx.lineTo(lData.obj.drawingPoint[1][0], lData.obj.drawingPoint[1][1])
  799. this.ctx.lineTo(lData.obj.drawingPoint[2][0], lData.obj.drawingPoint[2][1])
  800. this.ctx.lineTo(lData.obj.drawingPoint[5][0], lData.obj.drawingPoint[5][1])
  801. this.ctx.fillStyle = 'rgba(120,120,120,.15)'
  802. this.ctx.fill()
  803. this.paintingText(lData)
  804. }
  805. }
  806. }
  807. </script>
  808.  
  809.  

结尾

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

到此这篇关于使用canvas仿Echarts实现金字塔图的实例代码的文章就介绍到这了,更多相关canvas仿Echarts金字塔图内容请搜索w3xue以前的文章或继续浏览下面的相关文章,希望大家以后多多支持w3xue!

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号