地理可视化
地理可视化是数据可视化中最具表现力的形式之一。通过将数据映射到地理位置上,我们可以直观地展示地域分布、区域差异和空间模式。D3 的 d3-geo 模块提供了完整的地图绘制能力,包括投影变换、路径生成、球面数学计算等功能。
为什么选择 D3 绘制地图?
在众多地图库中,D3 的地理可视化有其独特优势。与 Leaflet、Mapbox 等专用于地图的库不同,D3 不会为你生成现成的地图底图,而是提供了一套从原始地理数据到最终可视化呈现的完整工具链。这意味着你可以完全控制地图的每一个细节——从投影方式到颜色填充,从边界样式到交互行为。
D3 地图可视化的典型应用场景包括:自定义投影的世界地图或国家地图;统计地图(Choropleth Map)展示各区域的数据分布;点地图标注特定位置;路径地图展示路线或流动方向。如果你的需求是标准的交互式地图(如需要底图、POI 搜索、路线规划),建议使用 Leaflet 或 Mapbox 等专业地图库。
GeoJSON 数据格式
GeoJSON 是一种用于编码地理数据的格式,它是 JSON 的扩展,专门用于表示地理要素。理解 GeoJSON 的结构是使用 D3 绘制地图的基础。
基本结构
一个 GeoJSON 对象通常表示一个地理要素(Feature)或要素集合(FeatureCollection):
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {
"name": "北京市",
"population": 21540000
},
"geometry": {
"type": "Point",
"coordinates": [116.4074, 39.9042]
}
},
{
"type": "Feature",
"properties": {
"name": "上海市"
},
"geometry": {
"type": "Polygon",
"coordinates": [[[...], [...], ...]]
}
}
]
}
几何类型
GeoJSON 支持多种几何类型,每种类型对应不同的地理要素:
Point(点):表示单个位置,坐标格式为 [经度, 纬度]。常用于标注城市、地标等。
{
"type": "Point",
"coordinates": [116.4074, 39.9042]
}
MultiPoint(多点):多个点的集合,坐标格式为二维数组。
LineString(线):表示一条路径,由多个点连接而成。常用于表示道路、河流、边界线等。
{
"type": "LineString",
"coordinates": [
[116.4074, 39.9042],
[121.4737, 31.2304],
[113.2644, 23.1291]
]
}
MultiLineString(多线):多条线的集合,比如一个城市的地铁网络。
Polygon(多边形):表示一个封闭区域,坐标是三维数组。最外层数组可以包含多个环(ring),第一个环是外边界,后续的环是内部的洞(如湖泊)。
{
"type": "Polygon",
"coordinates": [
[[100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0]]
]
}
MultiPolygon(多多边形):多个多边形的集合,比如由多个岛屿组成的国家或省份。
GeometryCollection(几何集合):不同类型几何对象的集合。
坐标顺序
GeoJSON 使用 [经度, 纬度] 的坐标顺序,这与某些地图服务(如 Google Maps)使用 [纬度, 经度] 不同。经度范围是 -180 到 180(西经到东经),纬度范围是 -90 到 90(南纬到北纬)。
地图投影
地球是一个球体(或更准确地说是椭球体),而地图是在平面上展示的。投影是将球面坐标转换为平面坐标的过程。任何投影都会带来某种程度的变形——面积、形状、距离或方向不可能同时保持不变。
投影的基本概念
投影将球面上的点 (经度、纬度)变换为平面上的点 。D3 提供了数十种投影实现,每种投影都有其特点和适用场景。
// 创建投影
const projection = d3.geoMercator()
.scale(1000) // 缩放比例
.center([104, 35]) // 中心点 [经度, 纬度]
.translate([width / 2, height / 2]); // 平移到画布中心
// 将经纬度坐标转换为像素坐标
const pixel = projection([116.4074, 39.9042]); // [x, y]
常用投影类型
墨卡托投影(Mercator):最常用的投影之一,特点是经线和纬线都是直线且互相垂直。常用于网页地图,但高纬度地区面积会严重变形(格陵兰岛看起来和非洲一样大)。
const projection = d3.geoMercator()
.scale(150)
.center([0, 0])
.translate([width / 2, height / 2]);
墨卡托投影无法表示极地区域(纬度接近 90 度时,y 坐标趋于无穷大),因此适合展示中低纬度地区。
等距圆柱投影(Equirectangular):最简单的投影,经度和纬度直接映射为 x 和 y 坐标。比例尺在赤道处正确,其他纬度会有变形。
const projection = d3.geoEquirectangular()
.scale(150)
.translate([width / 2, height / 2]);
正射投影(Orthographic):透视投影,仿佛从太空看地球。只能看到半球,常用于展示地球仪效果。
const projection = d3.geoOrthographic()
.scale(200)
.rotate([0, 0]) // 旋转地球
.translate([width / 2, height / 2]);
自然地球投影(Natural Earth):专为世界地图设计的折中投影,在各方面的变形都相对较小,视觉效果美观。
const projection = d3.geoNaturalEarth1()
.scale(150)
.translate([width / 2, height / 2]);
圆锥投影(Albers、Conic Equal Area):保持面积正确的投影,适合展示中纬度的大陆或国家。
// 阿尔伯斯等面积圆锥投影,适合美国地图
const projection = d3.geoAlbers()
.scale(1000)
.rotate([96, 0]) // 中央经线
.center([0, 38]) // 中心纬度
.parallels([29.5, 45.5]); // 标准纬线
方位投影(Azimuthal):从特定点投影,适合展示以某点为中心的区域。
// 等距离方位投影
const projection = d3.geoAzimuthalEqualDistance()
.scale(200)
.center([116.4, 39.9]) // 以北京为中心
.translate([width / 2, height / 2]);
投影配置方法
scale:设置投影的比例因子,值越大地图越大。比例因子的含义因投影而异,通常需要通过实验调整。
projection.scale(1000);
center:设置投影的中心点 [经度, 纬度]。
projection.center([104, 35]); // 中国大致中心
rotate:旋转地球,参数为 [lambda, phi, gamma](经度旋转、纬度旋转、自转)。
projection.rotate([0, -90]); // 从北极看
projection.rotate([0, 90]); // 从南极看
translate:设置投影结果的平移量,通常用于将地图居中。
projection.translate([width / 2, height / 2]);
fitSize / fitExtent:自动调整投影以适应指定的尺寸和边界,非常实用的方法。
// 自动适配画布尺寸
projection.fitSize([width, height], geojsonFeature);
// 带边距的适配
projection.fitExtent([[20, 20], [width - 20, height - 20]], geojsonFeature);
路径生成器
路径生成器 d3.geoPath() 是将 GeoJSON 数据转换为 SVG 路径字符串的核心工具。它内部会调用投影函数来转换坐标,然后生成对应的路径。
基本用法
// 创建路径生成器
const path = d3.geoPath()
.projection(projection); // 绑定投影
// 为 GeoJSON 要素生成路径字符串
const d = path(geojsonFeature);
// 输出类似: "M100.5,200.3L101.2,199.8..."
// 直接绑定到 SVG 的 d 属性
svg.selectAll('path')
.data(geojsonFeatures)
.join('path')
.attr('d', path)
.attr('fill', 'steelblue')
.attr('stroke', 'white');
计算几何中心
路径生成器可以计算 GeoJSON 要素的几何中心,常用于放置标签或标记点:
// 返回 [x, y] 像素坐标
const centroid = path.centroid(geojsonFeature);
// 在地图上添加标签
svg.selectAll('.label')
.data(geojsonFeatures)
.join('text')
.attr('transform', d => `translate(${path.centroid(d)})`)
.text(d => d.properties.name);
计算边界框
// 返回 [[x0, y0], [x1, y1]],即左上角和右下角坐标
const bounds = path.bounds(geojsonFeature);
// 可用于动态设置画布大小
const width = bounds[1][0] - bounds[0][0];
const height = bounds[1][1] - bounds[0][1];
计算面积
// 返回投影后的像素面积(球面面积应使用 d3.geoArea)
const area = path.area(geojsonFeature);
绘制中国地图示例
下面是一个完整的绘制中国地图的示例,包含数据加载、投影设置、路径绘制和基本交互:
// 尺寸设置
const width = 800;
const height = 600;
// 创建 SVG
const svg = d3.select('#chart')
.append('svg')
.attr('width', width)
.attr('height', height);
// 创建投影(墨卡托投影)
const projection = d3.geoMercator()
.center([104, 36]) // 中国大致中心
.scale(550)
.translate([width / 2, height / 2]);
// 创建路径生成器
const path = d3.geoPath()
.projection(projection);
// 加载 GeoJSON 数据(假设已有中国省份边界数据)
async function drawChinaMap() {
// 加载 GeoJSON 数据
const china = await d3.json('china.json');
// 使用 fitSize 自动适配
projection.fitSize([width, height], china);
// 绘制省份边界
svg.selectAll('path')
.data(china.features)
.join('path')
.attr('d', path)
.attr('fill', '#f0f0f0')
.attr('stroke', '#999')
.attr('stroke-width', 0.5)
.on('mouseover', function(event, d) {
d3.select(this)
.attr('fill', '#ddd');
})
.on('mouseout', function() {
d3.select(this)
.attr('fill', '#f0f0f0');
});
// 添加省份名称标签
svg.selectAll('text')
.data(china.features)
.join('text')
.attr('transform', d => `translate(${path.centroid(d)})`)
.attr('text-anchor', 'middle')
.attr('font-size', '10px')
.text(d => d.properties.name);
}
drawChinaMap();
这段代码展示了地图绘制的基本流程:加载 GeoJSON 数据;创建投影并使用 fitSize 自动适配;使用路径生成器将地理数据转换为 SVG 路径;添加基本的交互效果。
统计地图(Choropleth Map)
统计地图是最常见的地图可视化形式之一,通过颜色深浅来表示各区域的数据值。常见的应用包括人口密度图、选举结果图、收入分布图等。
颜色比例尺
统计地图的关键是将数据值映射到颜色。我们需要创建一个颜色比例尺:
// 假设有人口数据
const populationData = {
'北京市': 2154,
'上海市': 2487,
'广东省': 12601,
// ...
};
// 获取数据范围
const values = Object.values(populationData);
const minVal = d3.min(values);
const maxVal = d3.max(values);
// 创建颜色比例尺
const colorScale = d3.scaleSequential()
.domain([minVal, maxVal])
.interpolator(d3.interpolateBlues); // 蓝色渐变
// 或者使用分位数比例尺
const quantileScale = d3.scaleQuantile()
.domain(values)
.range(['#f7fbff', '#c6dbef', '#6baed6', '#2171b5', '#08306b']);
完整统计地图示例
async function drawChoroplethMap() {
// 加载地图数据和统计数据
const [geoData, statsData] = await Promise.all([
d3.json('regions.geojson'),
d3.csv('population.csv', d3.autoType)
]);
// 创建数据查找映射
const dataMap = new Map(statsData.map(d => [d.region, d.value]));
// 创建颜色比例尺
const values = statsData.map(d => d.value);
const colorScale = d3.scaleQuantile()
.domain(values)
.range(['#feedde', '#fdbe85', '#fd8d3c', '#e6550d', '#a63603']);
// 创建投影和路径生成器
const projection = d3.geoMercator()
.fitSize([width, height], geoData);
const path = d3.geoPath().projection(projection);
// 绘制地图
svg.selectAll('path')
.data(geoData.features)
.join('path')
.attr('d', path)
.attr('fill', d => {
const value = dataMap.get(d.properties.name);
return value ? colorScale(value) : '#ccc';
})
.attr('stroke', 'white')
.attr('stroke-width', 0.5)
.on('mouseover', function(event, d) {
d3.select(this).attr('stroke-width', 2);
// 显示提示框
showTooltip(event, d);
})
.on('mouseout', function() {
d3.select(this).attr('stroke-width', 0.5);
hideTooltip();
});
// 添加图例
const legend = svg.append('g')
.attr('transform', 'translate(20, 20)');
const legendColors = colorScale.range();
const legendThresholds = colorScale.quantiles();
legendColors.forEach((color, i) => {
legend.append('rect')
.attr('y', i * 20)
.attr('width', 20)
.attr('height', 18)
.attr('fill', color);
legend.append('text')
.attr('x', 25)
.attr('y', i * 20 + 14)
.text(i === 0 ? `< ${legendThresholds[0]}` :
i === legendColors.length - 1 ? `> ${legendThresholds[i-1]}` :
`${legendThresholds[i-1]} - ${legendThresholds[i]}`)
.attr('font-size', '12px');
});
}
点地图
点地图用于标注特定地理位置,比如城市、门店、事件发生地等。
基本点地图
// 城市数据
const cities = [
{name: '北京', coords: [116.4074, 39.9042], population: 2154},
{name: '上海', coords: [121.4737, 31.2304], population: 2487},
{name: '广州', coords: [113.2644, 23.1291], population: 1530},
{name: '深圳', coords: [114.0579, 22.5431], population: 1756}
];
// 创建大小比例尺(人口映射为圆的半径)
const sizeScale = d3.scaleSqrt()
.domain([0, d3.max(cities, d => d.population)])
.range([3, 15]);
// 绘制点
svg.selectAll('circle')
.data(cities)
.join('circle')
.attr('cx', d => projection(d.coords)[0])
.attr('cy', d => projection(d.coords)[1])
.attr('r', d => sizeScale(d.population))
.attr('fill', 'steelblue')
.attr('fill-opacity', 0.6)
.attr('stroke', 'white');
点与标签
// 绘制点和标签
const pointGroup = svg.selectAll('.city')
.data(cities)
.join('g')
.attr('class', 'city')
.attr('transform', d => `translate(${projection(d.coords)})`);
pointGroup.append('circle')
.attr('r', d => sizeScale(d.population))
.attr('fill', 'steelblue')
.attr('fill-opacity', 0.6);
pointGroup.append('text')
.attr('y', -10)
.attr('text-anchor', 'middle')
.attr('font-size', '11px')
.text(d => d.name);
地图交互
地图交互能极大提升数据探索体验。常见交互包括缩放平移、悬停高亮、点击选中、区域筛选等。
缩放与平移
使用 d3-zoom 为地图添加缩放和平移功能:
// 创建缩放行为
const zoom = d3.zoom()
.scaleExtent([1, 8]) // 缩放范围
.on('zoom', function(event) {
// 应用变换到地图组
g.attr('transform', event.transform);
});
// 应用缩放到 SVG
svg.call(zoom);
// 重置按钮
d3.select('#reset').on('click', function() {
svg.transition()
.duration(750)
.call(zoom.transform, d3.zoomIdentity);
});
悬停高亮与提示框
// 创建提示框
const tooltip = d3.select('body')
.append('div')
.attr('class', 'tooltip')
.style('position', 'absolute')
.style('display', 'none')
.style('background', 'rgba(0,0,0,0.8)')
.style('color', '#fff')
.style('padding', '10px')
.style('border-radius', '4px');
svg.selectAll('path')
.on('mouseover', function(event, d) {
// 高亮当前区域
d3.select(this)
.attr('fill', 'orange')
.raise();
// 显示提示框
tooltip
.style('display', 'block')
.style('left', (event.pageX + 15) + 'px')
.style('top', (event.pageY - 10) + 'px')
.html(`<strong>${d.properties.name}</strong><br/>人口: ${d.population}万`);
})
.on('mousemove', function(event) {
tooltip
.style('left', (event.pageX + 15) + 'px')
.style('top', (event.pageY - 10) + 'px');
})
.on('mouseout', function() {
d3.select(this).attr('fill', originalColor);
tooltip.style('display', 'none');
});
TopoJSON 简介
TopoJSON 是 GeoJSON 的扩展格式,通过拓扑编码大幅减小文件体积。在 TopoJSON 中,共享边界的区域只存储一次,而不是在每个区域中重复存储。
// 需要引入 TopoJSON 库
// <script src="https://unpkg.com/topojson-client@3"></script>
async function drawTopoJsonMap() {
const world = await d3.json('world-110m.json');
// 将 TopoJSON 转换为 GeoJSON
const countries = topojson.feature(world, world.objects.countries);
projection.fitSize([width, height], countries);
svg.selectAll('path')
.data(countries.features)
.join('path')
.attr('d', path)
.attr('fill', 'lightgray')
.attr('stroke', 'white');
// 绘制边界线(使用 mesh 只绘制共享边界一次)
svg.append('path')
.datum(topojson.mesh(world, world.objects.countries, (a, b) => a !== b))
.attr('d', path)
.attr('fill', 'none')
.attr('stroke', 'white')
.attr('stroke-width', 0.5);
}
TopoJSON 的优势在于文件体积小(通常只有 GeoJSON 的 20% 左右),特别适合网络传输。但它需要额外的转换步骤。
球面数学工具
d3-geo 还提供了一些球面数学计算工具,用于计算距离、面积、方位角等。
大圆距离
计算地球上两点之间的最短距离:
// 返回弧度,需要乘以地球半径得到实际距离
const distance = d3.geoDistance([116.4, 39.9], [121.5, 31.2]);
// 地球平均半径约 6371 km
const km = distance * 6371;
console.log(`北京到上海约 ${km.toFixed(0)} 公里`);
大圆路径
绘制两点之间的最短路径(大圆弧):
// 创建大圆路径生成器
const greatCircle = d3.geoGreatCircle()
.source([116.4, 39.9])
.target([121.5, 31.2]);
// 生成 GeoJSON LineString
const lineString = greatCircle();
// 绘制路径
svg.append('path')
.datum(lineString)
.attr('d', path)
.attr('fill', 'none')
.attr('stroke', 'red')
.attr('stroke-width', 2);
球面面积
// 返回球面面积(球面度),乘以地球半径平方得到实际面积
const area = d3.geoArea(geojsonFeature);
const sqKm = area * 6371 * 6371;
球面包含判断
// 判断点是否在多边形内
const point = [116.4, 39.9];
const isInside = d3.geoContains(geojsonPolygon, point);
获取地图数据
创建地图可视化需要 GeoJSON 或 TopoJSON 格式的地理数据。以下是常用的数据来源:
Natural Earth:提供多种分辨率的世界地图数据,包括国家边界、省份边界、河流、湖泊等。网址:https://www.naturalearthdata.com/
阿里云 DataV:提供中国省、市、县各级边界数据,适合绘制中国地图。网址:http://datav.aliyun.com/portal/school/atlas/area_selector
GADM:全球行政区划数据库,提供详细的国家内部行政边界。网址:https://gadm.org/
GeoJSON.io:在线 GeoJSON 编辑器,可以手动绘制和编辑地理数据。网址:http://geojson.io/
总结
D3 的地理可视化能力强大而灵活。通过理解 GeoJSON 数据格式、掌握投影变换、熟练使用路径生成器,你可以创建从简单的世界地图到复杂的交互式统计地图等各种可视化效果。
地图可视化的关键点在于:选择合适的投影类型以适应展示区域和数据特点;使用 fitSize 自动调整投影以简化配置;合理设计颜色比例尺以准确传达数据信息;添加交互功能以增强用户体验。
虽然学习曲线较陡,但 D3 地图可视化的灵活性和定制能力是其他图表库难以企及的。一旦掌握了基本原理,你就可以创造出独一无二的可视化作品。