Display 2 layers of geodata with Highmaps

by   JavaScript


Friday, 10 June 2016



Link to Github repo View Demo

Highmaps is a JavaScript library for creating schematic maps on web sites and web applications. Its built-in responsiveness and easy integration make it an ideal choice for representing geodata dynamically. In this post, I will demonstrate how to create a 2-layer map of Europe population density with drilldown effect to reflect countries' specific data. Nothing particularly fancy here, as we will closely follow the demo on the highmaps' website. However, we will make sure that our map displays data retrieved from a database via an ajax call instead of dummy data and that country flags pop up when we hover over the map. As for every post in my blog, code is built on a fresh new Laravel installation (v 5.2).

Render the demo map

Our starting point is the US map displayed as demo on the Highcharts website. From there on, we will apply a few modifications to the code to display a map of Europe and make it look and feel the way we want.

So we first add the relevant libraries inside the header tags.

index.blade.php

<script src="https://code.jquery.com/jquery-2.2.2.min.js"></script>
<script src="https://code.highcharts.com/maps/highmaps.js"></script>
<script src="https://code.highcharts.com/maps/modules/data.js"></script>
<script src="https://code.highcharts.com/maps/modules/drilldown.js"></script>
<script src="https://code.highcharts.com/mapdata/countries/us/us-all.js"></script>
<link href="https://netdna.bootstrapcdn.com/font-awesome/3.2.1/css/font-awesome.css" rel="stylesheet">

We also define a new div to hold the map inside body tags.

index.blade.php

<div id="container" style="height: 500px; min-width: 310px; max-width: 800px; margin: 0 auto"></div>

Then, let us add the core component of the map. This code is just a copy of the one you'll find on the Highmaps website. Further down we'll apply modifications to personalize the map.

index.blade.php

$(function () {
    var data = Highcharts.geojson(Highcharts.maps['countries/us/us-all']),
        // Some responsiveness
        small = $('#container').width() < 400;

    // Set drilldown pointers
    $.each(data, function (i) {
        this.drilldown = this.properties['hc-key'];
        this.value = i; // Non-random bogus data
    });

    // Instanciate the map
    $('#container').highcharts('Map', {
        chart : {
            events: {
                drilldown: function (e) {
                    if (!e.seriesOptions) {
                        var chart = this,
                            mapKey = 'countries/us/' + e.point.drilldown + '-all',
                            // Handle error, the timeout is cleared on success
                            fail = setTimeout(function () {
                                if (!Highcharts.maps[mapKey]) {
                                    chart.showLoading('<i class="icon-frown"></i> Failed loading ' + e.point.name);

                                    fail = setTimeout(function () {
                                        chart.hideLoading();
                                    }, 1000);
                                }
                            }, 3000);
                        // Show the spinner
                        chart.showLoading('<i class="icon-spinner icon-spin icon-3x"></i>'); // Font Awesome spinner

                        // Load the drilldown map
                        $.getScript('https://code.highcharts.com/mapdata/' + mapKey + '.js', function () {
                            data = Highcharts.geojson(Highcharts.maps[mapKey]);
                            // Set a non-random bogus value
                            $.each(data, function (i) {
                                this.value = i;
                            });
                            chart.hideLoading();
                            clearTimeout(fail);
                            chart.addSeriesAsDrilldown(e.point, {
                                name: e.point.name,
                                data: data,
                                dataLabels: {
                                    enabled: true,
                                    format: '{point.name}'
                                }
                            });
                        });
                    }
                    this.setTitle(null, { text: e.point.name });
                },
                drillup: function () {
                    this.setTitle(null, { text: 'USA' });
                }
            }
        },
        title : {
            text : 'Highcharts Map Drilldown'
        },
        subtitle: {
            text: 'USA',
            floating: true,
            align: 'right',
            y: 50,
            style: {
                fontSize: '16px'
            }
        },
        legend: small ? {} : {
            layout: 'vertical',
            align: 'right',
            verticalAlign: 'middle'
        },
        colorAxis: {
            min: 0,
            minColor: '#E6E7E8',
            maxColor: '#005645'
        },
        mapNavigation: {
            enabled: true,
            buttonOptions: {
                verticalAlign: 'bottom'
            }
        },
        plotOptions: {
            map: {
                states: {
                    hover: {
                        color: '#EEDD66'
                    }
                }
            }
        },
        series : [{
            data : data,
            name: 'USA',
            dataLabels: {
                enabled: true,
                format: '{point.properties.postal-code}'
            }
        }],
        drilldown: {
            activeDataLabelStyle: {
                color: '#FFFFFF',
                textDecoration: 'none',
                textShadow: '0 0 3px #000000'
            },
            drillUpButton: {
                relativeTo: 'spacingBox',
                position: {
                    x: 0,
                    y: 60
                }
            }
        }
    });
});

This results to the base map:

highmaps image

Modify the base map

