0%

使用d3绘制中国地图

最近在使用d3和threejs结合来做些3D特效,对D3不是很了解,所以通过这个用D3绘制中国地图并标点的例子来加深下对D3的理解。

准备

我使用的是目前最新版的D3v5.7.0。除过D3,我们还需要给地图着色,我们使用D3的库:d3-scale-chromatic

我们是通过svg来绘制地图的,需要一个中国地理信息的geojson文件,关于geojson文件规范参考:geojson,可以去Natural Earth上下载,这里我们使用现成的中国🇨🇳geojson文件:查看json文件

在地图绘制出来后,我们需要给一些城市标点,所以还需要整理一些城市的经纬度。

因为定位用到经纬度的转换,所以我们还需要了解下经纬度的知识。

  • 经线 地球上的一个点离本初子午线的南北方向走线以西(西经)或以东(东经)的度数。本初子午线的经度是0°,地球上其他地点的经度是向东到180°或向西180°。东经180°即西经180°,月等同于国际日期变更线,国际日期变更线的两边,日期相差一日。
  • 纬度 地球上的一个点与地球球心的连线和地球赤道面所成的线面角。其数值在[0, 90]之间,位于赤道以北的叫做北纬,记做N;位于赤道以南的叫做南纬,记做S。

东经为正数,西经为负数;北纬为正数,南纬为负数(上北下南,左西右东,东北为正,西南为负)。

开始

我是在angular项目里面开发的,所以只记录多出的步骤。

安装类库

安装d3.js

1
npm i d3 --save

安装d3-scale-chromatic

1
npm i d3-scale-chromatic --save

安装d3-geo(从球体到平面的投影):

1
npm i d3-geo --save

安装d3-array(d3-geo依赖):

1
npm i d3-array --save

页面元素

由于使用的angular框架,动态插入元素在组件里面会有问题,所以直接将元素放在页面上。我们需要绘制地图,然后当鼠标移上去的时候出现一个tooltips提示,所以我们的html为这样:

1
2
3
4
<div id="world">
<svg id="svg" #svg></svg>
<div id="tooltip"></div>
</div>

对应的css文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#world {
width: 100%;
height: 100%;
background: #888888;
overflow: hidden;
#svg {
width: 1024px;
height: 600px;
margin: 0 auto;
display: block;
}
#tooltip {
opacity: 0;
position: absolute;
padding: 10px;
background: #333333;
border: 2px solid #e8e8e8;
color: #33cc99;
font-size: 14px;
border-radius: 4px;
}
}

js准备

导入类库:

1
2
3
4
import * as THREE from 'three';
import * as d3 from 'd3';
import * as geo from 'd3-geo';
import * as d3Color from 'd3-scale-chromatic';

定义绘制的svg的大小:

1
2
const width = 1024;
const height = 600;

设置投影函数:

1
2
3
4
const projection = geo.geoMercator()
.scale(550)
.center([105, 38])
.translate([width / 2, height / 2]);

我们这里通过d3-geo的“墨卡托投影”来创建了一个投影方法,用来将球体投影到平面上。按行看看投影的配置:

  1. .scale(550) 投影的比例因子,可以按比例放大投影。
  2. .center([105, 38]) 将中心点设置为经度105,纬度38,这里正好是中国地图的中心点。
  3. .translate([width / 2, height / 2]) 将投影的中心设置为svg的中心。

综合一下,上述的设置可以保证我们将中国地图的投影正好投影在svg的中心,并且放大一定倍率。

绘制地图

前面我们准备好了基本页面框架,也设置好了投影的配置,现在可以用svg来绘制地图了

创建路径生成器

创建地理路径生成器,使用当前设置的投影:

1
const path = geo.geoPath(projection);

创建颜色比例尺

1
const colors = d3.scaleOrdinal(d3Color.schemeBrBG[11]);

加载地图json并生成地图

首先来加载json文件,我们使用d3内置的json请求方法:

1
2
3
4
async getJson() {
const data = await d3.json('/assets/data/china.geo.json');
return data;
}

读取svg元素,并设置svg的宽高:

1
2
3
const svg = d3.select('#svg')
.attr('width', width)
.attr('height', height);

这里使用异步方法,可以减少回调嵌套

