前言
本文主要记录如何用d3.js绘制地图,以及如何对地图进行缩放。理清楚大致的流程及步骤之后,其实并不是特别难。
大致的流程图如下:

接下来按如下顺序展开本文:
- 什么是GeoJSON
- 绘制地图
- 缩放监听
本文使用d3版本:v5.9.2
什么是GeoJSON
首先介绍GeoJSON,它是我们地图数据的来源。
直接看看来自维基百科的介绍
GeoJSON is an open standard format designed for representing simple geographical features, along with their non-spatial attributes. It is based on the JavaScript Object Notation (JSON).
The features include points (therefore addresses and locations), line strings (therefore streets, highways and boundaries), polygons (countries, provinces, tracts of land), and multi-part collections of these types. GeoJSON features need not represent entities of the physical world only; mobile routing and navigation apps, for example, might describe their service coverage using GeoJSON.
从中我们可以提取出如下关键点:
- 基于JSON格式
- 通过features保存地图点、线、多边形的绘制信息
维基百科中给了一个例子如下(截取部分):
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [102.0, 0.5]
},
"properties": {
"prop0": "value0"
}
},
{
"type": "Feature",
"geometry": {
"type": "LineString",
"coordinates": [
[102.0, 0.0], [103.0, 1.0], [104.0, 0.0], [105.0, 1.0]
]
},
"properties": {
"prop0": "value0",
"prop1": 0.0
}
},
......
可以看出,features数组中存储了绘制信息,这些绘制信息是如何绘图的呢?维基百科中也给出了相关示例,即通过type控制绘制类型,coordinates控制点、路径、形状。

这只是部分例子,感兴趣的同学直接访问维基百科吧。
综上,我们知道了什么GeoJSON。
获取GeoJSON
如何获取GeoJSON呢,使用搜索引擎可以查询到非常多的方法。这里国内地图数据可以使用阿里DATAV的地图选择器
本文中选择北京市地图,下载geojson

