Use D3.js to create a map with color gradients based on value fields

In the actual development, map is a common map, which is used to show the data differences between provinces and cities

Project address: Click to view

Article directory

Effect preview

Let's take a look at the finished renderings:

It clearly shows the differences between the data of each province. At the same time, there is visual map to show the scope of the data. Of course, there is no lack of the South China Sea Islands (not completed debugging)

Getting the DOM and inserting svg into the DOM will not be covered in detail. See the final detailed code. Here we start from getting geojson data

Get map geojson data

To draw a map, you must first have the geojson data of the map. In the v5 version of D3, use Promise instead of the callback method in the previous version:

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)
	              });
	      });

This code first obtains the projection of a map:

const projection = geoMercator()
    .fitSize([layout.getWidth(), layout.getHeight()], geoJson);

const path = geoPath().projection(projection);

Note that the fitSize API is used here. It is much more convenient to translate and scale than the previous one. In the previous version, we may write:

        /**
         *old method needs to calculate scale and center manually
         const projection = geoMercator()
            .translate([layout.getWidth() / 2, layout.getHeight() / 2])
            .scale(860).center([107, 40]);
         */

The fitSize used now can well draw the path of geojson in the center of the container and adapt the size. Of course, the premise of this method is that it needs the support of a standard geojson file. Otherwise, it can only use the previous method of translate and scale

Drawing svg elements

After obtaining the data, whether to draw it is the same way as before. Pay attention to the parameters passed in the data method;

The last added animation is to map the data and traverse the data to get the same name attribute in the data and path, and fill in the color

The addition of islands in the South China Sea

Because the general map of China will display the islands in the South China Sea according to the normal orientation, but it will bring some inconvenience and occupy some space in the data display map. So this time, we choose to introduce the islands in the South China Sea in the form of svg map for placement (note that the scale is not strictly scaled in the map). The same xml request is required:

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("")
        })

Nothing to say

Adding visual map

Finally, visual map is added to make the data display more specific

// Show gradient rectangle
        const linearGradient = svg.append("defs")
            .append("linearGradient")
            .attr("id", "linearColor")
            //Color gradient direction
            .attr("x1", "0%")
            .attr("y1", "100%")
            .attr("x2", "0%")
            .attr("y2", "0%");
        // //Set rectangle bar start color
        linearGradient.append("stop")
            .attr("offset", "0%")
            .attr("stop-color", '#8ABCF4');
        // //Set end color
        linearGradient.append("stop")
            .attr("offset", "100%")
            .attr("stop-color", '#18669A');

        svg.append("rect")
            //x. Coordinates of the upper left corner of the Y rectangle
            .attr("x", layout.getPadding().pl)
            .attr("y", layout.getHeight() - 83 - layout.getPadding().pb) // 83 is the height of the rectangle
            //Width and height of rectangle
            .attr("width", 16)
            .attr("height", 83)
            //Set the color by referring to the id above
            .style("fill", "url(#" + linearGradient.attr("id") + ")");
        //Set text

        // Data initial value
        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 for padding
            .text('market size')
            .classed("linear-text", true);
        //Data terminal value
        svg.append("text")
            .attr("x", layout.getPadding().pl + 16 + 8)
            .attr("y", layout.getHeight() - 83 - layout.getPadding().pb + 12) // 12 is the font size
            .text(format("~s")(maxData))
            .classed("linear-text", true)

It is also based on some elements of svg to form a visual map

Finish code

The last complete code is

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[]
    // [
    //     ["Guangdong", 1, 73016024],
    //     ["Henan", 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 and center need to be calculated manually
         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")
            });
        // Show gradient rectangle
        const linearGradient = svg.append("defs")
            .append("linearGradient")
            .attr("id", "linearColor")
            //Color gradient direction
            .attr("x1", "0%")
            .attr("y1", "100%")
            .attr("x2", "0%")
            .attr("y2", "0%");
        // //Set rectangle bar start color
        linearGradient.append("stop")
            .attr("offset", "0%")
            .attr("stop-color", '#8ABCF4');
        // //Set end color
        linearGradient.append("stop")
            .attr("offset", "100%")
            .attr("stop-color", '#18669A');

        svg.append("rect")
            //x. Coordinates of the upper left corner of the Y rectangle
            .attr("x", layout.getPadding().pl)
            .attr("y", layout.getHeight() - 83 - layout.getPadding().pb) // 83 is the height of the rectangle
            //Width and height of rectangle
            .attr("width", 16)
            .attr("height", 83)
            //Set the color by referring to the id above
            .style("fill", "url(#" + linearGradient.attr("id") + ")");
        //Set text

        // Data initial value
        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 for padding
            .text('market size')
            .classed("linear-text", true);
        //Data terminal value
        svg.append("text")
            .attr("x", layout.getPadding().pl + 16 + 8)
            .attr("y", layout.getHeight() - 83 - layout.getPadding().pb + 12) // 12 is the font size
            .text(format("~s")(maxData))
            .classed("linear-text", true)
    }
}

37 original articles published, praised 12, visited 60000+
Private letter follow

Tags: JSON xml Attribute

Posted on Sat, 07 Mar 2020 09:28:21 -0500 by thenior