使用 D3.js 创建根据值域颜色渐变的地图

在实际开发中,地图是个比较常见的图,用于展示各个省市之间的数据差异.

项目地址: 点击查看

文章目录

效果预览

先来看一下完成后的效果图:
使用 D3.js 创建根据值域颜色渐变的地图

很清晰的展示了各个省份的数据之间的差异.同时还有 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)
    }
}

上一篇:D3平移和缩放后的点击坐标(D3 click coordinates after pan and zoom)


下一篇:D3中的定量比例尺训练