---
title: "Visualize and animate the evolution of population data | Weather SDK | JavaScript maps SDK"
description: "Visualize and animate global population density using weather modules."
---

> For AI agents: a documentation index is available at [/llms.txt](https://docs.maptiler.com/llms.txt). Markdown versions of all pages are available by appending .md to any URL.

# 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](https://www.maptiler.com/news/2024/08/visualizing-population-density-on-javascript-maps/)
2.  [Preparing Gridded Data for visualization](https://docs.maptiler.com/guides/map-design/preparing-gridded-raster-data-for-visualization)
3.  [Global Population Density Data Processing](https://docs.maptiler.com/guides/map-tiling-hosting/data-processing/global-population-density-data-processing-with-maptiler-engine)

At MapTiler, we have developed the [Weather library](https://docs.maptiler.com/sdk-js/modules/weather/) 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.

```bash
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
    

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

```html
<link href='https://cdn.maptiler.com/maptiler-sdk-js/v4.2.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).

```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](https://cloud.maptiler.com/account/keys/). Make sure to [secure the key](https://docs.maptiler.com/guides/credentials/api-key/#secure-key) before you publish it.
    
2.  Create a custom layer using the MapTiler Weather `Tilelayer`. The [TileLayer](https://docs.maptiler.com/sdk-js/modules/weather/api/intensity/) consists of multiple timeframes, every frame is a tile pyramid at a different moment in time. The individual frames are smoothly animated.
    

```js
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](https://docs.maptiler.com/guides/map-tiling-hosting/data-processing/global-population-density-data-processing-with-maptiler-engine) tutorial.

```js
// 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

```js
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.
    

```html
<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.

```html
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

```css
#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
    

```html
<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

```css
#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.

```js
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.

```js
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.

```html
<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.

```css
.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.

```js
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.

```js
// 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.

```js
// 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.

### Complete source code

```bash
npm install --save @maptiler/sdk @maptiler/weather
```

#### main.js

```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 = (window.map = new Map({
  container: 'map', // container's id or the HTML element to render the map
  style: MapStyle.BACKDROP,  // stylesheet location
  zoom: 9.71,
  center: [85.4833, 27.6098],
  hash: true,
}));

const timeInfoContainer = document.getElementById("time-info");
const timeTextDiv = document.getElementById("time-text");
const timeSlider = document.getElementById("time-slider");
const pointerDataDiv = document.getElementById("pointer-data");
const playPauseButton = document.getElementById("play-pause-bt");
let pointerLngLat = null;

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

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

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

map.on('load', function () {
  map.setPaintProperty("Water", 'fill-color', "rgba(0, 0, 0, 0.4)");
  map.addLayer(customLayer, 'Water');
});

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

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

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

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

#### index.html

```html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="robots" content="noindex">
  <title>Global Population Density From 2000 to 2020</title>
  <link rel="stylesheet" href="./style.css">
</head>
<body>
  <div id="time-info">
    <span id="time-text"></span>
    <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>
  </div>
  <div id="variable-name">Population</div>
  <div id="pointer-data"></div>
  <div id="map"></div>
  <script type="module" src="./main.js"></script>
</body>
</html>
```

#### style.css

```css
body { margin: 0; padding: 0; font-family: sans-serif; }
#map { position: absolute; top: 0; bottom: 0; width: 100%; }
#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;
}

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

.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;
}
```

## Basic JavaScript setup

1.  Since we are going to display the population data on a map, let’s first create an instance of a map. Copy the following code, paste it into your favorite text editor, and save it as a `.html` file.

```html
<!DOCTYPE html>
      <html lang="en">
      <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Global Population Density From 2000 to 2020</title>
        <script src="https://cdn.maptiler.com/maptiler-sdk-js/v4.2.0/maptiler-sdk.umd.min.js"></script>
        <link href="https://cdn.maptiler.com/maptiler-sdk-js/v4.2.0/maptiler-sdk.css" rel="stylesheet" />
        <script src="https://cdn.maptiler.com/maptiler-weather/v4.0.1/maptiler-weather.umd.min.js"></script>
        <style>
          body { margin: 0; padding: 0; }
          #map { position: absolute; top: 0; bottom: 0; width: 100%; }
        </style>
      </head>
      <body>
        <div id="map"></div>
        <script>
            maptilersdk.config.apiKey = 'YOUR_MAPTILER_API_KEY_HERE';      
            const map = new maptilersdk.Map({
              container: 'map', // container's id or the HTML element to render the map
              style: maptilersdk.MapStyle.BACKDROP,
              zoom: 9.71,
              center: [85.4833, 27.6098],
              hash: true,
            });
        </script>
      </body>
      </html>
```

1.  Replace `YOUR_MAPTILER_API_KEY_HERE` with [your own API key](https://cloud.maptiler.com/account/keys/). Make sure to [secure the key](https://docs.maptiler.com/guides/credentials/api-key/#secure-key) before you publish it.
    
2.  Create a custom layer using the MapTiler Weather `Tilelayer`. The [TileLayer](https://docs.maptiler.com/sdk-js/modules/weather/api/intensity/) consists of multiple timeframes, every frame is a tile pyramid at a different moment in time. The individual frames are smoothly animated.
    

```js
const customLayer = new maptilerweather.TileLayer('population', {minZoom: 0, maxZoom: 7},
      [
        new maptilerweather.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: maptilerweather.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](https://docs.maptiler.com/guides/map-tiling-hosting/data-processing/global-population-density-data-processing-with-maptiler-engine) tutorial.

```js
// 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

```js
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.
    

```html
<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.

```html
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.

```css
#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.
    

```html
<div id="variable-name">Population</div>
    <div id="pointer-data"></div>
```

1.  Style the text that displays population density values.

```css
#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.

```js
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.

```js
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 to play/pause the map animation.

```html
<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

```css
.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.

```js
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.

```js
// 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.

```js
// 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.

### Complete source code

```html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Global Population Density From 2000 to 2020</title>
  <script src="https://cdn.maptiler.com/maptiler-sdk-js/v4.2.0/maptiler-sdk.umd.min.js"></script>
  <link href="https://cdn.maptiler.com/maptiler-sdk-js/v4.2.0/maptiler-sdk.css" rel="stylesheet" />
  <script src="https://cdn.maptiler.com/maptiler-weather/v4.0.1/maptiler-weather.umd.min.js"></script>
  <style>
    body { margin: 0; padding: 0; font-family: sans-serif; }
    #map { position: absolute; top: 0; bottom: 0; width: 100%; }
    #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;
    }

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

    .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;
    }
  </style>
</head>
<body>
  <div id="time-info">
    <span id="time-text"></span>
    <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>
  </div>
  <div id="variable-name">Population</div>
  <div id="pointer-data"></div>
  <div id="map"></div>
  <script>
      maptilersdk.config.apiKey = 'YOUR_MAPTILER_API_KEY_HERE';
      const map = (window.map = new maptilersdk.Map({
        container: 'map', // container's id or the HTML element to render the map
        style: maptilersdk.MapStyle.BACKDROP,  // stylesheet location
        zoom: 9.71,
        center: [85.4833, 27.6098],
        hash: true,
      }));

      const timeInfoContainer = document.getElementById("time-info");
      const timeTextDiv = document.getElementById("time-text");
      const timeSlider = document.getElementById("time-slider");
      const pointerDataDiv = document.getElementById("pointer-data");
      const playPauseButton = document.getElementById("play-pause-bt");
      let pointerLngLat = null;

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

      const customLayer = new maptilerweather.TileLayer('population', {minZoom: 0, maxZoom: 7},
        [
          new maptilerweather.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: maptilerweather.ColorRamp.builtin.TURBO.scale(0, Math.sqrt(45000)),
            smooth: true,
            opacity: 1
          }),
        ]
      );

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

      map.on('load', function () {
        map.setPaintProperty("Water", 'fill-color', "rgba(0, 0, 0, 0.4)");
        map.addLayer(customLayer, 'Water');
      });

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

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

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

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

  </script>
