Skip to content

A10 前端可视化与图形架构

目标:掌握前端可视化技术选型、渲染原理和图形架构设计,能够构建高性能数据可视化系统。


核心要点(TL;DR)

  • 可视化选型取决于数据规模、交互复杂度、渲染性能和团队能力。
  • SVG 适合静态、可交互、DOM 操作友好的场景;Canvas 适合像素级控制和大量图形;WebGL 适合大规模 3D/2D 渲染;WebGPU 是下一代高性能 GPU 计算标准。
  • 大数据量渲染的核心是减少绘制调用和 DOM 节点,常用手段包括分层、瓦片、LOD、虚拟化。
  • 图形架构需要抽象渲染层、数据层、交互层,降低与具体渲染技术的耦合。
  • 性能预算是可视化系统的关键约束,需从加载、渲染、交互三个维度设定指标。

1. 可视化技术选型

1.1 SVG

  • 矢量图形:基于 XML 的矢量格式,缩放不失真,适合高清屏(Retina)显示。
  • DOM 特性:每个图形都是 DOM 节点,天然支持事件绑定、CSS 样式和动画。
  • 适用场景:折线图、柱状图、饼图、流程图、地图标注、数据仪表盘。
  • 性能限制:节点数超过 5000 时 DOM 操作和事件监听开销明显上升;超过 1 万节点建议考虑 Canvas。
  • 优势技术<path> 元素可表达任意复杂形状;SMIL 动画可实现声明式动画;CSS filter 支持图形特效。
  • 最佳实践:使用 <g> 分组管理;利用 viewBox 实现响应式;复杂路径使用路径压缩。

1.2 Canvas 2D

  • 位图渲染:通过 JavaScript 直接控制像素,绘制后即销毁图形对象。
  • 适用场景:大量数据点(万级以上)、复杂动画、游戏引擎、实时数据流、图表库底层实现。
  • 事件处理:Canvas 本身不提供命中检测,需要自行实现:坐标转换 + 数学计算(点在线段/多边形内判断)。
  • 性能特点:单帧绘制效率高,适合批量绘制;状态切换(fillStyle、strokeStyle 等)有性能开销。
  • 高分屏适配:需要将 Canvas 的像素尺寸设为 CSS 尺寸的 devicePixelRatio 倍。

1.3 WebGL

  • GPU 加速:基于 OpenGL ES 2.0,利用 GPU 并行渲染大量图形。
  • 适用场景:大规模散点图(百万级)、3D 场景、粒子系统、实时视频处理。
  • 编程模型:基于着色器(Shader)编程,顶点着色器控制位置,片元着色器控制颜色。
  • 学习曲线:需理解 3D 数学(矩阵变换、四元数)、着色器语言 GLSL、渲染管线。
  • 封装库:Three.js(通用 3D)、Deck.gl(地理数据)、PixiJS(2D 加速)。

1.4 WebGPU

  • 下一代标准:取代 WebGL,更接近现代 GPU 架构(Vulkan/Metal/DirectX 12)。
  • 核心优势
    • 更低的 CPU 开销:减少状态验证和驱动层开销。
    • 计算着色器(Compute Shader):支持通用 GPU 计算,不仅是渲染。
    • 显存管理:显式控制缓冲区生命周期,减少内存碎片。
    • 多线程友好:可与 Web Worker 协同工作。
  • 现状:Chrome 113+ / Edge 113+ 已支持;Safari 和 Firefox 在实验阶段。
  • 适用场景:大规模数据处理(200 万+ 数据点)、实时物理模拟、图片/视频处理。
  • 封装库:Three.js(WebGPU 渲染器)、Babylon.js 6.0+。

1.5 选型对比

技术数据量交互性能开发复杂度兼容性移动端
SVG中小 (<5K)全平台一般
Canvas 2D大 (<100K)全平台较好
WebGL超大 (<1M)极高现代浏览器有限
WebGPU巨量 (>1M)极致极高仅 Chrome/Edge暂无

2. Canvas API 深入

2.1 2D 上下文 (CanvasRenderingContext2D)

Canvas 2D 上下文是 Canvas 绘制的核心 API,提供路径绘制、变换、像素操作、文本渲染等能力。

javascript
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');

// 基本路径绘制
ctx.beginPath();
ctx.moveTo(50, 50);
ctx.lineTo(200, 50);
ctx.lineTo(200, 150);
ctx.closePath();
ctx.fillStyle = '#3498db';
ctx.fill();
ctx.strokeStyle = '#2c3e50';
ctx.lineWidth = 2;
ctx.stroke();

