
本文详解如何将原始经纬度数据聚合成频次统计,并据此动态设置 d3 绘制的 svg 圆圈半径,实现地理热点强度可视化。核心在于使用 d3 的 `d3.group()` 对坐标去重计数,并将频次映射为圆圈半径(如 `r = baseradius * count`)。
在 D3 与 Leaflet 协同绘制地理散点图时,若原始数据包含大量重复的经纬度组合(例如用户打卡、事件上报等场景),直接为每条记录渲染一个圆圈会导致视觉重叠、遮挡严重,且无法直观反映空间分布的密度差异。理想的解决方案是:先聚合数据,再按频次缩放圆圈大小。
✅ 正确做法:数据聚合 + 比例映射
D3 v7 提供了简洁高效的聚合工具 d3.group()。我们以经纬度数组 [lat, lng] 为键进行分组(注意需转为字符串以支持对象键比较),再统计每组出现次数:
// 假设原始 data 是 d3.csv 加载的数组,每项含 sub_district_lat / sub_district_long 字段
const grouped = d3.group(data, d =>
[d.sub_district_lat, d.sub_district_long].toString()
);
// 转为标准数组并构造新数据结构:{ cnt, sub_district_lat, sub_district_long }
const aggregatedData = Array.from(grouped, ([_, records]) => ({
cnt: records.length,
sub_district_lat: records[0].sub_district_lat,
sub_district_long: records[0].sub_district_long
}));随后,在绑定数据时,将圆圈半径 r 属性改为基于 cnt 动态计算:
.attr("r", d => Math.max(4, 8 * Math.sqrt(d.cnt))) // 推荐:用 sqrt 缓解极端值放大效应⚠️ 注意:*不建议直接使用 `12 d.cnt`**(如原答案所示)。当某位置出现 100 次时,半径达 1200px,极易覆盖整屏且丧失可比性。更科学的做法是:使用 Math.sqrt(d.cnt) 或 Math.cbrt(d.cnt) 压缩尺度;设置最小半径(如 Math.max(3, ...))确保低频点仍可见;可结合 d3.scaleLinear() 进行精细化映射(见下文进阶示例)。
? 完整集成代码(D3 v7 + Leaflet)
<script src="https://d3js.org/d3.v7.js"></script>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
<div id="mapid" style="height: 500px;"></div>
<script>
const map = L.map('mapid').setView([52.52, 13.405], 5);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map);
L.svg().addTo(map); // 确保 SVG 图层存在
// 示例数据(实际中由 d3.csv("longAndLat.csv") 加载)
const rawData = [
{sub_district_lat: 52.52, sub_district_long: 13.405},
{sub_district_lat: 52.52, sub_district_long: 13.405},
{sub_district_lat: 52.49, sub_district_long: 13.37},
{sub_district_lat: 52.52, sub_district_long: 13.405},
{sub_district_lat: 52.51, sub_district_long: 13.42}
];
// 聚合:按经纬度分组计数
const aggregated = Array.from(
d3.group(rawData, d => [d.sub_district_lat, d.sub_district_long].toString()),
([_, records]) => ({
cnt: records.length,
lat: records[0].sub_district_lat,
lng: records[0].sub_district_long
})
);
// 创建缩放比例尺(可选但推荐)
const radiusScale = d3.scaleSqrt()
.domain(d3.extent(aggregated, d => d.cnt))
.range([4, 24]); // 频次从 min→max 映射到半径 4→24px
// 渲染圆圈
const svg = d3.select(map._container).select("svg");
const g = svg.select("g").empty() ? svg.append("g") : svg.select("g");
g.selectAll("circle")
.data(aggregated)
.join("circle")
.attr("cx", d => map.latLngToLayerPoint(L.latLng(d.lat, d.lng)).x)
.attr("cy", d => map.latLngToLayerPoint(L.latLng(d.lat, d.lng)).y)
.attr("r", d => radiusScale(d.cnt))
.attr("fill", "#e34a33")
.attr("fill-opacity", 0.7)
.attr("stroke", "#b30000")
.attr("stroke-width", 1);
// 响应式更新(地图移动/缩放时重定位)
function updateCircles() {
g.selectAll("circle")
.attr("cx", d => map.latLngToLayerPoint(L.latLng(d.lat, d.lng)).x)
.attr("cy", d => map.latLngToLayerPoint(L.latLng(d.lat, d.lng)).y);
}
map.on("moveend", updateCircles);
</script>? 关键注意事项
- 版本兼容性:务必使用 D3 v7+(d3.group 在 v4 中不可用);Leaflet v1.x 与 SVG 图层配合稳定。
- 性能优化:数千条记录聚合后通常只剩数百个唯一坐标,大幅提升渲染效率。
- 视觉合理性:优先使用 scaleSqrt() 而非线性缩放,避免高频点“吞噬”周边区域。
- 坐标精度:若原始数据含小数位数过多(如 10 位),建议预处理四舍五入(如 +d.lat.toFixed(4)),防止微小浮点误差导致分组失败。
- 交互增强:可为圆圈添加 title 属性显示具体频次,或绑定 on("click", ...) 实现钻取分析。
通过以上方法,你不仅能清晰呈现地理热点分布,还能让可视化结果具备真实的统计意义和专业表现力。