Ok, we now have a map similar to the official demo on Highcharts. When you click on a state, you get a more detailed view with the values for the counties, parishes or boroughs (this is how administrative entities below US States are called I believe. Correct me if I'm wrong). Note that these are dummy values simply representing each entity id. However we want to display the European map with real values instead. We first add the European geodata.

index.blade.php

<script src="http://code.highcharts.com/mapdata/custom/europe.js"></script>

Several changes have to be made in the code to match with the new map.

index.blade.php

var data = Highcharts.geojson(Highcharts.maps['custom/europe']),
---
mapKey = 'countries/' + e.point.drilldown + '/' + e.point.drilldown + '-all',
---
drillup: function () {
    this.setTitle(null, { text: 'Europe' });
}
---            
subtitle: {
    text: 'Europe',
    ---
},
---
series : [{
    ---
    name: 'Europe',
    ---
}],

As previously stated, instead of dummy values, we would like to display country and states population densities. Those values are retrieved from a database we still need to create.

Retrieve country values from DB

Values for population densities come from eurostat, the official portal for European statistics. Note that for simplification purposes, we only care about regional values for 3 countries: Germany, France and Switzerland. We build 2 different tables: countries and regions. Run php artisan migrate to add those tables in your database system and php artisan db:seed to populate the tables.

In the controller that serves the homepage, we add a new variable that collects data from the database.

HomeController.php

public function index() {
    $country_density = Country::lists('density', 'code');
    $country_id = Country::lists('id', 'code');

    return view::make('home')
        ->with('country_pop', $country_pop)
        ->with('country_density', $country_density)
        ->with('country_id', $country_id);
}

We then encode the variable's content in json. The Highcharts hc-key property matches the code property from our DB. This is how we link each country to its corresponding density value. We also retrieve the country id which will serve later for the drilldown process.

index.blade.php

var country_density = <?php echo json_encode($country_density); ?>
var country_id = <?php echo json_encode($country_id); ?>

$.each(data, function (i) {
    this.drilldown = this.properties['hc-key'];
    this.flag = this.drilldown.replace('UK', 'GB').toLowerCase();

    this.code = this.properties['hc-key'];

    this.value = country_density[this.code];

    this.id = country_id[this.code];
});
...
var id = e.point.id; // Get country id
...

Retrieve regional values via AJAX calls

For ajax calls to work on Laravel, we need to set up CSRF Protection.

index.blade.php

<meta name="csrf-token" content="{{ csrf_token() }}">
<script>
    $.ajaxSetup({
      headers: {
        'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
      }
    });
</script>

We can then write our ajax function that will send a post request to the controller and retrieve the requested value.

index.blade.php

function ajax(){
    return $.ajax({
        type: 'POST',
        data: {country_id: id},
        success: function(response) {
            region_density = response;
        },
        error: function (jqXHR, textStatus, errorThrown) {
            console.log('failure');
        }
    });
};

ajax().done(function(result) {
    $.each(data, function (i) {
        this.code = this.properties['hc-key'];

        this.value = region_density[this.code];
        this.flag = this.code;
    });

    chart.hideLoading();
    clearTimeout(fail);
    chart.addSeriesAsDrilldown(e.point, {
        name: e.point.name,
        data: data,

        dataLabels: {
            enabled: false,
            format: '{point.name}'
        }
    });
});

The controller method initiates the database query based on the country id. It then return a variable used as response of the ajax function.

HomeController.php

public function ajax() {

    $id = $_POST['country_id'];

    $regions = Region::where('country_id', '=', $id)->lists('density', 'code');

    return $regions;        
}

Let's not forget to define the new route. routes.php

Route::post('/', array('as' => 'ajax', 'uses' => 'HomeController@ajax'));

Display country flag and region flag in the legend

For this last step we'll add a very useful package called world flags sprite. As its name suggests, this package provide a list of all country flags in the world. I have also built a similar package for regional flags (only limited to Germany, France and Switzerland though). This way, flags can be added in the legend at both the national and regional level.

index.blade.php

<link rel="stylesheet" type="text/css" href="{{ asset('css/flags32.css') }}" />
<link rel="stylesheet" type="text/css" href="{{ asset('css/flags32_germany.css') }}" />

index.blade.php

tooltip: {
    headerFormat: '',
    useHTML: true,
    pointFormat: '<span class="f32"><span class="flag {point.flag}"></span></span>'
                            + '&nbsp&nbspPop.&nbsp{point.name}: <b>{point.value}</b>',
},

End Result

highmaps end result

Blog Search

About

Hi there! My name is Jean-Marc Kleger and I'm a web developer. Welcome to my blog where a share some tips on how to deal with a selection of challenges encountered in my day-to-day coding workflow. Most articles are related to their own Github repo so that you can quickly experiment the code. Don't hesitate also to make use of the comment section at the end of each post to share your knowledge on the topic. Enjoy the visit!