TOOL: Generating a D3 map of Denmark

Industrial energy consumptio n (gigajoule)

The popular Javascript library D3.js, is amazing for working with data visualizations of all kinds. I have used it in several smaller projects and needed to generate map visualizations on several occasions. When making map visualizations, I often start with one of the D3.js demos and the geojson files of Denmark made available by Neogeografen. As always, when following D3 guides and tutorials we end up generating the entire SVG DOM in the client code. This may be sufficient for simple graphs and charts (the "hello world"s of data visualisations), but I find it unsatisfying and a waste of resources to generate a map of Denmark, with regions and municipalities etc. on every client load. I want to be able to generate a SVG map of Denmark and tailor it a bit, so I can insert it as a DOM object and bind the specific data on the client side.

I have created a simple NodeJS command-line tool that utilizes D3-node to generate a map with feature selection, regions, municipalities etc. I figured it could be fun to generate different styles and resolutions, see below. With this tool it is possible to generate a map SVG as a stand-alone HTML file, as a SVG in a html DOM container or as a SVG file. It allows for easy copy-paste into any html page. The options are described in the GitHub repository. A few examples are illustrated below.

SVG map of regions in high-quality
SVG map of legal jurisdictions in low-quality

SVG map of municipalities in facet style
SVG map of municipalities in pixelated style

Adding data and interactivity

When the map file is generated, the next step is to add data and interactivity. For convinience, I have added the area codes from the Danish Statistical service to each shape and layer, making it easy to map data from the statistical service to the generated maps. When I use the maps generated I embed them using the <object> HTML element to embed the SVG and then access the SVG dom using its contentDocument. This allow me to style map elements, bind data and add interactivity.

A working example

For the example at the top I have fetched a dataset from the Danish Statistical service showing the industrial energy consumpition per municipality in Denmark. I had to clean the data a bit before using it (removing whitespace, seperating out the municiaplity ID in individual column etc.). You can download the file here. First we need to load the .csv file using D3:

        
        //Loading data using d3.csv feature
        d3.csv("../data/DK-municipalities-energy-gj.csv").then(function(data){
            //data is loaded and we can start working on it
        })

Then we use the data to find the minimum and maximum values go Gigajoules in the data to help create the colorscale:

        //Loading data using d3.csv feature
        d3.csv("../data/DK-municipalities-energy-gj.csv").then(function(data){
            
            //Colors from Color brewer 2
            var colors = ['#fee0d2', '#fcbba1', '#fc9272', '#fb6a4a', '#ef3b2c', '#cb181d', '#a50f15']

            //Using d3.min/max functions to find min/max Gigajoule value
            var minGJ = d3.min(data, function(d) { return parseInt(d.gj); })
            var maxGJ = d3.max(data, function(d) { return parseInt(d.gj); })
            //Creating the colorscale()
            var colorscale = d3.scaleQuantize()
                .domain([minGJ, macGJ]) 
                .range(colorValues)
            
        })

Final step before adding data. We need to access the concentDocument to query the SVG element we want to bind data to.

        //Loading data using d3.csv feature
        d3.csv("../data/DK-municipalities-energy-gj.csv").then(function(data){
        
            //Colors from Color brewer 2
            var colors = ['#fee0d2', '#fcbba1', '#fc9272', '#fb6a4a', '#ef3b2c', '#cb181d', '#a50f15']
        
            //Using d3.min/max functions to find min/max Gigajoule value
            var minGJ = d3.min(data, function(d) { return parseInt(d.gj); })
            var maxGJ = d3.max(data, function(d) { return parseInt(d.gj); })
            //Creating the colorscale()
            var colorscale = d3.scaleQuantize()
                .domain([minGJ, macGJ]) 
                .range(colorValues)
            
            //Accessing the document of the Object with contentWindow and then select SVG DOM. 
            var objDocument = document.querySelector("object#interactive").contentDocument
            var svgDom = objDocument.querySelector("svg")
            
        
        })

