js-lttb
Version:
Largest Triangle Three Buckets (LTTB) 下采样算法的 TypeScript 实现,用于减少图表节点数量并保留数据的视觉形状。
60 lines (59 loc) • 2.55 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
/**
* LTTB (Largest Triangle Three Buckets) 降采样算法
* @param data 输入数据,包含时间和值字段以及其他可选字段
* @param targetPoints 目标点数
* @param xKey X 轴字段名(默认 'time')
* @param yKey Y 轴字段名(默认 'value')
* @returns 降采样后的数据点,保留所有原始字段
*/
function lttb(data, targetPoints, xKey = 'time', yKey = 'value') {
if (!Array.isArray(data) || data.length <= 2 || targetPoints >= data.length) {
return data.slice();
}
if (targetPoints < 2) {
return [data[0], data[data.length - 1]];
}
const normalizedData = data.map((point) => {
const x = Number(point[xKey]);
const y = Number(point[yKey]);
if (isNaN(x) || isNaN(y)) {
throw new Error(`Invalid number for ${String(xKey)} or ${String(yKey)}`);
}
return { x, y, original: point };
});
const bucketCount = targetPoints - 2;
const bucketSize = (data.length - 2) / bucketCount;
const sampled = [normalizedData[0]];
const triangleArea = (p1, p2, p3) => Math.abs((p1.x * (p2.y - p3.y) + p2.x * (p3.y - p1.y) + p3.x * (p1.y - p2.y)) / 2);
for (let i = 0; i < bucketCount; i++) {
const startIdx = Math.floor(1 + i * bucketSize);
const endIdx = Math.min(Math.floor(1 + (i + 1) * bucketSize), data.length - 1);
const nextStart = endIdx;
const nextEnd = Math.min(Math.floor(1 + (i + 2) * bucketSize), data.length - 1);
const nextCount = Math.max(nextEnd - nextStart, 1);
const nextAvg = normalizedData.slice(nextStart, nextEnd).reduce((acc, p) => ({ x: acc.x + p.x, y: acc.y + p.y }), { x: 0, y: 0 });
const nextAvgX = nextAvg.x / nextCount;
const nextAvgY = nextAvg.y / nextCount;
let maxArea = -1;
let selectedPoint = null;
const prevPoint = sampled[sampled.length - 1];
for (let j = startIdx; j < endIdx; j++) {
const area = triangleArea(prevPoint, normalizedData[j], {
x: nextAvgX,
y: nextAvgY,
original: {},
});
if (area > maxArea) {
maxArea = area;
selectedPoint = normalizedData[j];
}
}
if (selectedPoint)
sampled.push(selectedPoint);
}
sampled.push(normalizedData[data.length - 1]);
return sampled.map((point) => point.original);
}
exports.default = lttb;