地理投影与地图
D3 的地理模块(d3-geo)提供 30+ 种地图投影、路径生成器和球面几何计算,是构建交互式数据地图的核心工具。
1. 投影体系
1.1 投影原理
图表渲染中…
1.2 常用投影
| 投影 | 函数 | 特点 | 适用场景 |
|---|---|---|---|
| 墨卡托 | geoMercator() | 等角,高纬度变形大 | 通用 Web 地图 |
| 等面积 | geoEqualEarth() | 面积准确 | 世界统计地图 |
| 正交 | geoOrthographic() | 3D 球体视角 | 地球仪效果 |
| 自然地球 | geoNaturalEarth1() | 视觉平衡 | 世界地图(推荐) |
| 阿尔伯斯 | geoAlbersUsa() | 美国专用复合投影 | 美国地图 |
| 方位等积 | geoAzimuthalEqualArea() | 从中心点等面积 | 区域地图 |
1.3 创建投影
javascript
import { geoMercator, geoNaturalEarth1, geoPath } from 'd3'
// 墨卡托投影
const projection = geoMercator()
.center([104, 37]) // 中心点(中国中心经度, 纬度)
.scale(600) // 缩放比例
.translate([width / 2, height / 2]) // 平移
// 经纬度 → 像素
const [x, y] = projection([116.4, 39.9]) // 北京
// → [约 380, 约 180]
// 像素 → 经纬度(反向)
const [lng, lat] = projection.invert([380, 180])1.4 fitSize 自适应
javascript
// 自动计算 scale 和 translate 使 GeoJSON 填满指定区域
const projection = geoNaturalEarth1()
.fitSize([width, height], geojsonData)
// 带边距
const projection = geoMercator()
.fitExtent([[20, 20], [width - 20, height - 20]], geojsonData)2. 路径生成器 d3.geoPath()
2.1 渲染 GeoJSON
javascript
import { geoPath, geoNaturalEarth1 } from 'd3'
const projection = geoNaturalEarth1().fitSize([800, 500], worldGeoJSON)
const pathGenerator = geoPath().projection(projection)
// 渲染国家
svg.selectAll('path')
.data(worldGeoJSON.features)
.join('path')
.attr('d', pathGenerator)
.attr('fill', d => colorScale(d.properties.value))
.attr('stroke', '#fff')
.attr('stroke-width', 0.5)2.2 Canvas 渲染
javascript
const pathGenerator = geoPath()
.projection(projection)
.context(ctx) // 绑定 Canvas 上下文
// 渲染
ctx.beginPath()
pathGenerator(feature)
ctx.fillStyle = '#ddd'
ctx.fill()
ctx.strokeStyle = '#999'
ctx.stroke()2.3 计算几何属性
javascript
const path = geoPath().projection(projection)
// 面积(像素²)
const area = path.area(feature)
// 质心(用于放置标签)
const [cx, cy] = path.centroid(feature)
// 边界框
const [[x0, y0], [x1, y1]] = path.bounds(feature)3. 数据地图(Choropleth)
3.1 等值区域图
javascript
import { geoPath, geoMercator, scaleSequential, interpolateYlOrRd, json } from 'd3'
async function renderChinaMap() {
// 加载 GeoJSON(中国省级边界)
const china = await json('/data/china-provinces.geojson')
// 加载统计数据
const stats = await json('/data/province-gdp.json')
const projection = geoMercator().fitSize([800, 600], china)
const path = geoPath().projection(projection)
const colorScale = scaleSequential()
.domain([0, d3.max(stats, d => d.gdp)])
.interpolator(interpolateYlOrRd)
const statMap = new Map(stats.map(d => [d.province, d.gdp]))
svg.selectAll('path')
.data(china.features)
.join('path')
.attr('d', path)
.attr('fill', d => colorScale(statMap.get(d.properties.name) || 0))
.attr('stroke', '#fff')
.attr('stroke-width', 0.5)
.on('mouseenter', function(event, d) {
select(this).attr('stroke', '#000').attr('stroke-width', 2)
showTooltip(d.properties.name, statMap.get(d.properties.name))
})
.on('mouseleave', function() {
select(this).attr('stroke', '#fff').attr('stroke-width', 0.5)
hideTooltip()
})
}3.2 气泡地图
javascript
// 在省会城市位置放置气泡
const cities = [
{ name: '北京', lng: 116.4, lat: 39.9, value: 41610 },
{ name: '上海', lng: 121.5, lat: 31.2, value: 44652 },
{ name: '广州', lng: 113.3, lat: 23.1, value: 28839 }
]
const rScale = scaleSqrt()
.domain([0, d3.max(cities, d => d.value)])
.range([5, 40])
svg.selectAll('circle')
.data(cities)
.join('circle')
.attr('cx', d => projection([d.lng, d.lat])[0])
.attr('cy', d => projection([d.lng, d.lat])[1])
.attr('r', d => rScale(d.value))
.attr('fill', 'rgba(255, 100, 100, 0.6)')
.attr('stroke', '#fff')4. 球面几何计算
javascript
import { geoDistance, geoInterpolate, geoGraticule } from 'd3'
// 两点间球面距离(弧度)
const dist = geoDistance([116.4, 39.9], [121.5, 31.2])
const km = dist * 6371 // → 约 1067 km
// 两点间插值(大圆航线)
const interpolate = geoInterpolate([116.4, 39.9], [-73.9, 40.7])
const midpoint = interpolate(0.5) // 航线中点
// 经纬网格
const graticule = geoGraticule().step([15, 15])
svg.append('path')
.datum(graticule)
.attr('d', path)
.attr('fill', 'none')
.attr('stroke', '#ddd')5. 地图交互
5.1 缩放与平移
javascript
import { zoom } from 'd3'
const zoomBehavior = zoom()
.scaleExtent([1, 8])
.on('zoom', (event) => {
g.attr('transform', event.transform)
// 缩放时调整线宽
g.selectAll('path').attr('stroke-width', 0.5 / event.transform.k)
})
svg.call(zoomBehavior)5.2 点击定位
javascript
svg.selectAll('path')
.on('click', function(event, d) {
const [[x0, y0], [x1, y1]] = path.bounds(d)
const dx = x1 - x0, dy = y1 - y0
const x = (x0 + x1) / 2, y = (y0 + y1) / 2
const scale = Math.min(8, 0.9 / Math.max(dx / width, dy / height))
svg.transition().duration(750).call(
zoomBehavior.transform,
d3.zoomIdentity
.translate(width / 2, height / 2)
.scale(scale)
.translate(-x, -y)
)
})6. GeoJSON 数据获取
| 数据源 | 内容 | 地址 |
|---|---|---|
| Natural Earth | 全球国家/省/河流 | naturalearthdata.com |
| DataV.GeoAtlas | 中国省/市/区边界 | datav.aliyun.com |
| US Census | 美国州/县边界 | census.gov |
| geojson.io | 在线编辑/导出 | geojson.io |
简化 GeoJSON
原始 GeoJSON 可能很大(> 10MB)。使用 mapshaper 工具简化:
bash
npx mapshaper china.geojson -simplify 10% -o china-simple.geojson7. 最佳实践
优先使用 fitSize
手动设置 center/scale/translate 容易出错。projection.fitSize([w, h], geojson) 自动适配。
大数据集用 Canvas
超过 500 个多边形时,Canvas 渲染性能远优于 SVG。
墨卡托的面积失真
墨卡托投影在高纬度严重放大面积(格陵兰看起来和非洲一样大)。统计地图优先使用 geoEqualEarth() 或 geoNaturalEarth1()。