学完了地图绘制、标注后,我们来做一个全国人口的柱状图。

是在地图上通过柱子显示人口数量那种。
类似这个:

但会比这个好看很多。
创建项目:
npx create-vite china-population-bar-chart

创建项目,进入项目,安装依赖:
pnpm install
pnpm install --save three
pnpm install --save-dev @types/three
改下 src/main.js
import './style.css';
import * as THREE from 'three';
import {
OrbitControls
} from 'three/addons/controls/OrbitControls.js';
import mesh from './mesh.js';
const scene = new THREE.Scene();
scene.add(mesh);
const light = new THREE.DirectionalLight(0xffffff);
light.position.set(500, 300, 600);
scene.add(light);
const light2 = new THREE.AmbientLight();
scene.add(light2);
const axesHelper = new THREE.AxesHelper(1000);
scene.add(axesHelper);
const width = window.innerWidth;
const height = window.innerHeight;
const camera = new THREE.PerspectiveCamera(60, width / height, 1, 10000);
camera.position.set(0, 200, 600);
camera.lookAt(0, 0, 0);
const renderer = new THREE.WebGLRenderer({
antialias: true
});
renderer.setSize(width, height)
function render() {
renderer.render(scene, camera);
requestAnimationFrame(render);
}
render();
document.body.append(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
创建 Scene、Light、Camera、Renderer
改下 style.css
body {
margin: 0;
}
然后创建 mesh.js
import * as THREE from 'three';
import { geoMercator } from 'd3-geo';
const chinaMap = new THREE.Group();
const mercator = geoMercator()
.center([105,34]).translate([0, 0]).scale(600)
const loader = new THREE.FileLoader();
loader.load('https://geo.datav.aliyun.com/areas_v3/bound/100000_full.json', function (data) {
const geojson = JSON.parse(data);
console.log(geojson);
geojson.features.forEach(feature => {
const province = new THREE.Group();
if (feature.geometry.type === 'Polygon') {
const polygon = createPolygon(feature.geometry.coordinates);
province.add(polygon);
} else if (feature.geometry.type === 'MultiPolygon') {
feature.geometry.coordinates.forEach(polygonCoords => {
const polygon = createPolygon(polygonCoords);
province.add(polygon);
});
}
chinaMap.add(province);
});
});
function createPolygon(coordinates) {
const group = new THREE.Group();
coordinates.forEach(item => {
const bufferGeometry = new THREE.BufferGeometry();
const vertices = [];
item.forEach(point => {
const [x, y] = mercator(point);
vertices.push(x, -y, 0);
});
const attribute = new THREE.Float32BufferAttribute(vertices, 3);;
bufferGeometry.attributes.position = attribute;
const lineMaterial = new THREE.LineBasicMaterial({
color: 'white'
});
const line = new THREE.Line(bufferGeometry, lineMaterial);
group.add(line);
});
return group;
}
export default chinaMap;
先用 Line + BufferGeometry 把中国地图轮廓画出来。
安装下 d3-geo
pnpm install --save d3-geo
pnpm install --save-dev @types/d3-geo
跑一下:
npm run dev


我们准备一下各省市人口的数据:
创建 data.js
const data = {
"北京市": 2154.2,
"天津市": 1560.2,
"上海市": 2428.14,
"重庆市": 3101.79,
"河北省": 7556.3,
"山西省": 3718.3,
"辽宁省": 4393.8,
"吉林省": 2704.4,
"黑龙江省": 3773.2,
"江苏省": 8050.7,
"浙江省": 5737,
"安徽省": 6324.1,
"福建省": 3941.16,
"江西省": 4647.9,
"山东省": 10047.24,
"河南省": 9605,
"湖北省": 5917,
"湖南省": 6899.3,
"广东省": 11346,
"海南省": 925.8,
"四川省": 8341,
"贵州省": 3600,
"云南省": 4830,
"陕西省": 3864,
"甘肃省": 2637.3,
"青海省": 603.2,
"台湾省": 2359.47,
"内蒙古自治区": 2534,
"广西壮族自治区": 4926,
"西藏自治区": 344.1,
"宁夏回族自治区": 688.3,
"新疆维吾尔自治区": 2486.7
};
export default data;
想在省市中心画一个柱子:

geojson.features.forEach(feature => {
if(!feature.properties.center) {
return;
}
const [x, y] = mercator(feature.properties.center);
const geometry = new THREE.BoxGeometry(10, 10, 100);
const material = new THREE.MeshPhongMaterial({
color: 'orange'
});
const bar = new THREE.Mesh(geometry, material);
bar.position.set(x, -y, 0);
chinaMap.add(bar);
});

高度改为 data 里的高度,并且要位移一半的距离,让柱子底部在地图上。

根据名字拿到人口数据,除以 100 做为高度。
然后 z 方向位移一半。
看下效果:

地图轮廓只有线条太空洞了,我们给它用 ExtrudeGeometry 画出来拉伸一下。
先画一下 Shape:


之前是把点放到 BufferGeometry 里用 Line 画轮廓线。
这次把点用 Shape 连起来,然后用 ExtrudeGeometry 拉伸下。
let first = true;
coordinates.forEach(item => {
const bufferGeometry = new THREE.BufferGeometry();
const vertices = [];
item.forEach(point => {
const [x, y] = mercator(point);
vertices.push(x, -y, 0);
if(first) {
shape.moveTo(x, -y);
} else {
shape.lineTo(x, -y);
}
first = false;
});
const attribute = new THREE.Float32BufferAttribute(vertices, 3);;
bufferGeometry.attributes.position = attribute;
const lineMaterial = new THREE.LineBasicMaterial({
color: 'white'
});
const line = new THREE.Line(bufferGeometry, lineMaterial);
group.add(line);
const geometry = new THREE.ExtrudeGeometry(shape, {
depth: 10
});
const material = new THREE.MeshPhongMaterial({
color: 'lightblue'
});
const mesh = new THREE.Mesh(geometry, material);
group.add(mesh);
});
看一下:

调一下位置:

mesh.position.z = -11;
厚度是 10,我们多移动一点距离,正好错开,不然会深度冲突。
看下效果:

这样,人口分布的基本效果就出来了。
案例代码上传了小册仓库
总结
这节我们画了人口分布的柱状图。
先拿到 geojson 的轮廓经纬度数据,用墨卡托转换为二维坐标。
用 BufferGeometry + Line 以及 Shape + ExtrudeGeometry 画出地图轮廓和形状。
然后在每个省市中心的位置,画一个柱状图,高度是根据人口数量算的。
这样,基本效果完成了,下节我们继续完善。