Chart.js 数据降采样(Data Decimation)插件完全指南:LTTB 与 Min/Max 算法配置与源码原理
【免费下载链接】Chart.jsSimple HTML5 Charts using the项目地址: https://gitcode.com/gh_mirrors/ch/Chart.js
导读
本指南深入解析 Chart.js 内置的 decimation(数据降采样)插件——它专为**大数据量折线图(line chart)**设计,可在图表生命周期起始阶段自动削减渲染点数,从而显著提升绘制性能与交互流畅度。文章将完整覆盖插件的全部配置项、两种内置算法(LTTB 与 Min/Max)的适用场景、六项启用前置要求,并结合仓库源码(src/plugins/plugin.decimation.js)与测试用例,讲透降采样在"何时触发、如何降、如何还原"三环节的底层实现。读完你将能直接复用一个基于 10 万数据点的完整示例配置,并理解在何种数据特征下应选择哪种算法。
为什么需要数据降采样
折线图的数据点一旦达到数万甚至十万级别,Canvas 渲染和事件命中检测都会成为明显的性能瓶颈。decimation 插件的思路很直接:在图表生命周期早期(元素更新之前)就把原始数据替换为一份精简后的数据子集,让后续的解析、布局、绘制与交互都只面对少量点。该插件定义于 src/plugins/plugin.decimation.js,插件 id 为decimation,可通过 官方示例 直观感受默认(不降采样)与启用降采样后同一份 10 万点数据的渲染差异。
需要强调的是,decimation 与 decimation 之外的其他性能手段 不同:它是有损压缩——通过牺牲部分数据细节换取渲染性能,因此必须按数据类型与业务诉求权衡算法。该插件默认关闭(enabled: false),需要显式开启。
配置选项
插件的配置命名空间为options.plugins.decimation,全局默认值定义于Chart.defaults.plugins.decimation。源码中插件对象的defaults字段(plugin.decimation.js)即承载了这些默认值。
| 名称 | 类型 | 默认值 | 描述 |
|---|---|---|---|
enabled | boolean | false | 是否启用降采样。注意插件默认关闭,需显式开启 |
algorithm | string | 'min-max' | 使用的降采样算法,可选'lttb'或'min-max',详见下文算法章节 |
samples | number | (无) | 仅当使用'lttb'算法时生效,表示输出数据集中的采样点数量。默认取 canvas 宽度,即每像素 1 个样本点 |
threshold | number | (无) | 当当前轴可见范围内的样本数大于该值时触发降采样。默认取 4 倍 canvas 宽度。注意:降采样后的点数可能仍高于threshold值 |
源码中与默认值/阈值直接相关的逻辑如下(plugin.decimation.js):
const threshold = options.threshold || 4 * availableWidth; if (count <= threshold) { // No decimation is required until we are above this threshold cleanDecimatedDataset(dataset); return; }其中availableWidth即chart.width(plugin.decimation.js)。也就是说,只有当可见数据点数超过 4 倍画布宽度时,降采样才会被真正触发;低于阈值时插件会静默跳过,并顺带清理可能残留的旧降采样状态。
动态切换算法与启用状态
插件的三个核心配置都可以在运行时通过chart.options.plugins.decimation.xxx修改并调用chart.update()生效。官方示例(data-decimation.md)提供了四种可交互切换的状态:
// 关闭(默认) chart.options.plugins.decimation.enabled = false; chart.update(); // min-max 算法 chart.options.plugins.decimation.algorithm = 'min-max'; chart.options.plugins.decimation.enabled = true; chart.update(); // LTTB 算法,50 个采样点 chart.options.plugins.decimation.algorithm = 'lttb'; chart.options.plugins.decimation.enabled = true; chart.options.plugins.decimation.samples = 50; chart.update(); // LTTB 算法,500 个采样点 chart.options.plugins.decimation.algorithm = 'lttb'; chart.options.plugins.decimation.enabled = true; chart.options.plugins.decimation.samples = 500; chart.update();enabled从true切回false时,插件会通过cleanDecimatedData(chart)遍历所有数据集、移除_decimated与_data并恢复原始data属性(详见下文"数据还原机制"),因此来回切换是安全的。
降采样算法:原理与适用场景
插件支持两种算法,通过algorithm选项选择。若传入不支持的算法名,插件会直接抛出Unsupported decimation algorithm 'xxx'错误(plugin.decimation.js),这也意味着算法名必须严格匹配'lttb'或'min-max'。
Largest Triangle Three Buckets(LTTB)降采样
LTTB 算法能够在保留数据整体趋势的前提下,将数据点数量压缩到非常少,特别适合"用少量点看趋势"的场景,例如长期监控曲线的宏观形态。其实现位于 plugin.decimation.js,注释明确指出该实现基于 Sveinn Steinarsson 的 flot-downsample 项目(MIT 许可)。
核心思路:先把数据在 x 轴上大致均分为若干个桶(bucket),再从每个桶中挑选出能与相邻两点构成最大三角形面积的点作为代表点,从而保证被选中的点是最能"凸显形状"的拐点。算法始终保留首点与末点:
const samples = options.samples || availableWidth; // 如果采样数不小于数据量,直接返回原始切片 if (samples >= count) { return data.slice(start, start + count); } ... decimated[sampledIndex++] = data[a]; // 首点 for (i = 0; i < samples - 2; i++) { // 计算每个桶的平均点,再在与相邻点构成的三角形中找面积最大者 ... decimated[sampledIndex++] = maxAreaPoint; ... } decimated[sampledIndex++] = data[endIndex]; // 末点其中samples的取值逻辑印证了文档说法:未显式指定samples时,默认取availableWidth(canvas 宽度),即每像素 1 个样本点。文档中的samples描述、默认行为与源码完全一致。
一个值得注意的实现细节:原版算法将maxArea初始化为 1,而本仓库改为初始化为-1。源码注释解释了原因——三角形面积恒为非负,在数据为水平直线(面积恒为 0)时,若初始值为 1 会导致nextA永不被赋值,下一轮循环中a变成undefined而崩溃。这一修复也由测试用例should not crash with uneven points(test/specs/plugin.decimation.tests.js)覆盖,该用例用 15552 个不均匀点验证了不抛异常。
Min/Max 降采样
Min/Max 算法保留数据的峰值与谷值,是"极值保持"型算法,非常适合噪声大、必须看到尖峰信号的时序数据(如振动、脉冲类监测数据)。其实现位于 plugin.decimation.js。
核心思路:将可见数据按 x 像素坐标映射并分组,同一像素列内的点只保留 y 值最小和最大的两个点(连同组首、组尾点一起输出),从而保证每个像素列上极值不丢失。源码注释明确说明"每个区间最多输出 4 个点"(组首、min、max、组尾):
if (truncX === prevX) { // 同一像素列内,维护 minY / maxY 及对应下标 ... } else { // Push up to 4 points, 3 for the last interval and the first point for this interval const intermediateIndex1 = Math.min(minIndex, maxIndex); const intermediateIndex2 = Math.max(minIndex, maxIndex); ... decimated.push(point); // 新区间的起点 }这也是文档所述"Min/Max 每个像素最多需要 4 个点"的由来。对比而言:
- LTTB:输出点数由
samples严格控制,压缩率最高,适合看趋势; - Min/Max:输出点数随像素宽度浮动(最多每像素 4 点),保真度更高,适合看极值/噪声信号。
六项启用前置要求(逐一对应源码验证)
文档明确指出,启用该插件前必须满足以下全部要求。这些要求并非文档空谈,每一条都能在插件源码的beforeElementsUpdate钩子中找到对应的守卫判断(plugin.decimation.js):
数据集的
indexAxis必须为'x':源码中resolve([indexAxis, chart.options.indexAxis]) === 'y'时直接跳过(L220-L223)。即不支持横向(y 轴索引)折线图,文档对应链接见 line.md 的 General 章节。数据集必须是折线(line)数据集:源码检查
meta.controller.supportsDecimation(L225)。supportsDecimation默认在基类 core.datasetController.js 中为false,仅在 controller.line.js(以及 scatter 控制器)中被置为true。因此 bar、doughnut 等图表类型天然不适用。X 轴必须为
'linear'或'time'类型:源码检查xAxis.type !== 'linear' && xAxis.type !== 'time'(L230-L234)。category等离散轴不支持,相关轴文档见 linear 轴 与 time 轴。数据必须无需解析,即
parsing必须为false:源码检查chart.options.parsing为真则跳过(L236-L239)。原因在于降采样算法直接以{x, y}对象形式读写数据(如data[j].x、data[j].y),需要数据已是解析后的内部格式。相关说明见>dataset._data = data; // 原始数据存入 _data delete dataset.data; Object.defineProperty(dataset, 'data', { configurable: true, enumerable: true, get: function() { return this._decimated; }, // 读操作返回降采样结果 set: function(d) { this._data = d; } // 写操作仍写回原始数据 });之后 Chart.js 内部及用户读取
dataset.data时拿到的都是降采样后的_decimated;而用户若给dataset.data赋新值,会被 setter 存入_data,下次更新时重新降采样。需要还原时,cleanDecimatedDataset(L156-L168)会删除_decimated、_data并将data重新定义为普通可写属性、恢复原始数据。该清理逻辑在插件destroy钩子(L284-L286)中也会执行,避免图表销毁后留下悬挂引用。此外,
_decimated标记还会传递给折线元素:折线控制器会把line._decimated = !!_dataset._decimated写入元素(controller.line.js),而 element.line.js 的绘制快速路径(useFastPath)会跳过降采样数据上的复杂插值计算,进一步提升渲染性能。完整可运行示例:10 万数据点的降采样配置
官方示例(docs/samples/advanced/data-decimation.md)给出了一个完整的、可直接运行的 10 万点折线图配置。它以 30 秒为间隔生成从
2021-04-01T00:00:00Z开始的 10 万个{x, y}时间序列点,其中绝大多数数据落在[0, 20),约 0.1% 的罕见数据落在[0, 100)——这种"罕见尖峰"分布恰好能体现两种算法的差异:const NUM_POINTS = 100000; Utils.srand(10); const start = Utils.parseISODate('2021-04-01T00:00:00Z').toMillis(); const pointData = []; for (let i = 0; i < NUM_POINTS; ++i) { const max = Math.random() < 0.001 ? 100 : 20; pointData.push({x: start + (i * 30000), y: Utils.rand(0, max)}); } const decimation = { enabled: false, // 先关闭,示例中通过 actions 动态切换 algorithm: 'min-max', }; const config = { type: 'line', data: { datasets: [{ borderColor: Utils.CHART_COLORS.red, borderWidth: 1, data: pointData, label: 'Large Dataset', radius: 0, }] }, options: { // 关闭动画与数据解析以获得最佳性能 animation: false, parsing: false, // 必须为 false,否则插件跳过 interaction: { mode: 'nearest', axis: 'x', intersect: false }, plugins: { decimation: decimation, }, scales: { x: { type: 'time', // 必须为 time 或 linear ticks: { source: 'auto', maxRotation: 0, // 关闭刻度旋转,提升性能 autoSkip: true, } } } } };该配置满足全部前置要求:
type: 'line'、x 轴为time、parsing: false、数据为预解析的{x, y}对象数组且按 x 升序。示例内置的 actions 面板支持在"不降采样 / min-max / LTTB(50) / LTTB(500)"四种状态间切换,用于对比压缩率与形态保真度。运行时动态切换的代码见上文"动态切换算法与启用状态"一节。测试验证:算法行为的关键断言
仓库测试(test/specs/plugin.decimation.tests.js)从多个角度锁定了插件行为,可作为配置预期的重要参考:
- 采样数上限保护:
samples: 100大于 10 个数据点时,输出仍为全部 10 个点(should draw all element if sample is greater than data based on canvas width); - 采样数精确控制:
samples: 7时输出恰好 7 个点(should draw the specified number of elements based on canvas width); - 阈值门槛:
samples: 5, threshold: 7时输出 5 个点,证明阈值只决定"是否触发"而非"输出多少"(should draw the specified number of elements based on threshold); - 可见范围裁剪:x 轴范围限定为 3–6 时,输出仅覆盖该范围(含范围前一点与范围内各点)的 5 个点(
should draw all element only in range),佐证降采样只在可见区间内进行; - 边界健壮性:15552 个不均匀点在
devicePixelRatio: 1.25下不抛异常(should not crash with uneven points),对应 LTTB 实现中maxArea初始化的修复。
这些断言与文档参数表、源码逻辑互相印证:
samples决定 LTTB 输出规模,threshold决定是否触发,可见范围决定处理的数据窗口。实践建议与注意事项
- 数据必须预排序:由于
parsing: false时数据需为内部格式且按 x 升序(见>【免费下载链接】Chart.jsSimple HTML5 Charts using the项目地址: https://gitcode.com/gh_mirrors/ch/Chart.js
- 采样数上限保护:
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考