使用d3创建地图以及点击实现下钻动效

效果预览

点击空白页面后退

主要涉及到的d3 api

由于d3.js的api众多,本文只介绍这次涉及到的api,推荐阅读官方文档,本文使用的是v5版本,相比之前版本api调用发生了较大的变化且增加了promise回调

d3-selection

Selections 允许强大的数据驱动文档对象模型 (DOM): 设置 attributes, styles, properties, HTML 或 text 内容等等。使用 data join 的 enter 和 exit 选择集可以用来根据具体的数据 add 或 remove 元素。

1
2
3
d3.selectAll("p").on("click", function() {
d3.select(this).style("color", "red")
})

Geographies (d3-geo)

d3-geo创建地理投影,绘制地图一般使用球面墨卡托投影,d3.geoMercator()

Projections

Projections用于将球形多边形几何形状转换为平面多边形几何形状,就是将球体坐标转换为笛卡尔坐标。

Path

地理路径生成器d3.geoPath:给定GeoJSON几何数据或要素对象后,它会生成SVG路径数据字符串或将路径呈现到Canvas。
例如,如果我们想要显示地图细节,我们可以定义如下所示的路径。

1
2
3
4
const projection = d3.geoMercator() // 首先生成投影
const path = d3.geoPath().projection(projection) // 绘制地理路径
svg.append("path")
.attr("d", path(jsonData)) // jsonData为地图数据

Transitions (d3-transition)

transition 是一个类 selection 的接口,用来对DOM进行动画修改。这种修改不是立即修改,而是在规定的事件内平滑过渡到目标状态。
应用过渡,首先要选中元素,然后调用selection.transition,并且设置期望的改变,例如:

1
2
3
d3.select("body")
.transition()
.style("background-color", "red")

Dispatches (d3-dispatch)

Dispatching是一个用来降低代码耦合度的便捷方式: 注册回调函数然后使用任意参数调用.很多D3组件比如d3-request,通过这种机制触发事件监听器。与 Nodejs 的 EventEmitter 类似,只不过每个监听器都有确定的名字以方便移除或替换。
例如为 start 和 end 事件创建分发:

1
var dispatch = d3.dispatch("start", "end")

然后使用 dispatch.on 注册回调函数:

1
2
3
dispatch.on("start", callback1)
dispatch.on("start.foo", callback2)
dispatch.on("end", callback3)

然后可以通过 dispatch.call 或者 dispatch.apply 来调用所有的 start 回调:

1
2
dispatch.call('start')
dispatch.call("end", {about: "I am a context object"}, "I am an argument")

d3-scale

Scales是将数据通过编码转换为视觉映射的数据,就是比例尺。
比例尺的作用有很多,比如颜色如何按照数据大小进行层次处理,比如地图的数据比例如何在一个长度固定的距离上进行映射等等…

使用scaleLinear创建一个比例尺,domain来固定输入范围, range是输出范围,数据可以是离散也可以是连续的。

1
scaleLinear().domain([min, max]).range(['red', 'blue']) // 数据由小 -> 大映射为红 -> 蓝

实现方法

1
<div class="map-content"></div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
      // 定义常量
const color = {
normalFill: '#7494ef',
hoverFill: '#ffbc46',
stroke: '#d5f2ff',
text: '#fff'
}
const svgId = 'mapSvg'
let path

// 绑定派发事件 监听窗口/下钻/鼠标事件
const dispatch = d3.dispatch('resize', 'drillDown', 'hover')
dispatch.on('resize', (res, dom) => {
window.addEventListener('resize', () => {
console.log('triggerResize', res.properties.name)
drawMap(res, dom)
})
})
dispatch.on('drillDown', dom => {
d3.select(dom)
.selectAll('path')
.on('click', function(d) {
const { name, level } = d.properties
if (level === 'district') return
const centroid = path.centroid(d)
const width = document.querySelector('.map-content').clientWidth
const height = document.querySelector('.map-content').clientHeight
d3.select('#' + svgId)
.transition()
.duration(500)
.ease(d3.easeLinear)
.style(
'transform',
` scale(5) translate(${width / 2 - centroid[0]}px,${height / 2 -
centroid[1]}px)`
)
.style('opacity', 0.1)
setTimeout(() => {
d3.json(`./map/${name}.json`).then(res => {
drawMap(res, dom)
})
}, 300)
})
document.querySelector('#' + svgId).addEventListener('click', e => {
if (e.target.tagName == 'svg') {
d3.json('./map/江苏省.json').then(res => {
drawMap(res, '.map-content')
})
}
})
})
dispatch.on('hover', (e) => {
d3.selectAll('path').on('mouseover', function(d) {
d3.select(this).attr('fill', color.hoverFill)
})
d3.selectAll('path').on('mouseout', function(d) {
d3.select(this).attr('fill', color.normalFill)
})
d3.selectAll('path').on('mousemove', function(d) {
console.log(d3.mouse(this))
})
})
// 绘制地图方法
function drawMap(res, dom) {
const width = document.querySelector('.map-content').clientWidth
const height = document.querySelector('.map-content').clientHeight
d3.select('#' + svgId).remove() // 下钻删除之前的地图dom
const projection = d3.geoMercator()
projection.fitExtent( // 找到地图中心点 并且保留20边距
[
[20, 20],
[width - 20, height - 20]
],
res
)
path = d3.geoPath().projection(projection)
const mapG = d3
.select(dom)
.append('svg')
.attr('id', svgId)
.attr('width', width)
.attr('height', height)
.style('transform', 'scale(2)')
.style('opacity', 0.1)
mapG
.selectAll('path')
.data(res.features, d => {
d.data = {
value: d3.randomUniform(1,10000)()
}
})
.enter()
.append('path')
.data(res.features)
.attr('stroke', color.stroke)
.attr('stroke-width', 1)
.attr('fill', color.normalFill)
.attr('d', path)
.attr('cursor', 'pointer')

mapG
.selectAll('text')
.data(res.features)
.enter()
.append('text')
.text(d => {
return d.properties.name
})
.style('transform', d => {
const centroid = path.centroid(d)
return `translate(${centroid[0] -
(d.properties.name.length / 2) * 14}px, ${centroid[1]}px)`
})
.attr('fill', color.text)
.attr('cursor', 'pointer')
.attr('font-size', '12px')

mapG
.transition()
.duration(300)
.ease(d3.easeLinear)
.style('transform', 'scale(1)')
.style('opacity', 1)

dispatch.call('hover')
dispatch.call('drillDown', this, '.map-content')
}
// 初始化数据
d3.json('./map/江苏省.json').then(res => {
drawMap(res, '.map-content')
dispatch.apply('resize', this, [res, '.map-content'])
})