// 绘制弧线
ctx.beginPath();
ctx.arc(100, 100, 50, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(231, 76, 60, 0.5)';
ctx.fill();

// 贝塞尔曲线
ctx.beginPath();
ctx.moveTo(50, 200);
ctx.quadraticCurveTo(150, 50, 250, 200);
ctx.strokeStyle = '#9b59b6';
ctx.lineWidth = 3;
ctx.stroke();

2.2 变换与矩阵操作

Canvas 的变换机制基于变换矩阵,通过 save/restore 管理状态栈。

javascript
function drawRotatedRect(ctx, x, y, w, h, angle, scale) {
    ctx.save();
    ctx.translate(x, y);
    ctx.rotate(angle);
    ctx.scale(scale, scale);
    ctx.fillStyle = '#2ecc71';
    ctx.fillRect(-w/2, -h/2, w, h);
    ctx.restore();
}

// 直接操作变换矩阵
ctx.setTransform(1, 0.5, 0, 1, 100, 100);
ctx.fillRect(0, 0, 50, 50);
ctx.resetTransform();

2.3 合成与裁剪

javascript
// 全局合成模式
ctx.globalCompositeOperation = 'lighter';  // 叠加模式

// 裁剪区域
ctx.beginPath();
ctx.arc(200, 200, 100, 0, Math.PI * 2);
ctx.clip();
// 后续绘制只在此圆形区域内显示

// 阴影
ctx.shadowColor = 'rgba(0, 0, 0, 0.3)';
ctx.shadowBlur = 10;
ctx.shadowOffsetX = 5;
ctx.shadowOffsetY = 5;

2.4 像素操作与图像处理

javascript
const imageData = ctx.getImageData(0, 0, width, height);
const data = imageData.data;

// 灰度化
for (let i = 0; i < data.length; i += 4) {
    const gray = 0.299 * data[i] + 0.587 * data[i+1] + 0.114 * data[i+2];
    data[i] = data[i+1] = data[i+2] = gray;
}

// 反色
for (let i = 0; i < data.length; i += 4) {
    data[i] = 255 - data[i];
    data[i+1] = 255 - data[i+1];
    data[i+2] = 255 - data[i+2];
}

ctx.putImageData(imageData, 0, 0);

2.5 Offscreen Canvas

OffscreenCanvas 允许在 Worker 线程中执行 Canvas 绘制,避免阻塞主线程。

javascript
// 主线程
const offscreen = canvas.transferControlToOffscreen();
worker.postMessage({ canvas: offscreen }, [offscreen]);

// Worker 线程
self.onmessage = (e) => {
    const canvas = e.data.canvas;
    const ctx = canvas.getContext('2d');
    
    function render() {
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        // 执行大量绘制操作
        for (let i = 0; i < 10000; i++) {
            ctx.fillRect(Math.random() * 800, Math.random() * 600, 2, 2);
        }
        requestAnimationFrame(render);
    }
    render();
};

2.6 路径性能优化

javascript
// 使用 Path2D 预编译路径
const path = new Path2D();
path.moveTo(0, 0);
path.lineTo(100, 0);
path.lineTo(50, 100);
path.closePath();

// 多次复用
for (let i = 0; i < 1000; i++) {
    ctx.save();
    ctx.translate(x[i], y[i]);
    ctx.fill(path);
    ctx.restore();
}

3. SVG 深入

3.1 viewBox 与视口机制

viewBox 定义 SVG 的内部坐标系,通过 preserveAspectRatio 控制缩放对齐方式。

xml
<svg viewBox="0 0 100 100" width="400" height="400"
     preserveAspectRatio="xMidYMid meet">
  <!-- 内部始终按 100x100 坐标系绘制 -->
  <rect x="10" y="10" width="80" height="80" fill="blue"/>
</svg>

preserveAspectRatio 参数

  • meet:等比缩放,完整显示(类似 object-fit: contain)
  • slice:等比缩放,裁切溢出(类似 object-fit: cover)
  • xMidYMid:居中对齐

3.2 SVG 滤镜系统

SVG 滤镜提供丰富的图形特效,通过 <filter><fe*> 元素组合实现。

xml
<defs>
  <!-- 发光效果 -->
  <filter id="glow">
    <feGaussianBlur stdDeviation="3" result="blur"/>
    <feMerge>
      <feMergeNode in="blur"/>
      <feMergeNode in="SourceGraphic"/>
    </feMerge>
  </filter>

  <!-- 投影 -->
  <filter id="shadow">
    <feDropShadow dx="2" dy="4" stdDeviation="4" flood-color="#000" flood-opacity="0.3"/>
  </filter>

  <!-- 颜色矩阵 (实现色相旋转) -->
  <filter id="colorize">
    <feColorMatrix type="hueRotate" values="90"/>
  </filter>
</defs>

<circle cx="50" cy="50" r="40" fill="red" filter="url(#glow)"/>

3.3 裁剪与蒙版

xml
<defs>
  <clipPath id="circleClip">
    <circle cx="100" cy="100" r="80"/>
  </clipPath>
  <mask id="gradientMask">
    <rect width="200" height="200" fill="url(#gradient)"/>
  </mask>
</defs>

<image x="0" y="0" width="200" height="200"
       clip-path="url(#circleClip)" href="image.jpg"/>

3.4 SVG 动画

SVG 支持三种动画方式:SMIL、CSS Animation、JavaScript 操作。

SMIL 声明式动画

xml
<circle cx="50" cy="50" r="20" fill="red">
  <animate attributeName="cx" from="50" to="200" dur="2s"
           repeatCount="indefinite" easing="ease-in-out"/>
  <animate attributeName="fill" values="red;blue;green;red" dur="4s"
           repeatCount="indefinite"/>
</circle>

<!-- 路径动画 -->
<path id="motionPath" d="M10,80 C40,10 65,10 95,80 S150,150 180,80" fill="none"/>
<circle r="5" fill="red">
  <animateMotion dur="3s" repeatCount="indefinite">
    <mpath href="#motionPath"/>
  </animateMotion>
</circle>

CSS 动画

css
@keyframes pulse {
  0% { r: 10; opacity: 1; }
  100% { r: 30; opacity: 0; }
}
circle.pulse {
  animation: pulse 2s ease-out infinite;
}

3.5 大规模 SVG 优化

当 SVG 节点超过 1000 时,需要采用以下策略:

javascript
// 1. 使用 requestAnimationFrame 批量更新
function batchUpdate(elements, attrs) {
    requestAnimationFrame(() => {
        elements.forEach((el, i) => {
            el.setAttribute('transform', `translate(${attrs.x[i]}, ${attrs.y[i]})`);
        });
    });
}

// 2. 使用 <use> 复用元素
const defs = svg.append('defs');
defs.append('circle').attr('id', 'dot').attr('r', 3).attr('fill', 'steelblue');
// 其他地方引用
svg.append('use').attr('href', '#dot').attr('x', 100).attr('y', 200);

// 3. 路径压缩:将多个点合并到一条路径
const pathData = points.map(p => `M${p.x},${p.y}L${p.x+1},${p.y}`).join('');
svg.append('path').attr('d', pathData).attr('stroke', 'blue').attr('stroke-width', 2);

// 4. 使用 innerHTML 替代逐个创建
svg.innerHTML = `<g>${data.map(d => `<rect x="${d.x}" y="${d.y}" width="10" height="10"/>`).join('')}</g>`;

3.6 事件代理

为大量 SVG 元素绑定事件时,使用事件代理模式:

javascript
// 不使用:直接绑定(O(n))
elements.forEach(el => el.addEventListener('click', handler));

// 使用:事件代理(O(1))
svg.addEventListener('click', (e) => {
    const target = e.target.closest('circle');
    if (!target) return;
    const idx = target.dataset.index;
    handler(data[idx]);
});

4. WebGL 与 Three.js 深入

4.1 WebGL 渲染管线

WebGL 基于可编程渲染管线,分为顶点着色器和片元着色器两个阶段:

顶点数据 → 顶点着色器 → 图元装配 → 光栅化 → 片元着色器 → 帧缓冲
   ↓                                                        ↑
  缓冲区                                                  纹理/帧缓冲

4.2 Three.js 核心组件

javascript
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

// 场景
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x1a1a2e);
scene.fog = new THREE.Fog(0x1a1a2e, 10, 50);  // 雾效果

// 相机
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(5, 5, 5);
camera.lookAt(0, 0, 0);

// 渲染器
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
renderer.toneMapping = THREE.ACESFilmicToneMapping;

// 光照
const ambientLight = new THREE.AmbientLight(0x404040, 0.5);
scene.add(ambientLight);

const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(5, 10, 5);
directionalLight.castShadow = true;
scene.add(directionalLight);

// 物体
const geometry = new THREE.SphereGeometry(1, 32, 32);
const material = new THREE.MeshStandardMaterial({
    color: 0x3498db,
    metalness: 0.7,
    roughness: 0.2
});
const sphere = new THREE.Mesh(geometry, material);
sphere.castShadow = true;
scene.add(sphere);

// 控制器
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;

// 动画循环
function animate() {
    requestAnimationFrame(animate);
    sphere.rotation.x += 0.01;
    controls.update();
    renderer.render(scene, camera);
}
animate();

4.3 几何体与缓冲区

javascript
// 自定义几何体:创建一个由三角形组成的网格
const vertices = new Float32Array([
    -1, -1, 0,   1, -1, 0,   0, 1, 0,  // 三角形1
    // ...更多顶点
]);
const colors = new Float32Array([
    1, 0, 0,   0, 1, 0,   0, 0, 1,
]);

const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));

// 索引缓冲区
const indices = new Uint16Array([0, 1, 2, 2, 3, 0]);
geometry.setIndex(new THREE.BufferAttribute(indices, 1));

// 法线计算
geometry.computeVertexNormals();

4.4 着色器材质 (ShaderMaterial)

javascript
// 顶点着色器
const vertexShader = `
    varying vec3 vPosition;
    varying vec3 vNormal;

    void main() {
        vPosition = position;
        vNormal = normalize(normalMatrix * normal);
        gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
`;

// 片元着色器
const fragmentShader = `
    uniform vec3 uColor;
    uniform float uTime;
    varying vec3 vPosition;
    varying vec3 vNormal;

    void main() {
        // 基于法线的光照效果
        vec3 lightDir = normalize(vec3(1.0, 1.0, 1.0));
        float diff = max(dot(vNormal, lightDir), 0.0);

        // 基于位置的颜色变化
        float pulse = sin(vPosition.x * 2.0 + uTime) * 0.5 + 0.5;

        vec3 finalColor = uColor * (0.3 + 0.7 * diff) * (0.8 + 0.2 * pulse);
        gl_FragColor = vec4(finalColor, 1.0);
    }
`;

const material = new THREE.ShaderMaterial({
    uniforms: {
        uColor: { value: new THREE.Color(0x3498db) },
        uTime: { value: 0 }
    },
    vertexShader: vertexShader,
    fragmentShader: fragmentShader,
});

4.5 粒子系统

javascript
const particleCount = 50000;
const positions = new Float32Array(particleCount * 3);
const colors = new Float32Array(particleCount * 3);

for (let i = 0; i < particleCount; i++) {
    positions[i * 3] = (Math.random() - 0.5) * 100;
    positions[i * 3 + 1] = (Math.random() - 0.5) * 100;
    positions[i * 3 + 2] = (Math.random() - 0.5) * 100;

    colors[i * 3] = Math.random();
    colors[i * 3 + 1] = Math.random();
    colors[i * 3 + 2] = Math.random();
}

