{T}

地理投影与地图

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.geojson

7. 最佳实践

优先使用 fitSize

手动设置 center/scale/translate 容易出错。projection.fitSize([w, h], geojson) 自动适配。

大数据集用 Canvas

超过 500 个多边形时,Canvas 渲染性能远优于 SVG。

墨卡托的面积失真

墨卡托投影在高纬度严重放大面积(格陵兰看起来和非洲一样大)。统计地图优先使用 geoEqualEarth()geoNaturalEarth1()