D3 Geospatial information visualization

你需要知道的:

JSON数据无法通过本地访问!!!换成Web服务器之后又说什么跨域访问,我真的太菜了什么都不懂。

通过Python搭建简单Web服务器 

最后:这个人真的是太赞了!!!

getJSON无法读取本地json数据的问题

我用的是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("&uarr;");
				
				//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("&darr;");

				//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("&larr;");

				//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("&rarr;");

				//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

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值