const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));

const material = new THREE.PointsMaterial({
    size: 0.5,
    vertexColors: true,
    transparent: true,
    blending: THREE.AdditiveBlending
});

const particles = new THREE.Points(geometry, material);
scene.add(particles);

5. 图表库对比

5.1 ECharts

  • 架构:基于 Canvas(ZRender 渲染层),5.0 后支持 SVG 渲染器和 WebGL 加速。
  • 优势:开箱即用,主题丰富,支持大数据量(通过 WebGL 采样渲染),移动端适配良好。
  • 定制性:通过 option 配置驱动,支持自定义系列(Custom Series)。
  • 性能:10 万+ 数据点仍能保持 30fps+,通过采样算法(large)自动降采样。
  • 适用场景:企业级报表、BI 系统、通用图表需求。
javascript
const chart = echarts.init(document.getElementById('main'), null, {
    renderer: 'canvas',  // 可选 'svg' 或 'webgl'
});
chart.setOption({
    animation: false,  // 大数据量时关闭动画
    xAxis: { type: 'category', data: categories },
    yAxis: { type: 'value' },
    series: [{
        type: 'scatter',
        data: largeDataset,
        large: true,  // 启用大数据量模式
        largeThreshold: 2000,
        symbolSize: 3,
    }],
    progressive: 400,  // 分片渐进渲染
});

5.2 D3.js

  • 架构:数据驱动 DOM,不封装图表,提供数据绑定、比例尺、布局等底层工具。
  • 渲染:默认 SVG,也可结合 Canvas、WebGL(d3-canvas、d3-webgl)。
  • 优势:完全灵活,可创建任何可视化形式,社区生态庞大。
  • 学习曲线:需要理解 Data Join、比例尺、布局等概念。
  • 适用场景:定制化可视化、学术图表、数据新闻、独特交互。
javascript
// D3 不提供图表类型,需要手动构建
const svg = d3.select('#chart').append('svg');
const x = d3.scaleBand().domain(data.map(d => d.name)).range([0, width]);
const y = d3.scaleLinear().domain([0, d3.max(data, d => d.value)]).range([height, 0]);

svg.selectAll('rect').data(data)
    .join('rect')
    .attr('x', d => x(d.name))
    .attr('y', d => y(d.value))
    .attr('width', x.bandwidth())
    .attr('height', d => height - y(d.value));

5.3 AntV / G2

  • 架构:图形语法驱动,提供 G2(统计图表)、G6(图分析)、L7(地理空间)。
  • 渲染:G2 默认 Canvas,支持 SVG;L7 使用 WebGL + Deck.gl。
  • 优势:语法简洁,统计变换内置,React 封装(@ant-design/charts)。
  • 适用场景:企业级 BI、图分析、地理可视化。
javascript
const chart = new G2.Chart({
    container: 'container',
    width: 800,
    height: 400,
});
chart.data(data);
chart.interval().position('name*value').color('name');
chart.render();

5.4 Chart.js

  • 架构:轻量级 Canvas 渲染,灵活可扩展。
  • 优势:API 简洁,包体小(~60KB gzip),类型定义完善。
  • 性能:适合万级以下数据,超过 5 万需降采样。
  • 适用场景:中小型项目、快速原型、嵌入式仪表。

5.5 Highcharts

  • 架构:SVG 渲染(早期),支持 Canvas 混合渲染。
  • 优势:兼容性极佳(支持 IE6+),文档详尽,交互丰富。
  • 授权:商业使用需付费(非 GPL)。
  • 适用场景:金融图表、时间序列分析、兼容性要求高的场景。

5.6 综合对比

维度EChartsD3.jsAntV/G2Chart.jsHighcharts
渲染引擎Canvas/SVG/WebGLSVG/CanvasCanvas/SVGCanvasSVG/Canvas
开箱即用★★★★★★★★★★★★★★★★★★★★
定制性★★★★★★★★★★★★★★★★★★
大数据量★★★★★★★★★★★★★★★
学习成本★★★★★★★★★★★★★
包体积~800KB~250KB~600KB~60KB~300KB
授权免费免费免费免费商业

5.7 选型决策树

数据量大 (10万+) 且需要标准图表 → ECharts / AntV
完全定制化可视化 → D3.js
简单图表需求 → Chart.js
图分析/关系图 → AntV G6
地理空间 → Mapbox / Deck.gl / L7
3D 可视化 → Three.js / Deck.gl
企业报表,兼容旧浏览器 → Highcharts

6. D3.js 深入

6.1 数据连接 (Data Join) 原理

Data Join 是 D3 的核心模式,将数据与 DOM 元素绑定,自动处理 enter、update、exit 三个状态。

javascript
// 数据连接三部曲
const selection = svg.selectAll('.bar')
    .data(dataset, d => d.id);  // 第二参数为 key 函数

// ENTER: 新增数据创建新元素
selection.enter()
    .append('rect')
    .attr('class', 'bar')
    .attr('width', 0)  // 初始状态
    .merge(selection)  // 合并 enter + update
    .transition()
    .attr('x', d => x(d.name))
    .attr('y', d => y(d.value))
    .attr('width', x.bandwidth())
    .attr('height', d => height - y(d.value));

// EXIT: 移除多余元素
selection.exit()
    .transition()
    .attr('width', 0)
    .remove();

// 通用 join() 写法 (D3 v5+)
svg.selectAll('.bar')
    .data(dataset, d => d.id)
    .join(
        enter => enter.append('rect').attr('class', 'bar'),
        update => update,
        exit => exit.remove()
    );

6.2 比例尺 (Scale)

D3 提供多种比例尺,实现数据空间到视觉空间的映射。

javascript
// 定量比例尺
const linear = d3.scaleLinear()        // 连续 → 连续
    .domain([0, 100]).range([0, width]);
const pow = d3.scalePow().exponent(2); // 幂次比例尺
const log = d3.scaleLog();             // 对数比例尺
const sqrt = d3.scaleSqrt();           // 平方根比例尺
const time = d3.scaleTime()            // 时间比例尺
    .domain([new Date(2020, 0, 1), new Date(2020, 11, 31)])
    .range([0, width]);

// 序数比例尺
const ordinal = d3.scaleOrdinal()       // 离散 → 离散
    .domain(['A', 'B', 'C'])
    .range(['#e41a1c', '#377eb8', '#4daf4a']);

const band = d3.scaleBand()            // 离散 → 连续(柱状图)
    .domain(['Mon', 'Tue', 'Wed'])
    .range([0, width])
    .padding(0.2);                      // 内边距

const point = d3.scalePoint()           // 离散 → 连续(散点图)
    .domain(['A', 'B', 'C'])
    .range([0, width]);

// 颜色比例尺
const viridis = d3.scaleSequential(d3.interpolateViridis)
    .domain([0, 1]);
const diverging = d3.scaleDiverging(d3.interpolateRdBu)
    .domain([-1, 0, 1]);

6.3 轴 (Axis)

javascript
const xAxis = d3.axisBottom(x)
    .ticks(10)                          // 刻度数
    .tickFormat(d3.format('.0f'))       // 格式化
    .tickSizeOuter(0);                  // 隐藏外端刻度

svg.append('g')
    .attr('class', 'x-axis')
    .attr('transform', `translate(0, ${height})`)
    .call(xAxis)
    .selectAll('.tick text')
    .style('font-size', '12px');

6.4 过渡与动画

javascript
// 基础过渡
d3.select('rect')
    .transition()
    .duration(1000)
    .delay(200)
    .ease(d3.easeElastic)
    .attr('width', 300);

// 逐元素交错动画
svg.selectAll('rect')
    .data(data)
    .join('rect')
    .transition()
    .delay((d, i) => i * 50)            // 交错延迟
    .attr('height', d => y(d.value));

