中国地图的鱼眼视图

通过使用鱼眼视图,在大尺度下被弱化的细节信息被显著强化,使得可视分析用户可以在大尺度下同时兼顾细节信息。

工具

  1. d3.js
  2. d3.geo.js
  3. d3-plugin fisheye.js
  4. china.json

实现代码

index.html

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>SvgEarthDemo</title>
</head>
<body>
<svg id="mysvg" width="3000" height="1500"></svg>
</body>
</html>

获取中国地图轮廓经纬度

china.json 是 GeoJson 格式的文件

1
2
3
4
5
function getChinaJson(){
let chinaText = $.ajax({url:"/json/china.json",async:false})
let chinaJson = decode(JSON.parse(chinaText.responseText))
return chinaJson
}

使用 d3.geo 在 svg 中绘制中国地图

d3.geoPath 绘制中国地图

使用 d3.geoPath 直接处理 GeoJson 对象,在 svg 中追加 path。
鱼眼畸变计算是针对节点的,在生成的 path 中很难找到具体的形状对象,该方式虽然简单易生成矢量地图但不适合进行鱼眼视图的转换。

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
// 设定投影
let projection = d3.geoMercator().center([70.591796875, 55.494140625]).scale(1500).translate([0,0]).rotate([20])

// 获取 GeoJson 对象
let chinaJson = getChinaJson()

// 设定路径生成器
let path = d3.geoPath().projection(projection)

// 计算经纬度网格
let graticule = d3.geoGraticule()
graticule.extent([[71,16],[137,54]]).step([2,2])
let grid = graticule()

// 经纬度网格和地图使用同一个路径生成器,会完全契合

// 绘制网格
svg.append("path")
.datum(grid)
.attr("class", "graticule")
.style("stroke", "#000")
.attr("d", path)

// 绘制中国
let svg = d3.select('#mysvg')
svg.append("g")
.selectAll("path")
.data(chinaJson.features)
.enter()
.append("path")
.attr("class", "province")
.attr("d", path)

svg line 绘制中国地图

通过解析 GeoJson 对象,获取各个点经纬度坐标,通过墨卡托投影转化成 svg 坐标,在 svg 中利用点连成线绘制 line。
在生成的 line 中可获取到起始位置 ( x1, y1, x2, y2 ) ,该方式虽然生成矢量地图较复杂但易于进行鱼眼视图的转换。

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
// 设定投影
let projection = d3.geoMercator().center([70.591796875, 55.494140625]).scale(1500).translate([0,0]).rotate([20])

// 获取 GeoJson 对象
let chinaJson = getChinaJson()

// 将经纬度转化为 svg 二维坐标
let vector2ChinaJson = getVector2ChinaJson()

// 将经纬度转化为 svg 二维坐标,并以 GeoJson 对象原格式输出
function getVector2ChinaJson(){
let data = []
chinaJson.features.forEach(areas => {
var areasData = {
properties: [],
coordinates: []
}
areasData.properties = areas.properties
areas.geometry.coordinates.forEach((points,i)=>{
var arr = []
points.forEach((point,j)=>{
if(point[0] instanceof Array){
var a = []
point.forEach(p=>{
a.push(lnglatToMercator(p))
})
arr.push(a)
}else{
arr.push(lnglatToMercator(point))
}
})
areasData.coordinates.push(arr)
})
data.push(areasData)
})
return data
}

// 墨卡托转化
function lnglatToMercator(point){
var points = []
var point = [point]
var point = projection(point)
console.log()
points.push(point)
// 返回二维坐标数组
return points
}

// 绘制中国地图
function drawChina(){
svg.append("g")
for(let j in vector2ChinaJson){
if(vector2ChinaJson[j].coordinates[0][0][0] instanceof Array){
vector2ChinaJson[j].coordinates.forEach( points =>{
for(let index = 0; index < points[0].length-1; index++){
svg.select("g")
.append("line")
.attr("x1", points[0][index][0])
.attr("y1", points[0][index][1])
.attr("x2", points[0][index+1][0])
.attr("y2", points[0][index+1][1])
.attr("stroke", "#000")
}
})
}else{
vector2ChinaJson[j].coordinates.forEach( points =>{
for(let index = 0; index < points.length-1; index++){
svg.select("g")
.append("line")
.attr("x1", points[index][0])
.attr("y1", points[index][1])
.attr("x2", points[index+1][0])
.attr("y2", points[index+1][1])
.attr("stroke", "#000")
}
})
}
}
}

