一、3D数据可视化概述
本教程将使用纯HTML5技术实现一个无需第三方库的3D数据可视化大屏,包含地理信息、实时数据和三维图表展示。
核心技术:
- 3D渲染:WebGL + Canvas
- 数据处理:JavaScript ES6+
- 动画效果:requestAnimationFrame
- 交互设计:Pointer Events API
实现功能:
- 3D地球模型与地理数据展示
- 动态数据柱状图
- 实时数据流渲染
- 交互式视角控制
- 自适应多种屏幕尺寸
二、项目结构与初始化
1. 基础HTML结构
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D数据可视化大屏</title>
</head>
<body>
<div class="dashboard-container">
<header class="dashboard-header">
<h1>全球数据监控中心</h1>
<div class="real-time-info">
<span id="current-time"></span>
<span id="data-update-time">最后更新: --</span>
</div>
</header>
<div class="dashboard-content">
<div class="panel globe-panel">
<canvas id="globeCanvas"></canvas>
</div>
<div class="panel chart-panel">
<canvas id="barChartCanvas"></canvas>
</div>
<div class="panel data-panel">
<canvas id="dataFlowCanvas"></canvas>
</div>
</div>
</div>
<script src="js/main.js"></script>
</body>
</html>
2. 基础JavaScript结构
创建js/main.js:
// 全局配置
const Config = {
globe: {
radius: 150,
rotationSpeed: 0.001
},
colors: {
earth: '#1E88E5',
land: '#4CAF50',
ocean: '#1976D2',
bar: '#FF5722',
highlight: '#FFC107'
}
};
// 主入口
document.addEventListener('DOMContentLoaded', () => {
initTimeDisplay();
initGlobe();
initBarChart();
initDataFlow();
setupEventListeners();
});
function initTimeDisplay() {
// 更新时间显示
function updateTime() {
const now = new Date();
document.getElementById('current-time').textContent =
`当前时间: ${now.toLocaleString('zh-CN', { hour12: false })}`;
}
setInterval(updateTime, 1000);
updateTime();
}
三、3D地球实现
1. WebGL初始化
function initGlobe() {
const canvas = document.getElementById('globeCanvas');
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
if (!gl) {
alert('您的浏览器不支持WebGL');
return;
}
// 调整Canvas尺寸
function resizeCanvas() {
const container = canvas.parentElement;
canvas.width = container.clientWidth;
canvas.height = container.clientHeight;
gl.viewport(0, 0, canvas.width, canvas.height);
}
window.addEventListener('resize', resizeCanvas);
resizeCanvas();
// 顶点着色器
const vsSource = `
attribute vec3 aPosition;
attribute vec3 aColor;
uniform mat4 uModelViewMatrix;
uniform mat4 uProjectionMatrix;
varying vec3 vColor;
void main() {
gl_Position = uProjectionMatrix * uModelViewMatrix * vec4(aPosition, 1.0);
vColor = aColor;
}
`;
// 片段着色器
const fsSource = `
precision mediump float;
varying vec3 vColor;
void main() {
gl_FragColor = vec4(vColor, 1.0);
}
`;
// 编译着色器
const vertexShader = compileShader(gl, vsSource, gl.VERTEX_SHADER);
const fragmentShader = compileShader(gl, fsSource, gl.FRAGMENT_SHADER);
const shaderProgram = gl.createProgram();
gl.attachShader(shaderProgram, vertexShader);
gl.attachShader(shaderProgram, fragmentShader);
gl.linkProgram(shaderProgram);
if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
console.error('着色器程序链接失败:', gl.getProgramInfoLog(shaderProgram));
return;
}
// 生成球体顶点数据
const { vertices, colors, indices } = generateSphereData(
Config.globe.radius,
64,
Config.colors.earth,
Config.colors.land
);
// 创建缓冲区
const vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
const colorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
const indexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);
// 渲染循环
let rotation = 0;
function render() {
gl.clearColor(0.0, 0.0, 0.1, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// 启用深度测试
gl.enable(gl.DEPTH_TEST);
// 设置透视矩阵
const aspect = canvas.width / canvas.height;
const projectionMatrix = mat4.create();
mat4.perspective(projectionMatrix, 45 * Math.PI / 180, aspect, 0.1, 1000.0);
// 设置模型视图矩阵
const modelViewMatrix = mat4.create();
mat4.translate(modelViewMatrix, modelViewMatrix, [0.0, 0.0, -400.0]);
mat4.rotate(modelViewMatrix, modelViewMatrix, rotation, [0, 1, 0]);
// 使用着色器程序
gl.useProgram(shaderProgram);
// 绑定顶点数据
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
const aPosition = gl.getAttribLocation(shaderProgram, 'aPosition');
gl.enableVertexAttribArray(aPosition);
gl.vertexAttribPointer(aPosition, 3, gl.FLOAT, false, 0, 0);
// 绑定颜色数据
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
const aColor = gl.getAttribLocation(shaderProgram, 'aColor');
gl.enableVertexAttribArray(aColor);
gl.vertexAttribPointer(aColor, 3, gl.FLOAT, false, 0, 0);
// 绑定索引数据
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
// 设置uniform变量
const uModelViewMatrix = gl.getUniformLocation(shaderProgram, 'uModelViewMatrix');
const uProjectionMatrix = gl.getUniformLocation(shaderProgram, 'uProjectionMatrix');
gl.uniformMatrix4fv(uModelViewMatrix, false, modelViewMatrix);
gl.uniformMatrix4fv(uProjectionMatrix, false, projectionMatrix);
// 绘制
gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_SHORT, 0);
// 更新旋转
rotation += Config.globe.rotationSpeed;
requestAnimationFrame(render);
}
render();
}
function compileShader(gl, source, type) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error('着色器编译错误:', gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
return shader;
}
2. 球体数据生成
function generateSphereData(radius, segments, baseColor, highlightColor) {
const vertices = [];
const colors = [];
const indices = [];
// 生成顶点
for (let lat = 0; lat <= segments; lat++) {
const theta = lat * Math.PI / segments;
const sinTheta = Math.sin(theta);
const cosTheta = Math.cos(theta);
for (let lon = 0; lon 0.7;
colors.push(
isLand ? highlightColor.r : baseColor.r,
isLand ? highlightColor.g : baseColor.g,
isLand ? highlightColor.b : baseColor.b
);
}
}
// 生成索引
for (let lat = 0; lat < segments; lat++) {
for (let lon = 0; lon < segments; lon++) {
const first = (lat * (segments + 1)) + lon;
const second = first + segments + 1;
indices.push(first, second, first + 1);
indices.push(second, second + 1, first + 1);
}
}
return { vertices, colors, indices };
}
四、3D柱状图实现
1. Canvas初始化
function initBarChart() {
const canvas = document.getElementById('barChartCanvas');
const ctx = canvas.getContext('2d');
// 调整Canvas尺寸
function resizeCanvas() {
const container = canvas.parentElement;
canvas.width = container.clientWidth;
canvas.height = container.clientHeight;
drawChart();
}
window.addEventListener('resize', resizeCanvas);
resizeCanvas();
// 模拟数据
const chartData = [
{ name: '一月', value: 120 },
{ name: '二月', value: 200 },
{ name: '三月', value: 150 },
{ name: '四月', value: 80 },
{ name: '五月', value: 170 },
{ name: '六月', value: 210 }
];
// 绘制图表
function drawChart() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
const margin = 40;
const maxValue = Math.max(...chartData.map(item => item.value));
const barWidth = (canvas.width - margin * 2) / chartData.length * 0.6;
const gap = (canvas.width - margin * 2) / chartData.length * 0.4;
const baseY = canvas.height - margin;
const scale = (canvas.height - margin * 2) / maxValue;
// 绘制坐标轴
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(margin, margin);
ctx.lineTo(margin, baseY);
ctx.lineTo(canvas.width - margin, baseY);
ctx.stroke();
// 绘制刻度
ctx.textAlign = 'right';
ctx.fillStyle = '#fff';
for (let i = 0; i {
const x = margin + index * (barWidth + gap);
const barHeight = item.value * scale;
const topY = baseY - barHeight;
// 柱体正面
ctx.fillStyle = Config.colors.bar;
ctx.fillRect(x, topY, barWidth, barHeight);
// 柱体顶部
ctx.fillStyle = shadeColor(Config.colors.bar, -20);
ctx.beginPath();
ctx.moveTo(x, topY);
ctx.lineTo(x + 15, topY - 15);
ctx.lineTo(x + 15 + barWidth, topY - 15);
ctx.lineTo(x + barWidth, topY);
ctx.closePath();
ctx.fill();
// 柱体侧面
ctx.fillStyle = shadeColor(Config.colors.bar, -40);
ctx.beginPath();
ctx.moveTo(x + barWidth, topY);
ctx.lineTo(x + barWidth + 15, topY - 15);
ctx.lineTo(x + barWidth + 15, baseY - 15);
ctx.lineTo(x + barWidth, baseY);
ctx.closePath();
ctx.fill();
// 文字标签
ctx.textAlign = 'center';
ctx.fillStyle = '#fff';
ctx.fillText(item.name, x + barWidth / 2, baseY + 20);
// 数值标签
ctx.fillText(item.value.toString(), x + barWidth / 2, topY - 10);
});
}
// 颜色处理函数
function shadeColor(color, percent) {
// 实现颜色深浅变化
// ...
}
}
五、数据流可视化
1. 实时数据流渲染
function initDataFlow() {
const canvas = document.getElementById('dataFlowCanvas');
const ctx = canvas.getContext('2d');
// 调整Canvas尺寸
function resizeCanvas() {
const container = canvas.parentElement;
canvas.width = container.clientWidth;
canvas.height = container.clientHeight;
initParticles();
}
window.addEventListener('resize', resizeCanvas);
resizeCanvas();
// 粒子系统
let particles = [];
const particleCount = 200;
function initParticles() {
particles = [];
for (let i = 0; i {
// 更新位置
p.x += Math.cos(p.direction) * p.speed;
p.y += Math.sin(p.direction) * p.speed;
// 边界检查
if (p.x canvas.width || p.y canvas.height) {
p.x = Math.random() * canvas.width;
p.y = Math.random() * canvas.height;
p.direction = Math.random() * Math.PI * 2;
}
// 绘制粒子
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.fillStyle = p.color;
ctx.fill();
// 绘制连接线
particles.forEach(p2 => {
const dx = p.x - p2.x;
const dy = p.y - p2.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 100) {
ctx.beginPath();
ctx.strokeStyle = `rgba(100, 200, 255, ${1 - distance / 100})`;
ctx.lineWidth = 0.5;
ctx.moveTo(p.x, p.y);
ctx.lineTo(p2.x, p2.y);
ctx.stroke();
}
});
});
requestAnimationFrame(animate);
}
animate();
}
六、交互功能实现
1. 地球旋转控制
function setupEventListeners() {
const globeCanvas = document.getElementById('globeCanvas');
let isDragging = false;
let lastX = 0;
let lastY = 0;
let rotationX = 0;
let rotationY = 0;
globeCanvas.addEventListener('pointerdown', (e) => {
isDragging = true;
lastX = e.clientX;
lastY = e.clientY;
globeCanvas.style.cursor = 'grabbing';
});
window.addEventListener('pointermove', (e) => {
if (!isDragging) return;
const deltaX = e.clientX - lastX;
const deltaY = e.clientY - lastY;
rotationY += deltaX * 0.01;
rotationX += deltaY * 0.01;
lastX = e.clientX;
lastY = e.clientY;
// 更新地球旋转 (需要修改前面的render函数)
});
window.addEventListener('pointerup', () => {
isDragging = false;
globeCanvas.style.cursor = 'grab';
});
// 支持触摸事件
globeCanvas.addEventListener('touchstart', (e) => {
e.preventDefault();
if (e.touches.length === 1) {
isDragging = true;
lastX = e.touches[0].clientX;
lastY = e.touches[0].clientY;
}
});
window.addEventListener('touchmove', (e) => {
e.preventDefault();
if (isDragging && e.touches.length === 1) {
const deltaX = e.touches[0].clientX - lastX;
const deltaY = e.touches[0].clientY - lastY;
rotationY += deltaX * 0.01;
rotationX += deltaY * 0.01;
lastX = e.touches[0].clientX;
lastY = e.touches[0].clientY;
}
});
window.addEventListener('touchend', () => {
isDragging = false;
});
}
2. 数据实时更新
// 模拟实时数据更新
function startDataUpdates() {
// 更新柱状图数据
setInterval(() => {
const chartData = [
{ name: '一月', value: Math.random() * 200 + 50 },
{ name: '二月', value: Math.random() * 200 + 50 },
{ name: '三月', value: Math.random() * 200 + 50 },
{ name: '四月', value: Math.random() * 200 + 50 },
{ name: '五月', value: Math.random() * 200 + 50 },
{ name: '六月', value: Math.random() * 200 + 50 }
];
drawChart(chartData);
// 更新最后更新时间
const now = new Date();
document.getElementById('data-update-time').textContent =
`最后更新: ${now.toLocaleTimeString('zh-CN', { hour12: false })}`;
}, 3000);
}
七、性能优化与部署
1. 性能优化技巧
- Canvas渲染优化:使用离屏Canvas预渲染静态元素
- WebGL优化:减少drawCall,合并几何体
- 动画优化:合理使用requestAnimationFrame
- 内存管理:及时清理不再使用的对象
2. 生产环境部署
建议的部署方案:
- 使用Web服务器(如Nginx)托管静态文件
- 启用Gzip压缩减少传输体积
- 配置适当的缓存策略
- 使用CDN加速资源加载
八、总结与扩展
本教程详细介绍了如何使用纯HTML5技术实现3D数据可视化大屏:
- 使用WebGL实现3D地球模型
- Canvas 2D绘制3D柱状图
- 粒子系统展示数据流动
- 实现交互式视角控制
- 优化性能确保流畅体验
进一步扩展方向:
- 接入真实API数据源
- 增加更多图表类型(如热力图、关系图)
- 实现数据下钻功能
- 开发VR/AR版本
完整项目代码已上传GitHub:https://github.com/example/html5-3d-dashboard