Use D3 to draw a map while solving the problem that the map coloring fitExtent fails

Using D3 to draw maps

First look at the renderings:

[External link picture transfer failed, the source site may have an anti-leeching mechanism, it is recommended to save the picture and upload it directly (img-ENi85nYQ-1678532273754)(null)]

First, the data geoJson used by D3 to draw maps can be obtained from the Alibaba Cloud data visualization platform:

[External link picture transfer failed, the source site may have an anti-leeching mechanism, it is recommended to save the picture and upload it directly (img-jyYrL5p3-1678532273772)(null)]

Then, among the commonly used projection methods for maps, I choose to use the Mercator projection, which is also commonly used by Baidu and Gaode maps.

We need to get a piece of data in GeoJson format from the data platform. The json data can be imported directly as an Object object, or we can use d3.json to get the remote Json data .This demonstration uses the data of a certain city as an example

  1. Introducing D3

    Native directly via

    Or: npm install d3

2. Create a container for map drawing

<body>
    <svg class="map" style="display: inline-block;"> </svg>
</body>
  1. The initialization of the SVG map container works:

     const svgW = 1000
        const svgH = 1000
        const colors = ['white', 'blue', 'red'];
        const svg = d3. select(".map")
        svg.attr('viewBox', `0 0 1000 1000`)
        svg.attr("width", svgW).attr("height", svgH)
            .attr('xmlns', 'http://www.w3.org/2000/svg')
    

    If you want to achieve responsive web design, set viewBox to get twice the result with half the effort

  2. Create G group and set projection parameters

    const g = svg.append('g').attr("class", 'path-wrap').attr("width", svgW).attr("height", svgH)
    const projection = d3. geoMercator()
            // .fitSize([svgW, svgH], mapJson)
            // Use the latitude and longitude of the center of a city
            .center([116.418757, 39.917544]) //chain writing, .center([longitude, latitude]) sets the center of the map (real dimension) //
            .scale([18000]) //.scale([value]) set the map zoom
        // .translate([svgW / 2, svgH / 2]) //.translate([x,y]) sets the offset
    

    One thing to note here is that the center needs to pass in the latitude and longitude parameters instead of the canvas parameters. We can download higher-level maps for this data, such as the map of Jiangsu Province, which will contain the center longitude and latitude of Nanjing. Use center scale This method belongs to manually adjusting the map to display the map properly. However, the official also provides a tool function that can automatically layout and zoom the map to adapt to the svg size – fitExtent , you can pass in a two-digit array to define the upper left and lower right corners of your canvas

    For example we can write:

    const projection = d3. geoMercator()
            .fitExtent([[0, 0], [svgW, svgH]], mapJson)
    

    or use the shorthand:

    const projection = d3. geoMercator()
            .fitSize([svgW, svgH], mapJson)
    

    fitSize sets the first coordinate data to 0,0 by default

    The second parameter needs a complete geoJson object, note that it is not geo.features so we don’t need center and scale, and they cannot be called after fitExtent , an error will be reported.

  3. Generate route data Draw maps

    const pathGenerator = d3. geoPath()
        .projection(projection)
    

    Pass our configured projection parameters to the projection method of geoPath, and you can get the d attribute of the path tag : MDN_path

    Then we bind the data, d3.data() uses geoJson.features instead of the whole geoJson Be sure to pay attention! In terms of map color, just use an existing d3 color scheme to fill it. If you have other needs, you can understand the configuration by yourself. For specific tutorials, please refer to: https://github.com/d3/d3-scale-chromatic,

    const mapDraw = g. selectAll("path")
            .data(mapJson.features) //data binding
        mapDraw. join("path")
            .attr("class", "continent-path")
            .attr("d", pathGenerator) //draw path
            .attr("stroke-width", 1.60347)
            .attr("stroke", "#000")
            .style("fill", (d, i) => d3.schemeSet2[i % 3])
       
    

    Then we can label our map with geographic names. The projection tool we configured can support passing in a set of geographic coordinates and return a set of two-dimensional coordinates for the positioning of Svg elements. We can usecentroid (centroid) in code>geoJson is used as our positioning benchmark, and it is enough to pass it to the transform attribute after conversion

     mapDraw. join('text').attr("class", "continent-text")
            .attr('transform', (d) => {<!-- -->
                return `translate(${<!-- -->projection(d.properties.centroid)})`
            })
            .text((d) => {<!-- -->
                return d.properties.name
            })
    

If your map only has a small dot, you may have set the wrong center data, be careful not to pass the latitude and longitude backwards

There is a pit you may step on:

Maybe in the fifth step, you will find that the map cannot be rendered, and you may encounter the following problems:

If you download map data from the Alibaba Cloud data platform like me, you may encounter the same problem as me, that is, using center and scale can be displayed normally Map, after using fitExtent the map is a small point or not displayed at all, this is the first pit I encountered:

Because D3 uses ellipsoidal math to calculate the projection of the map, instead of using the Cartesian coordinate system to process the data of geoJson like other tools, and D3 uses the right-hand rule to draw the map, and The existing geoJson specification is the opposite, which will result in the drawing area may also be “inverted”, as shown in the following figure:

[External link picture transfer failed, the source site may have an anti-leeching mechanism, it is recommended to save the picture and upload it directly (img-n7CXTm8A-1678532273824)(null)]

[External link picture transfer failed, the source site may have an anti-leeching mechanism, it is recommended to save the picture and upload it directly (img-dN29lk9Z-1678532273807)(null)]

I am not a related professional, if you need a more detailed explanation, you can move => https://github.com/d3/d3-geo/pull/79#issuecomment-280935242

Solving this problem is also very simple, we can use the turf tool – a geospatial data analysis and processing tool that can be used in browsers and Node to perform conversion,

We need to use the rewind function to ‘rewind’ our data, as the name implies, it is the rewinding operation in the tape, and the reverse winding operation on the geo data,\

Install the tools you need as needed:

npm i @turf/rewind

Process the geoJson data before drawing in D3:

 mapJson.features = mapJson.features.map(function (feature) {
     return turf. rewind(feature, { reverse: true });
 })

Next, use fitExtent to draw the map. Let me compare the effect of manually setting parameters and using fitExtent

[External link picture transfer failed, the source site may have an anti-leeching mechanism, it is recommended to save the picture and upload it directly (img-nKumrjim-1678532273788)(null)]

Manually adjust the zoom and centering problem. It is really difficult to find the appropriate size and offset value. It is better to adjust automatically

Here is the complete code:





    
    
    
    Document
    
    
    



<body>
    <svg class="map" style="display: inline-block;"> </svg>
</body>




All files here => https://github.com/3biubiu/D3ToDrawMap