You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

p5.js数据可视化项目:如何避免人口数据圆形元素重叠?

解决p5.js中圆形重叠的问题:碰撞检测与力导向布局

嘿,很高兴看到你在做这个人口数据可视化项目!随机位置导致圆形重叠确实会让数据可读性大打折扣,作为编程新手,咱们可以用碰撞检测+位置调整的方法(类似简单的力导向布局)来解决这个问题,这是可视化领域里常用的小技巧,实现起来也不算复杂。

第一步:重构代码,封装圆的对象

首先,我们把每个国家的圆形数据封装成对象,这样方便跟踪它们的位置、大小、颜色等属性,而不是在循环里临时生成:

let population;
let countryCircles = []; // 存储所有国家的圆形对象

function preload() {
  population = loadJSON("worldpopulation.json");
}

function setup() {
  createCanvas(1280, 720);
  
  // 初始化每个国家的圆形对象
  for (let i = 0; i < 50; i++) {
    let circleSize = population[i].population / 3000000;
    countryCircles.push({
      x: random(width), // 初始随机X位置
      y: random(height), // 初始随机Y位置
      size: circleSize,
      country: population[i].country,
      population: population[i].population,
      color: color(random(255), random(255), random(255)) // 随机颜色
    });
  }
}

第二步:添加碰撞检测与位置调整逻辑

接下来,我们在draw函数里,每次重绘时检查所有圆形的重叠情况,把重叠的圆互相推开。注意要去掉noLoop(),因为我们需要多次重绘来调整位置,直到所有圆都不再重叠:

function draw() {
  background(255); // 每次重绘清空画布
  let maxMovement = 0; // 用来跟踪所有圆的最大移动量,判断是否稳定
  
  // 遍历所有圆形,两两检查碰撞
  for (let i = 0; i < countryCircles.length; i++) {
    let circleA = countryCircles[i];
    let prevX = circleA.x;
    let prevY = circleA.y; // 记录调整前的位置,计算移动量
    
    for (let j = i + 1; j < countryCircles.length; j++) {
      let circleB = countryCircles[j];
      
      // 计算两个圆心之间的距离
      let distance = dist(circleA.x, circleA.y, circleB.x, circleB.y);
      // 两个圆不重叠的最小距离:两个半径之和(size是直径,所以半径是size/2)
      let minSafeDistance = (circleA.size / 2) + (circleB.size / 2);
      
      // 如果距离小于最小安全距离,说明重叠了,需要推开
      if (distance < minSafeDistance && distance > 0) {
        // 计算两个圆之间的方向向量
        let dx = circleB.x - circleA.x;
        let dy = circleB.y - circleA.y;
        let angle = atan2(dy, dx); // 方向角
        
        // 归一化方向向量,确保推开的力度一致
        let pushDirX = cos(angle);
        let pushDirY = sin(angle);
        
        // 计算重叠的距离,把两个圆分别向相反方向推开一半的重叠量
        let overlapAmount = minSafeDistance - distance;
        circleA.x -= pushDirX * overlapAmount * 0.5;
        circleA.y -= pushDirY * overlapAmount * 0.5;
        circleB.x += pushDirX * overlapAmount * 0.5;
        circleB.y += pushDirY * overlapAmount * 0.5;
        
        // 防止圆跑出画布边界
        circleA.x = constrain(circleA.x, circleA.size/2, width - circleA.size/2);
        circleA.y = constrain(circleA.y, circleA.size/2, height - circleA.size/2);
        circleB.x = constrain(circleB.x, circleB.size/2, width - circleB.size/2);
        circleB.y = constrain(circleB.y, circleB.size/2, height - circleB.size/2);
      }
    }
    
    // 计算当前圆的移动距离,更新最大移动量
    let movement = dist(prevX, prevY, circleA.x, circleA.y);
    if (movement > maxMovement) {
      maxMovement = movement;
    }
    
    // 绘制当前圆和文字
    fill(circleA.color);
    ellipse(circleA.x, circleA.y, circleA.size, circleA.size);
    fill(0);
    textAlign(CENTER);
    text(circleA.country, circleA.x, circleA.y - 5);
    // 用nf()格式化人口数字,避免过长的数字挤在一起
    text(nf(circleA.population, 0, 0), circleA.x, circleA.y + 5);
  }
  
  // 当所有圆的移动量都小于0.1时,说明已经稳定,停止重绘
  if (maxMovement < 0.1) {
    noLoop();
    console.log("所有圆形已稳定,停止重绘");
  }
}

关键知识点解释

  • 对象封装:把每个圆的属性(位置、大小、颜色等)放在一个对象里,方便统一管理和修改。
  • 碰撞检测:通过计算两个圆心的距离,和最小安全距离比较,判断是否重叠。
  • 位置调整:计算重叠方向,把两个圆向相反方向推开,同时用constrain限制位置,避免圆跑出画布。
  • 稳定判断:跟踪所有圆的移动量,当移动量足够小时停止循环,节省性能。

小优化建议

  • 可以调整overlapAmount * 0.5里的系数,比如改成0.3,这样圆的移动会更平缓,稳定得更快。
  • 如果想让大的圆更“重”,可以根据圆的大小调整推开的力度,比如大的圆移动量更小,小的圆移动量更大。
  • 可以给文字加个白色背景,避免和圆形颜色冲突,比如在绘制文字前先画一个小矩形填充白色。

内容的提问来源于stack exchange,提问作者Morgan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 07:59:13