// 自定义补间
d3.select('circle')
    .transition()
    .attrTween('cx', () => (t) => t * 200)  // t 从 0 到 1
    .styleTween('fill', () => d3.interpolateRgb('red', 'blue'));

6.5 力导向图 (Force Layout)

javascript
const simulation = d3.forceSimulation(nodes)
    .force('link', d3.forceLink(links)
        .id(d => d.id)
        .distance(100)
        .strength(0.5))
    .force('charge', d3.forceManyBody()
        .strength(-300)                  // 排斥力
        .distanceMin(10)
        .distanceMax(500))
    .force('center', d3.forceCenter(width / 2, height / 2))
    .force('collision', d3.forceCollide().radius(20))
    .force('x', d3.forceX(width / 2).strength(0.05))
    .force('y', d3.forceY(height / 2).strength(0.05))
    .alphaDecay(0.02)                   // 衰减系数
    .on('tick', ticked);

function ticked() {
    link.attr('x1', d => d.source.x)
        .attr('y1', d => d.source.y)
        .attr('x2', d => d.target.x)
        .attr('y2', d => d.target.y);

    node.attr('cx', d => d.x)
        .attr('cy', d => d.y);
}

// 拖拽交互
node.call(d3.drag()
    .on('start', (event, d) => {
        if (!event.active) simulation.alphaTarget(0.3).restart();
        d.fx = d.x;
        d.fy = d.y;
    })
    .on('drag', (event, d) => {
        d.fx = event.x;
        d.fy = event.y;
    })
    .on('end', (event, d) => {
        if (!event.active) simulation.alphaTarget(0);
        d.fx = null;
        d.fy = null;
    }));

6.6 地理投影 (Geo Projection)

javascript
const projection = d3.geoMercator()
    .center([104, 35])
    .scale(800)
    .translate([width / 2, height / 2]);

const path = d3.geoPath().projection(projection);

// 绘制地图
svg.selectAll('path')
    .data(topojson.feature(china, china.objects.provinces).features)
    .join('path')
    .attr('d', path)
    .attr('fill', d => colorScale(d.properties.value));

// 常见投影类型
d3.geoMercator()       // 墨卡托
d3.geoAlbers()         // 阿尔伯斯 (美国常用)
d3.geoOrthographic()   // 正射投影 (3D 地球)
d3.geoNaturalEarth1()  // 自然地球投影

7. 高性能渲染策略

7.1 分层渲染 (Layer-Based Rendering)

将静态背景、动态数据、交互元素分在不同层,只有变化层需要重绘。

javascript
// Canvas 分层架构
class CanvasLayers {
    constructor(container) {
        this.layers = {};  // 存储各层 Canvas
    }

    addLayer(name, options = {}) {
        const canvas = document.createElement('canvas');
        canvas.style.position = 'absolute';
        canvas.style.top = '0';
        canvas.style.left = '0';
        canvas.style.pointerEvents = options.interactive ? 'auto' : 'none';
        this.container.appendChild(canvas);

        this.layers[name] = {
            canvas,
            ctx: canvas.getContext('2d'),
            dirty: true,
        };
        return this.layers[name];
    }

    invalidate(name) {
        this.layers[name].dirty = true;
    }

    render() {
        requestAnimationFrame(() => {
            for (const [name, layer] of Object.entries(this.layers)) {
                if (!layer.dirty) continue;
                const ctx = layer.ctx;
                ctx.clearRect(0, 0, canvas.width, canvas.height);
                // 执行该层的绘制
                layer.draw(ctx);
                layer.dirty = false;
            }
        });
    }
}

// 使用
const layers = new CanvasLayers(container);
layers.addLayer('grid', { interactive: false });    // 静态网格
layers.addLayer('data', { interactive: false });     // 数据点
layers.addLayer('interaction', { interactive: true }); // 交互层

7.2 脏矩形 (Dirty Rectangles)

只重绘发生变化的区域,而非整个 Canvas。

javascript
class DirtyRectRenderer {
    constructor(canvas) {
        this.canvas = canvas;
        this.ctx = canvas.getContext('2d');
        this.dirtyRects = [];
    }

    markDirty(x, y, w, h) {
        this.dirtyRects.push({ x, y, w, h });
    }

    render() {
        for (const rect of this.dirtyRects) {
            // 只清除脏区域
            this.ctx.clearRect(rect.x, rect.y, rect.w, rect.h);
            // 只绘制脏区域内的内容
            this.drawInRect(rect);
        }
        this.dirtyRects = [];
    }
}

7.3 数据降采样

大数据量时通过降采样算法减少绘制点,保持视觉特征。

javascript
// LTTB (Largest Triangle Three Buckets) 算法
function lttb(data, threshold) {
    const len = data.length;
    if (threshold >= len || threshold === 0) return data;

    const sampled = [];
    const bucketSize = (len - 2) / (threshold - 2);

    sampled.push(data[0]);  // 保留第一个点

    for (let i = 0; i < threshold - 2; i++) {
        const start = Math.floor((i + 0) * bucketSize) + 1;
        const end = Math.floor((i + 1) * bucketSize) + 1;

        const avgX = data.slice(start, end).reduce((s, d) => s + d.x, 0) / (end - start);
        const avgY = data.slice(start, end).reduce((s, d) => s + d.y, 0) / (end - start);

        let maxArea = -1;
        let maxAreaPoint = data[start];
        const prev = sampled[sampled.length - 1];

        for (let j = start; j < end; j++) {
            const area = Math.abs(
                (prev.x - avgX) * (data[j].y - prev.y) -
                (prev.x - data[j].x) * (avgY - prev.y)
            ) * 0.5;
            if (area > maxArea) {
                maxArea = area;
                maxAreaPoint = data[j];
            }
        }
        sampled.push(maxAreaPoint);
    }

    sampled.push(data[len - 1]);  // 保留最后一个点
    return sampled;
}

// 使用:10 万点降到 1000 点
const sampledData = lttb(rawData, 1000);

7.4 虚拟滚动渲染

只渲染可视区域内的数据点,适用于时间序列和列表可视化。

javascript
function renderVisibleRange(ctx, data, viewStart, viewEnd, width, height) {
    const visibleData = data.filter(d => d.x >= viewStart && d.x <= viewEnd);
    const xScale = (width / (viewEnd - viewStart));

    ctx.clearRect(0, 0, width, height);
    ctx.beginPath();
    visibleData.forEach((d, i) => {
        const x = (d.x - viewStart) * xScale;
        const y = height - (d.value / maxValue) * height;
        i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
    });
    ctx.stroke();
}

7.5 Web Worker 渲染

将数据计算和渲染准备放到 Worker 线程。

javascript
// main.js
const worker = new Worker('render-worker.js');
const offscreen = canvas.transferControlToOffscreen();
worker.postMessage({ canvas: offscreen, type: 'init' }, [offscreen]);

worker.postMessage({ type: 'data', data: largeDataset });
worker.onmessage = (e) => {
    if (e.data.type === 'stats') {
        updateStats(e.data.stats);
    }
};

// render-worker.js
self.onmessage = (e) => {
    if (e.data.type === 'init') {
        canvas = e.data.canvas;
        ctx = canvas.getContext('2d');
    } else if (e.data.type === 'data') {
        const processed = processData(e.data.data);
        drawData(ctx, processed);
        self.postMessage({ type: 'stats', stats: computeStats(processed) });
    }
};

7.6 requestAnimationFrame 最佳实践

javascript
let animFrameId = null;
let lastFrameTime = 0;

function startLoop() {
    function frame(timestamp) {
        const delta = timestamp - lastFrameTime;
        lastFrameTime = timestamp;

        // 限制帧率到 30fps(CPU 敏感场景)
        if (delta < 33) {
            animFrameId = requestAnimationFrame(frame);
            return;
        }

        update(timestamp);
        render();

        animFrameId = requestAnimationFrame(frame);
    }
    animFrameId = requestAnimationFrame(frame);
}

function stopLoop() {
    if (animFrameId) {
        cancelAnimationFrame(animFrameId);
        animFrameId = null;
    }
}