</body>
</html>
```

### Complete source code

```html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Global Population Density From 2000 to 2020</title>
  <script src="https://cdn.maptiler.com/maptiler-sdk-js/v4.2.0/maptiler-sdk.umd.min.js"></script>
  <link href="https://cdn.maptiler.com/maptiler-sdk-js/v4.2.0/maptiler-sdk.css" rel="stylesheet" />
  <script src="https://cdn.maptiler.com/maptiler-weather/v4.0.1/maptiler-weather.umd.min.js"></script>
  <style>
    body { margin: 0; padding: 0; font-family: sans-serif; }
    #map { position: absolute; top: 0; bottom: 0; width: 100%; }
    #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;
    }

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

    .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;
    }
  </style>
</head>
<body>
  <div id="time-info">
    <span id="time-text"></span>
    <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>
  </div>
  <div id="variable-name">Population</div>
  <div id="pointer-data"></div>
  <div id="map"></div>
  <script>
      maptilersdk.config.apiKey = 'YOUR_MAPTILER_API_KEY_HERE';
      const map = (window.map = new maptilersdk.Map({
        container: 'map', // container's id or the HTML element to render the map
        style: maptilersdk.MapStyle.BACKDROP,  // stylesheet location
        zoom: 9.71,
        center: [85.4833, 27.6098],
        hash: true,
      }));

      const timeInfoContainer = document.getElementById("time-info");
      const timeTextDiv = document.getElementById("time-text");
      const timeSlider = document.getElementById("time-slider");
      const pointerDataDiv = document.getElementById("pointer-data");
      const playPauseButton = document.getElementById("play-pause-bt");
      let pointerLngLat = null;

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

      const customLayer = new maptilerweather.TileLayer('population', {minZoom: 0, maxZoom: 7},
        [
          new maptilerweather.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: maptilerweather.ColorRamp.builtin.TURBO.scale(0, Math.sqrt(45000)),
            smooth: true,
            opacity: 1
          }),
        ]
      );

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

      map.on('load', function () {
        map.setPaintProperty("Water", 'fill-color', "rgba(0, 0, 0, 0.4)");
        map.addLayer(customLayer, 'Water');
      });

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

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

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

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

  </script>
</body>
</html>
```

## 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](https://docs.maptiler.com/sdk-js/modules/weather/)

[Visualizing population density on JavaScript Maps](https://www.maptiler.com/news/2024/08/visualizing-population-density-on-javascript-maps/)

[Preparing Gridded Data for visualization](https://docs.maptiler.com/guides/map-design/preparing-gridded-raster-data-for-visualization)

[Global Population Density Data Processing](https://docs.maptiler.com/guides/map-tiling-hosting/data-processing/global-population-density-data-processing-with-maptiler-engine)

## Related examples

### [Weather custom popup](https://docs.maptiler.com/sdk-js/examples/weather-custom-popup/)

Examples

Create highly customizable popups for weather maps using MarkerLayout.

### [Weather layer switcher](https://docs.maptiler.com/sdk-js/examples/weather-layer-switcher/)

Examples

Switch weather layers and view detailed data under cursors.

### [Weather radar layer](https://docs.maptiler.com/sdk-js/examples/weather-radar/)

Examples

Visualize weather radar layers and view localized radar data.

### [Weather+ pressure isolines layer](https://docs.maptiler.com/sdk-js/examples/weather-plus-pressure-isolines/)

Examples

Visualize atmospheric pressure isolines using Weather Plus datasets.
