宁外一篇文章基础上进行封装优化,在vue中利用highcharts+colormap+canvas实现频谱瀑布图
介绍一下基础引用 colormap+echarts
封装频谱图
<!--
* @FilePath: \systemmonitor\view\src\components\common\Spectrum.vue
* @Description: 频谱图
-->
<template>
<div id="Frequency" class=" h100 w100"></div>
</template>
<script>
import { ToFixedVAl, findLabel, findValue, sizereverse, freqReverse, kHzReverse, HzReverse, customvalidateErrorStr } from '@/utils/util.js'
export default {
data() {
return {
ToFixedVAl:ToFixedVAl,
myChart: null, //频谱图
//频谱变化Echart实例的数据项
spectrumOption: {
animation:false,
title: {
show: false,
text: '频谱',
right: 10,
top: 10,
textStyle: {
color: '#BCBCBC',
fontSize: 12
}
},
tooltip: {
show: true,
trigger: 'axis',
padding: [4, 6, 4, 6],
textStyle: {
fontSize: 14
},
formatter: params => {
return '功率:' + ToFixedVAl(params[0].data[1], 4) + ' dBm' + '<br/>' + '频率:' + params[0].data[0] + ' MHz'
}
},
dataZoom: [
{
show: false,
type: 'slider',
filterMode: 'weakFilter',
showDataShadow: true,
bottom: 16,
height: 8,
startValue: 950,
endValue: 2150,
borderColor: 'transparent',
backgroundColor: '#e2e2e2',
handleIcon:
'path://M10.7,11.9H9.3c-4.9,0.3-8.8,4.4-8.8,9.4c0,5,3.9,9.1,8.8,9.4h1.3c4.9-0.3,8.8-4.4,8.8-9.4C19.5,16.3,15.6,12.2,10.7,11.9z M13.3,24.4H6.7v-1.2h6.6z M13.3,22H6.7v-1.2h6.6z M13.3,19.6H6.7v-1.2h6.6z', // jshint ignore:line
handleSize: 20,
handleStyle: {
shadowBlur: 6,
shadowOffsetX: 1,
shadowOffsetY: 2,
shadowColor: '#aaa'
},
labelFormatter: ''
},
{
type: 'inside',
filterMode: 'weakFilter'
}
],
grid: {
left: 20,
right: 30,
bottom: 6,
top: 15,
containLabel: true
},
xAxis: {
show: true,
// boundaryGap: true,
onZero: false,
// name: 'MHz',
nameTextStyle: {
fontSize: 10,
color: '#c1c2c6'
},
min: 950,
max: 2150,
minorTick: {
show: false,
lineStyle: {
color: '#797979'
}
},
minorSplitLine: {
show: false,
lineStyle: {
color: '#797979'
}
},
splitLine: {
show: true,
lineStyle: {
type: 'dashed',
color: '#797979'
}
},
axisTick: {
show: false
},
axisLabel: {
show: true,
textStyle: {
color: '#c1c2c6'
}
},
axisLine: {
show: true,
onZero: false,
lineStyle: {
type: 'solid',
color: '#797979'
}
// symbol: ['none', 'arrow'],
// symbolOffset: 10,
// symbolSize: [10, 10]
}
},
yAxis: {
// name: 'dBm',
nameTextStyle: {
fontSize: 10,
color: '#c1c2c6'
},
min: -120,
max: 0,
show: true,
splitLine: {
show: true,
lineStyle: {
type: 'dashed',
color: '#797979'
}
},
axisTick: {
show: false
},
axisLine: {
show: true,
lineStyle: {
type: 'solid',
color: '#797979'
}
},
axisLabel: {
show: true,
lineStyle: {
type: 'solid',
color: '#797979'
},
textStyle: {
color: this.$store.state.theme == 'lightTheme' ? '#606266' : '#c1c2c6',
fontSize: '12'
}
}
// axisLine: {
// show:true,
// lineStyle: {
// type: 'solid',
// color: '#797979'
// },
// // symbol: ['none', 'arrow'],
// // symbolOffset: 10,
// // symbolSize: [10, 10]
// }
},
series: [
{
type: 'line',
smooth: true,
showSymbol: false,
// tooltip: {
// show: true,
// trigger:'item',
// formatter:(e)=>{
// console.log(e)
// }
// },
// clip: false,
// itemStyle: {
// normal: {
lineStyle: {
width: '1',
color: '#56a641',
type: 'solid' // dotted虚线 solid:实线
},
// }
// },
emphasis: {
lineStyle: {
width: '1'
}
},
data: [
// [950, 0], [1000, 300], [1050, 0]
],
markArea: {
tooltip: {
show: false
},
itemStyle: {
color: 'rgba(64,158,225,0.4)'
},
data: [
// [{ xAxis: 980 }, { xAxis: 1020 }], [{ xAxis: 990 }, { xAxis: 1010 }]
]
},
markLine: {
symbol: 'none',
label: {
fontSize: 10,
color: '#ff8000',
position: 'insideMiddleTop',
// formatter: 'EBEM:{c}MHz'
formatter: params => {
if (params.data.type == 'signal') {
return params.data.xAxis + ' MHz'
} else {
return ''
}
}
},
lineStyle: {
color: '#ff8000',
width: 2,
type: 'dashed'
},
data: []
}
}
]
},
}
},
mounted() {
// 初始化echarts
this.initCharts();
//画布收缩同步监听
this.resizedom();
},
methods: {
// 更新series中data数据
setSeries(data){
this.spectrumOption.series[0].data = data;
this.myChart.setOption({series:this.spectrumOption.series})
},
// 更新标记线和标记区域
SetMark(areaData,lineData){
this.spectrumOption.series[0].markArea.data = areaData;
this.spectrumOption.series[0].markLine.data = lineData;
// 仅更新series避免dataZoom重置问题
this.myChart.setOption({series:this.spectrumOption.series})
},
// 仅更新线标记
SetMarkLine(Arr){
this.spectrumOption.series[0].markLine.data = Arr;
// this.myChart.setOption(this.spectrumOption)
// 仅更新series避免dataZoom重置问题
this.myChart.setOption({series:this.spectrumOption.series})
},
// 重新设置频谱范围
resetSpec(start,end,type){
// 重新设置频谱范围
this.spectrumOption.dataZoom[0].startValue = start;
this.spectrumOption.dataZoom[0].endValue = end;
this.spectrumOption.xAxis.min = start;
this.spectrumOption.xAxis.max =end;
if(type){
this.spectrumOption.series[0].data = [];
this.spectrumOption.series[0].markArea.data = [];
this.spectrumOption.series[0].markLine.data = [];
}
this.myChart.setOption(this.spectrumOption);
},
// 初始化频谱
initCharts() {
this.myChart = this.$echarts.init(document.getElementById('Frequency'))
this.myChart.setOption(this.spectrumOption)
let _this = this;
this.myChart.on('datazoom', res => {
//res里可获取滚动条当前起始未知start、end,二者皆为百分比
//通过此方法直接获取到缩放后的横纵坐标最小值
// _this.$refs.WaterFall.changeRange(_this.myChart.getOption().dataZoom[0].startValue,_this.myChart.getOption().dataZoom[0].endValue)
_this.$emit('changeWaterRange',_this.myChart.getOption().dataZoom[0].startValue,_this.myChart.getOption().dataZoom[0].endValue )
});
},
// dom自适应
resizedom() {
this.resizeEcharts(document.getElementById('Frequency'), this.myChart)
},
// 监听dom变化
resizeEcharts(echart, myChart) {
// 这里的echart就是我们的dom元素指的是:
// this.$refs.echart 或者 document.getElementById(“echart”)
// 这里的myChart指的是初始化的echarts实例:
// const myChart = echarts.init(this.$refs.echart)
const elementResizeDetectorMaker = require('element-resize-detector') // 引入监听dom变化的组件
const erd = elementResizeDetectorMaker()
// 监听id为echart的元素 大小变化
erd.listenTo(echart, function (element) {
// const width = element.offsetWidth
// const height = element.offsetHeight
myChart.resize()
})
}
},
}
</script>
<style scoped>
#Frequency {
background: linear-gradient(180deg, #353535, #000000, #353535);
}
</style>
封装瀑布图
<!--
* @FilePath: \systemmonitor\view\src\components\common\WaterFall.vue
* @Description: 瀑布图
-->
<template>
<div class="h100 w100 flex waterfallBox">
<!--图例-->
<div class="legend" ref="lengedContent">
<canvas ref="spectrogramLeftLegend"></canvas>
</div>
<!--瀑布图-->
<div class="waterFall" ref="waterFallContent">
<canvas ref="spectrogramDivInStation"></canvas>
</div>
</div>
</template>
<script>
export default{
data() {
return {
legendMaxNum: 0, //图例最大值
legendMinNum: -120, //图例最小值
colormap: [], //颜色库
pectromgrameTempArray: [], //瀑布图二维数组(用来显示数据做的临时存储)
spectromgrameIntervalIndex: 0, //瀑布定时器用到的计数标识
spectromgrameCtx: null, //瀑布图canvas二维绘图对象
spectromgrameCanvas: null, //瀑布图
spectromgrameGroupLength: 150, //瀑布图纵向显示多少组数据
curWindowAxis: { //保存当前窗口可视范围的横坐标轴起始频点
startFreq: 950,
endFreq: 2150
},
}
},
mounted() {
//创建颜色库
this.setColormap();
//创建canvas瀑布图画布
this.isClickedSpectromgrame();
},
methods: {
// 设置更改区间范围
changeRange(start,end){
this.$set(this.curWindowAxis,'startFreq',start)
this.$set(this.curWindowAxis,'endFreq',end)
},
// 清空瀑布图内容
clearContent(){
this.spectromgrameCtx.clearRect(0,0,this.spectromgrameCanvas.width,this.spectromgrameCanvas.width);
},
//创建颜色库
setColormap() {
let dx = this;
let colormap = require('colormap');
dx.colormap = colormap({
colormap: 'jet',
nshades: 150,
format: 'rba',
alpha: 1
})
},
//创建瀑布图图例
createLegendCanvas() {
let dx = this
let legendRefs = dx.$refs.spectrogramLeftLegend; //获取图例
legendRefs.width = this.$refs.lengedContent.offsetWidth;
legendRefs.height = this.$refs.lengedContent.offsetHeight;
dx.spectrogramLegendCanvasContent = legendRefs.getContext('2d');
let legendCanvas = document.createElement('canvas');
legendCanvas.width = 1;
let legendCanvasTemporary = legendCanvas.getContext('2d');
const imageData = legendCanvasTemporary.createImageData(1, dx.colormap.length);
for (let i = 0; i < dx.colormap.length; i++) {
const color = dx.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
}
legendCanvasTemporary.putImageData(imageData, 0, 0)
dx.spectrogramLegendCanvasContent.drawImage(legendCanvasTemporary.canvas,
0, 0, 1, dx.colormap.length, 0, 1, 30, legendRefs.height)
},
//创建瀑布图
createSpectrogrameCanvas() {
let _this = this;
this.spectromgrameCanvas = this.$refs.spectrogramDivInStation;
this.spectromgrameCtx = this.spectromgrameCanvas.getContext("2d");
this.spectromgrameCanvas.width = this.$refs.waterFallContent.offsetWidth;
this.spectromgrameCanvas.height = this.$refs.waterFallContent.offsetHeight; //瀑布图div不包括外边距和边框
},
getRad: function (degree) {
return degree / 180 * Math.PI;
},
//操作瀑布图选择框的回调方法
isClickedSpectromgrame: function () {
let _this = this;
this.$nextTick(() => {
_this.createLegendCanvas(); //绘制图例
_this.createSpectrogrameCanvas(); //创建瀑布图
})
},
//返回数据对应的Colormap颜色
colorMapData: function (data, outMin, outMax) {
let result;
if (data <= this.legendMinNum) {
result = outMin;
} else if (data >= this.legendMaxNum) {
result = outMax;
} else {
//Math.round四舍六入
result = Math.round(((data - this.legendMinNum) / (this.legendMaxNum - this.legendMinNum)) * (outMax - outMin));
}
return result;
},
/**
* @desc: 绘制单次频谱对应的瀑布图像
* @date: 2022/8/8
**/
drawRowToSpectromgrame: function (data) {
// TODO:后续需要优化根据实际数据创建canvas像素图
data = data.filter(item => item[0] >= this.curWindowAxis.startFreq && item[0] <= this.curWindowAxis.endFreq);
let _this = this;
this.$nextTick(() => {
let allCanvasHeight = _this.spectromgrameCanvas.height; //绘制瀑布图的区域除开底部坐标轴
let allCanvasWidth = _this.spectromgrameCanvas.width;
let canvasHeight = Math.ceil(allCanvasHeight / _this.spectromgrameGroupLength); //瀑布图每行的高度
let canvasWidth = Math.ceil(allCanvasWidth / data.length); //瀑布图,每个点占用的长度
/**** 注意:getImageData 的参考坐标始终在左上角和变换坐标轴无任何关系,即时将坐标轴远点修改到左下角 ***/
//第一块Mod板瀑布图
if (_this.spectromgrameCtx != null) {
let freqXisLeftValue = this.curWindowAxis.startFreq; //开始频点转换为MHz(时频图仅根据当前视图窗口可见范围绘制)
let freqXisRightValue = this.curWindowAxis.endFreq; //结束频点转换为MHz(时频图仅根据当前视图窗口可见范围绘制)
//获取到最近一次绘制的瀑布图数据图像
//指定区域(矩形左顶点从画布左小角(0,00)开始,长度为画布长度获取整个画布区间的图像,每一次将旧的图像上移)
let imgOld = _this.spectromgrameCtx.getImageData(0, 0, allCanvasWidth, allCanvasHeight);
const imageData = _this.spectromgrameCtx.createImageData(allCanvasWidth, 1); //创建了一组(行)新的瀑布图(单次频谱数据)
let pointStartIndex = Math.round((data[0][0] - freqXisLeftValue) / (freqXisRightValue - freqXisLeftValue) * allCanvasWidth);
//当上报频谱第一个载波频率点起始位置不等于坐标轴左侧点,需要将空白地方按最低电平值上色
if (pointStartIndex > 1) {
for (let leftIndex = 0; leftIndex < pointStartIndex; leftIndex++) {
let cindex = _this.colorMapData(data[0][1], 0, 130);
let leftColor = _this.colormap[cindex];
for (let j = 0; j < canvasWidth; j++) {
imageData.data[(leftIndex) * 4 + 0 + 4 * j] = leftColor[0];
imageData.data[(leftIndex) * 4 + 1 + 4 * j] = leftColor[1];
imageData.data[(leftIndex) * 4 + 2 + 4 * j] = leftColor[2];
imageData.data[(leftIndex) * 4 + 3 + 4 * j] = 255;
}
}
}
//遍历瀑布图数据给当行数据瀑布图上色
for (let pointIndex = 0; pointIndex < data.length; pointIndex++) {
let imageItemX = Math.round((data[pointIndex][0] - freqXisLeftValue) / (freqXisRightValue - freqXisLeftValue) * allCanvasWidth);
let spectrogrumDataIndex = _this.colorMapData(data[pointIndex][1], 0, 130);
let color = _this.colormap[spectrogrumDataIndex];
if (imageItemX == 0) {
imageItemX = imageItemX + 1;
}
for (let j = 0; j < canvasWidth; j++) {
imageData.data[(imageItemX - 1) * 4 + 0 + 4 * j] = color[0];
imageData.data[(imageItemX - 1) * 4 + 1 + 4 * j] = color[1];
imageData.data[(imageItemX - 1) * 4 + 2 + 4 * j] = color[2];
imageData.data[(imageItemX - 1) * 4 + 3 + 4 * j] = 255;
}
}
//当上报FFT最后一个点不等于坐标轴右侧最大值,需要将空白地方按最低电平值上色
if(data.slice(-1)[0][0] < this.curWindowAxis.endFreq) {
let lastItemX = Math.round((data.slice(-1)[0][0]-freqXisLeftValue) /(freqXisRightValue - freqXisLeftValue) * allCanvasWidth);
for(let curPointIndex = lastItemX; curPointIndex*4<imageData.data.length; curPointIndex++) {
let colorIndex = _this.colorMapData(data.slice(-1)[0][1],0,130);
var lastSegColor = _this.colormap[colorIndex];
for (let j = 0; j < canvasWidth; j++) {
imageData.data[(curPointIndex)*4 + 0+ 4 * j] = lastSegColor[0];
imageData.data[(curPointIndex)*4 + 1+ 4 * j] = lastSegColor[1];
imageData.data[(curPointIndex)*4 + 2+ 4 * j] = lastSegColor[2];
imageData.data[(curPointIndex)*4 + 3+ 4 * j] =255;
}
}
}
//这里需要for循环绘制很多次的原因是因为,imageData单行像素的高度为1像素,而实际我们需要显示的>1,因此每一次的频谱数据要绘制多次
for (let i = 0; i < canvasHeight; i++) {
/**putImageData(imageData,x,y,dirtyX,dirtyY,dirtyWidth,dirtyHight);
* 参数已有依次为:
* 1.将要放置的画布对象;
* 2.对象左上角的x坐标;
* 3.对象左上角的y坐标;
* 4.(可选)以像素统计,在画布上放置图像的位置;
* 5.(可选)以像素计,画布上放置图像的位置Y;
* 6.放置图像的宽度
* 7.放置图像的高度
*/
_this.spectromgrameCtx.putImageData(imageData, 0, allCanvasHeight - 1 - i);
}
_this.spectromgrameCtx.putImageData(imgOld, 0, -canvasHeight); //将上一次canvas整个画布区域的频谱数据像素上移
}
})
},
},
}
</script>
<style scoped>
.legend {
width: 46px;
height: 100%;
overflow: hidden;
text-align: right;
}
.legend canvas {
width: 30px;
height: 100%;
margin-left: 8px;
}
.waterFall {
width: calc(100% - 55px);
height: 100%;
position: relative;
}
.waterFall canvas {
width: calc(100% - 30px);
height: 100%;
}
.waterfallBox {
background: linear-gradient(180deg, #353535, #000000, #1f1f1f);
}
</style>
引用
<template>
<div class="w100 h100">
<div class="waterBox w100">
<!-- 瀑布图 -->
<WaterFall ref="WaterFall"/>
</div>
<div class="freq w100">
<!-- 频谱 -->
<Spectrum ref="Spectrum" @changeWaterRange="changeWaterRange" />
</div>
</div>
</template>
<script>
import WaterFall from '../../common/WaterFall.vue';
import Spectrum from '../../common/Spectrum.vue';
export default {
components:{
WaterFall,
Spectrum
},
mounted(){
this.initMessage()
},
methods:{
//举例说明传递参数调用方法
getMessage(data){
// var seriesData=[[950, -20],[951, -30],[952, -40],...];
// 仅更新series避免dataZoom重置问题
this.$refs.Spectrum.setSeries(data)
//渲染瀑布图
this.$refs.WaterFall.drawRowToSpectromgrame(data);
},
// 频谱缩放时需要更新瀑布图范围 这个地方主要是频谱开启缩放 瀑布图范围需要跟着更新 所以需要借助父元素调用瀑布图中的方法
changeWaterRange(start,end){
this.$refs.WaterFall.changeRange(start,end)
},
//重新设置瀑布图和频谱范围
reserveRange(){
// 重新设置瀑布图和频谱范围
this.$refs.WaterFall.changeRange(950,2150)
this.$refs.Spectrum.resetSpec(950,2150,true)
// 清空瀑布图内容
this.$refs.WaterFall.clearContent();
},
initMessage(){
var pin=this.RandomNumBoth(40,60);
var data=[];
for(var i =950;i<2150;i+=0.3){
if(i>=1200&&i<=1230){
let nums=this.RandomNumBoth(60,70);
data.push([i,nums])
}else if(i>=1450&&i<=1500){
let nums=this.RandomNumBoth(50,60);
yData.push(nums)
data.push([i,nums])
}else if(i>=1800&&i<=1850){
let num1=this.RandomNumBoth(40,50);
data.push([i,num1])
}else{
let num=this.RandomNumBoth(2,30);
data.push([i,num])
}
}
this.getMessage(data)
},
// 生成范围区间的值
RandomNumBoth(Min, Max) {
var Range = Max - Min;
var Rand = Math.random();
var num = Min + Math.round(Rand * Range); //四舍五入
return num;
},
}
}
<script>
<style>
.freq {
height: 27.5vh;
}
.waterBox{
height: 15vh;
}
#Frequency {
background: linear-gradient(180deg, #353535, #000000, #353535);
}
.w100{
width:100vw;
}
.h100{
height:100vh;
}
</style>