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:
- Visualizing population density on JavaScript Maps
- Preparing Gridded Data for visualization
- 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
- Install the npm package.
npm install --save @maptiler/sdk @maptiler/weather
-
Include the CSS file.
If you have a bundler that can handle CSS, you can
importthe 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' />
- 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,
});
-
Replace
YOUR_MAPTILER_API_KEY_HEREwith your own API key. Make sure to secure the key before you publish it. -
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:
- 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`
);
});
- 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');
});
-
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.
-
Create a slider bar to animate the population layer by years. Add this to your
htmlfile.
<div id="time-info">
<span id="time-text"></span>
<input type="range" id="time-slider" min="0" max="0" step="1">
</div>
- 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"}));
- Style the slide bar. Add this lines to your
cssfile
#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;
}
-
Finally, we will add the functionality of showing the population density corresponding to the cursor position.
-
Create the
HTMLelements to display the population density values. Add this lines in thehtmlfile
<div id="variable-name">Population</div>
<div id="pointer-data"></div>
- Style the text that displays population density values. Add into the
cssfile
#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;
}
- 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;
- Capture the
mousemoveevent 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);
});
- We already have our map working, now we are going to add a button into the
htmlto 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>
- Style the button and position it next to the time slider. Add this lines to your
cssfile.
.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;
}
- 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;
- 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;
});
- 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();
}
- Congratulations, you have created an animated map that shows global population density between the years 2000 and 2020.
npm install --save @maptiler/sdk @maptiler/weather
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;
});
<!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>
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;
}
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
Related examples
Weather custom popup
ExamplesCreate highly customizable popups for weather maps using MarkerLayout.
Weather+ pressure isolines layer
ExamplesVisualize atmospheric pressure isolines using Weather Plus datasets.