Visualize and animate the evolution of population data

In this tutorial, we will learn how to visualize and animate the evolution of the Global population density data with the MapTiler SDK Weather module.

Along with this tutorial, there is a news article about advanced data visualization and two more tutorials on preparing the data and processing it in MapTiler Engine:

  1. Visualizing population density on JavaScript Maps
  2. Preparing Gridded Data for visualization
  3. Global Population Density Data Processing

At MapTiler, we have developed the Weather library and we provide data to create many different animated layer types (temperature, wind with particles, cloud coverage, etc.) in a super easy way. This library could also be used for non-weather raster data visualization, animated and interpolated over time. And this is exactly what we are going to do with the population density!

NPM module setup

  1. Install the npm package.

npm install --save @maptiler/sdk @maptiler/weather
  1. Include the CSS file.

    If you have a bundler that can handle CSS, you can import the CSS or include it with a <link> in the head of the document via the CDN


import "@maptiler/sdk/dist/maptiler-sdk.css";

<link href='https://cdn.maptiler.com/maptiler-sdk-js/v4.1.0/maptiler-sdk.css' rel='stylesheet' />
  1. Since we are going to display the population data on a map, let’s first create an instance of a map. Include the following code in your JavaScript file (Example: app.js).

import { Map, MapStyle, config } from '@maptiler/sdk';
    import '@maptiler/sdk/dist/maptiler-sdk.css';
    import { ColorRamp, TileLayer, GradientColoringFragment } from '@maptiler/weather';
    
    config.apiKey = 'YOUR_MAPTILER_API_KEY_HERE';
    const map = Map({
      container: 'map', // container's id or the HTML element to render the map
      style: MapStyle.BACKDROP,
      center: [85.4833, 27.6098], // starting position [lng, lat]
      zoom: 9.71, // starting zoom
      hash: true,
    });
  1. Replace YOUR_MAPTILER_API_KEY_HERE with your own API key. Make sure to secure the key before you publish it.

  2. Create a custom layer using the MapTiler Weather Tilelayer. The TileLayer consists of multiple timeframes, every frame is a tile pyramid at a different moment in time. The individual frames are smoothly animated.


const customLayer = new TileLayer('population', {minZoom: 0, maxZoom: 7},
      [
        new GradientColoringFragment({
          decode: {
            // We use the red (`r`) channel but in this case, the same value is repeated on green and blue
            // so it does not really matter which channel we chose.
            channel: 'r',
            // Since we have encoded the square root of the density, the min and max are [0, 255]
            // which in terms of density means from 0 to 255*255 (65025)
            min: 0,
            max: 255,
          },
          // We scale the TURBO color ramp to go from `0` to `sqrt(45000)`
          // so that the max color of the color ramp represents a density of 45k people per sqkm
          stops: ColorRamp.builtin.TURBO.scale(0, Math.sqrt(45000)),
          smooth: true,
          opacity: 1
        }),
      ]
    );

We use the TURBO color ramp because it has a linear luminance, but there are more colorblind-friendly alternatives if your application requires it (for example, CIVIDIS).

Here is what the TURBO color ramp looks like:

  1. Adding the tilesets from MapTiler as sources. In this example, we will use the population density tilesets generated in the Global Population Density Data Processing tutorial.

// Description of the tileset per year
    const sources = [
      {
        year: 2000,
        tilesetID: "YOUR_MAPTILER_YEAR_2000_DATASET_ID_HERE"
      },
      {
        year: 2005,
        tilesetID: "YOUR_MAPTILER_YEAR_2005_DATASET_ID_HERE"
      },
      {
        year: 2010,
        tilesetID: "YOUR_MAPTILER_YEAR_2010_DATASET_ID_HERE"
      },
      {
        year: 2015,
        tilesetID: "YOUR_MAPTILER_YEAR_2015_DATASET_ID_HERE"
      },
      {
        year: 2020,
        tilesetID: "YOUR_MAPTILER_YEAR_2020_DATASET_ID_HERE"
      },
    ];

    sources.forEach((src) => {
      customLayer.addSource(
        // We use the first of January for each year
        Date.parse(`${src.year}-01-01T00:00:00.000Z`), 
        // Note the usage of the {zxy} pattern
        `https://api.maptiler.com/tiles/${src.tilesetID}/{zxy}.png`
      ); 
    });
  1. Add the custom layer to the map

