Author: codexu


Without further ado, first on the finished product:

Here’s another mini GIF:

Maybe a lot of you don’t know the spectrum diagram and the waterfall diagram, and I don’t know either… But our front end is responsible for the data in accordance with the rules of the display (the top line is spectrum, the bottom group is waterfall).

Technology selection

Frame: Vue(it doesn’t matter, I won’t talk about it anyway)

Data transfer: WebSocket

Spectrum Charts: HighCharts

Waterfall: Canvas

  • Why WebSocket?

Since the server is required to transfer data in real time, 30 frames of 1024 points per frame of animation are definitely more comfortable than Ajax polling, and the project has no browser compatibility requirements.

  • Why use HighCharts to chart the spectrum?

I did a test with HighCharts and ECharts. Although canvas has better performance than SVG, HighCharts is more fluent in rendering (HighCharts requires payment).

  • Why Canvas for waterfall?

Although it is convenient to use the data visualization chart library, due to the demanding performance requirements of this project, only large thermal maps like waterfall diagrams are used:

Do it with a thermal map, please rest assured, it will not be stuck into PPT, browser 5 seconds on time directly crash.

Component function separation

The entire component is split into three parts:

  • Parent component: responsible for WebSocket and server real-time communication, processing binary data, control rendering frequency, control start and pause, refresh components.

  • Child component > Spectrum Charts (HighCharts) : Provides addData method to fetch data i.e. render a frame, provides trigger zoom event to send to the parent component.

  • Subcomponent > Waterfall graph (Canvas) : Provides addData method similar to spectrum graph, spectrum graph after the scaling event, corresponding to its selected position to scale.

The parent component

WebSocket links to the server

Since there aren’t many operations, use native:

this.socket = new WebSocket('the ws: / / 192.168.2.250:8100 / socket')
this.socket.onopen = () = >{... }this.socket.onclose = () = >{... }Copy the code

To send instructions connected with the back end, we define three:

// Start fetching data
this.socket.send('start')
// Pause data retrieval
this.socket.send('pause')
// Restore the acquired data
this.socket.send('resume')
Copy the code

Listen for onMessage events:

this.socket.onmessage = (event) = > {
  const reader = new FileReader()
  reader.readAsArrayBuffer(event.data)
  reader.onload = e= > {
    if (e.target.readyState === FileReader.DONE) {
      // Process binary data}}}Copy the code

Working with binary data

I wanted to write this piece in a big way, but a few days ago I saw “Why is the video link address of video website blob?” Well written, shame on you, please shift to understand this piece, don’t forget to come back.

Control render frequency

The server is sending about 400 frames per second, so 400 frames per second is definitely not realistic and will result in lost frames.

Create an array to hold the data, delete the data per frame, send resume to retrieve the data when less than 100, and send pause to pause the data when more than 400.

At the server side of the send rate, the CPU usage is over 100%, there is a little bit of lag on fetch, but that is acceptable, as fetch can render for several seconds.

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) return
  const result = this.data[0]
  this.$refs.frequency.addData(result.data)
  this.$refs.waterFall.addData(result.data.map(item= > item[1]))
  this.data.shift()
}, this.refreshInterval)
Copy the code

Another advantage of using setInterval for timed rendering is that you can control the rendering frequency. Note the drag bar in the upper right corner of the component to reduce the rendering frequency on low powered computers.

spectrum

There are some differences between the HighCharts and ECharts configuration items, but it’s all a matter of configuration, look at the documentation, it’s easy, remember to turn off all animations.

addData()

this.chart.series[0].setData(data, true.false)
Copy the code

The parent component can trigger a frame rendering with $ref.addData()

The zoom

Set chart.zoomType to ‘x’ in the configuration, and select zoom for X-axis.

Chart.events.selection Configures selected events:

selection (event) {
  const pointWidth = (this.xAxisMax - this.xAxisMin) / 1024
  const ponitStart = Math.floor((event.xAxis[0].min - this.xAxisMin) / pointWidth)
  const ponitEnd = Math.floor((event.xAxis[0].max - this.xAxisMin) / pointWidth)
  this.$emit('frequencySelect', [ponitStart, ponitEnd])
},
Copy the code

Sends selected points to the parent component, which passes them to the waterfall diagram component.

The waterfall figure

This is the focus of this article, since some libraries have been removed for performance reasons.

Let’s start with a few concepts. Many of you are familiar with Canvas, but you probably haven’t paid much attention to them (pixel manipulation) :

  • createImageData()
  • putImageData()
  • DrawImage (), I think you know that

