一、JSON与GeoJSON
GeoJSON 是基于 JSON 的、 为 Web 应用而编码地理数据的一个标准。实际上,GeoJSON 并不是另一种格式, 而只是 JSON 非常特定的一种使用方法。
var w = 500;
var h = 300;
//Define path generator, using the Albers USA projection
var path = d3.geoPath()
.projection(d3.geoAlbersUsa());
//Create SVG element
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
//Load in GeoJSON data
d3.json("us-states.json", function (json) {
//Bind data and create one path per GeoJSON feature
svg.selectAll("path")
.data(json.features)
.enter()
.append("path")
.attr("d", path);
});
- 美国部分地图json数据(网上可下载)
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"id": "01",
"properties": {
"name": "Alabama"
},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
-87.359296,
35.00118
],
[
-85.606675,
34.984749
],
[
-85.431413,
34.124869
],
[
-85.184951,
32.859696
],
...
d3.json() 接受两个参数。第一个是要加载的文件的路径,第二个是在加载并解析 完 JSON 文件后执行的回调函数。
d3.json("us-states.json", function(json) {
//Bind data and create one path per GeoJSON feature
svg.selectAll("path")
.data(json.features)
.enter()
.append("path")
.attr("d", path);
});
d3.json() 与 d3.csv() 类似,都是异步执行的函数。换句话说, 浏览器在等待外部文件加载时,其他代码照样执行,不会受影响。因此,位于回调 函数后面的代码有可能先于回调函数本身执行
path.png最后,我们把GeoJSON的地理特征绑定到新创建的path元素,为每个特征值创建一个path
现在的问题是,这张地图并没有覆盖美国全境。要纠正这个问题,需要 修改我们使用的投影(projection)。所谓投影,就是一种折中算法,一种把 3D 空间“投影”到 2D 平面的方法。
- 定义D3投影
var projection = d3.geoAlbersUsa()
.translate([w / 2, h / 2])
现在唯一要修改的,就是明确告诉路径生成器,应该使用这个自定义的投影来生成所有路径
var path = d3.geoPath()
.projection(projection);
投影.png
同样的GeoJSON数据,添加一个scale()方法,但现在把投影居中了
var projection = d3.geoAlbersUsa()
.translate([w / 2, h / 2])
.scale([500]);
居中.png
修改填充颜色.png再添加一个style()语句,修改一下路径填充设置的颜色
二、等值区域
不同区域填充了不同值(深或浅)或颜色,以反映关联数据值的地图
首先,创建一个比例尺,将数据值作为输入,返回不同的颜色。这是等值区域地图的核心所在:
var color = d3.scaleQuantize() .range(["rgb(237,248,233)","rgb(186,228,179)","rgb(116,196,118)","rgb(49,163,84)","rgb(0,109,44)"]);
等值区域以量化的比例尺函数作为线性比例尺,但比例尺输出的则是离散的范围。这里输出的值可以是数值、颜色这里就是),或者其他你需要的值。这个比例尺适合把值分类为不同的组(bucket)。我们这里只分了 5 个组,实际上你想分几个就分几个。
这里用到了数据
us-ag-productivity-2004.csv
state,value
Alabama,1.1791
Arkansas,1.3705
Arizona,1.3847
California,1.7979
Colorado,1.0325
Connecticut,1.3209
Delaware,1.4345
...
这些数据来自美国农业部,报告内容是 2004 年每个州的农业生产力指标。
要加载这些数据,使用D3.csv(),在回调函数中,要设置彩色的量化比例尺的输入值域
d3.csv("us-ag-productivity.csv", function (data) {
//在回调函数中,要设置彩色的量化比例尺的输入值域
color.domain([
d3.min(data, function (d) { return d.value; }),
d3.max(data, function (d) { return d.value; })
]);
这里用到了 d3.min() 和 d3.max() 来计算并返回最小和最大的数据值,因此这个比例尺的输出值域是动态计算的。
接下来,跟前面一样,加载 JSON 地理数据。但不同的是,在这里我想把农业生产力的数据合并到 GeoJSON 中。为什么?因为我们一次只能给元素绑定一组数据。 GeoJSON 数据肯定必不可少,因为要据以生成路径,而我们还需要新的农业生产力数据。为此,就要把它们混合成一个巨大的数组,然后再把混合后的数据绑定到新创建的 path 元素。
d3.json("us-states.json", function (json) {
//混合农业生产力数据和GeoJSON
//循环农业生产力数据集中的每个值
for (var i = 0; i < data.length; i++) {
//取得州名
var dataState = data[i].state;
//取得数据值,并从字符串转换成浮点数
var dataValue = parseFloat(data[i].value);
//在GeoJSON中找到相应的州
for (var j = 0; j < json.features.length; j++) {
var jsonState = json.features[j].properties.name;
if (dataState == jsonState) {
//将数据复制到JSON中
json.features[j].properties.value = dataValue;
break;
}
}
}
遍历取得州的数据值,把它放到
json.features[j].properties.value
中,保证它能被绑定到元素,并在将来需要时可以被取出来。
最后,像以前一样创建路径,只是通过 style() 要设置动态的值,如果数据不存在,则设置成默认的浅灰色。
svg.selectAll("path")
.data(json.features)
.enter()
.append("path")
.attr("d", path)
.style("fill", function (d) {
//Get data value
var value = d.properties.value;
if (value) {
//If value exists…
return color(value);
} else {
//If value is undefined…
return "#ccc";
}
});
三、添加定位点
能够看到在那些生产力最高(或最低)的州有多少大城市会很有意思,也更有价值。我们只想得到最大的
50 个城市的数据,所以就把其他城市的数据都删掉了。再导出为 CSV,就有了以下数据:
rank,place,population,lat,lon
1,New York,8550405,40.71455,-74.007124
2,Los Angeles,3971883,34.05349,-118.245319
3,Chicago,2720546,41.88415,-87.632409
4,Houston,2296224,29.76045,-95.369784
5,Philadelphia,1567442,39.95228,-75.162454
6,Phoenix,1563025,33.44826,-112.075774
7,San Antonio,1469845,29.42449,-98.494619
8,San Diego,1394928,32.715695,-117.161719
9,Dallas,1300092,32.778155,-96.795404
10,San Jose,1026908,37.338475,-121.885794
……
在这个回调函数内部,可以用代码表达怎么用新创建的 circle 元素代表每个城市。 然后,根据各自的地理坐标,将它们定位到地图上:
d3.csv("us-cities.csv", function (data) {
svg.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("cx", function (d) {
return projection([d.lon, d.lat])[0];
})
.attr("cy", function (d) {
return projection([d.lon, d.lat])[1];
})
.attr("r", 5)
.style("fill", "yellow")
.style("stroke", "gray")
.style("stroke-width", 0.25)
.style("opacity", 0.75)
.append("title") //Simple tooltip
.text(function (d) {
return d.place + ": Pop. " + formatAsThousands(d.population);
});
});
以上代码的关键是通过 attr() 语句设定 cx 和 cy 值。可以通过 d.lon 和d.lat 获得原始的经度和纬度。
但我们真正需要的,则是在屏幕上定位这些圆形的 x/y 坐标,而不是地理坐标。
因此就要借助 projection(),它本质上是一个二维比例尺方法。给 D3 的比例尺传入一个值,它会返回另一个值。对于投影而言,我们传入两个数值,返回两个数值。(投影和简单的比例尺的另一个主要区别是后台计算,前者要复杂得多,后者只是一个简单的归一化映射。)
添加定位点.png地图投影接受一个包含两个值的数组作为输入,经度在前,纬度在后(这是 GeoJSON 格式规定的)。然后,投影就会返回一个包含两个值的数组,分别是屏幕上的 x/y 坐标值。因此,设定 cx 时使用 [0] 取得第一个值,也就是 x 坐标值;设定cy 时使用 [1] 取得第二个值,也就是 y 坐标值。
不过,这些点大小都一样啊,应该把人口数据反映到圆形大小上。为此,可以这样 引用人口数据:
.attr("r", function(d) {
return Math.sqrt(parseInt(d.population) * 0.00004);
})
这里先取得 d.population,把它传给 parseInt() 实现从字符串到整数值的转换, 再随便乘一个小数降低其量级,最后得到其平方根(把面积转换为半径)。
表示城市的圆点面积对应着人口.png最大的城市突出出来了。城市大小的差异很明显,这种情况可能更适合使用对数比例尺,尤其是在包含人口更少的城市的情况下。这时候就不用乘以0.00004 了,可以直接使用自定义的 D3 比例尺函数
这个例子的关键在于,我们整合了两个不同的数据集,把它们加载并显示在了地图上。(如果算上地理编码坐标,一共就是三个数据集)
四、移动地图
- 放大地图
var projection = d3.geoAlbersUsa()
.translate([w / 2, h / 2])
.scale([2000]);
- 数据加载后,创建上下左右四个方向键
- 创建方向键时,添加相同的class名称,方便后面添加点击事件
- 为方向键添加唯一的id,通过
d3.select(this).attr("id")
选择- 点击后,移动时添加过渡动画
var createPanButtons = function () {
//Create the clickable groups
//North
var north = svg.append("g")
.attr("class", "pan") //All share the 'pan' class
.attr("id", "north"); //The ID will tell us which direction to head
north.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", w)
.attr("height", 30);
north.append("text")
.attr("x", w / 2)
.attr("y", 20)
.html("↑");
//South
var south = svg.append("g")
.attr("class", "pan")
.attr("id", "south");
south.append("rect")
.attr("x", 0)
.attr("y", h - 30)
.attr("width", w)
.attr("height", 30);
south.append("text")
.attr("x", w / 2)
.attr("y", h - 10)
.html("↓");
//West
var west = svg.append("g")
.attr("class", "pan")
.attr("id", "west");
west.append("rect")
.attr("x", 0)
.attr("y", 30)
.attr("width", 30)
.attr("height", h - 60);
west.append("text")
.attr("x", 15)
.attr("y", h / 2)
.html("←");
//East
var east = svg.append("g")
.attr("class", "pan")
.attr("id", "east");
east.append("rect")
.attr("x", w - 30)
.attr("y", 30)
.attr("width", 30)
.attr("height", h - 60);
east.append("text")
.attr("x", w - 15)
.attr("y", h / 2)
.html("→");
//Panning interaction
d3.selectAll(".pan")
.on("click", function () {
//Get current translation offset
var offset = projection.translate();
//Set how much to move on each click
var moveAmount = 50;
//Which way are we headed?
var direction = d3.select(this).attr("id");
//Modify the offset, depending on the direction
switch (direction) {
case "north":
offset[1] += moveAmount; //Increase y offset
break;
case "south":
offset[1] -= moveAmount; //Decrease y offset
break;
case "west":
offset[0] += moveAmount; //Increase x offset
break;
case "east":
offset[0] -= moveAmount; //Decrease x offset
break;
default:
break;
}
//Update projection with new offset
projection.translate(offset);
//Update all paths and circles
svg.selectAll("path")
.transition()
.attr("d", path);
svg.selectAll("circle")
.transition()
.attr("cx", function (d) {
return projection([d.lon, d.lat])[0];
})
.attr("cy", function (d) {
return projection([d.lon, d.lat])[1];
});
});
};
- 给上下左右四个方向添加的矩形,添加CSS样式
.pan rect {
fill: black;
opacity: 0.2;
}
.pan text {
fill: black;
font-size: 18px;
text-anchor: middle;
}
.pan:hover rect,
.pan:hover text {
fill: blue;
}
五、添加拖拽效果
随着鼠标的移动,增加偏移量
var dragging = function(d) {
//Log out d3.event, so you can see all the goodies inside
console.log(d3.event);
//获取当前的偏移量
var offset = projection.translate();
//随着鼠标的移动,增加偏移量
offset[0] += d3.event.dx;
offset[1] += d3.event.dy;
//使用新的偏移量更新投影
projection.translate(offset);
//更新所有路径和圆
svg.selectAll("path")
.attr("d", path);
svg.selectAll("circle")
.attr("cx", function(d) {
return projection([d.lon, d.lat])[0];
})
.attr("cy", function(d) {
return projection([d.lon, d.lat])[1];
});
}
//定义拖拽行为
var drag = d3.drag()
.on("drag", dragging);
//创建一个容器,所有可平移元素都将位于其中
var map = svg.append("g")
.attr("id", "map")
.call(drag); //绑定拖拽行为
创建一个新的不可见背景矩形以捕获拖动事件
map.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", w)
.attr("height", h)
.attr("opacity", 0);
六、添加鼠标滚轮缩放
- 创建一个容器,所有可缩放的元素都将位于其中
- 初始化时,大概找一个国家的中心点
- 计算缩放比例时,乘以初始的系数
//Define what to do when panning or zooming
var zooming = function(d) {
// console.log(d3.event.transform);
//New offset array
var offset = [d3.event.transform.x, d3.event.transform.y];
//计算新的文件
var newScale = d3.event.transform.k * 2000;
//使用新的偏移和比例更新投影
projection.translate(offset)
.scale(newScale);
//更新所有路径和圆
svg.selectAll("path")
.attr("d", path);
svg.selectAll("circle")
.attr("cx", function(d) {
return projection([d.lon, d.lat])[0];
})
.attr("cy", function(d) {
return projection([d.lon, d.lat])[1];
});
}
//定义缩放行为
var zoom = d3.zoom()
.on("zoom", zooming);
//初始化时,大概找一个国家的中心点
var center = projection([-97.0, 39.0]);
//Create a container in which all zoom-able elements will live
var map = svg.append("g")
.attr("id", "map")
.call(zoom) //Bind the zoom behavior
.call(zoom.transform, d3.zoomIdentity //初始缩放变换
.translate(w/2, h/2)
.scale(0.25)
.translate(-center[0], -center[1]));
点击后使用固定的平移按钮缩放地图,触发缩放事件,按x、y缩放
d3.selectAll(".pan")
.on("click", function () {
//Set how much to move on each click
var moveAmount = 50;
//Set x/y to zero for now
var x = 0;
var y = 0;
//Which way are we headed?
var direction = d3.select(this).attr("id");
//Modify the offset, depending on the direction
switch (direction) {
case "north":
y += moveAmount; //Increase y offset
break;
case "south":
y -= moveAmount; //Decrease y offset
break;
case "west":
x += moveAmount; //Increase x offset
break;
case "east":
x -= moveAmount; //Decrease x offset
break;
default:
break;
}
// 触发缩放事件,按x、y缩放
map.transition()
.call(zoom.translateBy, x, y);
});
七、添加放大缩小按钮
- 右下角添加放大和缩小按钮
- 点击加号,放大为1.5倍;点击减号,缩小到0.75倍
var createZoomButtons = function () {
//Create the clickable groups
//Zoom in button
var zoomIn = svg.append("g")
.attr("class", "zoom") //All share the 'zoom' class
.attr("id", "in") //The ID will tell us which direction to head
.attr("transform", "translate(" + (w - 110) + "," + (h - 70) + ")");
zoomIn.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", 30)
.attr("height", 30);
zoomIn.append("text")
.attr("x", 15)
.attr("y", 20)
.text("+");
//Zoom out button
var zoomOut = svg.append("g")
.attr("class", "zoom")
.attr("id", "out")
.attr("transform", "translate(" + (w - 70) + "," + (h - 70) + ")");
zoomOut.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", 30)
.attr("height", 30);
zoomOut.append("text")
.attr("x", 15)
.attr("y", 20)
.html("–");
//Zooming interaction
d3.selectAll(".zoom")
.on("click", function () {
//Set how much to scale on each click
var scaleFactor;
//Which way are we headed?
var direction = d3.select(this).attr("id");
//Modify the k scale value, depending on the direction
switch (direction) {
case "in":
scaleFactor = 1.5;
break;
case "out":
scaleFactor = 0.75;
break;
default:
break;
}
//This triggers a zoom event, scaling by 'scaleFactor'
map.transition()
.call(zoom.scaleBy, scaleFactor);
});
};
放大和缩小.png
限制放大和缩小的倍数,避免无效的无限制放大
var zoom = d3.zoom()
.scaleExtent([0.2, 2.0])
.translateExtent([[-1200, -700], [1200, 700]])
.on("zoom", zooming);
迅速定位到某一位置/重置
<div>
<button id="pnw">Pacific Northwest</button>
<button id="reset">Reset</button>
</div>
d3.select("#pnw")
.on("click", function () {
map.transition()
.call(zoom.transform, d3.zoomIdentity
.translate(w / 2, h / 2)
.scale(0.9)
.translate(600, 300));
});
//Bind 'Reset' button behavior
d3.select("#reset")
.on("click", function () {
map.transition()
.call(zoom.transform, d3.zoomIdentity //Same as the initial transform
.translate(w / 2, h / 2)
.scale(0.25)
.translate(-center[0], -center[1]));
});
重置.png
添加label样式和数值
.label {
font-family: Helvetica, sans-serif;
font-size: 11px;
fill: black;
text-anchor: middle;
}
var zooming = function (d) {
svg.selectAll(".label")
.attr("x", function (d) {
return path.centroid(d)[0]
})
.attr("y", function (d) {
return path.centroid(d)[1]
})
}
可以使用d3中的format将数据格式化
var formatDecimals = d3.format(".3"); //e.g. converts 1.23456 to "1.23"
和地图坐标绑定
map.selectAll("text")
.data(json.features)
.enter()
.append("text")
.attr("class", "label")
.attr("x", function (d) {
return path.centroid(d)[0]
})
.attr("y", function (d) {
return path.centroid(d)[1]
})
.text(function(d){
if(d.properties.value){
return formatDecimals(d.properties.value)
}
})
label数据.png
网友评论