Friday, Dec 29, 2017
D3.js: Map of Croatia
Introduction
When you have a bunch of statistical data that has to be presented to end users, you need to find a way how to visualize that data, so it’s not boring and cumbersome to look at. This is where D3.js (Data-Driven Documents) comes into play. With D3.js, we can create interactive and animated data visualizations.
This tutorial will consist of two separate blog posts. In the first one, we will create a simple map of Croatia with a county view using TopoJSON and D3.js. The second blog post will explain how to create a bar chart graph with some tourism statistics taken from MINT, and how to show that in a tooltip when hovering over a certain county.
Let’s begin.
Map
To make things simple, we will start the process of creating a map using GeoJSON file for Croatia and convert it to TopoJSON
. If you have Shapefile and you want to convert it to TopoJSON
, check out this tutorial on Command-Line Cartography written by Mike Bostock.
To convert GeoJSON to TopoJSON
we need to install topojson-server using NPM:
npm install -g topojson-server
and then just call the following command:
geo2topo cro.geojson > croatia.json
If you were wondering about the differences between those two formats, TopoJSON
is actually an extension of GeoJSON
that encodes topology. TopoJSON
eliminates redundancy, allowing related geometries to be stored efficiently. This result in more compact files, often providing a reduction of 80% in file size.
Now let’s start building HTML and CSS for our map. Create an index.html in the same folder where we generated croatia.json using the following template:
<!DOCTYPE html>
<meta charset="utf-8">
<style>
</style>
<body>
<script src="http://d3js.org/d3.v4.min.js"></script>
<script src="http://d3js.org/topojson.v2.js"></script>
<script src="https://d3js.org/d3-queue.v3.min.js"></script>
<script>
</script>
</body>
Now we will create SVG for rendering two-dimensional geometry:
var width = 960,
height = 800;
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
The next step is to make a call to d3.json using d3-queue to load the TopoJSON
file:
d3.queue()
.defer(d3.json, "croatia.json")
.await(ready);
function ready(error, cro) {
if (error) throw error;
console.log(cro);
}
When you open your index.html and look at JavaScript console, you should see the topology object.
Now we just need two more things to render geography:
-
Path generator - takes the projected 2D geometry and formats it appropriately for SVG.
-
Projection - projects a point from sphere to a plane. In our case, we will be using d3.geoAlbers() projection for displaying the map.
The default position of d3.geoAlbers()
is somewhere in the US (around Hutchinson, Kansas), so we need to add parallels, center, rotate, scale and translate. After that, our code should look something like this:
var projection = d3.geoAlbers()
.parallels([40, 50])
.center([0, 45])
.rotate([-16, 0])
.scale(8000)
.translate([width / 2, height / 2]);
var path = d3.geoPath().projection(projection);
Although TopoJSON
is used to store data more efficiently than GeoJSON
, we need to convert that data back to the GeoJSON
for display - append a path element to which we will bind the converted data and add a selection.attr that will connect our data with selected elements:
function ready(error, cro) {
if (error) throw error;
svg.append("g")
.attr("class", "land")
.selectAll("path")
.data(topojson.feature(cro, cro.objects.cro_regions).features)
.enter().append("path")
.attr("d", path)
.on("mouseover", function (d) { })
.on("mouseout", function (d) { });
}
Finally, you should see something like this when you refresh index.html:
Maybe you noticed mouseover and mouseout events - we will use those for a tooltip a bit later. Now we will add county boundaries and a little formatting through CSS.
For boundaries, we will use topojson.mesh
for which we need to pass topology and geometry objects. Also, we will define an optional filter that will reduce the set of returned boundaries, which takes two arguments that represent two features on either side of the boundary.
svg.append("path")
.datum(topojson.mesh(cro, cro.objects.cro_regions,
function (a, b) { return a !== b; }))
.attr("class", "border")
.attr("d", path);
And we will add this CSS:
.land {
fill: #e5e5e5;
}
.land :hover {
fill: rgba(255, 158, 44, .5);
}
.border {
fill: none;
stroke: #fff;
stroke-linejoin: round;
stroke-linecap: round;
}
Tooltip
As a next step of building the interactive map, we will add a tooltip that will display the county name when we hover over it.
First, let’s define a tooltip:
var tooltip = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0)
.style("width", 600);
Now we can define the text and the position using d3.event.pageX
(returns the horizontal coordinate of the event relative to the whole document) and d3.event.pageY
(returns the vertical coordinate of the event relative to the whole document)`.
//replace empty vents with this code
.on("mouseover", function (d) {
var tip = "<h3>" + d.properties.name + "</h3>";
tooltip.html(tip)
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY) + "px");
tooltip.transition()
.duration(500)
.style("opacity", .7);
})
.on("mouseout", function (d) {
tooltip.transition()
.duration(500)
.style("opacity", 0);
});
To add tooltip styling:
.tooltip {
position: absolute;
max-width: 400px;
height: auto;
padding: 5px;
background-color: white;
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
border-radius: 5px;
-webkit-box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.4);
-moz-box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.4);
box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.4);
pointer-events: none;
font-family: sans-serif;
font-size: 12px;
}
And the final product should look something like this:
We are done with map creation, and our next step is to create a chart that will show detailed statistics for each county.
Stay tuned!