绑定鼠标滑动事件,更新鱼眼视图

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
// 鱼眼视图半径
let r = 150

// 鱼眼对象,distortion 为畸变系数
let fisheye = d3.fisheye.circular().radius(r).distortion(10)

// 当前焦点的鱼眼坐标
let f = null

// 防抖定时器
var timeObj = null

// 鱼眼坐标计算flag,false 表示当前未处于鱼眼视图,true 表示当前当前处于鱼眼视图
var changeFlag = false

// 给 svg 绑定鼠标移动事件
svg.on("mousemove", function(event){
// 当前处于计算鱼眼视图
if(changeFlag){
// 获取所有已计算为鱼眼视图下的 line 对象 (class = isChange),遍历所有对象,将每个 line ( x1, y1, x2, y2 ) 恢复初始坐标
// 即获取中国初始地图
d3.selectAll(".isChange")
.call(function (d) {
d._groups[0].forEach((line,j)=>{
let lineObj = d3.select(line)
lineObj.attr("x1", lineObj._groups[0][0].x1old)
.attr("y1", lineObj._groups[0][0].y1old)
.attr("x2", lineObj._groups[0][0].x2old)
.attr("y2", lineObj._groups[0][0].y2old)
.attr("class", "")
})
})
}
// 如果防抖定时器存在则清除定时器
if(timeObj){
clearTimeout(timeObj)
}
// 设置防抖定时器, 200ms 后计算鱼眼视图的坐标
timeObj = setTimeout(() => {
updateLines(event)
}, 200)
})

// 获取鱼眼视图内的 line 对象,并计算畸变后的坐标,再更新这些 line 对象的 ( x1, y1, x2, y2 )
function updateLines(event){
// d3.pointer( event, this ) 返回值为当前鼠标焦点 [ x, y ] 位置
let pointer = d3.pointer(event)
// 鱼眼聚焦于当前鼠标焦点
fisheye.focus(pointer)
// 返回当前焦点的鱼眼视图坐标
f = fisheye({x: pointer[0], y: pointer[1]})
// 在 svg 中遍历所有 line 对象
svg.selectAll("line")
.call(function (d) {
d._groups[0].forEach((line,j)=>{
// 获取每个 line 对象的 ( x1, y1, x2, y2 )
x1 = line.x1.animVal.value
y1 = line.y1.animVal.value
x2 = line.x2.animVal.value
y2 = line.y2.animVal.value
// 判断 line 对象的起点和终点是否有一个在鱼眼视图范围中,只要有一个在范围中就需要计算鱼眼畸变后的坐标并更新给 line 对象
if(((Math.pow(x1-f.x, 2)+Math.pow(y1-f.y, 2))<Math.pow(r, 2))||((Math.pow(x2-f.x, 2)+Math.pow(y2-f.y, 2))<Math.pow(r, 2))){
// 获取 line 起点畸变后坐标
fisheye1 = fisheye({x: x1, y: y1})
// 获取 line 终点畸变后坐标
fisheye2 = fisheye({x: x2, y: y2})
// 获取 line 对象,保存原始坐标值,更新畸变坐标值,为处于鱼眼视图中的 line 对象设置 class = isChange
d3.select(line)
.property("x1old", x1)
.property("y1old", y1)
.property("x2old", x2)
.property("y2old", y2)
.attr("x1", fisheye1.x)
.attr("y1", fisheye1.y)
.attr("x2", fisheye2.x)
.attr("y2", fisheye2.y)
.attr("class", "isChange")
// 当前视图为鱼眼视图,changeFlag 改为 true
changeFlag = true
}
})
})
}

添加缓动效果

1
2
3
4
5
6
7
8
9
// 在改变坐标之前添加缓动效果,动画执行时间为 300ms 
// 原始坐标改为畸变后的坐标,从畸变后的坐标改为原始坐标,都需要执行缓动动画
lineObj.attr("class", "")
.transition()
.duration(300)
.attr("x1", lineObj._groups[0][0].x1old)
.attr("y1", lineObj._groups[0][0].y1old)
.attr("x2", lineObj._groups[0][0].x2old)
.attr("y2", lineObj._groups[0][0].y2old)

注意事项

  • 避免大量的遍历
  • mouseover 和 mousemove 的区分