8. 实时数据可视化

8.1 WebSocket 流式数据

javascript
class RealTimeVisualization {
    constructor(url, options = {}) {
        this.url = url;
        this.buffer = [];
        this.maxPoints = options.maxPoints || 10000;
        this.sampleInterval = options.sampleInterval || 16;  // ~60fps
        this.ws = null;
        this.lastRender = 0;
    }

    connect() {
        this.ws = new WebSocket(this.url);
        this.ws.binaryType = 'arraybuffer';  // 使用二进制传输提高性能

        this.ws.onmessage = (event) => {
            // 解析二进制数据
            const view = new DataView(event.data);
            const point = {
                timestamp: view.getFloat64(0, true),
                value: view.getFloat64(8, true),
                symbol: view.getInt32(16, true),
            };
            this.addDataPoint(point);
        };

        this.ws.onclose = () => {
            setTimeout(() => this.connect(), 1000);  // 自动重连
        };
    }

    addDataPoint(point) {
        this.buffer.push(point);
        if (this.buffer.length > this.maxPoints) {
            this.buffer.splice(0, this.buffer.length - this.maxPoints);
        }
        this.scheduleRender();
    }

    scheduleRender() {
        requestAnimationFrame((timestamp) => {
            if (timestamp - this.lastRender < this.sampleInterval) return;
            this.lastRender = timestamp;
            this.render();
        });
    }

    render() {
        // 增量渲染逻辑
        const visible = this.getVisibleRange();
        this.drawChart(visible);
    }
}

8.2 环形缓冲区 (Ring Buffer)

避免频繁的内存分配和 GC 压力。

javascript
class RingBuffer {
    constructor(capacity) {
        this.buffer = new Float64Array(capacity * 2);  // x, y 交错存储
        this.capacity = capacity;
        this.head = 0;
        this.count = 0;
    }

    push(x, y) {
        const idx = (this.head + this.count) % this.capacity;
        this.buffer[idx * 2] = x;
        this.buffer[idx * 2 + 1] = y;
        if (this.count < this.capacity) {
            this.count++;
        } else {
            this.head = (this.head + 1) % this.capacity;  // 覆盖旧数据
        }
    }

    forEach(fn) {
        for (let i = 0; i < this.count; i++) {
            const idx = (this.head + i) % this.capacity;
            fn(this.buffer[idx * 2], this.buffer[idx * 2 + 1], i);
        }
    }

    toArray() {
        const result = [];
        this.forEach((x, y) => result.push({ x, y }));
        return result;
    }
}

8.3 增量绘制 (Incremental Rendering)

只绘制新增数据点,避免全量重绘。

javascript
class IncrementalChart {
    constructor(ctx, width, height) {
        this.ctx = ctx;
        this.width = width;
        this.height = height;
        this.prevCount = 0;
    }

    appendData(data) {
        const ctx = this.ctx;
        const newPoints = data.slice(this.prevCount);

        // 滚动平移(将已有内容左移)
        const scrollPixels = newPoints.length;
        ctx.drawImage(ctx.canvas, scrollPixels, 0,
            this.width - scrollPixels, this.height, 0, 0,
            this.width - scrollPixels, this.height);

        // 只绘制新增区域
        ctx.save();
        ctx.beginPath();
        ctx.rect(this.width - scrollPixels, 0, scrollPixels, this.height);
        ctx.clip();

        ctx.beginPath();
        newPoints.forEach((d, i) => {
            const x = this.width - scrollPixels + i;
            const y = this.height - (d.value / this.max) * this.height;
            i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
        });
        ctx.stroke();

        ctx.restore();
        this.prevCount = data.length;
    }
}

8.4 数据压缩与 Worker 处理

javascript
class DataProcessor {
    constructor() {
        this.worker = new Worker('data-processor.js');
        this.worker.onmessage = (e) => this.onProcessed(e.data);
    }

    pushRawData(rawData) {
        // 主线程只做传输,不做计算
        this.worker.postMessage({ type: 'process', data: rawData }, [
            rawData.buffer
        ]);
    }
}

// data-processor.js
self.onmessage = (e) => {
    if (e.data.type === 'process') {
        const raw = new Float64Array(e.data.data);
        const result = [];

        // 执行降采样、去噪、聚合
        for (let i = 0; i < raw.length; i += 2) {
            const aggregated = aggregate(raw, i, 100);
            result.push(aggregated);
        }

        self.postMessage({ type: 'result', data: result });
    }
};

9. GIS 与地图可视化

9.1 地图技术与选型

特性Mapbox GL JSLeafletCesiumJSOpenLayers
渲染引擎WebGLCanvas + SVGWebGLCanvas + WebGL
3D 能力2.5D (倾斜摄影)全球级 3D基本 3D
数据源Vector TileWMS/WMTS3D TilesWMS/WFS
大规模数据优秀 (WebGL)一般优秀 (3D Tiles)中等
学习成本
授权免费 (需 Token)免费免费 / 商业免费
适用场景数据可视化基础地图数字孪生GIS 平台

9.2 Mapbox GL JS 高级用法

javascript
mapboxgl.accessToken = 'your-token';

const map = new mapboxgl.Map({
    container: 'map',
    style: 'mapbox://styles/mapbox/dark-v11',
    center: [116.4, 39.9],  // 北京
    zoom: 10,
    pitch: 45,               // 倾斜角度
    bearing: 30,             // 旋转角度
});

// 添加数据层
map.on('load', () => {
    // 热力图
    map.addLayer({
        id: 'heatmap',
        type: 'heatmap',
        source: {
            type: 'geojson',
            data: 'data/points.geojson',
        },
        paint: {
            'heatmap-weight': ['get', 'mag'],
            'heatmap-intensity': 1,
            'heatmap-radius': 30,
            'heatmap-opacity': 0.8,
            'heatmap-color': [
                'interpolate', ['linear'], ['heatmap-density'],
                0, 'rgba(33,102,172,0)',
                0.2, 'rgb(103,169,207)',
                0.4, 'rgb(209,229,240)',
                0.6, 'rgb(253,219,199)',
                0.8, 'rgb(239,138,98)',
                1, 'rgb(178,24,43)'
            ]
        }
    });

    // 3D 建筑
    map.addLayer({
        id: '3d-buildings',
        type: 'fill-extrusion',
        source: 'composite',
        'source-layer': 'building',
        paint: {
            'fill-extrusion-color': ['get', 'color'],
            'fill-extrusion-height': ['get', 'height'],
            'fill-extrusion-base': ['get', 'min_height'],
            'fill-extrusion-opacity': 0.6,
        }
    });
});

// 交互事件
map.on('click', 'heatmap', (e) => {
    new mapboxgl.Popup()
        .setLngLat(e.lngLat)
        .setHTML(`<h3>${e.features[0].properties.name}</h3>`)
        .addTo(map);
});

9.2 GeoJSON 数据处理

javascript
// 高效渲染 GeoJSON 数据
class GeoJSONRenderer {
    constructor(map) {
        this.map = map;
        this.sourceId = 'geojson-source';
    }

    addGeoJSON(data, options = {}) {
        // 使用 worker 处理坐标转换
        const worker = new Worker('geo-worker.js');
        worker.postMessage({ type: 'process', geojson: data });

        worker.onmessage = (e) => {
            const processed = e.data;
            this.map.addSource(this.sourceId, {
                type: 'geojson',
                data: processed,
                buffer: 0,
                tolerance: options.simplify || 0.5,  // 简化路径
                cluster: options.cluster || false,
                clusterMaxZoom: options.clusterMaxZoom || 14,
                clusterRadius: options.clusterRadius || 50,
            });

            this.map.addLayer({
                id: 'points',
                type: 'circle',
                source: this.sourceId,
                paint: {
                    'circle-radius': ['case',
                        ['has', 'point_count'],  // 聚类标记
                        ['step', ['get', 'point_count'], 15, 10, 20, 50, 30],
                        6
                    ],
                    'circle-color': '#3498db',
                    'circle-opacity': 0.8,
                }
            });
        };
    }
}

