你需要知道的:
JSON数据无法通过本地访问!!!换成Web服务器之后又说什么跨域访问,我真的太菜了什么都不懂。
最后:这个人真的是太赞了!!!
我用的是HBilder,所以设置了一个内置web服务器就可以访问了
- 需要知道每个城市的经纬度坐标信息:地理编码(geocoding)服务能够根据地名查找地图,返回较为精确的经纬度坐标;
- 获得GeoJSON数据:
- 1.NaturalEarth (这里可以说一下有两种数据一个是cultural:表示与文化有关的分界线(国家线) physical:物理上的分界线(海线))2.美国人口普查局(Shapefile)
- 将高解析度的数据文件转化为低解析度的数据文件:Matt Bloch MapShapper
D3.js in action 第八章
8 地理空间信息可视化 Geospatial information visualization
path.countries {
stroke-width: 1;
stroke: #75739F;
fill: #5EAFC6;
}
circle.cities {
stroke-width: 1;
stroke: #4F442B;
fill: #FCBC34;
}
circle.centroid {
fill: #75739F;
pointer-events: none;
}
rect.bbox {
fill: none;
stroke-dasharray: 5 5;
stroke: #75739F;
stroke-width: 2;
pointer-events: none;
}
path.graticule {
fill: none;
stroke-width: 1;
stroke: #9A8B7A;
}
path.graticule.outline {
stroke: #9A8B7A;
}
path.merged {
fill: #9A8B7A;
stroke: #4F442B;
stroke-width: 2px;
}
8.1 地图组件
官方API:不同的projection
8.1.1 投影 Projection
Mercator Projection
代码:
d3.json("../data/world.geojson", createMap);
function createMap(countries) {
var aProjection = d3.geoMercator();
var geoPath = d3.geoPath().projection(aProjection);
//d3.geoPath()默认是albersUSA,是一种只适合USA地图的投影
d3.select("svg").selectAll("path").data(countries.features)
.enter()
.append("path")
.attr("d", geoPath)
.attr("class", "countries");
}
8.1.2 比例 scale
在不同的投影上,有不同的技巧来确定合适的比例可以展示整个地图。
比如说对于Mercator Projection:将宽度(width)除以2,然后将商除以π;找到一个合适的比例需要经系列的实验,但是这件事器在有了zoom之后会好办很多。
var aProjection = d3.geoMercator()
.scale(80)
.translate([250, 250]);
8.2 画地图
8.2.1 在地图上画点
var PromiseWrapper = (xhr, d) => new Promise(resolve => xhr(d, (p) => resolve(p)));
Promise
.all([
PromiseWrapper(d3.json, "../data/world.geojson"),
PromiseWrapper(d3.csv, "../data/cities.csv")
])
.then(resolve => {
createMap(resolve[0], resolve[1]);
});
function createMap(countries, cities) {
var projection = d3.geoMercator()
.scale(80)
.translate([250, 250]);
var geoPath = d3.geoPath().projection(projection);
d3.select("svg").selectAll("path").data(countries.features)
.enter()
.append("path")
.attr("class", "countries")
.attr("d", geoPath);
d3.select("svg").selectAll("circle").data(cities)
.enter()
.append("circle")
.attr("class", "cities")
.attr("r", 3)
.attr("cx", d => projection([d.x,d.y])[0])
.attr("cy", d => projection([d.x,d.y])[1]);
}
8.2.2 在地图上描面积
Mollweide Projection:equal-area projection
function createMap(countries, cities) {
var projection = d3.geoMollweide()
.scale(120)
.translate([250, 250]);
var geoPath = d3.geoPath().projection(projection);
var featureSize = d3.extent(countries.features, d => geoPath.area(d))
var countryColor = d3.scaleQuantize()
.domain(featureSize).range(colorbrewer.Reds[7]);
d3.select("svg").selectAll("path").data(countries.features)
.enter()
.append("path")
.attr("d", geoPath)
.attr("class", "countries")
.style("fill", d => countryColor(geoPath.area(d)))
.style("stroke", d => d3.rgb(countryColor(geoPath.area(d))).darker());
d3.select("svg").selectAll("circle").data(cities)
.enter()
.append("circle")
.attr("class", "cities")
.attr("r", 3)
.attr("cx", d => projection([d.x,d.y])[0])
.attr("cy", d => projection([d.x,d.y])[1]);
}
8.2.3 交互
计算某个区域的边界以及中心
function centerBounds(d) {
var thisBounds = geoPath.bounds(d);
var thisCenter = geoPath.centroid(d);//获得面积中心
console.log(thisBounds);
d3.select("svg")
.append("rect")
.attr("class", "bbox")
.attr("x", thisBounds[0][0]) //Bounds[0]表示最左边界
.attr("y", thisBounds[0][1]) //Bounds[1]表示最右边界
.attr("width", thisBounds[1][0] - thisBounds[0][0])
.attr("height", thisBounds[1][1] - thisBounds[0][1]);
d3.select("svg")
.append("circle")
.attr("class", "centroid")
.attr("r", 5)
.attr("cx", thisCenter[0]).attr("cy", thisCenter[1]);
}
function clearCenterBounds() {
d3.selectAll("circle.centroid").remove();
d3.selectAll("rect.bbox").remove();
}
8.3 更好的显示地图 Better mapping
8.3.1 方格图 Graticule
var graticule = d3.geoGraticule();
d3.select("svg").insert("path", "path.countries")
.datum(graticule)
.attr("class", "graticule line")
.attr("d", geoPath);
d3.select("svg").insert("path", "path.countries")
.datum(graticule.outline)
.attr("class", "graticule outline")
.attr("d", geoPath);
8.3.2 缩放 Zoom
var mapZoom = d3.zoom()
.on("zoom", zoomed);
var zoomSettings = d3.zoomIdentity
.translate(250, 250)
.scale(120);
d3.select("svg").call(mapZoom).call(mapZoom.transform, zoomSettings);
function zoomed() {
var e = d3.event;
projection.translate([e.transform.x, e.transform.y])
.scale(e.transform.k);//为projection设置新的位置以及比例
d3.selectAll("path.graticule").attr("d", geoPath);
d3.selectAll("path.countries").attr("d", geoPath);
d3.selectAll("circle.cities")
.attr("cx", d => projection([d.x,d.y])[0])
.attr("cy", d => projection([d.x,d.y])[1]);
}
8.3.3 使用button缩放
为了避免有一些人不知道双击,拖拽以及鼠标滚轮这些交互方式,生成button进行缩放
function zoomButton(zoomDirection) {
var width = 500;
var height = 500;
//需要计算x,y以及中心
if (zoomDirection == "in") {
var newZoom = projection.scale() * 1.5;
var newX = ((projection.translate()[0] - (width / 2)) * 1.5) + width / 2;
var newY = ((projection.translate()[1] - (height / 2)) * 1.5) + height / 2;
}
else if (zoomDirection == "out") {
var newZoom = projection.scale() * .75;
var newX = ((projection.translate()[0] - (width / 2)) * .75) + width / 2;
var newY = ((projection.translate()[1] - (height / 2)) * .75) + height / 2;
}
var newZoomSettings = d3.zoomIdentity
.translate(newX, newY)
.scale(newZoom);
d3.select("svg").transition().duration(500).call(mapZoom.transform, newZoomSettings);
}
d3.select("#controls").append("button")
.on("click", () => { zoomButton("in")}).html("Zoom In");
d3.select("#controls").append("button")
.on("click", () => { zoomButton("out")}).html("Zoom Out");
8.4 高级的地图 Advanced mapping
8.4.1 创建和旋转地球仪 Creating and rotating globes
不需要太麻烦,使用一个特殊的投影即可:orthographic projection
var projection = d3.geoOrthographic()
.scale(200)
.translate([250, 250])
.center([0,0]);
为了使他可以旋转起来,则需要隐藏那些不在当前视图的点
d3.selectAll("circle.cities")
.each(function (d, i) {
var projectedPoint = projection([d.x,d.y]);
var x = parseInt(d.x);
var display = x + currentRotate < 90 && x + currentRotate > -90
|| (x + currentRotate < -270 && x + currentRotate > -450)
|| (x + currentRotate > 270 && x + currentRotate < 450)
? "block" : "none";
d3.select(this)
.attr("cx", projectedPoint[0])
.attr("cy", projectedPoint[1])
.style("display", display);
})
问题:在这里使用的featureSize的大小是根据显示在地球仪上的大小(但是真实世界不会因为地球仪的旋转,面积而发生变化)
var geoPath = d3.geoPath().projection(projection);
var featureSize = d3.extent(countries.features, d => geoPath.area(d));
var countryColor = d3.scaleQuantize()
.domain(featureSize).range(colorbrewer.Reds[7]);
解决方法:使用d3.geoArea(),countries coloured by their geographic area, rather than their graphical area.
var realFeatureSize = d3.extent(countries.features, d => d3.geoArea(d));
var newFeatureColor = d3.scaleQuantize()
.domain(realFeatureSize)
.range(colorbrewer.Reds[7]);
更复杂巧妙的拖拽,可以查看bl.ocks.org
8.4.2 卫星投影 Satellite projection
样例地址:http://bl.ocks.org/emeeks/10173187
Tilt is the angle of the perspective on the data, whereas distance is the percentage of the radius of the earth.
8.5 TopoJSON data and functionality
github地址:TopoJSON
在使用TopoJSON之前,需要包含TopoJSON.js库。
8.5.1 加载数据 Loading data
var PromiseWrapper = (xhr, d) => new Promise(resolve => xhr(d, (p) => resolve(p)));
Promise
.all([
PromiseWrapper(d3.json, "../data/world.topojson"),
PromiseWrapper(d3.csv, "../data/cities.csv")
])
.then(resolve => {
createMap(resolve[0], resolve[1]);
});
function createMap(topoCountries) {
var countries =
topojson.feature(topoCountries, topoCountries.objects.countries);
}
8.5.2 合并 Merging
可以合并边界线
mergeAt(0);
//这里的0表示 小于或者大于 0°精度和维度
function mergeAt(mergePoint) {
var filteredCountries = topoCountries.objects.countries.geometries
.filter(d => {
var thisCenter = d3.geoCentroid(
topojson.feature(topoCountries, d) );
return thisCenter[1] > mergePoint? true : null;
});
d3.select("svg").append("g")
.datum(topojson.merge(topoCountries, filteredCountries))
.insert("path")
.attr("class", "merged")
.attr("d", geoPath);
}
Topojson提供了另一个函数:Topojson.mergeArcs,可以让你合并形状,并且保持返回值依旧为TopoJSON格式。
8.5.3 共享边界线的邻居 Neighbors
函数 Topojson.neighbors返回共享边界的所有features的数组
var neighbors =
topojson.neighbors(topoCountries.objects.countries.geometries);
// 每块根据i有一个编号
d3.selectAll("path.countries")
.on("mouseover", findNeighbors)
.on("mouseout", clearNeighbors);
function findNeighbors (d,i) {
d3.select(this).style("fill", "#FE9922");
d3.selectAll("path.countries")
.filter((p,q) => neighbors[i].indexOf(q) > -1).style("fill", "#41A368");
}
//p表示d,q表示i
function clearNeighbors () {
d3.selectAll("path.countries").style("fill", "#C4B9AC");
}
8.6 其他
Tile-based maps:https://docs.mapbox.com/mapbox-gl-js/api/
The tile data used in tile maps:https://observablehq.com/@d3/satellite
Hexbins:https://observablehq.com/@d3/hexbin-map
使用d3.geom.voronoi函数 从类似的点导出多边形:https://www.jasondavies.com/maps/voronoi/us-capitals/
Cartograms:比较统计地图:
https://www.jasondavies.com/maps/dorling-world/
https://bl.ocks.org/mbostock/4055908
Geomapping
给地图提供上下左右的方向键,按相应的键,地图往哪边移;
但是实际上,地图的移动方向是正好与键的方向相反,点右键,地图的起始点(x,y)得向左上走;
1.drag
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="https://d3js.org/d3.v4.js"></script>
<title></title>
<style type="text/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;
}
</style>
</head>
<body>
<script type="text/javascript">
//Width and height
var w = 500;
var h = 300;
//Define map projection
var projection = d3.geoAlbersUsa()
.translate([w/2, h/2])
.scale([2000]);
//Define path generator
var path = d3.geoPath()
.projection(projection);
//Define quantize scale to sort data values into buckets of color
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)"]);
//Colors taken from colorbrewer.js, included in the D3 download
//Number formatting for population values
var formatAsThousands = d3.format(","); //e.g. converts 123456 to "123,456"
//Create SVG element
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
//Define what to do when dragging
var dragging = function(d) {
//Log out d3.event, so you can see all the goodies inside
//console.log(d3.event);
//Get the current (pre-dragging) translation offset
var offset = projection.translate();
//Augment the offset, following the mouse movement
offset[0] += d3.event.dx;
offset[1] += d3.event.dy;
//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];
});
}
//Then define the drag behavior
var drag = d3.drag()
.on("drag", dragging);
//Create a container in which all pan-able elements will live
var map = svg.append("g")
.attr("id", "map")
.call(drag); //Bind the dragging behavior
//Create a new, invisible background rect to catch drag events
map.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", w)
.attr("height", h)
.attr("opacity", 0);
//Load in agriculture data
d3.csv("us-ag-productivity.csv", function(data) {
//Set input domain for color scale
color.domain([
d3.min(data, function(d) { return d.value; }),
d3.max(data, function(d) { return d.value; })
]);
//Load in GeoJSON data
d3.json("us-states.json", function(json) {
//Merge the ag. data and GeoJSON
//Loop through once for each ag. data value
for (var i = 0; i < data.length; i++) {
var dataState = data[i].state; //Grab state name
var dataValue = parseFloat(data[i].value); //Grab data value, and convert from string to float
//Find the corresponding state inside the GeoJSON
for (var j = 0; j < json.features.length; j++) {
var jsonState = json.features[j].properties.name;
if (dataState == jsonState) {
//Copy the data value into the JSON
json.features[j].properties.value = dataValue;
//Stop looking through the JSON
break;
}
}
}
//Bind data and create one path per GeoJSON feature
map.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";
}
});
//Load in cities data
d3.csv("us-cities.csv", function(data) {
map.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", function(d) {
return Math.sqrt(parseInt(d.population) * 0.00004);
})
.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);
});
createPanButtons();
});
});
});
//Create panning buttons
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];
});
});
};
</script>
</body>
</html>
如果有zooming(x, y, k),drag就可以省去了,因为zooming包含了drag.
2.zooming
//Define what to do when panning or zooming
var zooming = function(d) {
//Log out d3.event.transform, so you can see all the goodies inside
//console.log(d3.event.transform);
//New offset array
var offset = [d3.event.transform.x, d3.event.transform.y];
//Calculate new scale
var newScale = d3.event.transform.k * 2000;
//Update projection with new offset and scale
projection.translate(offset)
.scale(newScale);
//Update all paths and circles
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];
});
}
//Then define the zoom behavior
var zoom = d3.zoom()
.on("zoom", zooming);
//The center of the country, roughly
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 //Then apply the initial transform
.translate(w/2, h/2)
.scale(0.25)
.translate(-center[0], -center[1]));
3.drag需经过zooming
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;
}
//This triggers a zoom event, translating by x, y
map.transition()
.call(zoom.translateBy, x, y);
});
制作地图过程:
1. Find shapefiles
2. Choose a resolution
3. Simplify the shape
4. Convert to GeoJSON
5. Choose a projection