当前位置: 首页 > Web前端 > HTML5

Vue中使用WebSocket+HighCharts+Canvas实现高性能频谱图瀑布

时间:2023-04-05 21:00:13 HTML5

作者:codexu_废话不多说,先上传成品图:再放个小动画:可能很多同学不知道频谱图和瀑布图,在其实我也不懂。。。但是我们前端负责把数据按照规则展示出来(上面的折线图是频谱图,下面的是瀑布图)。技术选型框架:Vue(这个不重要,反正不说)数据传输:WebSocket谱图:HighCharts瀑布图:Canvas为什么要用WebSocket?因为服务器需要实时传输数据,所以要求达到30帧,每帧动画由1024个点组成,绝对比Ajax轮询舒服,而且本项目对浏览器兼容性没有要求。为什么要用HighCharts画频谱图?我做了一个测试,HighCharts和ECharts,canvas的性能虽然比svg好,但是同时渲染感觉HighCharts更流畅(HighCharts需要付费)。为什么瀑布图使用Canvas?虽然使用数据可视化图表库很方便,但是考虑到本项目对性能要求苛刻,所以只使用瀑布图这样的大范围热图:使用热图,请放心,PPT不会卡,而且浏览器会准时5秒后直接Crash。组件功能拆分整个组件拆分为三部分:父组件:负责WebSocket与服务器的实时通信,处理二进制数据,控制渲染频率,控制启动暂停,刷新组件。子组件>频谱图(HighCharts):提供addData方法,获取渲染一帧的数据,提供触发缩放事件,发送给父组件。Subcomponent>WaterfallChart(Canvas):提供和频谱图一样的addData方法。缩放事件发生后,频谱图将根据其选定的位置进行缩放。父组件WebSocket链接服务器操作不多,直接使用native方法:this.socket=newWebSocket('ws://192.168.2.250:8100/socket')this.socket.onopen=()=>{...}this.socket.onclose=()=>{...}连接后端并发送指令,这里我们定义三个://开始获取数据this.socket.send('start')//暂停获取数据this.socket.send('pause')//恢复获取数据this.socket.send('resume')监听onmessage事件:this.socket.onmessage=(event)=>{constreader=newFileReader()reader.readAsArrayBuffer(event.data)reader.onload=e=>{if(e.target.readyState===FileReader.DONE){//处理二进制数据}}}处理二进制数据本来想用大篇幅来写,不过前几天看到《为什么视频网站的视频链接地址是blob?》,写的很好,惭愧,请转载看懂这篇,别忘了再来。控制渲染频率。服务器每秒发送大约400条数据。每秒400帧肯定不现实,直接导致丢帧。解决方法:创建一个数组保存数据,每渲染一帧就删除这条数据,不足100条时发送resume继续采集,超过400条时发送pause暂停采集。服务端发送频率方面,cpu使用率会超过100%,获取的时候会有一点卡顿,不过可以接受,毕竟一次可以渲染好几秒时间。this.renderInterval=setInterval(()=>{if(this.data.length<=100&&this.socketPause===true){this.socket.send('resume')this.socketPause=false}if(this.data.length>=400&&this.socketPause===false){this.socket.send('pause')this.socketPause=true}if(this.data.length<=0)returnconstresult=this.data[0]this.$refs.frequency.addData(result.data)this.$refs.waterFall.addData(result.data.map(item=>item[1]))this.data.shift()},this.refreshInterval)使用setInterval定时渲染的另一个好处是可以控制渲染频率。注意组件右上角的拖动条,在低端电脑上可以降低渲染频率。SpectrogramHighCharts和ECharts在配置项上有一些差异,但都是配置问题。查文档,很简单,记得关掉所有动画。addData()this.chart.series[0].setData(data,true,false)父组件可以通过$ref.addData()触发渲染一帧缩放在配置中chart.zoomType设置为'x',设置为X轴选择缩放。chart.events.selection配置选择事件:selection(event){constpointWidth=(this.xAxisMax-this.xAxisMin)/1024constponitStart=Math.floor((event.xAxis[0].min-this.xAxisMin)/pointWidth)constponitEnd=Math.floor((event.xAxis[0].max-this.xAxisMin)/pointWidth)this.$emit('frequencySelect',[ponitStart,ponitEnd])},将选中的发送给parent组件点,然后通过父组件传递给瀑布图组件。由于性能原因,此处的瀑布图与某些库分离。很多朋友在这里不知道怎么操作。这是本文的重点。先了解几个概念。很多人都接触过Canvas,但是这几个大概没怎么关注过它(像素操作):createImageData()putImageData()drawImage()这个应该知道先创建两个canvas,一个用来显示整个效果(this.canvas),另一个保存生成的图片(this.waterFallDom,不会插入到dom上)。this.canvas=document.createElement('canvas')this.ctx=this.canvas.getContext('2d')this.waterFallDom=document.createElement('canvas')this.waterFallCtx=this.waterFallDom.getContext('2d')createImageDatacreateImageData(width,height)方法创建一个新的空白ImageData对象,两个参数,设置图片的宽高,这里工程一共需要1024个点:constimageData=this.waterFallCtx.createImageData(data.length,1)这时候生成了一张1024*1的空白图片,我们继续给每个像素点上色:for(leti=0;i=this.maxDb){returnoutMax}else{returnMath.round((data-this.minDb)/(this.maxDb-this.minDb)*outMax)}}colormap是一个二维数组,每个值代表[r,g,b,a],这里我生成了150种颜色,是渐变色,大家可以看看下面的图例是怎么生成colormap的?如果你打算用手写,那也没关系,只是手疼而已。推荐使用npm安装colormapthis.colormap=colormap({colormap:'jet',nshades:150,format:'rba',alpha:1})提供了多种配色,具体请参考文档.至此,我们生成了一张颜色为1024*1的图像,当然还是图像对象。putImageDataputImageData(imgData,x,y,dirtyX,dirtyY,dirtyWidth,dirtyHeight)将图像数据绘制到画布上:imgData:指定要放回画布上的ImageData对象。x:ImageData对象左上角的x坐标,以像素为单位。y:ImageData对象左上角的y坐标,以像素为单位。dirtyX:可选。水平值(x),以像素为单位,图像在画布上的放置位置。dirtyY:可选。水平值(y),以像素为单位,图像在画布上的放置位置。dirtyWidth:可选。用于在画布上绘制图像的宽度。dirtyHeight:可选。用于在画布上绘制图像的高度。this.waterFallCtx.putImageData(imageData,0,0)drawImage大家比较熟悉,就是在画布上绘制图像,那么我们就可以将this.waterFallCtx绘制到this.ctx中。drawImage(img,sx,sy,swidth,sheight,x,y,width,height);img:指定要使用的图像、画布或视频。sx:可选。开始剪裁的x坐标位置。sy:可选。开始剪裁的y坐标位置。宽度:可选。裁剪图像的宽度。尺寸:可选。裁剪图像的高度。x:将图像放置在画布上的x坐标位置。y:将图像放置在画布上的y坐标位置。宽度:可选。要使用的图像的宽度。(拉伸或收缩图像)高度:可选。要使用的图像的高度。(放大或缩小图像)this.ctx.drawImage(this.waterFallCtx.canvas,0,0,1024,1,0,0,width,height)这里sx和sy可以配合声谱图进行缩放操作。宽度和高度可以拉伸或缩小,显示效果比较好。比如只有两个像素的图片,拉伸到1000个像素时,就不是两种颜色的二分之一,而是完美的渐变。逼真的动态瀑布图上面我们将第一行图像绘制到画布中。这时候我们可能已经通过WebSockt获取了上百条数据。每次添加新一行图像时,上一行图像将成为下一行://将生成的图像向下移动一个像素this.waterFallCtx.drawImage(this.waterFallCtx.canvas,0,0,1024,300-1,0,1,1024,300-1)300表示一共保存300行图像,这些数据不应该是固定的,需要提前设置好,这里是为了演示方便。通过调用它自己的图像并重新绘制到它自己的图像,沿y轴向下偏移1个像素,高度为-1。这样,我们每增加一条数据,上面就会多出一行新的图像,生成的图像就会向下移动一个像素,我们的图像就从那时开始移动了。实现频谱图的缩放操作已经完成,并将起点和终点传给父组件,再由父组件传给瀑布组件,动态修改drawImage的clipping属性。结论由于这个项目是用Vue做的,所以我打算花时间把它做成一个Vue插件,做更多的可配置项。我曾经觉得这是一个不可能完成的项目,因为我太年轻了。公司老总提了几点建议,就做了。我将在这里与您分享。无论项目多么困难,总会有解决方案。