如何在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 © <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
相关产品推荐
相关产品推荐