9.3 瓦片渲染原理

地图瓦片是预先切分的图片或矢量数据块,通过按需加载实现海量数据展示。

瓦片坐标系:z/x/y
z = 缩放级别 (0-22)
x = 列号 (0 到 2^z - 1)
y = 行号 (0 到 2^z - 1)

请求示例:https://tile.openstreetmap.org/12/3456/2345.png

9.4 热力图实现

javascript
// Canvas 热力图实现
class Heatmap {
    constructor(canvas) {
        this.canvas = canvas;
        this.ctx = canvas.getContext('2d');
        this.gradient = this.createGradient();
    }

    render(points, options = {}) {
        const { radius = 30, blur = 15 } = options;
        const ctx = this.ctx;

        // 1. 绘制圆形渐变点
        points.forEach(({ x, y, weight = 1 }) => {
            const gradient = ctx.createRadialGradient(x, y, 0, x, y, radius);
            gradient.addColorStop(0, `rgba(0, 0, 0, ${weight})`);
            gradient.addColorStop(1, 'rgba(0, 0, 0, 0)');
            ctx.fillStyle = gradient;
            ctx.fillRect(x - radius, y - radius, radius * 2, radius * 2);
        });

        // 2. 将灰度图映射为彩色热力图
        const imageData = ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);
        const data = imageData.data;
        for (let i = 0; i < data.length; i += 4) {
            const intensity = data[i] / 255;  // alpha 通道作为强度
            const color = this.getColor(intensity);
            data[i] = color[0];
            data[i + 1] = color[1];
            data[i + 2] = color[2];
        }
        ctx.putImageData(imageData, 0, 0);
    }

    getColor(intensity) {
        // Jet 颜色映射
        const r = Math.min(1, Math.max(0, 1.5 - Math.abs(intensity * 4 - 3)));
        const g = Math.min(1, Math.max(0, 1.5 - Math.abs(intensity * 4 - 2)));
        const b = Math.min(1, Math.max(0, 1.5 - Math.abs(intensity * 4 - 1)));
        return [r * 255, g * 255, b * 255];
    }
}

10. 3D 场景优化

10.1 LOD (Level of Detail)

根据物体距离相机的远近距离,切换不同精度的模型。

javascript
class LODSystem {
    constructor(scene) {
        this.levels = [];
        this.scene = scene;
    }

    addLevel(mesh, distance) {
        this.levels.push({ mesh, distance });
        mesh.visible = false;
    }

    update(cameraPosition) {
        const dist = this.mesh.position.distanceTo(cameraPosition);

        // 从远到近检查
        for (let i = this.levels.length - 1; i >= 0; i--) {
            if (dist >= this.levels[i].distance) {
                // 显示当前级别,隐藏其他
                this.levels.forEach((l, j) => {
                    l.mesh.visible = j === i;
                });
                return;
            }
        }
        // 默认显示最高精度
        this.levels[0].mesh.visible = true;
    }

    // 自动生成 LOD 层级
    static autoGenerate(geometry, distances, scaleFactors) {
        const levels = [];
        distances.forEach((dist, i) => {
            const simplified = geometry.clone();
            const factor = scaleFactors[i] || 0.5;
            // 简化和合并顶点
            levels.push({
                geometry: simplified,
                distance: dist,
                vertexCount: Math.floor(geometry.attributes.position.count * factor),
            });
        });
        return levels;
    }
}

10.2 视锥体裁剪 (Frustum Culling)

只渲染在相机视锥体内的物体,Three.js 默认启用。

javascript
// Three.js 默认启用视锥体裁剪
const mesh = new THREE.Mesh(geometry, material);
mesh.frustumCulled = true;  // 默认值

// 自定义剔除
function customCulling(objects, camera, renderer) {
    const frustum = new THREE.Frustum();
    const matrix = new THREE.Matrix4().multiplyMatrices(
        camera.projectionMatrix, camera.matrixWorldInverse
    );
    frustum.setFromProjectionMatrix(matrix);

    objects.forEach(obj => {
        const sphere = obj.geometry.boundingSphere;
        if (sphere) {
            const center = sphere.center.clone().applyMatrix4(obj.matrixWorld);
            obj.visible = frustum.containsPoint(center);
        }
    });
}

10.3 实例化渲染 (InstancedMesh)

使用 GPU 实例化一次性绘制大量相同几何体。

javascript
// 创建 10000 个实例
const count = 10000;
const geometry = new THREE.SphereGeometry(0.1, 8, 6);  // 低面数
const material = new THREE.MeshStandardMaterial({
    color: 0x3498db,
});

const instancedMesh = new THREE.InstancedMesh(geometry, material, count);
instancedMesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage);

const matrix = new THREE.Matrix4();
const position = new THREE.Vector3();
const quaternion = new THREE.Quaternion();
const scale = new THREE.Vector3();

for (let i = 0; i < count; i++) {
    position.set(
        (Math.random() - 0.5) * 100,
        (Math.random() - 0.5) * 100,
        (Math.random() - 0.5) * 100
    );
    quaternion.setFromEuler(
        new THREE.Euler(Math.random() * Math.PI, Math.random() * Math.PI, 0)
    );
    scale.setScalar(0.5 + Math.random() * 0.5);

    matrix.compose(position, quaternion, scale);
    instancedMesh.setMatrixAt(i, matrix);
}
instancedMesh.instanceMatrix.needsUpdate = true;
scene.add(instancedMesh);

// 更新实例
function updateInstances(time) {
    for (let i = 0; i < count; i++) {
        position.y += Math.sin(time + i) * 0.01;
        matrix.setPosition(position);
        instancedMesh.setMatrixAt(i, matrix);
    }
    instancedMesh.instanceMatrix.needsUpdate = true;
}

10.4 纹理图集 (Texture Atlas)

将多个小纹理合并在一个大纹理中,减少纹理切换和 draw call。

javascript
// 创建纹理图集
function createTextureAtlas(images, atlasWidth, atlasHeight) {
    const canvas = document.createElement('canvas');
    canvas.width = atlasWidth;
    canvas.height = atlasHeight;
    const ctx = canvas.getContext('2d');

    const cols = Math.ceil(Math.sqrt(images.length));
    const cellW = atlasWidth / cols;
    const cellH = atlasHeight / cols;

    images.forEach((img, i) => {
        const col = i % cols;
        const row = Math.floor(i / cols);
        ctx.drawImage(img, col * cellW, row * cellH, cellW, cellH);
    });

    return new THREE.CanvasTexture(canvas);
}

// 使用纹理图集
const atlasTexture = createTextureAtlas(spriteImages, 1024, 1024);
const material = new THREE.SpriteMaterial({ map: atlasTexture });

10.5 对象池 (Object Pool)

减少频繁创建和销毁对象的 GC 压力。

javascript
class ObjectPool {
    constructor(factory, initialSize = 100) {
        this.factory = factory;
        this.pool = [];
        this.active = new Set();

        for (let i = 0; i < initialSize; i++) {
            this.pool.push(factory());
        }
    }

    acquire() {
        let obj = this.pool.pop();
        if (!obj) {
            obj = this.factory();  // 池空时创建新对象
        }
        obj.visible = true;
        this.active.add(obj);
        return obj;
    }

    release(obj) {
        obj.visible = false;
        this.active.delete(obj);
        this.pool.push(obj);
    }

    releaseAll() {
        this.active.forEach(obj => this.release(obj));
    }
}

// 粒子对象池
const particlePool = new ObjectPool(() => {
    const geometry = new THREE.PlaneGeometry(0.5, 0.5);
    const material = new THREE.SpriteMaterial({ map: particleTexture });
    const sprite = new THREE.Sprite(material);
    sprite.visible = false;
    return sprite;
}, 500);

10.6 合并几何体 (Geometry Merging)

将多个静态几何体合并为一个,减少 draw call。

javascript
import { mergeGeometries } from 'three/addons/utils/BufferGeometryUtils.js';