map.on('load', function () {
      map.setPaintProperty("Water", 'fill-color', "rgba(0, 0, 0, 0.4)");
      map.addLayer(customLayer, 'Water');
    });
  1. If we reload the map, we can see the population layer corresponding to the first dataset (year 2000). Next, we are going to add a time bar to our application so we can animate the population layer.

  2. Create a slider bar to animate the population layer by years. Add this to your html file.


<div id="time-info">
      <span id="time-text"></span>
      <input type="range" id="time-slider" min="0" max="0" step="1">
    </div>
  1. Add the functionality to the slide bar to select dates between the years 2000 and 2020.

const timeInfoContainer = document.getElementById("time-info");
    const timeTextDiv = document.getElementById("time-text");
    const timeSlider = document.getElementById("time-slider");

    timeSlider.min = Date.parse("2000-01-01T00:00:00.000Z");
    timeSlider.max = Date.parse("2020-01-01T00:00:00.000Z");

    timeSlider.addEventListener("input", etv => {
      customLayer.setAnimationTime(parseInt(timeSlider.value));
      timeTextDiv.innerText = (new Date(parseInt(timeSlider.value)).toLocaleDateString("en", {year: "numeric"}));
    })

    timeTextDiv.innerText = (new Date("2000-01-01T00:00:00.000Z").toLocaleDateString("en", {year: "numeric"}));
  1. Style the slide bar. Add this lines to your css file

#time-info {
      position: fixed;
      width: 60vw;
      bottom: 0;
      z-index: 1;
      margin: 10px;
      text-shadow: 0px 0px 5px black;
      color: white;
      font-size: 18px;
      font-weight: 500;
      text-align: center;
      left: 0;
      right: 0;
      margin: auto;
      padding: 20px;
    }

    #time-text {
      font-size: 22px;
      font-weight: 600;
    }

    #time-slider {
      width: 100%;
      height: fit-content;
      left: 0;
      right: 0;
      z-index: 1;
      filter: drop-shadow(0 0 7px #000a);
      margin-top: 10px;
    }
  1. Finally, we will add the functionality of showing the population density corresponding to the cursor position.

  2. Create the HTML elements to display the population density values. Add this lines in the html file


<div id="variable-name">Population</div>
    <div id="pointer-data"></div>
  1. Style the text that displays population density values. Add into the css file

#pointer-data {
      z-index: 1;
      position: fixed;
      font-size: 20px;
      font-weight: 900;
      margin: 27px 0px 0px 10px;
      color: #fff;
      text-shadow: 0px 0px 10px #0007;
    }

    #variable-name {
      z-index: 1;
      position: fixed;
      font-size: 20px;
      font-weight: 500;
      margin: 5px 0px 0px 10px;
      color: #fff;
      text-shadow: 0px 0px 10px #0007;
    }
  1. Select the HTML element where to display the information related to the population density and create the variable where to save the cursor position.

const pointerDataDiv = document.getElementById("pointer-data");
    let pointerLngLat = null;
  1. Capture the mousemove event to display the population density values ​​corresponding to the cursor position.

map.on('mouseout', function(evt) {
        if (!evt.originalEvent.relatedTarget) {
          pointerDataDiv.innerText = "";
          pointerLngLat = null;
        }
      });

      function updatePointerValue(lngLat) {
        if (!lngLat) return;
        pointerLngLat = lngLat;
        const value = customLayer.pick(lngLat.lng, lngLat.lat);
        if (!value) {
          pointerDataDiv.innerText = "";
          return;
        }
        pointerDataDiv.innerText = `${value[0].toFixed(1)} people/km²`
      }

      map.on('mousemove', (e) => {
        updatePointerValue(e.lngLat);
      });
  1. We already have our map working, now we are going to add a button into the html to play/pause the map animation.

