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

如何在D3+Leaflet中按经纬度组合频次缩放地图圆圈?

按经纬度频次缩放圆圈的实现步骤

1. 聚合经纬度数据,统计频次

原始数据包含重复的经纬度组合,先通过分组计算每个坐标的出现次数,避免重复绘制同一位置的多个圆圈:

// 加载CSV数据后,先处理聚合逻辑
var aggregatedData = d3.nest()
  .key(function(d) { return `${d.sub_district_lat},${d.sub_district_long}`; }) // 用经纬度拼接成唯一标识
  .rollup(function(v) { return {
    lat: v[0].sub_district_lat,
    lng: v[0].sub_district_long,
    count: v.length // 统计该坐标的出现频次
  }; })
  .entries(data)
  .map(function(d) { return d.value; }); // 提取聚合后的核心数据

2. 创建半径比例尺

用平方根比例尺将频次数值映射到合适的半径范围(视觉上面积比半径更能体现数量差异,平方根比例尺可让面积与频次成正比):

// 获取频次的极值,作为比例尺定义域
var counts = aggregatedData.map(function(d) { return d.count; });
var minCount = d3.min(counts);
var maxCount = d3.max(counts);

// 创建比例尺,将频次映射到3-30的半径区间(可根据需求调整范围)
var radiusScale = d3.scaleSqrt()
  .domain([minCount, maxCount])
  .range([3, 30]);

3. 修改圆圈绘制逻辑

基于聚合后的数据绘制圆圈,半径由比例尺根据频次计算得出:

// 替换原有的圆圈绘制代码
d3.select("#mapid")
  .select("svg")
  .selectAll("circle")
  .data(aggregatedData)
  .enter()
  .append("circle")
    .attr("cx", function(d){ return map.latLngToLayerPoint([d.lat, d.lng]).x })
    .attr("cy", function(d){ return map.latLngToLayerPoint([d.lat, d.lng]).y })
    .attr("r", function(d) { return radiusScale(d.count); }) // 用频次计算半径
    .style("fill", "red")
    .attr("stroke", "red")
    .attr("stroke-width", 1)
    .attr("fill-opacity", .2)

4. 保留位置更新逻辑

原有的地图拖拽/缩放后更新位置的逻辑无需修改,因为半径只由频次决定,不会随地图变化:

function update() {
  d3.selectAll("circle")
    .attr("cx", function(d){ return map.latLngToLayerPoint([d.lat, d.lng]).x })
    .attr("cy", function(d){ return map.latLngToLayerPoint([d.lat, d.lng]).y })
}

map.on("moveend", update)

完整修改后的代码

<!DOCTYPE html>
<meta charset="utf-8">

<!-- Load d3.js and the geo projection plugin -->
<script src="https://d3js.org/d3.v4.js"></script>
<script src="https://d3js.org/d3-geo-projection.v2.min.js"></script>

<!-- Load Leaflet -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.3.4/dist/leaflet.css" integrity="sha512-puBpdR0798OZvTTbP4A8Ix/l+A4dHDD0DGqYW6RQ+9jxkRFclaxxQb/SJAWZfWAkuyeQUytO7+7N4QKrDh+drA==" crossorigin=""/>
<script src="https://unpkg.com/leaflet@1.3.4/dist/leaflet.js" integrity="sha512-nMMmRyTVoLYqjP9hrbed9S+FzjZHW5gY1TWCHA5ckwXZBadntCNs8kEqAWdrb9O7rxbCaA4lKTIWjDXZxflOcA==" crossorigin=""></script>

<!-- Create an element where the map will take place -->
<div id="mapid"></div>

<style>
#mapid { height: 2000px; }
</style>
<script>

var map = L
  .map('mapid')
  .setView([52.5200, 13.4050], 5);   // center position + zoom

// Add a tile to the map = a background. Comes from OpenStreetmap
L.tileLayer(
    'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
    attribution: 'Map data &copy; <a href="https://www.openstreetmap.org/">OpenStreetMap</a>',
    maxZoom: 14,
    }).addTo(map)

L.svg().addTo(map);


d3.csv("longAndLat.csv", function(data) {

    // 聚合数据,统计每个经纬度的出现频次
    var aggregatedData = d3.nest()
      .key(function(d) { return `${d.sub_district_lat},${d.sub_district_long}`; })
      .rollup(function(v) { return {
        lat: v[0].sub_district_lat,
        lng: v[0].sub_district_long,
        count: v.length
      }; })
      .entries(data)
      .map(function(d) { return d.value; });

    // 创建半径比例尺
    var counts = aggregatedData.map(function(d) { return d.count; });
    var minCount = d3.min(counts);
    var maxCount = d3.max(counts);
    var radiusScale = d3.scaleSqrt()
      .domain([minCount, maxCount])
      .range([3, 30]);

    // 绘制圆圈,半径由频次决定
    d3.select("#mapid")
    .select("svg")
    .selectAll("circle")
    .data(aggregatedData)
    .enter()
    .append("circle")
        .attr("cx", function(d){ return map.latLngToLayerPoint([d.lat, d.lng]).x })
        .attr("cy", function(d){ return map.latLngToLayerPoint([d.lat, d.lng]).y })
        .attr("r", function(d) { return radiusScale(d.count); })
        .style("fill", "red")
        .attr("stroke", "red")
        .attr("stroke-width", 1)
        .attr("fill-opacity", .2)

// Function that update circle position if something change
function update() {
  d3.selectAll("circle")
    .attr("cx", function(d){ return map.latLngToLayerPoint([d.lat, d.lng]).x })
    .attr("cy", function(d){ return map.latLngToLayerPoint([d.lat, d.lng]).y })
}

// If the user change the map (zoom or drag), I update circle position:
map.on("moveend", update)

});
</script>

关键说明

  • 数据聚合:通过d3.nest()合并重复坐标,避免地图上同一位置叠加多个圆圈,同时得到每个坐标的频次数据。
  • 平方根比例尺:相比线性比例尺,它能让圆圈面积与频次成正比,更符合人类对数量差异的视觉感知。
  • 半径范围调整:range([3,30])可根据数据分布和视觉需求修改,确保圆圈既不会过小难以识别,也不会过大遮挡地图内容。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.01 14:35:40