const geometries = [];
for (let i = 0; i < 1000; i++) {
    const geo = new THREE.BoxGeometry(1, 1, 1);
    geo.translate(Math.random() * 100, Math.random() * 100, Math.random() * 100);
    geometries.push(geo);
}

const mergedGeometry = mergeGeometries(geometries);
const mergedMesh = new THREE.Mesh(mergedGeometry, material);
scene.add(mergedMesh);

11. 可视化架构设计

11.1 分层架构

一个成熟的可视化平台需要清晰的分层架构,实现关注点分离。

应用层 (Application)
├── Dashboard
├── BI 报表
├── 数据大屏
├── 组件层 (Components)
├── 折线图
├── 柱状图
├── 散点图
├── 关系图
├── 地图
├── 渲染引擎层 (Render Engine)
├── SVGRender
├── CanvasEngine
├── WebGLRender
├── 数据处理层 (Data Pipeline)
├── 数据转换
├── 降采样
├── 聚合计算
├── 交互事件层 (Interaction)
├── 缩放/平移
├── 刷选/框选
├── 提示框/下钻
├── 基础服务层 (Infrastructure)
├── 主题
├── 事件
├── 动画
├── 配置
└── 国际化

11.2 插件系统设计

可视化引擎需要灵活的插件机制,支持扩展功能。

javascript
class VisualizationPlugin {
    constructor(name) {
        this.name = name;
        this.hooks = {};
    }

    on(type, handler) {
        if (!this.hooks[type]) this.hooks[type] = [];
        this.hooks[type].push(handler);
    }

    install(engine) {
        // 插件安装时执行
    }
}

class VisualizationEngine {
    constructor(options = {}) {
        this.plugins = new Map();
        this.hooks = new Map();
        this.renderer = null;
        this.dataPipeline = new DataPipeline();
        this.themeManager = new ThemeManager(options.theme);
    }

    use(plugin) {
        if (this.plugins.has(plugin.name)) {
            throw new Error('Plugin already registered: ' + plugin.name);
        }
        this.plugins.set(plugin.name, plugin);
        plugin.install(this);

        for (const [type, handlers] of Object.entries(plugin.hooks)) {
            if (!this.hooks.has(type)) this.hooks.set(type, []);
            this.hooks.get(type).push(...handlers);
        }
        return this;
    }

    emit(type, context) {
        const handlers = this.hooks.get(type) || [];
        for (const handler of handlers) {
            handler(context);
        }
    }

    render(data) {
        this.emit('beforeRender', { data });
        const processed = this.dataPipeline.process(data);
        this.emit('afterProcess', { data: processed });
        this.renderer.draw(processed);
        this.emit('afterRender', { data: processed });
    }
}

11.3 主题系统设计

javascript
class ThemeManager {
    constructor(theme) {
        this.themes = new Map();
        this.current = theme || 'light';
        this.registerDefault();
    }

    registerDefault() {
        this.register('light', {
            background: '#ffffff',
            textColor: '#333333',
            axisColor: '#cccccc',
            colors: ['#5470c6', '#91cc75', '#fac858', '#ee6666'],
            fontFamily: 'sans-serif',
            fontSize: 12,
            animation: { duration: 1000, easing: 'ease' },
        });

        this.register('dark', {
            background: '#141414',
            textColor: '#e0e0e0',
            axisColor: '#333333',
            colors: ['#3fb1e3', '#6be6c1', '#626c91', '#a0a7e6'],
            fontFamily: 'sans-serif',
            fontSize: 12,
            animation: { duration: 1000, easing: 'ease' },
        });
    }

    register(name, config) {
        this.themes.set(name, config);
    }

    get(key) {
        const theme = this.themes.get(this.current);
        return key ? theme[key] : theme;
    }

    setTheme(name) {
        if (!this.themes.has(name)) {
            throw new Error('Theme not found: ' + name);
        }
        this.current = name;
        this.apply();
    }

    apply() {
        document.documentElement.style.setProperty('--viz-bg', this.get('background'));
        document.documentElement.style.setProperty('--viz-text', this.get('textColor'));
    }
}

11.4 数据管道设计

javascript
class DataPipeline {
    constructor() {
        this.transforms = [];
    }

    pipe(transform) {
        this.transforms.push(transform);
        return this;
    }

    process(data) {
        return this.transforms.reduce((acc, transform) => {
            return transform(acc);
        }, data);
    }
}

const pipeline = new DataPipeline();
pipeline
    .pipe(filterNull)
    .pipe(sortByTime)
    .pipe(lttb.bind(null, 1000))
    .pipe(normalizeValues);

const result = pipeline.process(rawData);

12. 性能预算与优化策略

12.1 性能预算指标

性能预算是可视化系统上线前必须设定的量化指标,涵盖加载、渲染和交互三个维度。

指标类别指标名称目标值警告值临界值
加载首次内容渲染 (FCP)< 1s> 2s> 4s
加载最大内容渲染 (LCP)< 2.5s> 4s> 6s
加载包体积 (gzip)< 200KB> 500KB> 1MB
渲染首帧渲染时间< 500ms> 1s> 3s
渲染持续帧率 (FPS)> 55< 45< 30
渲染帧渲染耗时< 8ms> 12ms> 16ms
交互点击响应时间< 50ms> 100ms> 300ms
交互缩放/平移延迟< 100ms> 200ms> 500ms
内存总内存占用< 100MB> 200MB> 400MB
内存GPU 显存< 200MB> 500MB> 1GB
数据初始数据加载< 3s> 5s> 10s
数据增量更新延迟< 16ms> 33ms> 100ms

12.2 优化优先级矩阵

按照"投入产出比"排列优化策略:

高优先级 (投入小,收益大)
├── 1. 数据降采样 (LTTB、M4)
├── 2. Canvas 替代 SVG (DOM 节点 > 5000)
├── 3. 关闭不必要的动画和特效
├── 4. 开启 progressive 渐进式渲染
├── 5. 使用 requestAnimationFrame 节流

中优先级 (投入中,收益大)
├── 6. 分层渲染 (静态/动态/交互 分Layer)
├── 7. Web Worker 数据处理
├── 8. 虚拟滚动/只渲染可见区域
├── 9. 对象池模式减少 GC
├── 10. 纹理图集减少 draw call

低优先级 (投入大,收益视场景而定)
├── 11. WebGL/WebGPU 迁移
├── 12. Offscreen Canvas 渲染
├── 13. 自定义着色器优化
├── 14. Service Worker 数据缓存
└── 15. WebAssembly 数据处理

12.3 加载优化

javascript
// 1. 代码分割 (Code Splitting)
// 可视化库按需加载
const ECharts = () => import('echarts');
const D3 = () => import('d3');

// 2. 数据分级加载
async function loadData(level) {
    if (level === 'summary') {
        return await fetch('/api/summary');      // 聚合数据
    } else if (level === 'detail') {
        return await fetch('/api/detail');       // 详细数据
    }
}

// 3. 数据缓存策略
const dataCache = new Map();
function getDataWithCache(url, ttl = 60000) {
    const cached = dataCache.get(url);
    if (cached && Date.now() - cached.time < ttl) {
        return cached.data;
    }
    return fetch(url).then(res => res.json()).then(data => {
        dataCache.set(url, { data, time: Date.now() });
        return data;
    });
}

// 4. 预加载关键数据
window.addEventListener('load', () => {
    if ('requestIdleCallback' in window) {
        requestIdleCallback(() => {
            prefetchNextLevelData();
        });
    }
});

12.4 渲染优化

javascript
// 1. 帧率监控
class FPSMonitor {
    constructor(warnThreshold = 45) {
        this.frames = 0;
        this.lastTime = performance.now();
        this.warnThreshold = warnThreshold;
    }

    tick() {
        this.frames++;
        const now = performance.now();
        if (now - this.lastTime >= 1000) {
            const fps = this.frames;
            this.frames = 0;
            this.lastTime = now;

            if (fps < this.warnThreshold) {
                console.warn('Low FPS detected:', fps);
                this.onLowFPS?.(fps);
            }
            return fps;
        }
    }
}