And then we can start adding the data. Typically, we would append SVG elements (e.g. path) using the data, but here we need to bind the data values to existing elements. Therefor we iterate over the elements in the data and use the municipality id to select the SVG group in the map and change the fill attribute on each path.

        //Loading data using d3.csv feature
        d3.csv("../data/DK-municipalities-energy-gj.csv").then(function(data){
            
            //Colors from Color brewer 2
            var colors = ['#fee0d2', '#fcbba1', '#fc9272', '#fb6a4a', '#ef3b2c', '#cb181d', '#a50f15']
            
            //Using d3.min/max functions to find min/max Gigajoule value
            var minGJ = d3.min(data, function(d) { return parseInt(d.gj); })
            var maxGJ = d3.max(data, function(d) { return parseInt(d.gj); })
            //Creating the colorscale()
            var colorscale = d3.scaleQuantize()
                .domain([minGJ, macGJ]) 
                .range(colorValues)
                
                
            //Accessing the document of the Object with contentWindow and then select SVG DOM. 
            var objDocument = document.querySelector("object#interactive").contentDocument
            var svgDom = objDocument.querySelector("svg")
            
            //Iterate through the data elements and use the values to change color and add data.
            data.forEach(function (d) {
                d3.select(svgDom)
                    .selectAll("g#municipality" + d.id)
                    .selectAll("path")
                    .attr("fill", function () {
                        //Using the colorscale to calculate the fill-color based on gigajoule value
                                return colorscale(d.gj)
                        })
                })
            
            })

When it comes to interactivity, there are several ways to do that. Here we will do two things. First, we will add a simple hover feature and then we will use the data to provide a tooltip. Let us start by adding the D3 eventlisteners to our SVG groups representing the individual municipalities.

            //Loading data using d3.csv feature
            d3.csv("../data/DK-municipalities-energy-gj.csv").then(function(data){
                        
                //Colors from Color brewer 2
                var colors = ['#fee0d2', '#fcbba1', '#fc9272', '#fb6a4a', '#ef3b2c', '#cb181d', '#a50f15']
                        
                //Using d3.min/max functions to find min/max Gigajoule value
                var minGJ = d3.min(data, function(d) { return parseInt(d.gj); })
                var maxGJ = d3.max(data, function(d) { return parseInt(d.gj); })
                //Creating the colorscale()
                var colorscale = d3.scaleQuantize()
                    .domain([minGJ, macGJ]) 
                    .range(colorValues)
                            
                            
                //Accessing the document of the Object with contentWindow and then select SVG DOM. 
                var objDocument = document.querySelector("object#interactive").contentDocument
                var svgDom = objDocument.querySelector("svg")
                        
                //Iterate through the data elements and use the values to change color and add data.
                data.forEach(function (d) {
                    d3.select(svgDom)
                    .selectAll("g#municipality" + d.id)
                    .selectAll("path")
                    .attr("fill", function () {
                        //Using the colorscale to calculate the fill-color based on gigajoule value
                        return colorscale(d.gj)
                    })
                    .on('mouseover', function () {
                        //what happens when the mouse enters element
                    }).on('mouseout', function () {
                        //what happens when the mouse leaves element
                    });
                })
            })

Next, we want to style the element the mouse is over in the SVG using CSS classes and styles. Now, when wanting to use styles as a mechanism for interactivity, using the <object> HTML element creates a small challenge. Since this technique essentially embeds the SVG as a DOM document isolated from the page DOM, we cannot style the SVG elements with CSS from the outside. We have to append a style element to the SVG dom for it to apply. This allow us to add and remove the class on the parent node of the path that is hovered on.

            //Loading data using d3.csv feature
            d3.csv("../data/DK-municipalities-energy-gj.csv").then(function(data){
                                
                //Colors from Color brewer 2
                var colors = ['#fee0d2', '#fcbba1', '#fc9272', '#fb6a4a', '#ef3b2c', '#cb181d', '#a50f15']
                                
                //Using d3.min/max functions to find min/max Gigajoule value
                var minGJ = d3.min(data, function(d) { return parseInt(d.gj); })
                var maxGJ = d3.max(data, function(d) { return parseInt(d.gj); })
                //Creating the colorscale()
                var colorscale = d3.scaleQuantize()
                    .domain([minGJ, macGJ]) 
                    .range(colorValues)
                                    
                                    
                //Accessing the document of the Object with contentWindow and then select SVG DOM. 
                var objDocument = document.querySelector("object#interactive").contentDocument
                var svgDom = objDocument.querySelector("svg")

                //Adding stylesheet to the SVG inside the contentDocument
                var style = objDocument.createElementNS("http://www.w3.org/2000/svg", "style");
                style.textContent = 'g.municipality.hover path { stroke-width:1px; }'
                                
                //Iterate through the data elements and use the values to change color and add data.
                data.forEach(function (d) {
                    .d3.select(svgDom)
                    .selectAll("g#municipality" + d.id)
                    .selectAll("path")
                    .attr("fill", function () {
                        //Using the colorscale to calculate the fill-color based on gigajoule value
                        return colorscale(d.gj)
                    })<
                    .on('mouseover', function () {
                        //Adding a 'hover' class to parent group when element is mouse over
                        d3.select(this.parentNode).attr("class", "municipality hover")
                    }).on('mouseout', function () {
                        //Removing the hover class again on mouse out
                        d3.select(this.parentNode).attr("class", "municipality")
                    });
                })
            })

