前端-uniapp-频谱瀑布图制作

483 阅读3分钟

效果展示

image.png

引入颜色生成器 colormap

  1. 初始化vue环境,引入colormap依赖
npm i colormap
  1. 导入
const colormap = require('colormap')
  1. 初始化colormap
created() {
    // 此处生成 150 个渐变色
  this.colormap = colormap({
    colormap: 'jet',
    nshades: 150,
    format: 'rba',
    alpha: 1
  });
},

制作瀑布图图例

  1. 画布样式调整
<!-- 瀑布图图例 -->
<view>
  <canvas canvas-id="legend"
    id="legend"
    style="margin-left:60rpx; margin-right: 5rpx; width: 10px; height: 150px;">
  </canvas>
</view>
  1. 初始化图例
initLegend() {
  // 构建像素点数据
  let colorList = [];
  for (let i = this.colormap.length - 1; i >= 0; i--) {
    const color = this.colormap[i];
    colorList.push(color[0])
    colorList.push(color[1])
    colorList.push(color[2])
    colorList.push(255)
  }
  // 图像像素点数据,一维数组,每四项表示一个像素点的rgba
  const data = new Uint8ClampedArray(colorList)
  // 声明画布对象
  const legend = uni.createCanvasContext('legend');
  // 将像素点绘制在画布上
  // 绘制时宽度和高度注意保持一致,否则会绘制失败
  uni.canvasPutImageData({
    canvasId: 'legend',
    x: 0,
    y: 0,
    width: 1,
    data: data,
    success(res) {
      // 当前画布指定区域的内容导出生成指定大小的图片
      uni.canvasToTempFilePath({
        canvasId: 'legend',
        x: 0,
        y: 0,
        width: 1,
        heigth: CANVAS_HEIGHT,
        quality: 1,
        success(res) {
          // 绘制图像至画图
          legend.drawImage(res.tempFilePath, 0, 0, 10, CANVAS_HEIGHT)
          legend.draw();
        }
      })
    },
    fail(res) {
      console.info("失败标识:", res)
    }
  }, this)
},

绘制瀑布图

1. 布局代码
<view>
  <canvas canvas-id="soldierCanvas"
    style="height: 150px; width: 300px;"
    ref="myCanvas">
  </canvas>
</view>
  1. 逻辑代码 2.1 取色值
/**
* 取色值
* data: 对应y轴数据
* outMin: 颜色下限取值
* outMax: 颜色上限取值
* lineUpLimit: y轴曲线上限值
*/
squeeze(data, outMin, outMax) {
  if (data <= 0) {
    return outMin
  } else if (data >= this.lineUpLimit) {
    return outMax
  } else {
    return Math.round(data / this.lineUpLimit * outMax)
  }
},

2.2 绘制瀑布图

async drawWaterfallCurve(points) {
  const that = this;
  // 调整采样点
  points = this.adjustSampleData(points);
  let dataLength = points.length;
  // 获取像素点集合
  let arr = [];
  for (let i = 0; i < dataLength; i++) {
    const cindex = this.squeeze(points[i], 0, 149)
    const color = this.colormap[cindex]
    arr.push(color[0])
    arr.push(color[1])
    arr.push(color[2])
    arr.push(255)
  }
  let data = new Uint8ClampedArray(arr)
  points = null;
  arr = [];
  // 1. 截取画布全图像
  uni.canvasToTempFilePath({
    canvasId: 'soldierCanvas',
    x: 0,
    y: 0,
    width: CANVAS_WIDTH,
    height: CANVAS_HEIGHT,
    quality: 0.5,
    success(res) {
      // 截取图后,重新绘制到画图上并使其向下移动 1像素 距离
      that.soldierCanvas.drawImage(res.tempFilePath, 0, 1, CANVAS_WIDTH, CANVAS_HEIGHT);
      that.soldierCanvas.draw(true, () => {
        res = null;
        // 绘制 1像素 高的图像在画图起始位置
        uni.canvasPutImageData({
          canvasId: 'soldierCanvas',
          x: 0,
          y: 0,
          width: CANVAS_WIDTH,
          data: data,
          success(res) {
            data = null;
          },
          fail(res) {
            console.info("失败标识:", res)
          }
        })
      })
    },
    fail(res) {
      console.info("获取图像失败标识:", res)
    }
  })
},

