vue3 实现并封装一个瀑布图组件

瀑布图

背景

在图表展示的业务场景中,常用的各种直方图、折线图和饼图;但是想要对大量流式数据有个直观的展示,这就需要瀑布图了。瀑布图多应用于频谱数据管理,通过对比不同的色差很容易找到对应的频率,接下来我们展示一下Vue3如何实现一个瀑布图的效果。

原理

利用canvasdrawImage API,通过将不同数值的数据转化为色素点,流式数据的一帧一帧的加载,整个图像就像瀑布一样流下来,所以被称为瀑布图,对于寻找对应频率信号很有用。

canvasdrawImage 方法介绍

const ctx = canvas.getContext('2d')
ctx.drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh)
  • image 表示页面的图片,或者canvas图像
  • 参数sx, sy 代表从源图片的剪切坐标
  • 参数 sw, sh 代表从源图片的剪切宽高
  • 参数dx, dy 代表目标图片/canvas的粘贴坐标
  • 参数 sw, sh 代表目标图片/canvas的粘贴宽高

代码

接下来,我们一起看代码实现

初始化容器和瀑布图数据

  • 创建一个div,作为我们瀑布图的容器

      <template>
        <div ref="heatmap" style="height: 300px;" />
      </template>
    
      <script setup> 
        const heatmap = ref(null)
      </script>
    
  • 定义props传入data,通过监听data的变化,更新瀑布图

      const props = defineProps({
        data: {
          type: Array,
          default: () => []
        },
        minDb: {
          type: Number, // 颜色图例最小值
          default: 0
        },
        maxDb: {
          type: Number, // 颜色图例最大值
          default: 0
        }
      })
      const legendWidth = 120 // 图例宽度
      const tempHeight = 50 // 临时画布高度,也是一页瀑布图帧数
      const containerHeight = 300 // 容器高度
      watchEffect(() => {
        updateWaterFallPlot(props.data)
      })
    

定义 canvas 上下文和 引用ColorMap 完成左侧图例

首先下载ColorMap, 命令行中执行npm install ColorMap -S命令

import ColorMap from 'colormap'

const state = reactive({
  canvasCtx: null,
  fallsCanvasCtx: null,
  legendCanvasCtx: null,
  canvasWidth: 0,
  colormap: []
})

初始化主要变量

const initComponent = () => {
  if (!heatmap.value) {
    return
  }
  // 获取容器宽高
  const { clientWidth, clientHeight } = heatmap.value
  // 初始化颜色图
  const colormap = initColormap()
  // 创建画布
  const { fallsCanvasCtx, canvasCtx, legendCanvasCtx, canvas } = createCanvas(clientWidth, clientHeight)
  // 绘制左边颜色图图例
  drawLegend(canvasCtx, legendCanvasCtx, colormap)
  state.canvasCtx = canvasCtx
  state.colormap = colormap
  state.fallsCanvasCtx = fallsCanvasCtx
  state.legendCanvasCtx = legendCanvasCtx
  state.canvasDom = canvas
}
  • canvasCtx 瀑布图canvas的上下文
  • fallsCanvasCtx 临时画布上下文,用于平铺所有数据
  • legendCanvasCtx 图例canvas的上下文,用于画图例
  • createCanvas 创建画布,生成三个上下文
  • drawLegend 创建颜色图例
createCanvas 创建画布

创建主体的画布,图例画布和临时画布

const createCanvas = (width, height) => {
  // 创建用来绘制的画布
  const fallsCanvas = document.createElement('canvas')
  fallsCanvas.width = 0
  fallsCanvas.height = height
  const fallsCanvasCtx = fallsCanvas.getContext('2d')

  // 创建最终展示的画布
  const canvas = document.createElement('canvas')
  canvas.className = 'main_canvas'
  canvas.height = height - 2
  canvas.width = width
  heatmap.value.appendChild(canvas) // 唯一显示的canvas
  const canvasCtx = canvas.getContext('2d')

  // 创建图例图层画布
  const legendCanvas = document.createElement('canvas')
  legendCanvas.width = 1
  const legendCanvasCtx = legendCanvas.getContext('2d')
  return {
    fallsCanvasCtx,
    canvasCtx,
    legendCanvasCtx,
    canvas
  }
}

绘制颜色图例

