何大小成

D3 · 地图 · 可视化

最近都在学习D3和地图结合的东西,作为知识储备。于是自己就搞了一些小小栗子作为分享,很简单,所以高手可以略过啦哈哈。

需求: 通过一组json数据(比如是中国旅游业产值的数据(虚构)),我要把这些数据在地图上可视化显示,产值比较低的用浅颜色表示,产值高的则用深颜色表示。

实现的效果:

TopoJSON & GeoJSON 关系

TopoJSON是GeoJSON按拓扑学编码后的扩展形式,相比GeoJSON直接使用Polygon之类的几何体来表示图形的方法,TopoJSON中每个几何体都是将共享边(arcs)整合而成的。
对比一下
TopoJSON:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
"type": "Topology",
"transform": {
"scale": [0.0023415374016230355, 0.0018039985796827979],
"translate": [73.60225630700012, 18.19318268400005]
},
"objects": {
"china": {
"type": "GeometryCollection",
"geometries": [{
"arcs": [
[0, 1, 2, 3, 4, 5, 6]
],
"type": "Polygon",
"properties": {
"id": 1,
"name": "甘肃"
}
}]
}
}
}

GeoJSON:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"type": "FeatureCollection",
"features": [{
"type": "Feature",
"properties": {
"id": 1,
"name": "甘肃"
},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[104.35851932200904, 37.40123159456249],
[104.46450768428224, 37.440247301072134],
[104.68950687084538, 37.41192861571304]
...(省略)
]
]
}
}
}

用TopoJSON最大优点:文件缩小了80%,消除了冗余,也就是说TopoJSON只相当于GeoJSON的20大小。

D3的应用中尽可能使用TopoJSON。

所以在我们项目里,中国边界使用了TopoJSON格式的数据。

产值数据

这一部分数据虽然是虚构,但是这一部分就是我们平时从接口调用得来的数据。
部分内容如下:

1
2
3
4
5
6
7
8
9
{
"name": "中国",
"provinces":
[
{"name": "北京", "value": 14149 },
{"name": "天津", "value": 2226.41}
...
]
}

provinces就是每一个省份的值,name是省份名称,value是旅游业产值。

构建地图

重点来了。

d3.json请求数据

1
2
3
d3.json("tourism.json", function(error, valuedata){
// valuedata 就是读取到的数据
})

重组数据

将读取到的数据存到数组values,令其索引号为各省的名称。这里的作用就是:当我们为各省份填充色的时候可以循环读取。

1
2
3
4
5
6
7
8
var values = {};
for(var i=0; i<valuedata.provinces.length; i++){
var name = valuedata.provinces[i].name;
var value = valuedata.provinces[i].value;
values[name] = value;
}
console.log(values)
// {北京: 14149, 天津: 2226.41, 河北: 1544.94}

设定比例尺

1
2
3
4
5
6
7
//求产值的最大值和最小值,并作为比例尺定义域的最大值&最小值
var maxvalue = d3.max(valuedata.provinces, function(d){ return d.value; });
var minvalue = 0;
//定义一个线性比例尺,将最小值和最大值之间的值映射到[0, 1]
var linear = d3.scale.linear()
.domain([minvalue, maxvalue])
.range([0, 0.5]);

颜色

颜色插值函数以浅蓝色和深蓝色为边界,也就是说,省份的旅游产业值越大,蓝色越深。

1
2
3
4
5
//定义最小值和最大值对应的颜色
var a = d3.rgb(0,255,255); //浅蓝色
var b = d3.rgb(0,0,255); //蓝色
//颜色插值函数
var computeColor = d3.interpolate(a,b);

设定省份颜色

1
2
3
4
5
provinces.style("fill", function(d,i){
var t = linear( values[d.properties.name] );
var color = computeColor(t);
return color.toString();
});

到了这里,我们得到这样的地图:

标识

虽然我们知道颜色越深产值越高,但是用户不知道。所以,我们缺少了一个什么颜色对应什么数值的一个标识。可以采用:左下角添加一个矩形,颜色渐变,然后再加对应的数值。

svg渐变中渐变是<defs><linearGradient>结合。这里不做详细介绍,举个例子:
1. 定义在,给渐变定义id号。

1
2
3
4
5
6
<defs>
<linearGradient id="svg" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0" stop-color="#f00"></stop>
<stop offset="100%" stop-color="#000"></stop>
</linearGradient>
</defs>
  1. 使用
1
2
3
<svg width="400" height="399">
<rect fill="url(#svg)" x="10" y="10" width="100" height="100"/>
</svg>

那么用D3代码定义一个线性渐变如下:

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
//定义一个线性渐变
var defs = svg.append("defs");

var linearGradient = defs.append("linearGradient")
.attr("id","linearColor")
.attr("x1","0%")
.attr("y1","0%")
.attr("x2","100%")
.attr("y2","0%");

var stop1 = linearGradient.append("stop")
.attr("offset","0%")
.style("stop-color",a.toString());

var stop2 = linearGradient.append("stop")
.attr("offset","100%")
.style("stop-color",b.toString());

//添加一个矩形,并应用线性渐变
var colorRect = svg.append("rect")
.attr("x", 20)
.attr("y", 490)
.attr("width", 140)
.attr("height", 30)
.style("fill","url(#" + linearGradient.attr("id") + ")");

最后,在举行在添加文字:

1
2
3
4
5
6
7
8
9
10
11
12
//添加文字
var minValueText = svg.append("text")
.attr("x", 20)
.attr("y", 490)
.attr("dy", "-0.3em")
.text(() => minvalue);

var maxValueText = svg.append("text")
.attr("x", 160)
.attr("y", 490)
.attr("dy", "-0.3em")
.text(()=> maxvalue);

最后把代码整理:

See the Pen d3-map by 何大小成 (@hopkinson) on CodePen.