1
2
3
4
5
6
7
8
9
10
11
12
this.getJson().then(data => {
svg.selectAll('path')
.data(data.features)
.enter()
.append('path')
.attr('d', path)
.attr('fill', function (d, i) {
return colors(i);
})
.attr('stroke', 'rgba(255, 255, 255, 1')
.attr('stroke-width', 1);
});

逐行来分析代码:

  1. .selectAll('path') 选中svg中所有匹配path的元素节点
  2. .data(data.features) 绑定当前选择器和数据。data的操作是“update”,表明选择的dom元素已经和数据进行了绑定
  3. .enter() 返回输入(enter)选择:当前选择中存在,但是当前dom元素中还不存在的每个数据元素的占位符节点。此方法只在由data()方法返回的更新选择中定义。此外,输入选择(enter)只定义了appendinsertselectcall操作符
  4. .append('path') 在当前选择的每个元素最后追加具有指定名称的新元素,返回包含追加元素的新选择
  5. .attr('d', path) 为所有选中元素设置名称为”d”的属性,值为path里面的每个值。即给svg添加的path元素设置d属性,d属性的值是需要绘制的路径。
  6. .attr('fill', function(d, i) {return colors(i)}) 为前面设置的svg的path元素填充颜色,颜色是从前面的设置的颜色比例尺中读取。
  7. .attr('stroke', 'rgba(255, 255, 255, 1')) 和上面一样,为svg的path元素根据路径描边,颜色为白色。
  8. .attr('stroke-width', 1) 同上,为svg的path元素设置路径描边的宽度。
    具体d3的选择器方法可以参考这个wiki

综合一下:从json中得到地图的路径,然后挨个添加到svg中的path标签,然后填充地图,并给路径描边。运行一下,可以看到已经生成了中国地图:
china-map
nice~😆

ps:发现颜色比例尺不是很够,导致有些地方有重复色块,目前没什么解决方案。写了个随机生成颜色的方案,刷新一下都不一样,哈哈哈~

1
return '#' + (Math.floor(Math.random() * 0xFFFFFF)).toString(16);

定位城市坐标

我们需要通过已知的城市的经纬度来在地图上定点。经纬度是球面的描述,而我们的地图是二维平面,我们需要有个转换的方法,这就要用到前面定义的墨卡托投影方法了。转换方法:

1
2
// log 经度,lat 纬度
const coor = projection([log, lat]);

城市坐标json

偷懒一点,这里没请求json文件,而是直接定义好的经纬度数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const places = [
{
'name': '北京',
'log': '116.3',
'lat': '39.9'
},
{
'name': '上海',
'log': '121.4',
'lat': '31.2'
},
{
'name': '深圳',
'log': '113',
'lat': '22'
}
];

标点并画圆

通过转换的坐标来给svg添加g元素进行定点:

1
2
3
4
5
6
7
8
9
const location = svg.selectAll('.location')
.data(places)
.enter()
.append('g')
.attr('class', 'location')
.attr('transform', (d) => {
const coor = projection([d.log, d.lat]);
return 'translate(' + coor[0] + ',' + coor[1] + ')';
});

通过定的点给svg的g元素添加circle元素,并填充颜色画圆。

1
2
3
4
location.append('circle')
.attr('r', 4)
.attr('fill', '#e91e63')
.attr('class', 'location');

添加鼠标互动

获得tooltip的dom节点:

1
const tooltip = d3.select('#tooltip');

给svg的g标签添加鼠标效果,鼠标一上去出现tooltip文字,并将圆圈放大二倍,且伴随着延时动画;鼠标移走也是同样相反的动画。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
location.on('mouseover', function (d) {
tooltip.html('当前城市:' + d.name)
.style('left', d3.event.pageX + 20 + 'px')
.style('top', d3.event.pageY + 20 + 'px')
.style('opacity', 1);
d3.select(this).select('circle').transition()
.duration(150)
.attr('r', 8);
}).on('mouseout', function () {
tooltip.style('opacity', 0);
d3.select(this)
.select('circle')
.transition()
.duration(150)
.attr('r', 4);
});

ok,完成~


点击查看源代码
点击查看demo

码字辛苦,打赏个咖啡☕️可好?💘