在d3中地图的创建通常用d3-geo来实现的,GeoJSON是一种用于表示地理结构(几何图形、特性或特性集合)的格式,我们选择Natural Earth来作为练手数据
数据读入与参数设置
首先读入数据
1 2
| const countryShapes = await d3.json('./data/world-geojson.json') const dataset = await d3.csv('./data/world_bank_data.csv')
|
console.log一下我们可以看到json数据的内容,其中包含了四个关键字:crs, features, name, type
我们在feature里面挑选关键词并创建访问器函数,通过访问国家ID来找到人口增长数据集中的度量值
1 2
| const countryNameAccessor = d => d.properties["NAME"] const countryIdAccessor = d => d.properties["ADM0_A3_IS"]
|
然后来看一下这个csv文件
这个数据集是一个数组,它将每个国家多次列出,每次都使用不同的度量,以Series Name键命名,这里我们只用到每年人口的增长,所以把它抠出来
1
| const metric = "Population growth (annual %)"
|
然后我们将检查数据集数组中的每一项。如果条目的“系列名称”与我们的度量不匹配,我们将不做任何事情。如果匹配,我们将向metricDataByCountry对象添加一个新值:键是项目的“国家代码”
1 2 3 4 5
| let metricDataByCountry = {} dataset.forEach(d => { if (d['Series Name'] != metric) return metricDataByCountry[d['Country Code']] = d['2017 [YR2017]'] || 0 })
|
然后我们来选择一个投影方式,我们先创建一个球体,随后选择一种投影方式
1 2 3 4 5
| const sphere = { type: 'Sphere' } const projection = d3 .geoEqualEarth() .fitWidth(dimensions.boundedWidth, sphere) const pathGenerator = d3.geoPath(projection)
|
fitWidth中的两个参数分别表示图示的宽度和要投影的对象
pathGenerator()有一个.bounds()方法,它将返回一个[x, y]坐标数组,描述指定GeoJSON对象的边界框
1
| const [[x0, y0], [x1, y1]] = pathGenerator.bounds(sphere)
|
最后我们来定义一下参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| let dimensions = { width: window.innerWidth * 0.9, margin: { top: 10, right: 10, bottom: 10, left: 10, }, }
dimensions.boundedWidth = dimensions.width - dimensions.margin.left - dimensions.margin.right dimensions.boundedHeight = y1 dimensions.height = dimensions.boundedHeight + dimensions.margin.top + dimensions.margin.bottom
|
创建比例尺
接下来要创建一个比例尺,将度量值(人口增长数量)转换为颜色值
1 2 3 4 5 6 7 8 9
| const metricValues = Object.values(metricDataByCountry)
const metricValueExtent = d3.extent(metricValues) const maxChange = d3.max([-metricValueExtent[0], metricValueExtent[1]]) const colorScale = d3 .scaleLinear() .domain([-maxChange, 0, maxChange]) .range(['indigo', 'white', 'darkgreen'])
|
因为增长率存在负增长,所以我们需要获得绝对值的最大值,然后创建一个两段的比例尺,用两种不同颜色表示正负
画图
准备工作完成,我们开始画这个地图
先把轮廓画出来
1 2 3
| const earth = bounds.append("path") .attr("class", "earth") .attr("d", pathGenerator(sphere))
|
然后通过d3.geoGraticule10()函数每隔10度画一个分割线
1 2 3 4
| const graticuleJson = d3.geoGraticule10() const graticule = bounds.append("path") .attr("class", "graticule") .attr("d", pathGenerator(graticuleJson))
|
最后将国家数据画上
1 2 3 4 5 6 7 8 9 10 11 12
| const countries = bounds .selectAll('.country') .data(countryShapes.features) .enter() .append('path') .attr('class', 'country') .attr('d', pathGenerator) .attr('fill', d => { const metricValue = metricDataByCountry[countryIdAccessor(d)] if (typeof metricValue === undefined) return '#e2e6e9' return colorScale(metricValue) })
|
再额外加个标题和比例尺说明
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
| const legendGroup = wrapper .append('g') .attr( 'transform', `translate(${120},${ dimensions.width < 800 ? dimensions.boundedHeight - 30 : dimensions.boundedHeight * 0.5 })` )
const legendTitle = legendGroup .append('text') .attr('y', -23) .attr('class', 'legend-title') .text('Population growth')
const legendByline = legendGroup .append('text') .attr('y', -9) .attr('class', 'legend-byline') .text('Percent change in 2017')
const defs = wrapper.append('defs') const legendGradientId = 'legend-gradient' const gradient = defs .append('linearGradient') .attr('id', legendGradientId) .selectAll('stop') .data(colorScale.range()) .enter() .append('stop') .attr('stop-color', d => d) .attr( 'offset', (d, i) => `${ (i * 100) / 2 }%` )
const legendWidth = 120 const legendHeight = 16 const legendGradient = legendGroup .append('rect') .attr('x', -legendWidth / 2) .attr('height', legendHeight) .attr('width', legendWidth) .style('fill', `url(#${legendGradientId})`)
const legendValueRight = legendGroup .append('text') .attr('class', 'legend-value') .attr('x', legendWidth / 2 + 10) .attr('y', legendHeight / 2) .text(`${d3.format('.1f')(maxChange)}%`)
const legendValueLeft = legendGroup .append('text') .attr('class', 'legend-value') .attr('x', -legendWidth / 2 - 10) .attr('y', legendHeight / 2) .text(`${d3.format('.1f')(-maxChange)}%`) .style('text-anchor', 'end')
|
这里书上介绍了一个神奇的东西,navigator,可以获取浏览器用户的位置,这里我们用一个小圆圈显示出来
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| navigator.geolocation.getCurrentPosition(myPosition => { console.log(myPosition)
const [x, y] = projection([ myPosition.coords.longitude, myPosition.coords.latitude ])
const myLocation = bounds.append("circle") .attr("class", "my-location") .attr("cx", x) .attr("cy", y) .attr("r", 0) .transition().duration(500) .attr("r", 10) })
|
互动部分和先前类似,加入了一个小牌子显示信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| countries .on("mouseenter", onMouseEnter) .on("mouseleave", onMouseLeave)
const tooltip = d3.select("#tooltip")
function onMouseEnter(datum) { tooltip.style("opacity", 1) const metricValue = metricDataByCountry[countryIdAccessor(datum)] tooltip.select("#country").text(countryNameAccessor(datum)) tooltip.select("#value").text(`${d3.format(",.2f")(metricValue || 0)}`)
const [centerX, centerY] = pathGenerator.centroid(datum) const x = centerX + dimensions.margin.left const y = centerY + dimensions.margin.top
tooltip.style("transform", `translate(` + `calc( -50% + ${x}px),` + `calc(-100% + ${y}px)` + `)`) } function onMouseLeave() { tooltip.style("opacity", 0) }
|
[演示地址]