使用 D3.js 創建根據值域顏色漸變的地圖

在實際開發中,地圖是個比較常見的圖,用於展示各個省市之間的數據差異.

項目地址: 點擊查看

效果預覽

先來看一下完成後的效果圖:
map

很清晰的展示了各個省份的數據之間的差異.同時還有 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)
    }
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章