图表散点与大量标记
Chart 库内部多半还是 Canvas 2D 或 SVG。自己画时,先分清 轴/网格(少) 和 系列点(多)。点一多,2D 的同步 fill 会先于「算法不够」把帧打爆。
1. 分层
网格 + 坐标轴:几乎静态,resize/换域才重绘
系列:数据 dirty 才重绘
十字准星 + tooltip:DOM 或动态层,跟指针
轴用世界(数据)→ view 的独立 affine,不一定复用白板 Camera,但公式同类:
export interface PlotScale {
xMin: number;
xMax: number;
yMin: number;
yMax: number;
padLeft: number;
padRight: number;
padTop: number;
padBottom: number;
width: number;
height: number;
}
export function dataToView(s: PlotScale, x: number, y: number): { x: number; y: number } {
const w = s.width - s.padLeft - s.padRight;
const h = s.height - s.padTop - s.padBottom;
return {
x: s.padLeft + ((x - s.xMin) / (s.xMax - s.xMin)) * w,
y: s.padTop + (1 - (y - s.yMin) / (s.yMax - s.yMin)) * h,
};
}
Y 向上是数据惯例,Canvas Y 向下,必须翻转。漏翻转是「图是对的但倒了」。
2. 点怎么画
| 数量级 | 策略 |
|---|---|
| < 2k | 按颜色分桶 Path2D 一次 fill,见 性能 |
| 2k–2万 | 视口 LOD:聚合到像素;一像素多点只画一次 |
| > 2万仍要每点交互 | WebGL / 专用 chart(ECharts GL、regl) |
export function paintDedupedPoints(
ctx: CanvasRenderingContext2D,
scale: PlotScale,
points: readonly { x: number; y: number }[],
radius: number,
): void {
const seen = new Set<number>();
const path = new Path2D();
for (const p of points) {
const v = dataToView(scale, p.x, p.y);
const ix = Math.round(v.x);
const iy = Math.round(v.y);
const key = (ix << 16) ^ iy;
if (seen.has(key)) continue;
seen.add(key);
path.moveTo(v.x + radius, v.y);
path.arc(v.x, v.y, radius, 0, Math.PI * 2);
}
ctx.fill(path);
}
这是屏幕像素去重,不是统计 bin。密度图要用真正的 grid count 再上色。
3. 拾取
不要 isPointInPath 对两万个圆。把点放进 网格索引,查询鼠标附近 1–2 格,再比距离。
Tooltip 用 DOM overlay,内容可复制、可读屏。Canvas 里 fillText 当 tooltip 是倒退。
4. 动画与大数据
追加流式点:只脏系列层。不要每秒重建轴。时间窗移动是改 xMin/xMax + 系列重绘,网格线按新域生成。
5. 失败形态
| 症状 | 原因 |
|---|---|
| 图倒了 | Y 没翻转 |
| 1万点卡成幻灯片 | 每点 beginPath+fill,还有阴影 |
| hover 错点 | 命中用了数据坐标去比 CSS |
| 缩轴后线宽狂变 | 线宽画在数据空间 |
| Retina 点变椭圆 | x/y scale 各算各的 dpr |
权威资料
- 无图表专规。拾取与分桶见本体系 03/06;超量用 WebGL 选型。
核对日期:2026-08-26