Skip to content

179. 实战:全国人口柱状地图

Published:

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

image.png

是在地图上通过柱子显示人口数量那种。

类似这个:

image.png

但会比这个好看很多。

创建项目:

npx create-vite china-population-bar-chart

image.png

创建项目,进入项目,安装依赖:

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

image.png

image.png

我们准备一下各省市人口的数据:

创建 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;

想在省市中心画一个柱子:

image.png

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);
});

2025-08-09 14.47.44.gif

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

image.png

根据名字拿到人口数据,除以 100 做为高度。

然后 z 方向位移一半。

看下效果:

2025-08-09 14.55.49.gif

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

先画一下 Shape:

image.png

image.png

之前是把点放到 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);
});

看一下:

2025-08-09 15.19.20.gif

调一下位置:

image.png

mesh.position.z = -11;

厚度是 10,我们多移动一点距离,正好错开,不然会深度冲突。

看下效果:

2025-08-09 15.25.45.gif

这样,人口分布的基本效果就出来了。

案例代码上传了小册仓库

总结

这节我们画了人口分布的柱状图。

先拿到 geojson 的轮廓经纬度数据,用墨卡托转换为二维坐标。

用 BufferGeometry + Line 以及 Shape + ExtrudeGeometry 画出地图轮廓和形状。

然后在每个省市中心的位置,画一个柱状图,高度是根据人口数量算的。

这样,基本效果完成了,下节我们继续完善。

评论