ECharts 大数据量渲染:千万级数据点的降采样与聚合策略

📅 2026/7/16 17:44:09
ECharts 大数据量渲染:千万级数据点的降采样与聚合策略
ECharts 大数据量渲染千万级数据点的降采样与聚合策略1000 万个数据点塞进 ECharts页面直接卡死不是 ECharts 不行是你没有做降采样。一、问题浏览器不是数据库Canvas 也不是无限画布先搞清楚一个事实前端图表的瓶颈不在数据量而在渲染的像素数。你一屏就 1920×1080 像素折线图横轴满打满算也就一两千个可视点。给你 1000 万个数据点99.9% 的点都堆叠在同一个像素上用户根本看不清还白白消耗了浏览器的渲染和内存。ECharts 的处理机制是这样的每个数据点都会被解析成图形元素线段的连接点、柱子的矩形然后通过 Canvas 或 SVG 渲染。百万级数据点 百万级图形元素 浏览器内存飙升 渲染线程阻塞 页面卡死。为什么 LTTB 比均匀降采样更能保留视觉特征均匀降采样每隔 N 个取一个相当于给数据加了低通滤波器——高频信号尖峰、谷底全部被滤掉了。如果你的时序数据里有一个CPU 在 14:35 从 30% 跳到 95%的瞬间尖峰均匀降采样大概率会跳过这个时间点图表上完全看不到这个异常。LTTB 的设计目标是保留对人类视觉最显著的特征——三角形的面积越大说明这个点偏离趋势线越远视觉上越突出。所以 LTTB 天然倾向保留异常点而这恰恰是数据分析最需要看到的。二、LTTB最推荐的时序降采样算法LTTBLargest Triangle Three Buckets是目前在时序数据降采样领域最广泛使用的算法。它的核心思想是把数据分成若干桶每桶保留一个能最好代表该桶趋势的点。跟简单的每隔 N 个取一个均匀降采样比LTTB 能保留数据的尖峰和谷底——这些恰恰是数据分析最关心的异常点。import numpy as np def lttb_downsample(x_data, y_data, target_points): LTTBLargest Triangle Three Buckets降采样算法 原理把数据分成 target_points 个桶每桶选一个点。 选点标准当前点与前一个已选点和下一桶的平均点形成的三角形面积最大。 三角形面积越大说明该点在视觉上越突出偏离趋势线越远。 参数 - x_data, y_data: 原始数据x 和 y 分离适配大多数时序数据 - target_points: 目标降采样后的点数建议 1000-2000 返回降采样后的 (x, y) 数组 data_length len(x_data) # 如果数据量 目标点数不需要降采样 if data_length target_points: return np.array(x_data), np.array(y_data) # 第一步数据分桶 # 首尾点直接保留折线图两端必须准确 # 中间的点均匀分配到 target_points - 2 个桶 bucket_size (data_length - 2) / (target_points - 2) sampled_x [x_data[0]] sampled_y [y_data[0]] last_selected_idx 0 for i in range(target_points - 2): bucket_start int((i 1) * bucket_size) 1 bucket_end int((i 2) * bucket_size) 1 bucket_end min(bucket_end, data_length - 1) # 第二步计算下一桶的平均点用于三角形面积计算 next_bucket_start bucket_end next_bucket_end int((i 3) * bucket_size) 1 next_bucket_end min(next_bucket_end, data_length) avg_x_next np.mean([ x_data[j] for j in range(next_bucket_start, next_bucket_end) ]) avg_y_next np.mean([ y_data[j] for j in range(next_bucket_start, next_bucket_end) ]) # 第三步在当前桶里找最突出的点 max_area -1 max_idx bucket_start for j in range(bucket_start, bucket_end): area abs( (x_data[last_selected_idx] - avg_x_next) * (y_data[j] - y_data[last_selected_idx]) - (x_data[last_selected_idx] - x_data[j]) * (avg_y_next - y_data[last_selected_idx]) ) * 0.5 if area max_area: max_area area max_idx j sampled_x.append(x_data[max_idx]) sampled_y.append(y_data[max_idx]) last_selected_idx max_idx sampled_x.append(x_data[-1]) sampled_y.append(y_data[-1]) return np.array(sampled_x), np.array(sampled_y)LTTB 算法的复杂度是 O(N)100 万数据点降到 1000 个Python 里几秒就能跑完。前端的渲染负载从 100 万降低到 1000性能提升是数量级的。三、服务端降采样 vs 前端降采样数据到底在哪做降采样有两种方案方案 A服务端降采样推荐from flask import Flask, request, jsonify app Flask(__name__) app.route(/api/timeseries) def get_timeseries(): 服务端降采样 API 前端传入视口宽度像素后端自动计算合适的目标点数 原则保留点数 ≤ 视口宽度的 2 倍就够了 start_time request.args.get(start) end_time request.args.get(end) viewport_width int(request.args.get(width, 1200)) target_points min(viewport_width * 2, 2000) x_sampled, y_sampled lttb_downsample( raw_data[timestamp], raw_data[value], target_points ) return jsonify({ data: [{x: float(x), y: float(y)} for x, y in zip(x_sampled, y_sampled)], original_count: len(raw_data), sampled_count: len(x_sampled) })方案 B前端降采样适合静态数据const option { xAxis: { type: time }, yAxis: { type: value }, series: [{ type: line, sampling: lttb, // ECharts 内置的 LTTB 降采样 large: true, largeThreshold: 2000, data: rawData }], dataZoom: [{ type: slider, start: 0, end: 100 }] };四、Hexbin散点图的大数据量方案散点图的降采样特别难因为单纯的去点会丢失分布信息。10 万个散点降成 2000 个点可能一个密集区域在降采样后只剩几个点看起来像数据稀疏。这时候该用 Hexbin六边形聚合把画布分割成六边形网格统计每个六边形内的点数用颜色深浅表示密度。import numpy as np import pandas as pd def hexbin_aggregate(x, y, grid_size50): 六边形聚合把散点数据转成密度热力图 x_norm (x - x.min()) / (x.max() - x.min()) if x.max() x.min() else np.zeros_like(x) y_norm (y - y.min()) / (y.max() - y.min()) if y.max() y.min() else np.zeros_like(y) hex_x (x_norm * grid_size).astype(int) hex_y (y_norm * grid_size * 2 / np.sqrt(3)).astype(int) df pd.DataFrame({hx: hex_x, hy: hex_y}) density df.groupby([hx, hy]).size().reset_index(namecount) return density 踩坑提醒ECharts 的sampling: lttb和large: true不能解决所有问题。ECharts 的内置降采样只作用于渲染层——原始 1000 万个数据点仍然存在 JavaScript 的内存里。如果数据是通过 API 全量返回的这 1000 万个 JSON 对象在浏览器里占的内存就是天花板。服务端降采样的意义不在于算法更好而在于根本不让 1000 万个点进浏览器。viewport_width * 2的 2 倍系数只适合 1x 显示屏。Retina 屏2x应该设成viewport_width * devicePixelRatio * 2否则在高分屏上折线会模糊。但也不要设成 4 倍——4K 屏幕 3840×4×230720 个点虽然渲染不卡但 JS 内存和序列化传输都吃不住。实际上 2000 个点对任何屏幕都够了。Hexbin 的grid_size不能拍脑袋。50×50 的网格在 800×600 的画布上没问题但放到 4K 投影仪上每个六边形宽 80 像素观感像马赛克。正确做法是根据前端传来的画布像素数动态计算grid_size canvas_width / 20每个六边形约 20 像素宽。五、总结ECharts 加载千万级数据卡死的本质是浏览器在渲染它不需要渲染的东西。优化策略总结图表类型推荐策略效果时序折线图LTTB 降采样保留尖峰谷底视觉无损柱状图/聚合图时间窗口预聚合后端算好汇总值散点图Hexbin 聚合用密度代替散点不丢分布信息大数据监控看板服务端降采样 数据分层后端根据缩放级别返回不同粒度核心原则传给前端的点数 需要的像素数不是原始数据行数。你 1920 像素宽的屏幕给 500 个点就够了500 万点是浪费。另外提一句如果数据大到连后端降采样都慢几十亿行加一层 OLAP 引擎ClickHouse/Doris做预聚合把聚合逻辑从应用层推到数据库层。本文由朱大喜原创数据可视化的坑还有很多欢迎关注后续更新~