// 2. 自适应帧率
class AdaptiveRenderer {
    constructor() {
        this.targetFPS = 60;
        this.frameInterval = 1000 / this.targetFPS;
        this.lastFrame = 0;
    }

    setTargetFPS(fps) {
        this.targetFPS = fps;
        this.frameInterval = 1000 / fps;
    }

    shouldRender(timestamp) {
        if (timestamp - this.lastFrame < this.frameInterval) {
            return false;
        }
        this.lastFrame = timestamp;
        return true;
    }

    // 根据设备性能自动调整
    autoAdapt() {
        const memory = navigator.deviceMemory;
        const cores = navigator.hardwareConcurrency;
        if (memory < 4 || cores < 4) {
            this.setTargetFPS(30);  // 低端设备降帧
        }
    }
}

// 3. 绘制调用合并
function batchDraw(ctx, shapes) {
    // 合并相同状态的绘制调用
    ctx.beginPath();
    shapes.forEach(s => {
        ctx.rect(s.x, s.y, s.w, s.h);
    });
    ctx.fillStyle = '#3498db';
    ctx.fill();
    ctx.strokeStyle = '#2980b9';
    ctx.stroke();
}

// 4. Canvas 离屏预渲染
const offscreen = document.createElement('canvas');
offscreen.width = 2000;
offscreen.height = 2000;
const offCtx = offscreen.getContext('2d');

// 将静态内容预渲染到离屏 Canvas
preRenderStaticContent(offCtx);

// 主循环中只需复制已渲染的离屏 Canvas
function renderFrame() {
    ctx.clearRect(0, 0, width, height);
    ctx.drawImage(offscreen, 0, 0);  // 单片绘制
    // 只渲染动态更新部分
    renderDynamicContent(ctx);
}

12.5 内存优化

javascript
// 1. 对象池模式
class CanvasObjectPool {
    constructor(createFn, resetFn, size = 100) {
        this.pool = [];
        this.createFn = createFn;
        this.resetFn = resetFn;

        for (let i = 0; i < size; i++) {
            this.pool.push(createFn());
        }
    }

    acquire() {
        return this.pool.pop() || this.createFn();
    }

    release(obj) {
        this.resetFn(obj);
        this.pool.push(obj);
    }
}

// 2. TypedArray 替代普通数组
const positions = new Float32Array(count * 3);  // 代替 Array
const indices = new Uint32Array(count);
const colors = new Uint8Array(count * 4);

// 3. 及时释放纹理和几何体 (Three.js)
function disposeObject(obj) {
    if (obj.geometry) {
        obj.geometry.dispose();
    }
    if (obj.material) {
        if (obj.material.map) obj.material.map.dispose();
        obj.material.dispose();
    }
    if (obj.children) {
        obj.children.forEach(disposeObject);
    }
}

// 4. 避免内存泄漏
class ChartInstance {
    destroy() {
        this.renderer.dispose();
        this.resizeObserver?.disconnect();
        this.animationFrame && cancelAnimationFrame(this.animationFrame);
        this.eventListeners.forEach((fn, type) => {
            this.element.removeEventListener(type, fn);
        });
        this.eventListeners.clear();
    }
}

12.6 性能监控与调试

javascript
// 性能标记
class PerformanceMarker {
    constructor(name) {
        this.name = name;
        this.marks = {};
    }

    mark(key) {
        this.marks[key] = performance.now();
    }

    measure(from, to, label) {
        const duration = this.marks[to] - this.marks[from];
        console.log(`[${this.name}] ${label || from + '->' + to}: ${duration.toFixed(2)}ms`);

        // 发送到监控平台
        if (window.__MONITOR__) {
            window.__MONITOR__.reportMetric(this.name, label, duration);
        }

        return duration;
    }
}

// 使用
const pm = new PerformanceMarker('scatter-render');
pm.mark('data-fetch');
await fetchData();
pm.mark('data-process');
processData(data);
pm.mark('render');
renderChart(data);
pm.mark('done');

pm.measure('data-fetch', 'data-process', '数据处理耗时');
pm.measure('data-process', 'render', '渲染耗时');
pm.measure('render', 'done', '完成渲染');
pm.measure('data-fetch', 'done', '总耗时');

常见误区

误区正确理解
所有图表都用 ECharts根据场景选型,复杂自定义需 D3/WebGL
可视化只关心美观数据准确性、性能、可访问性同样重要
一次性渲染所有数据大数据量需要降采样和虚拟化
Canvas 一定比 SVG 快小数据量 SVG 开发效率更高,交互更方便
WebGL 能解决所有性能问题WebGL 有上下文限制(16 个)、移动端兼容性差
忽略移动端性能移动端 GPU 和内存有限,触控交互需优化
动画越花哨越好动画应有意义,减少不必要的动画开销
所有数据都实时渲染静态数据预渲染,差异更新更高效

相关领域

  • F03 Browser:渲染原理(像素管道、图层合成)。
  • A03 Performance:性能优化(加载、渲染、内存)。
  • F08 Data Structures:图算法、空间索引(R-Tree、Quadtree)。
  • E06/E07 React/Vue:组件化封装(声明式图表组件、SSR 可访问性)。
  • F12 Network:WebSocket 与数据传输(二进制协议、压缩)。
  • F13 WebAssembly:计算密集型任务(数据处理、物理模拟)。
  • A07 Testing:可视化测试(截图对比、数据断言、性能基准)。

推荐学习资源

  • 书籍:《D3.js 数据可视化实战》《交互式数据可视化》《WebGL 编程指南》
  • 文档:MDN Canvas API、Three.js 文档、Mapbox GL JS 文档
  • 工具:Chrome DevTools Performance、Lighthouse、React Profiler
  • 社区:Observable (D3 示例)、bl.ocks.org、three.js examples

WebGPU 计算着色器示例

javascript
// 初始化 WebGPU
async function initWebGPU() {
    const adapter = await navigator.gpu.requestAdapter();
    const device = await adapter.requestDevice();

    // 计算着色器
    const shaderCode = `
        @group(0) @binding(0) var<storage, read> input: array<f32>;
        @group(0) @binding(1) var<storage, read_write> output: array<f32>;

        @compute @workgroup_size(256)
        fn main(@builtin(global_invocation_id) id: vec3<u32>) {
            let idx = id.x;
            if (idx >= arrayLength(&input)) { return; }
            output[idx] = input[idx] * 2.0;  // 并行双倍计算
        }
    `;

    const shaderModule = device.createShaderModule({ code: shaderCode });

    // 计算管道
    const computePipeline = device.createComputePipeline({
        layout: 'auto',
        compute: { module: shaderModule, entryPoint: 'main' },
    });

    // 缓冲区
    const inputData = new Float32Array([1, 2, 3, 4, 5, 6, 7, 8]);
    const inputBuffer = device.createBuffer({
        size: inputData.byteLength,
        usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
    });
    device.queue.writeBuffer(inputBuffer, 0, inputData);

    const outputBuffer = device.createBuffer({
        size: inputData.byteLength,
        usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
    });

    // 编码和执行
    const commandEncoder = device.createCommandEncoder();
    const passEncoder = commandEncoder.beginComputePass();
    passEncoder.setPipeline(computePipeline);
    passEncoder.setBindGroup(0, device.createBindGroup({
        layout: computePipeline.getBindGroupLayout(0),
        entries: [
            { binding: 0, resource: { buffer: inputBuffer } },
            { binding: 1, resource: { buffer: outputBuffer } },
        ],
    }));
    passEncoder.dispatchWorkgroups(Math.ceil(inputData.length / 256));
    passEncoder.end();

    device.queue.submit([commandEncoder.finish()]);
}

标签#visualization #svg #canvas #webgl #graphics #webgpu #d3 #echarts #threejs #performance

最后更新:2026-07-06


本领域学习进度

学习进度0 / 43 (0%)

基于 MIT 协议发布