3D globe map in D3.js – Populated Places on Earth 🌍

3 min read

#30DayMapChallenge Day 1. Points

Populated Places on Earth 🌍

3D globe map in D3.js and Canvas - page view
Points

This is a 3D globe map in D3.js and Canvas, that I have made for the #30DayMapChallenge on Twitter in 2021. You can find out more about the challenge and my other maps in this post.

Goal: A map with points.

Solution: Display cities on a globe map.

Live: https://maptheclouds.com/playground/30-day-map-challenge/points/

Tweet: https://twitter.com/maptheclouds/status/1455203199716143121

Code: https://github.com/alexaac/map-challenges/tree/master/points

Description:

The visualization shows the most populated places on Earth. The bigger the circle, the greater the population.
Cities with a bigger rank are more brightly coloured.

Data: Natural Earth

Tags: #D3js #QGIS #mapshaper #canvas #interaction #globe #earth

Inspiration:

https://stackoverflow.com/questions/15907171/d3-canvas-globe-mouse-events
https://observablehq.com/@alainro/can-quadtree-speed-up-zoom-of-many-points-on-a-canvas

3D globe map in D3.js and Canvas – making of

3D globe map in D3.js and Canvas - magnified view
3D globe map in D3.js and Canvas - gif animation

Challenges: For the 3D globe map in D3.js and Canvas, I have downloaded the countries and cities from Natural Earth, because it was one of the free data sources for the challenge. First, I have explored it in QGIS, then I started mapping it on a globe using D3.js

The user can interact with the map by zooming, panning and clicking the cities, however, if you click at a smaller zoom you might not see the city so well. To improve the user interaction, I have set the map to rotate and center on the selected city.

The biggest challenge was to visualize many points on the globe and keep the interaction fast enough at the same time. In order to address this, I have chosen to draw the points using Canvas instead of loading the data as D3.js selections in the DOM.

// Draw the cities
  citiesData.features.forEach((city) => {
    context.beginPath();

    cityPath(city);

    context.lineWidth = 0.5 / (transform.k < 1 ? 1 : transform.k);
    context.strokeStyle = '#000';
    context.stroke();

    context.fillStyle = colorScale(city.properties.pop_max);
    context.fill();
  });

The issue with canvas is that you can’t interact directly with the points anymore, so I had to do a reverse search based on the mouse click to find the nearest point and display a label for it.

/* Get coordinates at mouse click, transform them into 
     geographic coordinates and search nearest cities */
  function mapClick() {
    // Can't apply transformations unless scale 1
    if (transform.k !== 1) return;

    var mousePos = d3.mouse(this);
    var p = projection.invert(mousePos);

    if (p == undefined || !p[0] || !p[1]) {
      return false;
    }

    const city = findCityAtMapClick(mousePos, width, height, renderArgs);

    if (city) {
      selected = city;

      // Mutates projection - TODO try to use pure functions
      transition(city.geometry.coordinates, renderArgs);
    }
  }

Another solution that helped was to use d3.quadtree to speed up the search.

  const tree = d3
    .quadtree()
    .extent([
      [-1, -1],
      [width + 1, height + 1],
    ])
    .x((d) => d.geometry.coordinates[0])
    .y((d) => d.geometry.coordinates[1])
    .addAll(citiesData.features);

  const found = search(projection, tree, mousePos);

TODO:

It would be nice to better refine the search, so that the city is being picked more accurate on mouse click, and make the globe spin on left click, but I will leave those improvements for another challenge. 🙂

To conclude, I must say I have enjoyed making this map and I have learned more about globe maps in D3.js and Canvas interactions in general.

Leave a Reply

Your email address will not be published. Required fields are marked *