“使用 Three.js 的 WebGL 小实验。实现了一个互动性强的 3D 数据可视化效果,展示了郑州各区的常住人口数据,同时提供了视觉上很有表现力的落日背景和交互式视角控制。”

<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Three.js 场景中的 ECharts 图表 - 郑州人口画像</title>
<style>
body { margin: 0; overflow: hidden; }
canvas { display: block; }
</style>
</head>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
<script>
// 创建 ECharts 的画布
const echartsCanvas = document.createElement('canvas');
echartsCanvas.width = 512;
echartsCanvas.height = 512;
const chart = echarts.init(echartsCanvas);
// 配置 ECharts 柱状图(郑州各区人口数据,包含男性和女性)
const option = {
title: {
text: '郑州各区常住人口 (2020)',
left: 'center',
top: '2%',
textStyle: { color: '#fff' }
},
legend: {
data: ['男性', '女性'],
top: '10%',
left: 'center',
textStyle: { color: '#fff' },
selectedMode: true // 允许点击切换显示
},
grid:{
top:'20%'
},
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
formatter: function(params) {
return `${params[0].name}:<br/>男性: ${params[0].value} 万人<br/>女性: ${params[1].value} 万人`;
}
},
xAxis: {
type: 'category',
data: ['金水', '管城', '中原', '二七', '惠济', '上街', '中牟', '荥阳', '新郑'],
axisLabel: { color: '#fff' }
},
yAxis: {
type: 'value',
name: '人口 (万人)',
axisLabel: { color: '#fff' }
},
series: [
{
name: '男性',
type: 'bar',
data: [81.60, 55.43, 52.86, 52.35, 31.82, 10.26, 61.58, 56.45, 44.46],
itemStyle: { color: '#5470c6' }
},
{
name: '女性',
type: 'bar',
data: [77.40, 52.57, 50.14, 49.65, 30.18, 9.74, 58.42, 53.55, 42.17],
itemStyle: { color: '#91cc75' }
}
],
backgroundColor: 'rgba(0,0,0,0.1)' // 透明背景
};
chart.setOption(option);
// Three.js 场景设置
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 2, 5);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// 创建落日天空效果(渐变背景)
const gradientTexture = new THREE.CanvasTexture(generateGradientCanvas());
scene.background = gradientTexture;
function generateGradientCanvas() {
const canvas = document.createElement('canvas');
canvas.width = 512;
canvas.height = 512;
const context = canvas.getContext('2d');
const gradient = context.createLinearGradient(0, 0, 0, 512);
gradient.addColorStop(0, '#1e3a8a'); // 深蓝色(天空顶部)
gradient.addColorStop(0.5, '#f97316'); // 橙色(落日中部)
gradient.addColorStop(1, '#dc2626'); // 红色(地平线)
context.fillStyle = gradient;
context.fillRect(0, 0, 512, 512);
return canvas;
}
// 创建土地
const groundGeometry = new THREE.PlaneGeometry(100, 100);
const groundMaterial = new THREE.MeshLambertMaterial({ color: 0x228B22 }); // 绿色草地
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
ground.rotation.x = -Math.PI / 2; // 旋转使其水平
ground.position.y = -1; // 放置在场景下方
scene.add(ground);
// 创建显示 ECharts 纹理的平面
const texture = new THREE.CanvasTexture(echartsCanvas);
const material = new THREE.MeshBasicMaterial({ map: texture, side: THREE.DoubleSide });
const geometry = new THREE.PlaneGeometry(4, 4);
const plane = new THREE.Mesh(geometry, material);
plane.position.y = 1; // 稍微抬高以避免与地面重叠
scene.add(plane);
// 添加环境光
const ambientLight = new THREE.AmbientLight(0xffa07a, 0.6); // 偏暖的环境光以匹配落日
scene.add(ambientLight);
// 添加方向光,模拟落日光线
const directionalLight = new THREE.DirectionalLight(0xff4500, 0.7); // 橙红色光线
directionalLight.position.set(-5, 3, -5); // 光从低角度射入
scene.add(directionalLight);
// 添加轨道控制器并限制角度
const controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.minPolarAngle = 0; // 限制最小垂直角度(仰角)为水平(0 弧度)
controls.maxPolarAngle = Math.PI / 2; // 限制最大垂直角度为水平(90度,π/2 弧度)
controls.update();
// 动画循环
function animate() {
requestAnimationFrame(animate);
texture.needsUpdate = true; // 更新纹理以反映图表变化
renderer.render(scene, camera);
}
animate();
// 处理窗口大小调整
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
</script>
</body>
</html>