绘制地图
现在我们可以开始绘制地图了。为了方便学习,不发各种请求和跨域问题的解决,我们直接把GeoJSON中的内容复制出来到单独的js文件中设置变量。
//BeijingGeo.js
const BeijingGEoJson = {
type: "FeatureCollection",
features: [
{
type: "Feature",
properties: {
adcode: 110101,
name: "东城区",
center: [116.418757, 39.917544],
......
}
接下来就可以正式开始了
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8'>
<title></title>
<script src="../d3_library/d3.js"></script>
<!-- 将BeijingGeoJson文件以js对象放在js文件中,输出变量为BeijingGeoJson -->
<script src="../source/js/BeijingGeo.js"></script>
</head>
<body>
<svg class="chart">
</svg>
</body>
<script>
/**
* 基本配置
*/
const svgWidth = 500;
const svgHeight = 500;
const padding = 30;
const svg = d3.select(".chart")
.attr("height", svgHeight)
.attr("width", svgWidth);
</script>
</html>
投影(Projection)设置
投影把球形多面图形转换为平面多面图形,比如地球投影成地图。
Projections transform spherical polygonal geometry to planar polygonal geometry.——d3 Projections
d3中提供了如下几类投影:
- Azimuthal(方位)
- Composite(复合)
- Conic(圆锥)
- Cylindrical(圆柱)
本文中我们使用墨卡托(Mercator)投影,它是我们日常生活中使用最为广泛的投影,平时看到的百度地图,谷歌地图等都是使用墨卡托投影。
墨卡托投影是一种正轴等角圆柱投影,在d3中的api为d3.geoMercator()
现在我们开始获取投影并配置
const projection = d3.geoMercator() //墨卡托投影
.center([someValue, someValue]) //链式写法,.center([longitude, latitude])设置地图中心
.scale([someValue]) //.scale([value])设置地图缩放
.translate([someValue, someValue]) //.translate([x,y])设置偏移
但是由于不同的GeoJSON数据,配置此投影是相对麻烦的。幸运的是,d3提供了projection.fitExtent API,我们只需传入左上角坐标点,右下角坐标点和GeoJSON,它就能把地图绘制在给定画布的中心。
const x0 = padding;
const y0 = padding;
const x1 = svgWidth - padding * 2;
const y1 = svgHeight - padding * 2;
const projection = d3.geoMercator().fitExtent(
[
[x0, y0],
[x1, y1],
], BeijingGeoJson);
地理路径生成函数(geographic path generator)
获取地理路径生成函数非常简单,只需一行代码就可搞定
const pathGenerator = d3.geoPath()
.projection(projection); //配置上投影
数据绑定之后,我们就会使用此函数绘制svg中的path元素,勾勒出地图图形
数据绑定,使用地理路径生成函数绘制地图
接下来来到绘图的一步了,svg中的地图是用path元素勾勒的,我们
const mapPath = svg.selectAll("path")
.data(BeijingGeoJson.features) //数据绑定
.join("path")
.attr("d", pathGenerator) //绘制path
.attr("stroke-width", 0.5)
.attr("stroke", "#000000")
.attr("fill", "#ffffff");
至此,就已经绘制出基本的地图了。

本文不仅仅绘制地图,接下来我们顺便学习下缩放、拖拽。
缩放、拖拽地图
我们只需在基础版本代码基础之上添加zoom行为就可以了,代码如下
function zoomed() {
const t = d3.event.transform;
svg.attr("transform", `translate(${t.x}, ${t.y}) scale(${t.k})`);
}
const zoom = d3.zoom()
.scaleExtent([1, 3]) //设置监听范围
.on("zoom", zoomed); //设置监听事件
svg.call(zoom); //应用
至此我们可以进行缩放及平移。

但是,在缩放比例为1的时候,进行拖拽平移会发现一个非常怪异的行为,即出现抖动(这里可能因为gif帧数较底看不出来,感兴趣的同学可以下载此处github代码尝试:地图拖拽——“抖动版”)。

问题解决
为什么会造成抖动呢?原因是元素(如此例为svg元素)监听事件触发时,zoom会记录点击的鼠标位置,而拖拽时又改变了被点击元素位置,因此移动时会造成抖动。
如下gif,我们可以看见svg的位置一直在改变!

那如何解决问题呢,非常简单,我们只要使得被监听元素位置不改变就可以了(此例中为svg元素)
我们在原来的svg中插入<g>标签,把地图的path路径放在<g>标签中,事件仍然挂载在svg上,而拖拽、缩放时应用于<g>元素即可,这样被监听元素的位置就不会改变了。
const mapContainer = svg.append("g"); //添加mapContainer装载地图绘制内容
//...
//...
const mapPath = mapContainer.selectAll("path")
.data(BeijingGeoJson.features) //数据绑定
.join("path")
//...
//...
function zoomed() {
const t = d3.event.transform;
mapContainer.attr("transform", `translate(${t.x}, ${t.y}) scale(${t.k})`); //改变mapContainer的位置及缩放
}
//...
//...
svg.call(zoom)

至此,就不会出现抖动问题了。GitHub地址如下:不会抖动的地图拖拽
结语
此文记录了学习绘制地图的步骤,也是最基本的步骤,但是掌握此步骤及流程后要添加其他操作,诸如点击事件,path路径上色,标签添加等等非常简单,同基本的d3操作一致。
若有不足之处,还请大佬指教。
Demo的三个GitHub地址:
参考资料
地图绘制
Making a map using D3.js
d3官网——d3-geo
StackOverflow Center a map in d3 given a geoJSON object
d3官网——projection.fitExtent(extent, object) <>
d3官网——d3.geoPath
path.centroid(可以根据feature计算paprojected planar)
D3制作地球(二) —— Projection的分类
知乎——地图常用的投影方法有哪些?
Command-Line Cartography, Part 1
第十五课 地图——十二月咖啡馆
zoom
d3.behavior.zoom jitters, shakes, jumps, and bounces when dragging
understanding zoom behavior bindings
D3 force layout: making pan on drag (zoom) smoother