// 绘制颜色图图例
const drawLegend = (canvasCtx, legendCanvasCtx, colormap) => {
  const imageData = legendCanvasCtx.createImageData(1, colormap.length)
  // 遍历颜色图集合
  for (let i = 0; i < colormap.length; i++) {
    const color = colormap[i]
    imageData.data[imageData.data.length - i * 4 + 0] = color[0]
    imageData.data[imageData.data.length - i * 4 + 1] = color[1]
    imageData.data[imageData.data.length - i * 4 + 2] = color[2]
    imageData.data[imageData.data.length - i * 4 + 3] = 255
  }
  // 将颜色集合放到图例上
  legendCanvasCtx.putImageData(imageData, 0, 0)
  canvasCtx.drawImage(
    legendCanvasCtx.canvas,
    0, // source x
    0, // source y
    1, // source width
    colormap.length, // souce height
    40, // d x 目标
    0, // d y 目标
    legendWidth / 4, // d width 
    canvasCtx.canvas.height// d height
  )
  canvasCtx.font = '12px Arial'
  canvasCtx.textAlign = 'end'
  canvasCtx.fillStyle = '#fff'
  // 绘制图例刻度
  const x = (legendWidth * 3) / 4 - 10
  canvasCtx.fillText(100, x, 12)
  canvasCtx.fillText(0, x, containerHeight - 6)
  const dur = (props.maxDb - props.minDb) / 10
  for (let i = 1; i < 10; i++) {
    canvasCtx.fillText(props.minDb + dur * i, x, containerHeight * (10 - i) / 10 + i)
  }
}

根据传入data更新瀑布图 updateWaterFallPlot

// 统计更新渲染次数
const renderNum = ref(0)
const updateWaterFallPlot = data => {
  const len = data.length
  if (len !== state.canvasWidth) {
    state.canvasWidth = len
    state.fallsCanvasCtx.canvas.width = len
  }
  renderNum.value ++
  // 先在用于绘制的画布上绘制图像  
  addWaterfallRow(data)
  // 再将画好的图像显示再页面中
  drawFallsOnCanvas(len)
  
  if (renderNum.value > tempHeight) {
    // 如果渲染次数大于容器高度,更新容器高度
    state.canvasDom.height = renderNum.value * containerHeight / tempHeight
  }
}
临时画布上绘制图像 addWaterfallRow
const firstRender = ref(true)
const addWaterfallRow = data => {
  // 先将已生成的图像向下移动一个像素, 第一次渲染不用移动
  const height = 50 // 临时画布的高度
  if (!firstRender.value) {
    state.fallsCanvasCtx.drawImage(
      state.fallsCanvasCtx.canvas, // 当前cavas
      0,
      0,
      data.length,
      height,
      0,
      1,
      data.length,
      height
    )
  } else {
    firstRender.value = false
  }

  // 再画一行的数据
  const imageData = rowToImageData(data)
  state.fallsCanvasCtx.putImageData(imageData, 0, 0)
}
绘制单行图像
const rowToImageData = data => {
  const imageData = state.fallsCanvasCtx.createImageData(data.length, 1)
  for (let i = 0; i < imageData.data.length; i += 4) {
    const cIndex = getCurrentColorIndex(data[i / 4])
    const color = state.colormap[cIndex]
    imageData.data[i + 0] = color[0]
    imageData.data[i + 1] = color[1]
    imageData.data[i + 2] = color[2]
    imageData.data[i + 3] = 255
  }
  return imageData
}

// 获取数据对应的颜色图索引
const getCurrentColorIndex = data => {
  const outMin = 0
  const outMax = state.colormap.length - 1
  if (data <= props.minDb) {
    return outMin
  } else if (data >= props.maxDb) {
    return outMax
  } else {
    return round(((data - props.minDb) / (props.maxDb - props.minDb)) * outMax)
  }
}
将临时画布的图像剪切到正式瀑布图上 drawFallsOnCanvas
// 将绘制好的图像显示在主页面中
const drawFallsOnCanvas = len => {
  const canvasWidth = state.canvasCtx.canvas.width
  const canvasHeight = state.canvasCtx.canvas.height
  if (!state.fallsCanvasCtx.canvas.width) return
  state.canvasCtx.drawImage(
    state.fallsCanvasCtx.canvas,
    -1,
    0,
    len + 1,
    tempHeight,
    legendWidth + 5,
    0,
    canvasWidth - legendWidth - 25,
    canvasHeight
  )
}

调用示例

  <waterFallPlot
    :data="dataList"
    :min-db="100"
    :max-db="0"
  />

模拟流数据

function getData(n, num = 90) {
  var arr = [],
    i,
    a,
    b,
    c,
    spike;
  for (i = 0; i < n; i = i + 1) {
    if (i % 10 === 0) {
      a = 2 * Math.random();
    }
    if (i % 100 === 0) {
      b = 2 * Math.random();
    }
    if (i % 1000 === 0) {
      c = 2 * Math.random();
    }
    if (i % 500 < 5) {
      spike = 100 * Math.random();
    } else {
      spike = 0;
    }
    arr.push(2 * Math.sin(i / 100) + a + b + c + spike + Math.random() * 5 - num);
  }
  return arr;
}
const dataList = ref([])
setInterval(() => {
  dataList.value = getData(1000, 90)
}, 1000)

瀑布图展示

瀑布图

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值