Maps with Leaflet, TopoJSON & Chroma.js

In this post I will show you how to create a world map with random colored countries following these steps:

  1. Convert GeoJSON to TopoJSON2.
  2. Add TopoJSON support to Leaflet
  3. Display and style country polygons
  4. Use Chromajs to get proper colors

DEMOSource

In the last year I created a lot of map applications with Leaflet. It’s nice to work with Leaflet, because it is very well documented, works great on mobile devices and handles many user actions like zooming or panning for you. Moreover it has a very lively community and there are lots of great plugins that extend the core functionality.

A Leaflet map

If you never worked with Leaflet before you should read the quick start guide in order to understand what is happening here. We create a basic fullscreen map with the following lines of code:

  <!DOCTYPE html>  
  <html>  
  <head>  
    <meta charset="UTF-8">
    <title>maps with leaflet & topojson</title>
    <link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet-0.7.3/leaflet.css" />
    <style>
      html,body,#worldmap{
        height:100%;
      }
    </style>
  </head>  
  <body>  
    <div id="worldmap"></div>  

    <script src="http://cdn.leafletjs.com/leaflet-0.7.3/leaflet.js"></script>
    <script>
    // create a map object and set the view to the coordinates 44,-31 with a zoom of 10
    var map = L.map('worldmap').setView([44,-31], 3);
    </script>
  </body>  
  </html>

This is the starting point for our map. For now it’s just a Leaflet map object without any layers.

From Geo- to TopoJSON

Natively Leaflet supports GeoJSON, which is great but if you want to display a lot of geometries, TopoJSON is much more effective because of its smaller footprint.

“Topojson eliminates redundancy, offering much more compact representations of geometry than with Geojson” https://github.com/mbostock/topojson

For this example we use a GeoJSON file that contains every country as a polygon and the name of each coutry as a property. In order to convert the file to TopoJSON we use the topojson command-line application.

Installation (requires node.js and npm):
$ npm install -g topojson
Usage:
$ topojson -p -o countries.topo.json -- countries.geo.json

By default topojson kills all feature properties of the input file. With -p we tell topojson to preserve all of these. If you prefer an online tools to convert your files, there are plenty of it:

After converting the ~250kb GeoJSON file we get a TopoJSON file that only has around 100kb.

TopoJSON support for Leaflet

First we need to add topojson.js to our site. This file handles the converting from TopoJSON to GeoJSON in the browser, which is necessary to display the geo data.

<script src="http://d3js.org/topojson.v1.min.js"></script>

Leaflet doesn’t know anything about TopoJSON, so we need to extend it in order to be able to add TopoJSON directly as a tilelayer. Therefor we use this snippet:

L.TopoJSON = L.GeoJSON.extend({  
  addData: function(jsonData) {    
    if (jsonData.type === "Topology") {
      for (key in jsonData.objects) {
        geojson = topojson.feature(jsonData, jsonData.objects[key]);
        L.GeoJSON.prototype.addData.call(this, geojson);
      }
    }    
    else {
      L.GeoJSON.prototype.addData.call(this, jsonData);
    }
  }  
});
// Copyright (c) 2013 Ryan Clark

The snippet creates a new class called TopoJSON which extends the GeoJSON class and overwrites its addData() method in order to handle TopoJSON input.

To use the new class we create a new TopoJSON layer. After that we load our countries.topo.json file via ajax and add the data to the layer.

var topoLayer = new L.TopoJSON();

$.getJSON('data/countries.topo.json')
  .done(addTopoData);

function addTopoData(topoData){  
  topoLayer.addData(topoData);
  topoLayer.addTo(map);
}

After these steps we have a map that looks like this:

Nicer colors with chromajs

It is a common use case that you want to colorize geometries depending on a value. With chromajs you can create color scales and let chroma calculate the correct color for a specific value. For this example we want to get different blue tones that reach from #D5E3FF to #003171. Our values are random numbers between zero and one that we create with Math.random(). To initialize the color scale we use the scale and the domain function. We pass an array with the min- and max-color to the scale and the input range to the domain function.

var colorScale = chroma  
  .scale(['#D5E3FF', '#003171'])
  .domain([0,1]);

Now colorScale() is a function that we can use to get a color based on a value. If we want to get a color in hex format we use the function like this:

var fillColor = colorScale(0.25).hex();

To colorize the countries we need to iterate over each layer (in this case every country) with the eachLayer() function:

topoLayer.eachLayer(handleLayer);

For each layer we call a function handleLayer() where we style the layer with some path options. We also use our colorScale() function to create a fillcolor:

function handleLayer(layer){  
  var randomValue = Math.random(),
    fillColor = colorScale(randomValue).hex();

  layer.setStyle({
    fillColor : fillColor,
    fillOpacity: 1,
    color:'#555',
    weight:1,
    opacity:0.5
  });

  layer.on({
    mouseover: enterLayer,
    mouseout: leaveLayer
  });
}

As you can see we also added some event listeners to react on a mouseover and a mouseout event. We give the layer a bigger enterLayer() function and set the text of our little tooltip to the name of the hovered country. The leaveLayer() function resets the styles and hides the tooltip:

var $tooltip = $('.country-name');

function enterLayer(){  
  var countryName = this.feature.properties.name;
  $tooltip.text(countryName).show();

  this.bringToFront();
  this.setStyle({
    weight:2,
    opacity: 1
  });
}

function leaveLayer(){  
  $tooltip.hide();

  this.bringToBack();
  this.setStyle({
    weight:1,
    opacity:.5
  });
}

Putting all the stuff together gives us this nice map:

You can find the complete source here.

comments powered by Disqus