Start by creating two canvas, one to display the overall effect (this.canvas) and one to hold the generated image (this.waterfalldom, which will not be inserted into the DOM).

this.canvas = document.createElement('canvas')
this.ctx = this.canvas.getContext('2d')
this.waterFallDom = document.createElement('canvas')
this.waterFallCtx = this.waterFallDom.getContext('2d')
Copy the code

createImageData

CreateImageData (width,height) creates a new blank ImageData object with two parameters, setting the image width and height.

const imageData = this.waterFallCtx.createImageData(data.length, 1)
Copy the code

At this point, a blank image of 1024 * 1 is generated, and we continue to color each pixel:

for (let i = 0; i < imageData.data.length; i += 4) {
  const cindex = this.squeeze(data[i / 4].0.150)
  const color = this.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
Copy the code

Imagedata. data is an array that draws one pixel per four values, corresponding to:

  • R – Red (0-255)
  • G – Green (0-255)
  • B – Blue (0-255)
  • A-alpha channel (0-255; 0 is transparent, 255 is fully visible)

This. squeeze is the corresponding point in the colormap computed from the data.

squeeze (data, outMin, outMax) {
  if (data <= this.minDb) {
    return outMin
  } else if (data >= this.maxDb) {
    return outMax
  } else {
    return Math.round((data - this.minDb) / (this.maxDb - this.minDb) * outMax)
  }
}
Copy the code

Colormap is a two-dimensional array, each value represents [r, g, b, a], here I generated 150 colors, a gradient, you can see the illustration.

  • How to generate colorMap?

That’s fine if you’re going to write by hand, except that it hurts. It is recommended to install ColorMap using NPM

this.colormap = colormap({
  colormap: 'jet'.nshades: 150.format: 'rba'.alpha: 1
})
Copy the code

A variety of color schemes are provided, please refer to the documentation for details.

At this point, we have generated an image with a color of 1024 * 1, which is of course an image object.

putImageData

PutImageData (imgData, x, y, dirtyX, dirtyY, dirtyWidth, dirtyHeight) image data to the canvas:

  • ImgData: Specifies the ImageData object to be put back into the canvas.
  • X: The x-coordinate of the upper-left corner of the ImageData object, in pixels.
  • Y: The y coordinate of the upper-left corner of the ImageData object, in pixels.
  • DirtyX: optional. Horizontal value (x), in pixels, the position of the image on the canvas.
  • DirtyY: optional. Horizontal value (y), in pixels, the position of the image on the canvas.
  • DirtyWidth: optional. The width used to draw the image on the canvas.
  • DirtyHeight: optional. The height used to draw the image on the canvas.
this.waterFallCtx.putImageData(imageData, 0.0)
Copy the code

drawImage

We can draw this.waterFallCtx onto this. CTX.

drawImage(img,sx,sy,swidth,sheight,x,y,width,height);

  • Img: Specifies the image, canvas, or video to use.
  • Sx: optional. The x position at which the shear began.
  • Sy: optional. The y position where the shear started.
  • Swidth: optional. The width of the clipped image.
  • Sheight: optional. The height of the clipped image.
  • X: The x coordinate position of the image on the canvas.
  • Y: The y coordinate position of the image on the canvas.
  • Width: optional. The width of the image to use. (Stretch or zoom out image)
  • Height: optional. The height of the image to use. (Stretch or zoom out image)
this.ctx.drawImage(this.waterFallCtx.canvas,0.0.1024.1.0.0, width, height)
Copy the code

Here sX and SY can cooperate with the spectrum diagram to scale.

Width and height can be scaled up or reduced to produce a good display effect. For example, an image with only two pixels can be stretched to 1000 pixels with a perfect gradient instead of two colors in half.

Realistic dynamic waterfall diagram

Now that we have drawn the first row of images onto the canvas, we may have hundreds of rows of data from WebSockt. For each new row, the previous row is the next:

// Move the generated image down one pixel
this.waterFallCtx.drawImage(this.waterFallCtx.canvas,
  0.0.1024.300 - 1.0.1.1024.300 - 1)
Copy the code

300 means to save a total of 300 lines of images. These data should not be fixed and should be set up in advance. Here is for the convenience of demonstration.

By calling its own image and redrawing it to its own downward offset Y-axis 1 pixel, height -1 image.

So every time we add a piece of data, we get a new line of images at the top, the generated image moves down one pixel, and our image moves from there.

Implement the zoom

The spectrum has been scaled, and the starting and ending points are passed to the parent component, which in turn passes to the waterfall component to dynamically modify the clipping properties of the drawImage.

code

Jsfiddle.net/codexu/ugva…