In real development, a map is a fairly common diagram to show the data differences between provinces and cities. We can use this method to draw any map we need as long as we have the GeoJSON file we need to draw the map.

Project Address: Click to view @[TOC]

Results the preview

Take a look at the finished image:

It clearly shows the differences between provinces. There is also a visualMap to show the scope of the data. Of course, China cannot miss the South China Sea Islands area (uncompleted commissioning).

Get the DOM, and insert SVG into the DOM without going into the details of the final code. Start by getting the GeoJSON data.

Get the map GeoJSON data

To draw a map, you first need to have the geoJSON data for the map. In version V5 of D3, Promise was used instead of the callback from 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)}); });Copy the code

This code starts by getting a map projection:

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

const path = geoPath().projection(projection);
Copy the code

Note that we’re using the fitSize API, which is much easier to translate and scale after getting a projection than we used to. In previous versions, we might have said:

        /** * Old method requires manual calculation of scale and center const projection = geoMercator().translate([layout.getwidth () / 2, layout.getHeight() / 2]) .scale(860).center([107, 40]); * /
Copy the code

FitSize is now used to nicely draw the geoJSON path in the center of the container. Of course, this method is useful only if it is supported by a canonical GeoJSON file. Otherwise, we’ll just have to translate and scale.

Drawing SVG elements

Once you have the data, you plot it in much the same way you plotted it before. Notice the parameters passed to the data method;

Finally, the animation is added to carry out data mapping and data traversal, to obtain the same name attribute in the data and fill it with color.

Addition of Nanhai Zhudao

The south China Sea Islands will be displayed in the normal orientation on general Chinese maps, but this will bring some inconvenience and occupy some space in the data display map. So this time the choice is to bring in the South China Sea Islands as an SVG image for placement (note the scale – the image is not scaled strictly to scale). This also requires an XML request:

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

Nothing to say.

VisualMap to add

Finally, the visualMap was added to make the data presentation more concrete.

// Displays a gradient rectangle bar
        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%");
        // // Sets the start color of the rectangle bar
        linearGradient.append("stop")
            .attr("offset"."0%")
            .attr("stop-color".'#8ABCF4');
        // // Set the end color
        linearGradient.append("stop")
            .attr("offset"."100%")
            .attr("stop-color".'#18669A');

        svg.append("rect")
            // The upper-left coordinates of the x,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 the rectangle
            .attr("width".16)
            .attr("height".83)
            // Reference the id above to set the color
            .style("fill"."url(#" + linearGradient.attr("id") + ")");
        // Set the text

        // Initial data 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 end 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)
Copy the code

The visualMap is also formed from elements in SVG.

Complete code

The final 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 requires manual calculation of scale and 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")
            });
        // Displays a gradient rectangle bar
        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%");
        // // Sets the start color of the rectangle bar
        linearGradient.append("stop")
            .attr("offset"."0%")
            .attr("stop-color".'#8ABCF4');
        // // Set the end color
        linearGradient.append("stop")
            .attr("offset"."100%")
            .attr("stop-color".'#18669A');

        svg.append("rect")
            // The upper-left coordinates of the x,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 the rectangle
            .attr("width".16)
            .attr("height".83)
            // Reference the id above to set the color
            .style("fill"."url(#" + linearGradient.attr("id") + ")");
        // Set the text

        // Initial data 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 end 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)}}Copy the code