Visualizing time series data on the map

In this article, I’ll show how to create a map-based visualization of multi-dimensional data – time, location and value. As an example, I’m going to use the public daily statistics for nCoV-2019 “coronavirus” outbreak.

By the end of January 2020, the “coronavirus” outbreak started to become one of the top world concerns. Despite the high amount of information that authorities and news agencies provide every day, it is surprisingly hard to see the big picture behind all those numbers.

As most situational data, virus outbreak numbers have a clear location component. Therefore map-based visualization can be extremely helpful.

We’re going to take the raw data and create a simple web-based dashboard that looks like this:

Coronavirus outbreak visualization
Coronavirus outbreak visualization

Preparing data

As the first step, I took the daily statistics data kindly made available here by the Center for Systems Science and Engineering at John Hopkins University.

Raw data were normalized, aggregated by date and geocoded with Geoapify Geocoding API. For convenience, the resulting data for each day were represented in the GeoJSON format, that looks like this:

{
        "type": "FeatureCollection",
        "features": [{
          "type": "Feature",
          "geometry": {
            "type": "Point",
            "coordinates": [121.4554, 31.20327]
          },
          "properties": {
            "Place": "Shanghai",
            "Confirmed": 9
          }
        },
        ...
}

Configuring map

As the next step, I created a simple webpage with Mapbox GL data visualization library, “dark” basemap and two layers – “case-circles” and “case-labels”. Both layers configured to use the GeoJSON data source and dynamically take the number of confirmed cases from each GeoJSON Feature “properties.Confirmed” field.

// please get your own free ApiKey at https://myprojects.geoapify.com
var geoapifyApiKey = '7e99a2fb2e9b41ae9e40742f24c33d75';

var map = new mapboxgl.Map({
  container: 'map',
  style: `https://maps.geoapify.com/v1/styles/dark-matter/style.json?apiKey=${geoapifyApiKey}`,
  center: [15, 21],
  refreshExpiredTiles: false,
  zoom: 1.1
});

map.on('load', function() {
  map.addSource('cases', {
    'type': 'geojson',
    data: null
  });

  map.addLayer({
    'id': 'case-circles',
    'type': 'circle',
    'source': 'cases',
    'paint': {
      'circle-color': '#FF0000',
      'circle-opacity': 0.5,
      'circle-radius': [
        'interpolate',
        ["exponential", 0.9],
        ['get', 'Confirmed'],
        1,
        5,
        5000,
        15
      ]
    }
  });

  map.addLayer({
    'id': 'case-labels',
    'type': 'symbol',
    'source': 'cases',
    'layout': {
      'text-field': [
        'to-string', ['get', 'Confirmed']
      ],
      'text-font': [
        'Open Sans Bold',
        'Arial Unicode MS Bold'
      ],
      'text-size': 12
    },
    'paint': {
      'text-color': 'rgba(0,0,0,0.5)'
    }
  });
});

Due to the significant difference in the number of confirmed cases per location, the “case-circles” layer was configured to use an exponential scale, to emphasize new locations as they appear on the map.

Adding interactivity

Additionally, I’ve added the date slide that triggers map update accordingly to the selected date and allows to see outbreak dynamics.

<div class="map-overlay top">
    <div class="map-overlay-inner">
        <h2 id="date"></h2>
        <input id="slider" type="range" min="0" max="1" step="1" value="0" />
        <h2 id="count">Confirmed cases</h2>
    </div>
</div>

<script>
var casesHistory = getCasesHistory()
var dates = Object.keys(casesHistory)

document.getElementById('slider').max = dates.length - 1

function updateMap(dateId) {
  var date = dates[dateId]
  var casesForDate = casesHistory[date]
  document.getElementById('date').textContent = `Date: ${date}`;
  document.getElementById('count').textContent = `Confirmed cases: ${casesForDate.total}`;
  map.getSource('cases').setData(casesForDate.geojson);
}

document
  .getElementById('slider')
  .addEventListener('input', function(e) {
    var dateId = parseInt(e.target.value, 10);
    updateMap(dateId);
});

updateMap(0);
</script>

Going further

I hope that you now have the general idea of how to create similar visualizations. To make it easier, I’ve prepared a free JSFiddle example to help you get started.

Still too complicated? Don’t worry, and get in touch with us. We’ll be happy to help you with your project.

Map animation with Mapbox GL

What can be more eye-catching than smooth, interactive, three-dimensional map animation? Impress your visitors by displaying your map data in stunning 3D!

Previously we have compared OpenLayers and Leaflet map libraries. Both are great choices if you want to add interactive map to your website. They are free, battle-tested, extensible and supported by active communities.

But what if you need a very fast, animated, eye-catching 3D map? Both Leaflet API and OpenMapLayersAPI do not support 3D and free-form map rotation. In this case Mapbox GL map library can be a great option.

Mapbox GL JS is one of the most advanced JavaScript map rendering libraries when it comes to smooth interactive animation. Mapbox GL API is slightly harder to use and not compatible with existing Leaflet plugins and examples. But it is using hardware-accelerated WebGL technology to dynamically draw data with the speed and smoothness of a video game.

In this article I’ll show you how to animate and implement 3D map rotation around a selected point. The end result should look like this:

Map rotation animation in 3D with Mapbox GL

If you short on time and just looking for the complete HTML code, you can find it at the bottom of the page.

Setting up our 3D map

As first step, we need to add imports for Mapbox GL library and its CSS style sheet into the HEAD of our page

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/mapbox-gl/0.53.1/mapbox-gl.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/mapbox-gl/0.53.1/mapbox-gl.js"></script>

Once imported, we can define a <div> element which will host map visualization and add Javascript code which will initialize Mapbox GL to render our map in 3D with specified initial location, zoom, pitch and bearing. Please do not forget to put your real API key instead of “YOUR_API_KEY” placeholder in the map tiles URL.

<div id='map'></div>
<script>
var map = new mapboxgl.Map({
    container: 'map',
    style: {
        "version": 8,
        "sources": {
            "basemap": {
                "type": "raster",
                // map tile source 
                "tiles": [
                    "https://maps.geoapify.com/v1/tile/carto/{z}/{x}/{y}.png?api_key=YOUR_API_KEY"
                ],
                "tileSize": 256
            }
        },
        "layers": [{
            "id": "basemap",
            "type": "raster",
            "source": "basemap",
            "minzoom": 0,
            "maxzoom": 22
        }]
    },
    center: [-73.991462, 40.724637], // starting position
    zoom: 12, // starting zoom
    pitch: 60, // starting pitch in degrees
    bearing: 0, // starting bearing in degree
});

This should give us basic interactive 3D map. You should be able to move the map to a different locations, zoom in and out, and change view angle.

Interactive 3D map view of Manhattan, NY
Interactive 3D map view of Manhattan, NY

Adding map rotation animation

As next step, let’s add dynamic map animation, which will change view angle to create effect of flying around the map center. Please add the following code into our <script> block and refresh the page:

function rotateCamera(timestamp) {
    // rotate at approximately ~10 degrees per second
    map.rotateTo((timestamp / 100) % 360, {duration: 0});
    // request the next frame of the animation
    requestAnimationFrame(rotateCamera);
}

map.on('load', function () {
    // start the animation
    rotateCamera(0);
});

Final steps

As the last step, let’s add basic map navigation controls and basemap attribution.

// map navigation controls
map.addControl(new mapboxgl.NavigationControl());

// attribution
map.addControl(new mapboxgl.AttributionControl({
	compact: false,
	customAttribution: 'Powered by <a href="https://geoapify.com/">Geoapify</a> | © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}));
Complete map with controls and attribution
Complete map with controls and attribution

Complete HTML code

Final version of the HTML page could look like this. Please feel free to copy the code, insert API key and open it in your browser to see how it works.

<html>
<head>
    <meta charset='utf-8' />
    <title>Mapbox GL example: rotate map animation in 3D</title>
    <meta name='viewport' content='initial-scale=1,maximum-scale=1,user-scalable=no' />
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/mapbox-gl/0.53.1/mapbox-gl.css" />
    <script src="https://cdnjs.cloudflare.com/ajax/libs/mapbox-gl/0.53.1/mapbox-gl.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>
var map = new mapboxgl.Map({
    container: 'map',
    style: {
        "version": 8,
        "sources": {
            "basemap": {
                "type": "raster",
                // map tile source 
                "tiles": [
                    "https://maps.geoapify.com/v1/tile/carto/{z}/{x}/{y}.png?api_key=YOUR_API_KEY"
                ],
                "tileSize": 256
            }
        },
        "layers": [{
            "id": "basemap",
            "type": "raster",
            "source": "basemap",
            "minzoom": 0,
            "maxzoom": 22
        }]
    },
    center: [-73.991462, 40.724637], // starting position
    zoom: 12, // starting zoom
    pitch: 60, // starting pitch in degrees
    bearing: 0, // starting bearing in degree
});

function rotateCamera(timestamp) {
    // rotate at approximately ~10 degrees per second
    map.rotateTo((timestamp / 100) % 360, {duration: 0});
    // request the next frame of the animation
    requestAnimationFrame(rotateCamera);
}

map.on('load', function () {
    // start the animation
    rotateCamera(0);
});

// map navigation controls
map.addControl(new mapboxgl.NavigationControl());

// attribution
map.addControl(new mapboxgl.AttributionControl({
	compact: false,
	customAttribution: 'Powered by <a href="https://geoapify.com/">Geoapify</a> | © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}));
	
</script>

</body>
</html>

Summary

Mapbox GL generally requires more JavaScript code to be written than Leaflet and can be more complicated to maintain.

So, you may ask when to use Leaflet and when to use Mapbox GL?

The answer is simple – if you don’t need 3D and extremely fast animation then Leaflet would be the best choice. It has biggest community, best documentation, extensive set of plugins and works well in any situation. And if you really need advanced 3D maps with animations and top rendering speed on modern devices – then Mapbox GL is your friend.