
本文介绍如何将原始经纬度数据按坐标组合频次进行聚合,并动态缩放 svg 圆形标记半径,实现地理热点可视化。核心是使用 d3 的 `d3.group()` 统计重复坐标,再将频次映射为圆半径。
在 D3 与 Leaflet 混合开发中,直接对原始 CSV 数据逐行渲染圆形(如 d3.csv(...).enter().append("circle"))会导致同一位置多次绘制重叠小圆,无法体现“密度”或“热度”。真正有效的解决方案是:先聚合数据,再渲染唯一坐标点,并以频次驱动视觉编码(如半径大小)。
✅ 正确步骤概览
- 加载并预处理数据:使用 d3.csv() 读取数据;
- 按经纬度组合分组统计:利用 d3.group() 将相同 [lat, lng] 的记录归为一组;
- 生成聚合后的新数据集:每个元素包含 cnt(出现次数)、sub_district_lat、sub_district_long;
- 绑定聚合数据并设置半径:将 r 属性设为 d => baseRadius * d.cnt(可进一步用 d3.scaleSqrt() 实现更合理的视觉比例);
- 保持地图交互响应性:保留 map.on("moveend", update) 逻辑,确保缩放/拖拽时坐标实时重投影。
? 示例代码(D3 v7 + Leaflet)
// 1. 聚合数据:按 [lat, lng] 字符串键分组
const grouped = d3.group(data, d =>
`${+d.sub_district_lat.toFixed(6)},${+d.sub_district_long.toFixed(6)}`
);
// 2. 转换为带频次的对象数组
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
}));
// 3. 创建比例尺(推荐!避免半径随频次线性爆炸)
const radiusScale = d3.scaleSqrt()
.domain(d3.extent(aggregatedData, d => d.cnt))
.range([4, 24]); // 最小半径4px,最大24px
// 4. 渲染圆形(注意:绑定 aggregatedData,非原始 data)
d3.select("#mapid")
.select("svg")
.selectAll("circle")
.data(aggregatedData)
.enter()
.append("circle")
.attr("cx", d => map.latLngToLayerPoint([d.sub_district_lat, d.sub_district_long]).x)
.attr("cy", d => map.latLngToLayerPoint([d.sub_district_lat, d.sub_district_long]).y)
.attr("r", d => radiusScale(d.cnt)) // ✅ 使用比例尺,更科学
.style("fill", "#e34a33")
.attr("stroke", "#b30000")
.attr("stroke-width", 0.8)
.attr("fill-opacity", 0.7);⚠️ 关键注意事项
- 精度处理:原始经纬度可能存在浮点误差(如 52.52000000000001 vs 52.52),建议先统一保留 5–6 位小数再拼接键,否则相同位置会被视为不同分组;
- 性能优化:若数据量极大(>10k 条),d3.group() 仍高效,但应避免在 update() 中重复聚合——聚合只需执行一次;
- 视觉合理性:*切勿直接使用 `r = k cnt线性缩放**。频次差异大时,高频点会过度遮盖邻近区域。d3.scaleSqrt()或d3.scalePow().exponent(0.3)` 更符合人眼对面积的感知;
- D3 版本兼容性:d3.group() 是 D3 v6+ 新增 API;若必须用 v4/v5,可用 d3.nest() 替代(语法稍冗长);
- Leaflet SVG 层:确保 L.svg().addTo(map) 在 d3.csv() 前执行,否则 d3.select("svg") 可能为空。
? 总结
将地理点频次映射为视觉尺寸,本质是「数据聚合 → 比例映射 → 坐标投影 → SVG 渲染」四步闭环。抛弃“一个坐标画一个圆”的直觉做法,转向“一个聚合点画一个缩放圆”的分析思维,才能构建真正可读、可扩展、专业的地理热力图。后续还可叠加 d3.tip() 实现悬停显示频次,或用 d3.contourDensity() 生成平滑热力面——但一切始于扎实的频次聚合。










