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.

Administrative boundaries vs postcode boundaries

Maps are often used to visualize statistics. Demographical or socioeconomic data give a great overview of a location and allows to make conclusions about a territory. Often the data is shown are colored regions on the map which represent administrative boundaries or postcode boundaries.

What is the difference between the two types of boundaries?

OK, you have some data and would like to visualize it on a map inside regions. Firstly, you need to decide if you want to show it with administrative boundaries or postcode boundaries. Moreover, you think about how to combine/split up into bigger/smaller regions.

At the moment there are 2 common ways to break down a territory into regions:

  • by postcode or ZIP code boundaries;
  • by administrative boundaries.

For both cases, you need to keep in mind that a number of hierarchy levels may be different from country to country. Mixing the two options is also a bad idea, while then regions may intersect for some locations.

Postcode boundaries to visualize the data

So, you decided to visualize the data by postcode or ZIP code boundaries. This may be the simplest way to split the data by regions. But there are a few details you need to consider.

Firstly, you need to understand that postcodes are not translated into polygons in some standard way. Originally it’s service routes, which are represented as points or street sides. However, there approximated polygons for the post- or ZIP codes boundaries in most GIS data systems, but they may contain holes for some territories.

Secondly, don’t forget that in most cases postcode should go together with the country name. It could happen that locations in different counties have the same postcode. For example, the postcode 1204 could belong to Geneva, Switzerland or Dhaka, Bangladesh.

2 locations have the same postcode
Postcode 1204 is the same for Geneva, Switzerland and Dhaka, Bangladesh

Usually, postcode boundaries have 3-4 levels starting from a country level.

Administrative boundaries to visualize the data

This way to divide territories is more natural from the GIS point of view. The planet is already divided into administrative regions. It’s possible to combine the data by countries and event by continents.

The administrative division has 3-4 levels from county level depending on the size of the territory.

Drawing boundaries on a map and performance

It often happens that GIS data has big volumes. It may badly impact browser performance when you keep too many polygons data in memory. Following a simple strategy may decrease the risk and make data visualization fast:

  • Keep in memory only regions which are visible for a viewport and release memory for non-visible ones
  • To avoid having too many small polygons on the map, replace children polygons by a bigger parent polygon when the user zooms out
Lower to higher level of administrative boundaries
Lower to higher level of administrative boundaries
  • Use the level of details you need. Do not try to visualize the most detailed polygons

Where to get administrative or postcode boundaries polygons?

There are many ways to get the polygons data:

  • the data is open and available to download for some countries
  • GIS databases (e.g., OpenStreetMap data) contain boundaries polygons and you can query the data from there
  • use API and get update data when you need it

Geoapify offers APIs for administrative boundaries. Register and try it for free.