在實際開發中,地圖是個比較常見的圖,用於展示各個省市之間的數據差異.
項目地址: 點擊查看
效果預覽
先來看一下完成後的效果圖:
很清晰的展示了各個省份的數據之間的差異.同時還有 visualMap 來展示數據的範圍.當然不能缺少的是中國的南海諸島區域(未完成調試).
獲取DOM,向DOM 中插入 svg 就不在贅述,具體看最後的詳細代碼.這裏從獲取 geojson 數據開始.
獲取地圖 geojson 數據
要想繪製地圖,首先要有地圖的 geojson 數據.在 D3 的 v5 版本中,使用 Promise 替代了之前版本中的回調方式:
json('../json/chinawithoutsouthsea.json')
.then(geoJson => {
const projection = geoMercator()
.fitSize([layout.getWidth(), layout.getHeight()], geoJson);
const path = geoPath().projection(projection);
const paths = svg
.selectAll("path.map")
.data(geoJson.features)
.enter()
.append("path")
.classed("map",true)
.attr("fill", "#fafbfc")
.attr("stroke", "white")
.attr("class", "continent")
.attr("d", path)
.on('mouseover', function (d: any) {
select(this)
.classed('path-active', true)
})
.on('mouseout', function (d: any) {
select(this)
.classed('path-active', false)
})
const t = animationType();
// animationType = function() {
// return d3.transtion().ease()
// }
paths.transition(t)
.duration(1000)
.attr('fill', (d: any) => {
let prov = d.properties.name;
let curProvData = data.find((provData: any) => provData[0] === prov.slice(0, 2))
return color(curProvData ? curProvData[2] : 0)
});
});
這段代碼首先是獲取一個地圖的投影:
const projection = geoMercator()
.fitSize([layout.getWidth(), layout.getHeight()], geoJson);
const path = geoPath().projection(projection);
注意這裏,使用的是 fitSize API,它比以往使用的獲取投影之後,進行 translate 以及 scale 要方便的多,在之前的版本中,我們可能要寫:
/**
* old method 需要手動計算scale 以及 center
const projection = geoMercator()
.translate([layout.getWidth() / 2, layout.getHeight() / 2])
.scale(860).center([107, 40]);
*/
現在使用的 fitSize 可以很好的將 geojson 的路徑繪製在容器的中心.並自適應大小.當然,這種方法好用的前提是需要一個規範的 geojson 文件的支持.不然還是隻能使用之前的 translate 並 scale 的方法.
繪製 svg 元素
獲取了數據之後,就是對其進行繪製,還是與之前繪製圖表的方式差不多. 注意傳入 data 方法的參數;
最後添加的動畫是進行數據的映射對數據遍歷,獲取到數據中與路徑中 name 屬性相同的,進行顏色的填充.
南海諸島的添加
由於一般的中國地圖會將南海諸島區域按照正常的方位展示,但是這樣在數據展示圖上會帶來一定的不便以及佔用一些空間.所以這次選擇的是將南海諸島以 svg 圖的形式引入進來進行放置(注意比例尺-圖中未嚴格按照比例尺進行縮放).這同樣的需要 xml 請求:
xml("../json/southchinasea.svg").then(xmlDocument => {
svg.html(function () {
return select(this).html() + xmlDocument.getElementsByTagName("g")[0].outerHTML;
});
const southSea = select("#southsea")
let southSeaWidth = southSea.node().getBBox().width / 5
let southSeaH = southSea.node().getBBox().height / 5
select("#southsea")
.classed("southsea", true)
.attr("transform", `translate(${layout.getWidth()-southSeaWidth-24},${layout.getHeight()-southSeaH-24}) scale(0.2)`)
.attr("")
})
沒啥說的.
visualMap 的添加
最後就是 visualMap 的添加,讓數據展示更加具體.
// 顯示漸變矩形條
const linearGradient = svg.append("defs")
.append("linearGradient")
.attr("id", "linearColor")
//顏色漸變方向
.attr("x1", "0%")
.attr("y1", "100%")
.attr("x2", "0%")
.attr("y2", "0%");
// //設置矩形條開始顏色
linearGradient.append("stop")
.attr("offset", "0%")
.attr("stop-color", '#8ABCF4');
// //設置結束顏色
linearGradient.append("stop")
.attr("offset", "100%")
.attr("stop-color", '#18669A');
svg.append("rect")
//x,y 矩形的左上角座標
.attr("x", layout.getPadding().pl)
.attr("y", layout.getHeight() - 83 - layout.getPadding().pb) // 83爲矩形的高
//矩形的寬高
.attr("width", 16)
.attr("height", 83)
//引用上面的id 設置顏色
.style("fill", "url(#" + linearGradient.attr("id") + ")");
//設置文字
// 數據初值
svg.append("text")
.attr("x", layout.getPadding().pl + 16 + 8)
.attr("y", layout.getHeight() - layout.getPadding().pb)
.text(0)
.classed("linear-text", true);
// visualMap title
svg.append("text")
.attr("x", layout.getPadding().pl)
.attr("y", layout.getHeight() - 83 - layout.getPadding().pb - 8) // 8爲padding
.text('市場規模')
.classed("linear-text", true);
//數據末值
svg.append("text")
.attr("x", layout.getPadding().pl + 16 + 8)
.attr("y", layout.getHeight() - 83 - layout.getPadding().pb + 12) // 12 爲字體大小
.text(format("~s")(maxData))
.classed("linear-text", true)
也是根據 svg 中的一些元素來形成 visualMap 圖.
完成代碼
最後完整的代碼是
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { json, xml } from 'd3-fetch';
import { scaleLinear } from 'd3-scale'
import Layout from 'ember-d3-demo/utils/d3/layout';
import { geoPath, geoMercator } from 'd3-geo';
import { max, min } from 'd3-array';
import { select } from 'd3-selection';
import { format } from 'd3-format';
import {animationType} from '../../../../utils/d3/animation';
interface D3BpMapArgs {
data: any[]
// [
// ["廣東", 1, 73016024],
// ["河南", 1, 60152736],
// ...
// ]
width: number
height: number
}
export default class D3BpMap extends Component<D3BpMapArgs> {
@action
initMap() {
let layout = new Layout('.bp-map')
let { width, height, data } = this.args
if (width) {
layout.setWidth(width)
}
if (height) {
layout.setHeight(height)
}
const container = layout.getContainer()
//generate svg
const svg = container.append('svg')
.attr('width', layout.getWidth())
.attr('height', layout.getHeight())
.style('background-color', '#FAFBFC');
/**
* old method 需要手動計算scale 以及 center
const projection = geoMercator()
.translate([layout.getWidth() / 2, layout.getHeight() / 2])
.scale(860).center([107, 40]);
*/
const maxData = max(data.map((datum: any[]) => datum[2]))
const minData = min(data.map((datum: any[]) => datum[2]))
const color = scaleLinear().domain([0, maxData])
.range(['#B8D4FA', '#18669A']);
// .range(["#E7F0FE","#B8D4FA","#8ABCF4","#5CA6EF",
// "#3492E5",
// "#1E7EC8",
// "#18669A"
// ])
xml("../json/southchinasea.svg").then(xmlDocument => {
svg.html(function () {
return select(this).html() + xmlDocument.getElementsByTagName("g")[0].outerHTML;
});
const southSea = select("#southsea")
let southSeaWidth = southSea.node().getBBox().width / 5
let southSeaH = southSea.node().getBBox().height / 5
select("#southsea")
.classed("southsea", true)
.attr("transform", `translate(${layout.getWidth()-southSeaWidth-24},${layout.getHeight()-southSeaH-24}) scale(0.2)`)
.attr("")
return json('../json/chinawithoutsouthsea.json')
})
.then(geoJson => {
const projection = geoMercator()
.fitSize([layout.getWidth(), layout.getHeight()], geoJson);
const path = geoPath().projection(projection);
const paths = svg
.selectAll("path.map")
.data(geoJson.features)
.enter()
.append("path")
.classed("map",true)
.attr("fill", "#fafbfc")
.attr("stroke", "white")
.attr("class", "continent")
.attr("d", path)
.on('mouseover', function (d: any) {
select(this)
.classed('path-active', true)
})
.on('mouseout', function (d: any) {
select(this)
.classed('path-active', false)
})
const t = animationType();
paths.transition(t)
.duration(1000)
.attr('fill', (d: any) => {
let prov = d.properties.name;
let curProvData = data.find((provData: any) => provData[0] === prov.slice(0, 2))
return color(curProvData ? curProvData[2] : 0)
});
// return xml("../json/southchinasea.svg")
});
// 顯示漸變矩形條
const linearGradient = svg.append("defs")
.append("linearGradient")
.attr("id", "linearColor")
//顏色漸變方向
.attr("x1", "0%")
.attr("y1", "100%")
.attr("x2", "0%")
.attr("y2", "0%");
// //設置矩形條開始顏色
linearGradient.append("stop")
.attr("offset", "0%")
.attr("stop-color", '#8ABCF4');
// //設置結束顏色
linearGradient.append("stop")
.attr("offset", "100%")
.attr("stop-color", '#18669A');
svg.append("rect")
//x,y 矩形的左上角座標
.attr("x", layout.getPadding().pl)
.attr("y", layout.getHeight() - 83 - layout.getPadding().pb) // 83爲矩形的高
//矩形的寬高
.attr("width", 16)
.attr("height", 83)
//引用上面的id 設置顏色
.style("fill", "url(#" + linearGradient.attr("id") + ")");
//設置文字
// 數據初值
svg.append("text")
.attr("x", layout.getPadding().pl + 16 + 8)
.attr("y", layout.getHeight() - layout.getPadding().pb)
.text(0)
.classed("linear-text", true);
// visualMap title
svg.append("text")
.attr("x", layout.getPadding().pl)
.attr("y", layout.getHeight() - 83 - layout.getPadding().pb - 8) // 8爲padding
.text('市場規模')
.classed("linear-text", true);
//數據末值
svg.append("text")
.attr("x", layout.getPadding().pl + 16 + 8)
.attr("y", layout.getHeight() - 83 - layout.getPadding().pb + 12) // 12 爲字體大小
.text(format("~s")(maxData))
.classed("linear-text", true)
}
}