
本文详解如何在 p5.js 中结合 mappa 库实现世界人口数据的地理可视化,重点解决因国家名称格式不一致导致的坐标匹配失败、椭圆未渲染等常见错误,并提供可运行的修正代码与最佳实践。
本文详解如何在 p5.js 中结合 mappa 库实现世界人口数据的地理可视化,重点解决因国家名称格式不一致导致的坐标匹配失败、椭圆未渲染等常见错误,并提供可运行的修正代码与最佳实践。
在使用 p5.js + Mappa 进行地理数据可视化时,一个典型痛点是:数据加载看似正常,但地图上的图形(如椭圆)完全不显示。你提供的代码逻辑整体合理——加载 JSON、映射经纬度、归一化人口尺寸、转换像素坐标并绘制——但存在两个关键性结构性缺陷,直接导致 data 数组为空或坐标无效,最终 ellipse() 无处可画。
? 核心问题定位与修复
1. popu.length 计算错误 → 数据未正确解析
你的 vn.json 是标准数组格式([{country:"Afghanistan",...},...]),但 preload() 中 loadJSON("vn.json") 后,popu 是一个异步加载的 Promise 对象引用,在 setup() 执行时可能尚未完成解析。更严重的是:即使加载完成,popu.length 在某些环境或 JSON 解析异常下可能返回 undefined 或 0,导致 for (let i = 0; i
✅ 修复方案:使用 then() 确保数据就绪,并显式校验类型
function preload() {
// 使用 Promise 链确保 JSON 加载完成后再进入 setup
popu = loadJSON("vn.json");
latlon = loadJSON("latlon.json");
}
function setup() {
canvas = createCanvas(800, 600);
trainMap = mappa.tileMap(options);
trainMap.overlay(canvas);
// ✅ 关键:等待数据加载完成(p5.js 1.9+ 推荐用 promise)
// 若使用旧版 p5.js,请在 preload 中用 loadJSON 的回调函数
if (!popu || !latlon) {
console.error("JSON data failed to load!");
return;
}
// ✅ 强制校验 popu 是否为数组
if (!Array.isArray(popu)) {
console.error("vn.json must be a valid JSON array");
return;
}
2. 国家标识符不匹配 → 坐标查找失败
这是最隐蔽也最关键的错误:
- vn.json 中 "country": "Afghanistan" 是全称;
- latlon.json 中的键却是 "af"、"ae" 等ISO 2-letter 代码(注意 "ad"、"af" 等小写键名)。
因此 latlon[country] 永远返回 undefined,后续 address[0] 报错,循环中断,data 无法填充。
✅ 推荐修复策略:统一为 ISO 代码(更健壮、易扩展)
修改 vn.json,将 country 字段替换为 ISO 2-letter 代码,并确保与 latlon.json 键名严格一致(小写):
[
{ "country": "af", "population": 37172386 },
{ "country": "al", "population": 2866376 },
{ "country": "dz", "population": 42228429 },
{ "country": "as", "population": 55465 },
{ "country": "ad", "population": 77006 }
]
? 提示:可借助 ISO 3166-1 alpha-2 标准表批量转换,避免手动错误。
3. 尺寸映射逻辑优化(预防 NaN)
原代码中:
let minD = sqrt(minSubs); // 若 minSubs=0 → sqrt(0)=0 let maxD = sqrt(maxSubs); // map(value, 0, maxD, 1, 20) → 当 minD===0 时,map() 可能失效
✅ 加固写法:
let minD = sqrt(Math.max(1, minSubs)); // 防止 sqrt(0)
let maxD = sqrt(maxSubs);
for (let coun of data) {
const normalized = map(sqrt(coun.count), minD, maxD, 1, 20);
coun.diameter = Math.max(1, normalized); // 确保最小直径 ≥1px
}
✅ 完整修正版 setup() 与 draw()(含错误防护)
function setup() {
canvas = createCanvas(800, 600);
trainMap = mappa.tileMap(options);
trainMap.overlay(canvas);
// ✅ 数据校验与安全遍历
if (!Array.isArray(popu) || typeof latlon !== 'object') {
console.error("Invalid data format: popu must be array, latlon must be object");
return;
}
let maxSubs = 0;
let minSubs = Infinity;
for (let i = 0; i = 2) {
const lat = parseFloat(address[0]);
const lon = parseFloat(address[1]);
const count = Number(entry.population);
if (!isNaN(lat) && !isNaN(lon) && !isNaN(count) && count > 0) {
data.push({ lat, lon, count });
maxSubs = Math.max(maxSubs, count);
minSubs = Math.min(minSubs, count);
}
}
}
if (data.length === 0) {
console.warn("No valid country data mapped. Check vn.json country codes and latlon.json keys.");
return;
}
// ✅ 安全归一化
const minD = Math.sqrt(Math.max(1, minSubs));
const maxD = Math.sqrt(maxSubs);
for (let coun of data) {
const norm = map(Math.sqrt(coun.count), minD, maxD, 1, 20);
coun.diameter = Math.max(1, norm);
}
}
function draw() {
clear();
// ✅ 确保地图已就绪再绘制
if (!trainMap || data.length === 0) return;
for (let coun of data) {
const pix = trainMap.latLngToPixel(coun.lat, coun.lon);
// ✅ 添加坐标有效性检查
if (pix && !isNaN(pix.x) && !isNaN(pix.y)) {
fill(frameCount % 255, 0, 200, 150); // 提高透明度确保可见
const zoom = trainMap.zoom();
const scl = Math.pow(2, zoom);
ellipse(pix.x, pix.y, coun.diameter * scl);
}
}
}
? 注意事项与进阶建议
- 坐标精度:latlon.json 中的经纬度应为数字而非字符串("42.5000" → 42.5),否则 parseFloat() 是必须的;
- 性能优化:大量国家时,draw() 中重复调用 trainMap.zoom() 和 latLngToPixel() 开销大,建议在 setup() 中预计算像素位置;
- 交互增强:可添加 mousePressed() 检测点击椭圆,用 dist() 判断是否命中,实现 tooltip 或详情面板;
- 替代方案:若长期维护,推荐改用 GeoJSON 格式 + p5.Geometry,天然支持地理投影与拓扑关系。
遵循以上修正,你的世界人口椭圆图将清晰、稳定地呈现在 Leaflet 地图上——每个椭圆直径正比于人口规模的平方根,视觉层次分明,地理定位精准。