2.3 数据抽样算法

仅供参考
因画图宽度像素点有限,对应展示数据有限,需对上报数据进行处理
画布整体左右拖动逻辑暂无,所以需要对数据采样进行处理

adjustSampleData(points) {
  // 1. 获取振动曲线纵坐标范围
  let yAxisStart = this.yStartLocation ? this.yStartLocation : 0;
  let yAxisEnd = this.yEndLocation ? this.yEndLocation : CANVAS_WIDTH;
  // // 排除异常情况
  if (yAxisStart >= yAxisEnd) {
    yAxisStart = 0;
    yAxisEnd = CANVAS_WIDTH;
  }
  // 2. 截取
  let renderPoints = points.slice(yAxisStart, yAxisEnd);
  // 3. 判断点数是否为 300(匹配画布像素点)
  let renderPointLength = renderPoints.length;
  let remainder = 0;
  if (renderPointLength < CANVAS_WIDTH) {
    // 待补位数
    remainder = CANVAS_WIDTH - (renderPointLength % CANVAS_WIDTH);
    // while循环次数
    let loopCount = Math.floor(remainder / renderPointLength) + 1;
    // while循环控制条件
    let _loopCount = 1;
    // 已补点数
    let remainder_count = 0;
    while (_loopCount <= loopCount) {
      for (let i = 0; i < renderPoints.length; i += _loopCount) {
        if (remainder_count !== remainder) {
          // 在当前位置之后插入元素
          renderPoints.splice(i + 1, 0, renderPoints[i]);
          i++;
          remainder_count++;
          continue;
        }
      }
      _loopCount++;
    }
  } else if (renderPointLength > CANVAS_WIDTH) {
    // ============= 方式一 抽样取整 =================
    // 间隔取点数
    let interval = Math.floor(renderPointLength / CANVAS_WIDTH);
    // 剩余数
    let surplus = renderPointLength % CANVAS_WIDTH;
    let surplusCount = 0;
    let temp_arr = [];
    for (let i = 0; i < renderPoints.length; i += interval) {
      if (surplusCount !== surplus) {
        let tempArr = renderPoints.slice(i, i + interval + 1);
        let temp_max = Math.max(...tempArr);
        temp_arr.push(temp_max);
        i++;
        surplusCount++;
        continue;
      }
      if (interval === 1) {
        temp_arr.push(renderPoints[i]);
      } else {
        let tempArr = renderPoints.slice(i, i + interval);
        let temp_max = Math.max(...tempArr);
        temp_arr.push(temp_max);
      }
    }
    renderPoints = temp_arr;
    // ================= 方式二 补整抽样 ===================
    // 待补0位数
    // remainder = CANVAS_WIDTH - (renderPointLength % CANVAS_WIDTH);
    // let remainder_count = 0;
    // for (let i = 0; i < renderPoints.length; i++) {
    //   // 补0
    //   if (remainder_count === remainder) {
    //     break;
    //   }
    //   renderPoints.splice(i + 1, 0, 0);
    //   i++;
    //   remainder_count++;
    // }
    // // 取点间隔
    // let interval = renderPoints.length / CANVAS_WIDTH;
    // let temp_arr = [];
    // for (let j = 0; j < renderPoints.length; j += interval) {
    //   let temp_max = 0;
    //   for (let k = 0; k < interval - 1; k++) {
    //     temp_max = Math.max(renderPoints[j + k], renderPoints[j + k + 1])
    //   }
    //   temp_arr.push(temp_max);
    // }
    // renderPoints = temp_arr;
    // temp_arr = [];
  }
  return renderPoints;
},