<div class="slider-container">
      <button id="play-pause-bt" class="button"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-play-fill" viewBox="0 0 16 16">
          <path d="m11.596 8.697-6.363 3.692c-.54.313-1.233-.066-1.233-.697V4.308c0-.63.692-1.01 1.233-.696l6.363 3.692a.802.802 0 0 1 0 1.393"/>
        </svg></button>
      <input type="range" id="time-slider" min="0" max="0" step="1">
    </div>
  1. Style the button and position it next to the time slider. Add this lines to your css file.

.slider-container {
      display: flex;
    }

    .button {
      cursor: pointer;
      width: auto;
      padding: 8px;
      border-radius: 25px;
      font-size: 10px;
      text-align: center;
      font-family: sans-serif;
      font-weight: bold;
      margin-right: 8px;
      border-width: 0;
    }
  1. Get the button to add some events so you can animate the map.

const pointerDataDiv = document.getElementById("pointer-data");
    const playPauseButton = document.getElementById("play-pause-bt");
    let pointerLngLat = null;
  1. Play/pause the animation when the button is clicked.

// When clicking on the play/pause
    let isPlaying = false;
    playPauseButton.addEventListener("click", () => {
      if (isPlaying) {
        customLayer.animateByFactor(0);
        playPauseButton.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-play-fill" viewBox="0 0 16 16">
          <path d="m11.596 8.697-6.363 3.692c-.54.313-1.233-.066-1.233-.697V4.308c0-.63.692-1.01 1.233-.696l6.363 3.692a.802.802 0 0 1 0 1.393"/>
        </svg>`;
      } else {
        customLayer.animateByFactor((3.154e+10)*2); //aprox millisecond in a year
        playPauseButton.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-pause-fill" viewBox="0 0 16 16">
          <path d="M5.5 3.5A1.5 1.5 0 0 1 7 5v6a1.5 1.5 0 0 1-3 0V5a1.5 1.5 0 0 1 1.5-1.5m5 0A1.5 1.5 0 0 1 12 5v6a1.5 1.5 0 0 1-3 0V5a1.5 1.5 0 0 1 1.5-1.5"/>
        </svg>`;
      }

      isPlaying = !isPlaying;
    });
  1. Update the time slider and date based on the animation time.

// Called when the animation is progressing
    customLayer.on("tick", () => {
      refreshTime();
      updatePointerValue(pointerLngLat);
    });

    // Update the date time display
    function refreshTime() {
      const d = Number.parseInt(customLayer.getAnimationTime().toFixed(0));
      timeTextDiv.innerText = (new Date(d).toLocaleDateString("en", {year: "numeric"}));
      timeSlider.value = d.toString();
    }
  1. Congratulations, you have created an animated map that shows global population density between the years 2000 and 2020.

Conclusion

As we can see in the example when it comes to time-wise interpolation and animation, the population density has not changed that much since the year 2000, so we only see minor variations.

This is just an example of what we can build. Here are other ideas of visualization you could build using a comparable process as a hobby or that could suit your business:

  • Global Carbon Emissions
  • Global Temperature Anomalies
  • World GDP Growth
  • Life Expectancy Trends
  • Global Energy Consumption
  • Income Inequality
  • Natural Disaster Frequency
  • Global Internet Penetration
  • Education and Literacy Rates
  • Global Health Indicators
  • Biodiversity Loss
  • Access to Clean Water
  • Global Hunger and Food Security
  • Mental Health Statistics
  • Global Education Spending
  • Political Freedom and Press Freedom
  • Custom Weather data

If the data is available, then we can visualize it!

Learn more

Check out the Weather JS module reference

Visualizing population density on JavaScript Maps

Preparing Gridded Data for visualization

Global Population Density Data Processing

Weather custom popup

Weather custom popup

Examples

Create highly customizable popups for weather maps using MarkerLayout.

Weather layer switcher

Weather layer switcher

Examples

Switch weather layers and view detailed data under cursors.

Weather radar layer

Weather radar layer

Examples

Visualize weather radar layers and view localized radar data.

Weather Plus pressure isolines layer

Weather+ pressure isolines layer

Examples

Visualize atmospheric pressure isolines using Weather Plus datasets.

Was this helpful?