Visualizing Geographical Data with DataMaps

Recently I wanted to display tracking data on a map in our backend. After looking around for the best solution, I decided to go with vector maps using the datamaps library instead of Google Maps or LeafLetJs and on this article I would like to share my findings on how easy it was to use it.

What is DataMaps?

Datamaps is a javascript library designed to display interactive geographical data visualizations on maps. It's SVG-based, and relies on the It supports chropleths, bubble maps and, as it states on its site, is not limited to just that, as it provides a plugin system mechanism to allow the addition of any type of visualization over its maps.

Displaying Vector Map

Datamaps has USA and World SVG-based maps, but for this article we are going to use the USA based map. We are going to display the number of sessions per USA state have been recorded. Also, we are using Yii2 Framework for the example, but you can easily port this code to any of your preferred frameworks.

Register Asset Files

First thing is to register the required javascript files. In Yii2 what we do is to create an asset bundle. I have downloaded d3.min.js, topjson.min.js and datamaps.all.js files and placed them on our web/js folder (I could use npm or bower but I am not going to make this article more complicated than what actually is). The class looks like this:

namespace backend\assets;

use yii\web\AssetBundle;

class MapAsset extends AssetBundle
{
    public $basePath = '@webroot';
    public $baseUrl = '@web';

    public $js = [
        'js/d3.min.js',
        'js/topojson.min.js',
        'js/datamaps.all.js',
    ];
}

As all Yii2 developer knows, we are ready to register our files like the following on our view:

use backend\assets\MapAsset;

/** @var \yii\web\View $this */
MapAsset::register($this);

Initialize the map

Once we have the assets registered, we are ready to write our javascript to render our beautiful SVG map:

<div id="vectorMap" style="height: 400px;"></div>
<?php 
// I highly recommend you to write an external javascript file and 
// register it differently than what I do here. This is simply for demo 
// purposes
$this->registerJs("
window.vectorMap = new Datamap({
        element: document.getElementById('vectorMap'), 
        scope: 'usa',
				projection: 'equirectangular',
    });
    // This will render iso state codes on our map
    window.vectorMap.labels();
");
?>

The above would render a map like this:

Easy right? Well, I had the initial map displayed now there was a time to add some styling and functionality. The map had to be responsive, and also I wanted to have a scale of colors according to the total number of visits, the more visits the darker the color. Also, I wanted to display some information when hovering a state. The way I could accomplish that was to make use of fills, responsive and geographyConfig configuration options of DataMaps object but in order to make work properly I first had to get the data.

Styling & Data Display

The way DataMaps knows the relation between the data displayed and its geographical data is by the ISO codes. As we are using the USA map and its states, that means that our data should contain info in an JSON object whose attribute names are actually the ISO codes of the geographical data it displays. For example, we are going to use this data:

var data = {
    "CA": { 
        "usage": "14",
        "fillKey": "moreThanTen"
    },
    "IL": {
        "usage": "2",
        "fillKey": null
    },
    "KS": {
        "usage": "17",
        "fillKey": "moreThanTen"
    },
    "KY": {
        "usage": "1",
        "fillKey": null
    },
    "NY": {
        "usage": "18",
        "fillKey": "moreThanTen"
    },
    "PA": {
        "usage": "8",
        "fillKey": "moreThanZero"
    },
    "PM": {
        "usage": "1",
        "fillKey": "moreThanZero"
    }
}

I would like you to put attention on usage and fillKey attributes of that object. The first tell us the amount of users where tracked on that state and the latest fillKey tells DataMaps which color to use to fill that particular region. When fillKey is null that tells DataMaps to use defaultFill instead. Updating our previous code will help you understand:

<div id="vectorMap" style="height: 400px;"></div>
<?php 
$this->registerJs("
var data = {...}; // see above

window.vectorMap = new Datamap({
        responsive: true, // this will make it responsive
        element: document.getElementById('vectorMap'), 
        scope: 'usa',
        projection: 'equirectangular',
        // color fills to be used to fill regions if our data provides the fillKey 
        fills: {
                moreThanThousand: '#800026',
                moreThanFiveHundred: '#BD0026',
                moreThanTwoHundred: '#E31A1C',
                moreThanHundred: '#FC4E2A',
                moreThanFifty: '#FD8D3C',
                moreThanTwenty: '#FEB24C',
                moreThanTen: '#FED976',
                moreThanZero: '#FFEDA0',
                defaultFill: '#FFFFCC'
        },
        data: data, 
        geographyConfig: {
            highlightBorderColor: '#bada55', // on hover highlight the border
            highlightBorderWidth: 2, // the border size on hover highlight
            popupTemplate: function (geography, data) { // display a popup
                if(data) { // some geographic locations on our data object, do not have information
                    return '<div class="hoverinfo"><strong>' + data.usage + '</strong> users on ' + 
                        geography.properties.name + '</div>';
                }
            }
        }
		});
    // This will render iso state codes on our map
    window.vectorMap.labels();
   // This will update map size on window resize :) 
    window.addEventListener('resize', function() {
        window.vectorMap.resize();
    });
");
?>

We have done lots of stuff there, but if you follow the comments it will be really easy to understand. We have added responsive configuration option and set it to true, that tells DataMaps that we want our map to be responsive. Unfortunately, is not an automatic option (that would be great), thats why we were forced to add window.vectorMap.resize() instruction when our window resizes. Then we have added fills that is actually an object which keys are referenced in our data to specifically change the look of our map. Also, we have modify the way our States behave by adding some functionality on our geographic data. That is done by using the geographyConfig option of DataMaps. On this case, we simply display a tooltip with the number of visits (data.usage).

The popupTemplate function, we check whether the hovered geography has data or not. The argument data is filled with the objects injected on the data passed. As you can see on the data object sample above, we do not have all information about all states. That means that some of the states will provide null as data object. Thats why we checked for it on the phpupTemplate function.

The above code and data will display a map like the following:

Cool right? And the best of all is that is free.

For those wondering how I accomplished to recreate the data object in PHP, you could do it this way:


// This is very lame, please use a different approach. Its 
// only for demo purposes. You could instead write a service. 
// Static methods are evil XD
class DataHelper {
   
    public static function getStateRegionFill($count)
    {
        if ($count > 1000)
            return 'moreThanThousand';
        if ($count > 500)
            return 'moreThanFiveHundred';
        if ($count > 200)
            return 'moreThanTwoHundred';
        if ($count > 100)
            return 'moreThanHundred';
        if ($count > 50)
            return 'moreThanFifty';
        if ($count > 20)
            return 'moreThanTwenty';
        if ($count > 10)
            return 'moreThanTen';
        if($count > 0)
            return 'moreThanZero';
        return null;
    }
    
    public static function getStatesMapData()
    {
        // using Yii2 
        $rows = \Yii::$app
            ->get('trackingDb')
            ->createCommand(
                'select state_code as code, count(state_code) as sessions from tbl_tracking group by state_code'
            )->queryAll();

        $data = [];
        foreach ($rows as $row) {
            $data[$row['code']]['usage'] = $row['sessions'];
            $data[$row['code']]['fillKey'] = static::getStateRegionFill($row['sessions']);
        }

        return json_encode($data);
    }
}

References

Posted by Antonio Ramirez

Antonio Ramirez is the co-founder and co-innovator of 2amigOS! Consulting Group LLC. Open Source believer and technology enthusiast.