Highcharts 加载大批量散点图界面报错,
"highcharts"版本"^11.1.0"
vue2脚手架,使用 highcharts 绘制折线图加散点图
同样的代码配置,折线图能加载三十万数据,没有问题,而散点图,加载一千个数据就已经崩溃了
界面上没有报错,有警告
highcharts.js:12 Highcharts warning #12: www.highcharts.com/errors/12/
百度得到的结果就是数据量太大了
<script>
import Highcharts, { color } from 'highcharts';
import Boost from "highcharts/modules/boost.js";
import highstock from 'highcharts/modules/stock.js';
import exporting from 'highcharts/modules/exporting.js';
import exportData from 'highcharts/modules/export-data.js';
const moment = require('moment')
import { cloneDeep } from 'lodash'
highstock(Highcharts)
Boost(Highcharts);
exporting(Highcharts);
exportData(Highcharts);
import webSocket from './webSocket'
const instructName = '频率'
export default {
mixins: [webSocket],
props: {
paramIdList: {
type: Array,
default: () => []
},
cmdPoints: {
type: Array,
default: () => []
},
colorList: {
type: Array,
default: () => []
},
startTime: {
type: String,
default: () => ''
},
endTime: {
type: String,
default: () => ''
},
},
data() {
return {
highchart: null,
taggingList: [], // 获取到所有带有标注的点
};
},
watch: {
paramIdList: {
deep: true,
handler: function (newV) {
if (newV.length > 0) {
let arr = []
newV.forEach((obj, index) => {
arr.push({
animation: false,
type: "line",
name: obj,
data: [],
color: this.colorList[index + 1]
})
});
arr.push({
animation: false,
type: "scatter",
name: instructName,
data: [],
marker: {
radius: 4,
enabled: true, // 确保标记是启用的
symbol: 'circle'
},
color: this.colorList[0],
yAxis: 1
})
this.drawChart(arr);
this.wsSend()
}
}
},
cmdPoints: {
deep: true,
immediate: true,
handler: function (newV) {
if (newV.length > 0) {
this.setPoints(newV)
}
}
}
},
activated() {
this.doWebSocket()
},
methods: {
setPoints(arr) {
let arrLength = arr.length
let chunkSize = 2000
let seriesObj = this.highchart.series.find(item => item.name == instructName)
seriesObj.setData(arr.slice(0, chunkSize))
// seriesObj.setData(arr)
// for (let i = chunkSize; i < arrLength; i++) {
// // seriesObj.addPoint(arr[i], true, false);
// // seriesObj.setData(arr.slice(i, i + chunkSize))
// }
},
drawChart(series) {
let that = this
console.time('Timer')
this.highchart = Highcharts.stockChart('highchartsBox', {
series,
boost: {
useGPUTranslations: true,
usePreAllocated: true
},
exporting: {
buttons: {
enabled: true,
contextButton: {
menuItems: [{
text: '查看全屏', // 菜单项的文本
onclick: function () {
this.fullscreen.toggle()
}
}]
}
}
},
chart: {
animation: false,
width: null,
zoomType: 'x',
spacingLeft: 50,
spacingRight: 50,
scrollablePlotArea: {
minWidth: 600,
scrollPositionX: 1
},
events: {
click: function () { },
load: function () { }
}
},
xAxis: {
top: "80%", // 设置X轴距离顶部的位置
height: "0%", // 设置X轴的高度
type: 'datetime', // 设置x轴为时间戳格式
ordinal: false,
minRange: 1000,
tickInterval: 1000,
labels: {
// step: 1, // 设置为1以显示所有的X轴坐标点
enabled: false
},
},
stockTools: {
gui: {
enabled: false // 启用左侧菜单栏
}
},
scrollbar: {
enabled: false // 禁用滚动条
},
accessibility: {
enabled: false
},
time: {
useUTC: false
},
rangeSelector: {
enabled: true, // 禁用范围选择器
inputEnabled: false, // 是否启用输入框
selected: null, // 禁用默认选中的按钮
buttons: [{
type: 'all',
text: '全部'
}]
},
yAxis: [
{
labels: {
align: 'left'
},
height: '80%',
resize: {
enabled: true
},
gridLineWidth: 1, // 设置分割线的宽度
// tickInterval: 0.01, // 设置刻度间隔为 1
gridLineDashStyle: 'Dash',
gridLineColor: '#e0e0e0', // 设置分割线的颜色
opposite: false //
},
{
labels: {
align: 'left'
},
visible: false, // 设置为 false 隐藏该 Y 轴
top: '80%',
height: '20%',
tickPositions: [-1, 0],
min: -1, // 设置合适的最小值
max: 0,
offset: 0,
gridLineWidth: 1, // 设置分割线的宽度
tickInterval: 0.01, // 设置刻度间隔为 1
gridLineDashStyle: 'Dash',
gridLineColor: '#e0e0e0', // 设置分割线的颜色
opposite: false //
}
],
navigator: {
enabled: true, // 启用缩略图
adaptToUpdatedData: true, // 根据数据更新缩略图
series: {
color: '#ccc', // 缩略图的颜色
lineWidth: 1, // 缩略图的线宽
fillOpacity: 0.2 // 填充透明度
},
xAxis: {
labels: {
formatter: function () {
return moment(this.value).format('YYYY-MM-DD HH:mm:ss.SSS')
},
style: {
fontSize: '10px'
},
enabled: true
},
minRange: 1000 // 缩略图的最小范围
}
},
// 节点点击事件
plotOptions: {
series: {
// dataGrouping: {
// enabled: true, // 启用数据分组
// approximation: 'average', // 聚合方式:平均值
// groupPixelWidth: 10 // 每个分组的宽度(像素)
// },
point: {
events: {
click: function () {
that.sendPoint(this)
}
}
}
}
},
tooltip: {
enabled: true, // 是否显示 tooltip
backgroundColor: 'rgba(255, 255, 255, 1)',
borderWidth: 2,
borderColor: 'black',
borderRadius: 5,
useHTML: false,
shared: false, // 使 tooltip 对所有系列共享
crosshairs: true, // 显示十字准线
formatter: function () {
return that.drawTooltip(this)
}
},
});
console.timeEnd("Timer");
},
chartsMarker(e) {
let scatterObj = {}
this.highchart.series.forEach(i => {
if (i.name == instructName) {
scatterObj = i
}
})
// 高亮对应的散点
let count = 0
for (let i = 0; i < scatterObj.data.length; i++) {
const ele = scatterObj.data[i];
if (ele.marker && ele.marker.fillColor == 'red') {
ele.update({
marker: {
enabled: true,
radius: 4,
fillColor: this.colorList[0], // 设置标记的填充颜色
}
})
count++
}
if (ele.params.sendTime == e.sendTime) {
ele.update({
marker: {
enabled: true,
radius: 6,
fillColor: 'red', // 设置标记的填充颜色
},
});
count++
}
if (count == 2) {
break
}
}
},
// 绘制鼠标提示框
drawTooltip(obj) {
let str = ''
if (obj.series.name == instructName) {
let param = obj.point.params
str = `${param.sendTime}<br/>指令ID:${param.cmdId}<br/>指令名称:${param.cmdName}<br/>目标设备:${param.targetDevice}`
} else {
let points = obj.points
let str1 = ''
points.forEach(i => {
str1 += `${i.series.name}:${i.y}<br/>`
})
str = `${moment(obj.x).format('YYYY-MM-DD HH:mm:ss.SSS')}<br/>${str1}`
}
return str
},
// 节点 点击事件
sendPoint(obj) {
if (obj?.params?.cmdId) {
if (obj.options?.dataLabels?.enabled) {
obj.update({
marker: {
enabled: true,
radius: 4,
fillColor: this.colorList[0], // 设置标记的填充颜色
},
dataLabels: {
enabled: false,
}
});
} else {
let params = obj.params
obj.update({
marker: {
enabled: true,
radius: 6,
fillColor: '#00f', // 设置标记的填充颜色
},
dataLabels: {
enabled: true,
backgroundColor: this.colorList[0],
borderWidth: 1,
borderColor: 'black',
borderRadius: 5,
crop: false,
overflow: 'none', // 使标签不被裁剪
allowOverlap: true, // 允许重叠
// useHTML: true,
formatter: function () {
return `${params.sendTime}<br/>指令ID:${params.cmdId}<br/>指令名称:${params.cmdName}<br/>目标设备:${params.targetDevice}`
},
style: {
fontWeight: 'normal',
textShadow: 'none', // 禁用阴影
color: '#FFF'
},
x: 0,
y: -20
},
});
}
this.$emit('clickPointNode', obj)
} else {
var lineColor = obj.series.color;
if (!obj.marker || !obj.marker.enabled) {
obj.update({
marker: {
enabled: true,
radius: 6
},
dataLabels: {
enabled: true,
backgroundColor: lineColor,
borderWidth: 1,
borderColor: 'black',
borderRadius: 5,
// crop: false,
// overflow: 'none', // 使标签不被裁剪
allowOverlap: true, // 允许重叠
// useHTML: true,
formatter: function () {
return `${moment(obj.x).format('YYYY-MM-DD HH:mm:ss.SSS')}<br/>${obj.series.name}:${obj.y}`
},
style: {
fontWeight: 'normal',
textShadow: 'none', // 禁用阴影
color: '#FFF'
}
},
});
} else {
obj.update({
marker: {
enabled: false,
},
dataLabels: {
enabled: false,
}
});
}
// 强制重新绘画这个图形
// obj.series.chart.redraw();
// 异常点标注暂时不作
// this.openTagging(obj)
}
},
openTagging(obj) {
let msg = `参数ID:${obj.series.name}<br/>当前坐标点:(${moment(obj.x).format('YYYY-MM-DD HH:mm:ss.SSS')},${obj.y})`
this.$prompt(msg, '异常点标注', {
dangerouslyUseHTMLString: true,
inputPlaceholder: '请输入异常标注信息',
inputErrorMessage: '异常标注信息不可为空',
inputPattern: /\S/,
showInput: true,
}).then(({ value }) => {
let params = {
name: obj.series.name,
x: obj.x,
y: obj.y,
value: value
}
this.submitTagging(params)
})
},
async submitTagging(params) {
setTimeout(() => {
// let res = await api(params)
this.$message.success('当前点位标注成功')
}, 1000);
},
async getTagging() {
setTimeout(() => {
// let res = await api(params)
this.taggingList = []
}, 1000);
},
},
};
</script>
折线图绘制逻辑,将数据先保存,等数据完全返回,就直接绘制,没有问题,目前能渲染三十万数据
// 处理接收到的消息
wsOnmessage(event) {
const message = JSON.parse(event.data)
let { currentBatch, totalBatch, data } = message
Object.keys(data).forEach(i => {
if (this.socketDataObj.hasOwnProperty(i)) {
this.socketDataObj[i] = [...this.socketDataObj[i], ...data[i]]
} else {
this.socketDataObj[i] = data[i]
}
})
if (currentBatch == totalBatch) {
let series = this.highchart.series
series.forEach((item, index) => {
if (this.socketDataObj.hasOwnProperty(item.name)) {
series[index].setData(this.socketDataObj[item.name])
}
})
}
},
散点图绘制逻辑,散点图数据是正常接口返回,
setPoints(arr) {
let arrLength = arr.length
let chunkSize = 2000
let seriesObj = this.highchart.series.find(item => item.name == instructName)
seriesObj.setData(arr.slice(0, chunkSize))
// seriesObj.setData(arr)
// for (let i = chunkSize; i < arrLength; i++) {
// // seriesObj.addPoint(arr[i], true, false);
// // seriesObj.setData(arr.slice(i, i + chunkSize))
// }
},
图形整体就是N条折线图,一个散点图,使用双Y轴,因为散点表示频率,只有时间X轴,没有y轴,所以设置散点图的点是【{x: 时间戳,y:-1,params:{其他参数}}】。第二根y轴设置位置在第一根y轴下方,设置隐藏,这样能看起来是在一起的。
界面整体处理逻辑就是,先将图生成出来,但是没有点。因为折线图数据量比较大,使用ws传递,接收到数据之后先保存,等数据完全传输,使用setData 一并绘制多条折线图。折线图绘制没有问题,目前测试三十万数据,可以完全绘制。
不能使用 数据聚和,没十个点取平均值等设置 客户要求如此
// dataGrouping: {
// enabled: true, // 启用数据分组
// approximation: 'average', // 聚合方式:平均值
// groupPixelWidth: 10 // 每个分组的宽度(像素)
// },
我的问题就是关于,散点图。数据约有上万条,我设置切割数据为一千条时候,界面就可以完全加载,散点图正常,折线图正常。
但是我设置散点图切割一千二百条数据时候就会报警告,
highcharts.js:12 Highcharts warning #12: www.highcharts.com/errors/12/
散点图使用addPoint 循环会崩溃,并且数据量很多,不能使用这个
seriesObj.addPoint(arr[i], true, false);
折线图就是使用setData 一并将数据添加的,但是到散点图就不行了,
seriesObj.setData(arr.slice(i, i + chunkSize))
为什么同样的配置,一个能绘制三十万数据,而另一个只能绘制一千条数据,我想要的是将散点图同样绘制十万级数据。
界面整体效果 如 https://segmentfault.com/q/1010000045158901 展示
附带效果截图,
这个图是散点截取一千之后显示得
这个图是散点截取两千之后显示得,散点图根本加载不出来
报错信息如图所示
使用在线编辑可以显示一亿数据,同样的配置
以下是在线编辑代码,
<script src="https://code.highcharts.com/stock/highstock.js"></script>
<script src="https://code.highcharts.com/stock/modules/exporting.js"></script>
<script src="https://code.highcharts.com/stock/modules/boost.js"></script>
<script src="https://code.highcharts.com/stock/modules/export-data.js"></script>
<script src="https://code.highcharts.com/modules/accessibility.js"></script>
<div id="container"></div>
// Create the chart
const data = [],
n = 1000000;
for (let i = 0; i < n; i += 1) {
data.push({ x: i, y: -1 });
}
var aaa = Highcharts.stockChart("container", {
series: [
{
animation: false,
type: "scatter",
name: "频率",
data: [],
marker: {
radius: 4,
enabled: true, // 确保标记是启用的
symbol: "circle"
},
yAxis: 1
// type: "scatter",
// data: data,
// marker: {
// radius: 0.5
// },
}
],
boost: {
useGPUTranslations: true,
usePreAllocated: true
},
exporting: {
buttons: {
enabled: true,
contextButton: {
menuItems: [
{
text: "查看全屏", // 菜单项的文本
onclick: function () {
this.fullscreen.toggle();
}
}
]
}
}
},
chart: {
animation: false,
width: null,
zoomType: "x",
spacingLeft: 50,
spacingRight: 50,
scrollablePlotArea: {
minWidth: 600,
scrollPositionX: 1
},
events: {
click: function () {},
load: function () {}
}
},
xAxis: {
top: "80%", // 设置X轴距离顶部的位置
height: "0%", // 设置X轴的高度
type: "datetime", // 设置x轴为时间戳格式
ordinal: false,
minRange: 1000,
tickInterval: 1000,
labels: {
// step: 1, // 设置为1以显示所有的X轴坐标点
enabled: false
}
},
stockTools: {
gui: {
enabled: false // 启用左侧菜单栏
}
},
scrollbar: {
enabled: false // 禁用滚动条
},
accessibility: {
enabled: false
},
time: {
useUTC: false
},
rangeSelector: {
enabled: true, // 禁用范围选择器
inputEnabled: false, // 是否启用输入框
selected: null, // 禁用默认选中的按钮
buttons: [
{
type: "all",
text: "全部"
}
]
},
yAxis: [
{
labels: {
align: "left"
},
height: "80%",
resize: {
enabled: true
},
gridLineWidth: 1, // 设置分割线的宽度
// tickInterval: 0.01, // 设置刻度间隔为 1
gridLineDashStyle: "Dash",
gridLineColor: "#e0e0e0", // 设置分割线的颜色
opposite: false //
},
{
labels: {
align: "left"
},
visible: false, // 设置为 false 隐藏该 Y 轴
top: "80%",
height: "20%",
tickPositions: [-1, 0],
min: -1, // 设置合适的最小值
max: 0,
offset: 0,
gridLineWidth: 1, // 设置分割线的宽度
tickInterval: 0.01, // 设置刻度间隔为 1
gridLineDashStyle: "Dash",
gridLineColor: "#e0e0e0", // 设置分割线的颜色
opposite: false //
}
],
navigator: {
enabled: true, // 启用缩略图
adaptToUpdatedData: true, // 根据数据更新缩略图
series: {
color: "#ccc", // 缩略图的颜色
lineWidth: 1, // 缩略图的线宽
fillOpacity: 0.2 // 填充透明度
},
xAxis: {
labels: {
style: {
fontSize: "10px"
},
enabled: true
},
minRange: 1000 // 缩略图的最小范围
}
},
tooltip: {
enabled: true, // 是否显示 tooltip
backgroundColor: "rgba(255, 255, 255, 1)",
borderWidth: 2,
borderColor: "black",
borderRadius: 5,
useHTML: false,
shared: false, // 使 tooltip 对所有系列共享
crosshairs: true // 显示十字准线
}
});
function setPoints() {
let seriesObj = aaa.series.find((item) => item.name == "频率");
seriesObj.setData(data);
}
setPoints();
可以设置属性turboThreshold