автор:codexu
Без лишних слов, давайте начнем с готового изображения продукта:
Вот еще мини-анимация:
Многие студенты могут не знать спектрограммы и каскадные диаграммы, но я тоже не знаю... Но наш фронтенд отвечает за отображение данных по правилам (верхняя линия графика - это спектрограмма, а нижняя - водопад) Диаграмма).
Технический отбор
Фреймворк: Vue (не важно, я все равно не буду об этом говорить)
Передача данных: WebSocket
Спектрограмма: HighCharts
Схема водопада: холст
- Зачем использовать веб-сокет?
Поскольку серверу необходимо передавать данные в режиме реального времени, необходимо достичь 30 кадров, и каждый кадр анимации состоит из1024Это определенно удобнее, чем Ajax-опрос, и в этом проекте нет требований к браузерной совместимости.
- Зачем использовать HighCharts для построения спектрограмм?
Сделал тест, HighCharts и ECharts, хотя производительность canvas сильнее, чем у svg, но при этом рендер чувствуется, что HighCharts плавнее (HighCharts надо платить).
- Почему каскадные диаграммы используют Canvas?
Хотя удобно использовать библиотеку диаграмм визуализации данных, учитывая высокие требования к производительности этого проекта, в качестве каскадных диаграмм можно использовать только большие тепловые карты:
Сделайте это с тепловой картой, будьте уверены, она не застрянет в PPT, и браузер вылетит сразу через 5 секунд.
Разделение функций компонентов
Вся сборка разделена на три части:
-
Родительский компонент: отвечает за связь в реальном времени между WebSocket и сервером, обработку двоичных данных, управление частотой рендеринга, управление запуском и паузой и обновление компонента.
-
Подкомпоненты > Спектрограмма (HighCharts): Предоставляет метод addData, который визуализирует кадр при получении данных, обеспечивает запуск событий масштабирования и отправляет их родительскому компоненту.
-
Подкомпонент > Водопадная диаграмма (холст): Предоставляет тот же метод addData, что и спектрограмма.После того, как спектрограмма имеет событие масштабирования, она масштабирует выбранную позицию.
родительский компонент
Связанный сервер WebSocket
Поскольку операций не так много, используйте собственный метод напрямую:
this.socket = new WebSocket('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) => {
const reader = new FileReader()
reader.readAsArrayBuffer(event.data)
reader.onload = e => {
if (e.target.readyState === FileReader.DONE) {
// 处理二进制数据
}
}
}
Работа с бинарными данными
Я хотел написать это произведение на большом пространстве, но увидел его несколько дней назад.«Почему адрес ссылки на видео веб-сайта с видео представляет собой большой двоичный объект? 》, Письмо очень хорошее, мне стыдно, пожалуйста, переведите, чтобы понять это произведение, не забудьте вернуться.
Управление частотой рендеринга
Сервер отправляет около 400 штук данных примерно в одну секунду, а 400 кадров в секунду определенно нереально, что непосредственно приводит к потере кадра.
Решение: создайте массив для сохранения данных, удаляйте этот фрагмент данных каждый раз при рендеринге кадра, отправляйте резюме, чтобы продолжить выборку, когда элементов меньше 100, и отправьте паузу, чтобы приостановить выборку, когда элементов больше 400.
Что касается частоты отправки на стороне сервера, то загрузка процессора превысит 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) 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)
Еще одним преимуществом использования setInterval для временного рендеринга является то, что вы можете контролировать частоту рендеринга.Обратите внимание на полосу перетаскивания в правом верхнем углу компонента, которая может снизить частоту рендеринга на слабых компьютерах.
Спектрограмма
Есть некоторые различия между элементами конфигурации HighCharts и ECharts, но все они связаны с конфигурацией. Ознакомьтесь с документацией. Это очень просто. Не забудьте отключить все анимации.
addData()
this.chart.series[0].setData(data, true, false)
Родительский компонент может инициировать рендеринг кадра через $ref.addData().
зум
В конфигурации chart.zoomType установлено значение «x» и установлено масштабирование выбора по оси X.
chart.events.selection настраивает события выбора:
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])
},
Отправьте выбранную точку в родительский компонент, а затем передайте ее компоненту каскадной диаграммы через родительский компонент.
водопадная диаграмма
Некоторые библиотеки отделены отсюда из соображений производительности. Многие мелкие партнеры не знают, как это сделать здесь. Этому посвящена данная статья.
Сначала поймите несколько концепций, многие люди соприкасались с Canvas, но, по оценкам, им не уделялось особого внимания (манипулирование пикселями):
- createImageData()
- putImageData()
- drawImage() должен знать это
Сначала создайте два холста, один для отображения общего эффекта (this.canvas), а другой для хранения сгенерированного изображения (this.waterFallDom, которое не будет вставлено в дом).
this.canvas = document.createElement('canvas')
this.ctx = this.canvas.getContext('2d')
this.waterFallDom = document.createElement('canvas')
this.waterFallCtx = this.waterFallDom.getContext('2d')
createImageData
Метод createImageData(width,height) создает новый пустой объект ImageData, два параметра, устанавливает ширину и высоту изображения, здесь для проекта требуется всего 1024 точки:
const imageData = this.waterFallCtx.createImageData(data.length, 1)
В это время генерируется пустое изображение 1024*1, и мы продолжаем раскрашивать каждый пиксель:
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
imageData.data — это массив, который рисует пиксель для каждых четырех значений, соответствующих:
- R - красный (0-255)
- G - зеленый (0-255)
- Б - Синий (0-255)
- A — альфа-канал (0-255; 0 — прозрачный, 255 — полностью видимый)
this.squeezeЭто вычисление соответствующей точки в палитре на основе данных.Об этом особо нечего сказать:
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)
}
}
colormapпредставляет собой двумерный массив, каждое значение представляет [r, g, b, a], здесь я сгенерировал 150 цветов, которые являются градиентными цветами, вы можете увидеть легенду ниже.
- Как создать карту цветов?
Если вы планируете писать от руки, это нормально, но ваша рука немного болит. Здесь рекомендуется использовать установку npmcolormap
this.colormap = colormap({
colormap: 'jet',
nshades: 150,
format: 'rba',
alpha: 1
})
Доступны различные цвета, подробности см. в документации.
На данный момент мы сгенерировали изображение с цветом 1024 * 1, которое, конечно же, является объектом изображения.
putImageData
putImageData(imgData,x,y,dirtyX,dirtyY,dirtyWidth,dirtyHeight) рисует данные изображения на холсте:
- imgData: указывает объект ImageData, который нужно вернуть на холст.
- x: координата x верхнего левого угла объекта ImageData в пикселях.
- y: координата y верхнего левого угла объекта ImageData в пикселях.
- грязныйX: необязательно. Горизонтальное значение (x) в пикселях, где разместить изображение на холсте.
- грязныйY: необязательно. Горизонтальное значение (y) в пикселях, где разместить изображение на холсте.
- грязная ширина: необязательно. Ширина, используемая для рисования изображения на холсте.
- грязная высота: необязательно. Высота, используемая для рисования изображения на холсте.
this.waterFallCtx.putImageData(imageData, 0, 0)
drawImage
Это всем знакомо, то есть нарисовать изображение на канве, тогда мы можем дорисовать this.waterFallCtx до this.ctx.
drawImage(img,sx,sy,swidth,sheight,x,y,width,height);
- img: определяет изображение, холст или видео для использования.
- секс: необязательно. Координата x, с которой начинается отсечение.
- сы: необязательный. Координата 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 динамически изменяется.