Small multiple maps using d3

October 21st 2014 by Christopher

Some datasets consist of geo-referenced values and a time component. We can visualize this type of data using “small multiples”. This means that we are creating multiple small representations of the visualization side by side to make the data visually comparable.

Here are some examples:

What I like about this type of visualization is that it enables direct comparison by having all data displayed simultaneously. Another cool thing is that it works well on mobile devices because the small maps do not have to resize to fit on smaller screens.

This tutorial shows how to create small multiple maps that are interactive.

DEMOSource

1. preparation

This visualization is created using Javascript + d3js. For loading the data for the visualization, the queue plugin will be used. You can use the following snippet to quickly include the libraries into your project.

<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="http://d3js.org/queue.v1.min.js"></script>

To get started, we create an empty container in our markup. The visualization will be rendered inside this container:

<div id="vis"></div>

That’s the whole markup we need, everything else will be created using Javascript. We just have to add some styling as well:

html,  
body {  
    margin: 0;
    padding: 0;
}
#vis {
    margin: 0 auto;
    max-width: 960px;
    width: 100%;
}
#vis div {
    float: left;
    position: relative;
}
#vis path {
    fill: #2ca25f;
    stroke: #FFF;
    stroke-width: 1px;
}
#vis p.legend {
    width: 100%;
    text-align: center;
    position: absolute;
    bottom: 0;
    left: 0;
    font-weight: bold;
    font-size: 11px;
}

2. loading the data

To create this type of visualization, we need to load two things into our script. The first thing is a geojson or topojson file of the area that should be visible. For this demo, I chose the states of Germany as an example, which can be downloaded here.

Additionally, we need to get some data to color the different maps. I generated a fake dataset for this example.

The dataset is structured like this: For each year we want to display, we have an object in the data array. This object consists of a key (the year) and the values for each state.

{
    "data": [{
        "Hessen": 50,
        "Sachsen-Anhalt": 53,
        "Mecklenburg-Vorpommern": 100,
        //...
    },
    {//...}
   ]
}

We can now use the d3-queue library to load the data into our application.

queue()  
    .defer(d3.json, 'ger-states.json')
    .defer(d3.json, 'data.json')
    .await(visualize);

After loading the data, the visualize function is called with the loaded data.

3. visualizing

For each item in the dataset, we now append a div container to the visualization wrapper. Inside of the container, the map should be rendered. Therefore, we call the function createMap.

var width = 150,  
    height = 180;

function visualize(error, states, data) {  
    var visualizationWrapper = d3.select('#vis');
    data.data.forEach(function(data, i) {
        var wrapper = visualizationWrapper
            .append('div')
            .style({
                width: width + 'px',
                height: height + 'px'
            });

        createMap(wrapper, states, data)
    });
}

Inside of the createMap function, we now create a svg map using d3. For a detailed explanation on how to render a basic map from a shapefile, you can read Let’s make a map by Mike Bostock.

At first, we need a projection and a path generator. There are many projections available in d3. For this demo we will be using a mercator projection:

var projection = d3.geo.mercator().scale(600).translate([-30, 700]);

The path generator is needed to render the svg path using the geojson data and the projection. So we initialize it by handing over the projection function:

var path = d3.geo.path().projection(projection);

Now we are ready to visualize the data. First, we append the legend, which is simply a paragraph showing the key of the dataset.

We are then creating a new svg element which fits into the wrapper we handed over. After that, we need to append the paths for all states in our geojson file. Therefore, we are setting the d attribute of the path by using the path generator we have initialized before.

function createMap(wrapper, geo, data) {  
    wrapper.append('p')
        .text(data.key)
        .attr('class', 'legend');

    var svg = wrapper.append('svg')
        .attr({
            width: width,
            height: height
        });

    svg.selectAll('path')
        .data(geo.features)
        .enter()
        .append('path')
        .attr('d', path)
        .attr('class', function(d) {
            return d.properties.GEN.toLowerCase();
        });
}

As a result you should see these map shapes, all having the same color:

The next step is setting the opacity of the shapes according to the value of the state. Therefore, we create a linear scale that returns the opacity of a shape for the input domain:

var opacity = d3.scale.linear().domain([0, 100]).range([0.2, 1]);

I hardcoded the values for the input domain for demo purposes. You could better use d3.min() and d3.max() to implement a dynamic scale which adjusts depending on the dataset.

We can now set the the opacity of each shape in the loop using the created scale:

.style('opacity', function(d) {
    var value = data.values[d.properties.GEN];
    return opacity(value);
})

As you can see, we are accessing the value by getting the index (the name of the state) from the shape and retrieving the value from the dataset with it.

Now the maps should be colored like this:

As the last step we will connect the maps, so that they display their values on mouse events.

4. making it interactive

This part is a bit tricky: If we mouseover a state in one map, we want all other maps to show the value for that state as well.

To achieve this, we first add event listeners to each shape of a map:

.on('mouseenter', function(d) {/* ... */})
.on('mouseleave', function(d) {/* ... */})

As you might have recognized, we have given each shape a class name (the name of the state) to identify them. This can now be used to select all shapes by their class name if we mouseover a state.

What we want to do ist to notify all shapes on the other maps which have the same class name to make them display their data.

For this, we can use the following function:

function notify(selector, eventName) {  
    d3.selectAll(selector)[0].forEach(function(el, i) {
        var shape = d3.select(el);
        shape.on(eventName)(shape);
    });
}

The function first selects all elements matching the given selector and fires an event for each of them. The last thing we have to do is now to listen to these events on each shape to update the label. If we leave the shape, we fire an event as well to get back to the initial state.

This is how we create the whole interactive part of the visualization:

svg.selectAll('path')  
    .data(geo.features)
    .enter()
    .append('path')
    .attr('d', path)
    .style('opacity', function(d) {
        var value = data.values[d.properties.GEN];
        return opacity(value);
    })
    .attr('class', function(d) {
        return d.properties.GEN.toLowerCase()
    })
    .on('mouseenter', function(d, i) {
        notify('.' + d.properties.GEN.toLowerCase(), 'select')
    })
    .on('mouseleave', function(d) {
        notify('.' + d.properties.GEN.toLowerCase(), 'unselect')
    })
    .on('select', function(self) {
        var geoData = self.data();
        self.node().parentNode.parentNode.getElementsByTagName('p')[0].innerHTML = data.values[geoData[0].properties.GEN];
    })
    .on('unselect', function(self) {
        self.node().parentNode.parentNode.getElementsByTagName('p')[0].innerHTML = data.key;
    });

The two events that are fired on all shapes are select and unselect. We can listen to these events and react accordingly.

Now we have the final result of the visualization:

The whole source is available on bl.ocks.org

comments powered by Disqus