Highcharts 加载大批量散点图界面为什么不能加载?

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();
阅读 1.1k
avatarAI BotBETA

问题分析

Highcharts 报出的警告 #12 通常与渲染性能相关,尤其是在处理大量数据点时。在你的情况下,虽然你已经使用 Boost 模块来尝试优化渲染性能,但在处理大量散点图数据时仍然遇到了问题。这可能是由于以下几个原因:

  1. 数据量大:即使你使用 Boost 模块,大量数据点(如数千个散点)仍可能对浏览器造成压力,特别是如果这些点频繁更新或重绘。
  2. 散点图的特殊性:散点图在视觉上需要为每个点单独绘制标记,这比绘制连续的折线图更消耗资源。
  3. 浏览器限制:不同的浏览器和设备有不同的性能限制,一些设备可能无法有效处理大量的 DOM 操作或 Canvas 渲染。

解决方案

  1. 减少数据量

    • 如果可能,尝试减少需要显示的数据点数量。可以使用数据聚合(如分组平均)来减少点的数量,同时保持数据的总体趋势。
  2. 优化数据加载

    • 正如你已经在做的那样,通过分块加载数据来减少一次性加载的数据量。但是,确保在加载新数据块时不会造成界面的卡顿。
  3. 使用数据分组

    • Highcharts 提供了数据分组功能(dataGrouping),这可以帮助减少显示的数据点数量,同时保持图表的可读性。你可以在 plotOptions 中为 series 配置 dataGrouping
  4. 优化散点图标记

    • 减少散点图标记的大小和复杂度。在 Highcharts 中,可以通过设置 marker.radius 来减少标记的大小。
    • 考虑在图表中启用或禁用标记的显示,根据用户的交互(如缩放或平移)来动态调整。
  5. 检查并优化浏览器性能

    • 确保你的应用没有其他性能瓶颈,如大量的 DOM 操作、复杂的 CSS 选择器或不必要的 JavaScript 计算。
    • 尝试在不同的浏览器和设备上测试你的图表,以查看性能差异。
  6. 使用 WebGL(如果可能)

    • 虽然 Highcharts 的 Boost 模块已经利用了 WebGL 来加速渲染,但如果你在处理极端大量的数据时,可以考虑使用其他基于 WebGL 的图表库,这些库可能更适合处理大量数据。
  7. 调试和性能分析

    • 使用浏览器的开发者工具(如 Chrome DevTools)来分析图表的渲染性能。检查是否有长时间的 JavaScript 执行或大量的重绘/回流。

结论

处理大量数据点时,优化渲染性能是关键。通过减少数据量、优化数据加载、使用数据分组、优化标记显示、检查浏览器性能以及进行性能分析,你可以显著提高 Highcharts 图表的性能和响应性。

1 个回答

可以设置属性turboThreshold

plotOptions: {
   series: {
    turboThreshold: 0 //设置0关掉性能阈值检查,或者设置为你的数据量最大值
  }
},
撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题
宣传栏