Final step is to add a tool tip that helps navigate the different municipalities and seeing the energy consumption in numbers. This is slightly more complex and we have several options. We can either add a <text> element to the SVG or use a HTML element added to the parent document outside the <object>. We will do the former, as it gives us more options on styling etc. First, we add the tooltip with a simple inline style.

            <object type="image/svg+xml" id="interactive" data="../svgs/dk-municipalities-mid.svg"></object>
            <div id="tooltip"
                style="position:absolute;background:white;border:1px solid black;padding:3px;
                border-radius: 3px;display:none;font-size:0.8em;">
            </div>
            

Then we make it so that the tool tip appears in the right position with text from the dataset on mouse over and remove it again on mouse out. We use getBoundingClientRect() instead of SVG getBBox(), because some SVG elements might be affected by translation etc.

            //Loading data using d3.csv feature
            d3.csv("../data/DK-municipalities-energy-gj.csv").then(function(data){
                                        
                //Colors from Color brewer 2
                var colors = ['#fee0d2', '#fcbba1', '#fc9272', '#fb6a4a', '#ef3b2c', '#cb181d', '#a50f15']
                                        
                //Using d3.min/max functions to find min/max Gigajoule value
                var minGJ = d3.min(data, function(d) { return parseInt(d.gj); })
                var maxGJ = d3.max(data, function(d) { return parseInt(d.gj); })
                //Creating the colorscale()
                var colorscale = d3.scaleQuantize()
                    .domain([minGJ, macGJ]) 
                    .range(colorValues)
                                            
                                            
                //Accessing the document of the Object with contentWindow and then select SVG DOM. 
                var objDocument = document.querySelector("object#interactive").contentDocument
                var svgDom = objDocument.querySelector("svg")
        
                //Adding stylesheet to the SVG inside the contentDocument
                var style = objDocument.createElementNS("http://www.w3.org/2000/svg", "style");
                style.textContent = 'g.municipality.hover path { stroke-width:1px; }'
                                        
                //Iterate through the data elements and use the values to change color and add data.
                data.forEach(function (d) {
                    .d3.select(svgDom)
                    .selectAll("g#municipality" + d.id)
                    .selectAll("path")
                    .attr("fill", function () {
                        //Using the colorscale to calculate the fill-color based on gigajoule value
                        return colorscale(d.gj)
                    })<
                    .on('mouseover', function () {
                        //Getting the position of the SVG element
                        var bb = this.getBoundingClientRect()
                        //Adding text to tool tip
                        tooltip.innerHTML = `${d.city}<br>${d.gj} GJ`
                        //Positioning the tool tip
                        tooltip.style.left = bb.left+bb.width + "px"
                        tooltip.style.top = bb.top + "px"
                        //Displaying the tool tip on mouseover
                        tooltip.style.display = "block"

                        //Adding a 'hover' class to parent group when element is mouse over
                        d3.select(this.parentNode).attr("class", "municipality hover")
                    }).on('mouseout', function () {
                        //Removing the tool tip on mouse out
                        tooltip.style.display = "none"
                        //Removing the hover class again on mouse out
                        d3.select(this.parentNode).attr("class", "municipality")
                    });
                })
            })

That's it! Now we have walked through the working example in the top of the page. I hope you found it useful. For comments and suggestions I refer to the github page. You can also send me an email if you have questions.

@heenrikgithubemail
CC BY-NC Henrik Korsgaard 2018
Looking for Work