Your population density map of Switzerland

Viktor Brusylovets
7 min readMay 21, 2017

--

This article is inspired by excellent multi-part tutorial Command-Line Cartography, written by Mike Bostock, and fascination of maps. I highly recommend to read all four articles of the tutorial. After i followed the articles and created a choropleth of Texas, i decided to make a map for Switzerland, b/c its boundaries data are easy to find in Internet. So this article is about — how i generated population density map of Switzerland.

Source: The Federal Statistical Office

First, install required npm packages (You should already have node and npm installed):

npm install -g csvtojson shapefile topojson ndjson-cli
npm install -g d3 d3-geo-projection d3-scale-chromatic

If you take a look at the map on top, you’ll see, that Swiss boundary crosses large lakes in a few places. One option is to leave them not filled, but by default white color means uninhabited territories (and lakes truly are), so we need lake boundaries to fill lakes blue to explain it clearly. Also good to have boundaries of cantons, it makes map better. We need:

  1. Population density data for each municipalities.
  2. Boundaries of municipalities;
  3. Boundaries of lakes;
  4. Boundaries of cantons;

Population density defines a color of municipalities on the map, so in fact we will have 3 “layers” to draw: boundaries of municipalities + color, which depends on population density; boundaries of lakes + blue color, and boundaries of cantons + stroke of the line. That’s all we need.

Population density data

The Federal Statistical Office provides different statistical data for Switzerland. Use https://www.bfs.admin.ch/bfs/en/home/statistics/regional-statistics/regional-portraits-key-figures/communes.assetdetail.328146.html link or, in case the link is outdated, try to search by “key data of all communes”. You can download xls file and convert it to csv (i used LibreOffice to open file and saved it as csv with name population-density.csv to ~/projects/swiss-population-dansity-map/ folder.

Top 9 lines of csv file contains headers, which we don’t need, so remove it with tail command:

tail -n +10 population-density.csv \
> population-density-no-headers.csv

Convert population-density-no-headers.csv file to JSON:

csvtojson --noheader=true population-density-no-headers.csv \
> population-density.json

If we need to manipulate and draw polygons, much easier to work with NDJSON format (each line of a file is a valid JSON):

ndjson-cat population-density.json \
| ndjson-split \
| ndjson-map 'd={id:parseInt(d.field1),residents:d.field3.replace(",", ""),density:d.field5.replace(",", "")}' \
> population-density.ndjson

Also in command above, we simplified data, b/c we need only density values and municipalities ids. Now population-density.ndjson file contains simple JSON lines:

{"id":1,"residents":"1959","density":"248"}

Boundaries

The Federal Office of Topography swisstopo provides open data for administrative boundaries of Switzerland. You can download data file from https://shop.swisstopo.admin.ch/en/products/landscape/boundaries3D (zipfile size is 600 MB) or if url is outdated, try to search by “swiss boundaries3D”. But, i think, the better choice is to use data from swiss-maps. The repository contains scripts, which allow you to generate TopoJSON files for boundaries of administrative units and lakes of Switzerland. Data are a bit old (2015), but give us all we need to draw the map.

At first, swisstopo provides coordinates in Swiss coordinate system, which look a bit odd, at first glance. [2765769.1224999987,1213634.7800000012] is an example of Swiss coordinates. It’s already projected (planar), but coordinates require some “normalization” to use them to draw the map. swiss-maps does it for us.

Second, you already have optimized TopoJSON files. You can read about TopoJSON in Command-Line Cartography, Part 3.

Last, but not least, swiss-maps provides lakes boundaries, which i have not found in new boundaries3D dataset (maybe, i just missed it).

Now time to use swiss-maps to generate TopoJSON files. You need git to download swiss-maps:

cd ~/projects
git clone https://github.com/interactivethings/swiss-maps.git
cd swiss-maps

I just cloned swiss-maps in ~/projects/ folder.

NOTE: if you try to work inside swiss-map to create the map (without creating separate folder for your project), you will have a conflict of npm modules versions, b/c swiss-maps uses old versions of npm packages locally installed (for instance, d3), but we use new version of d3, globally installed. So i recommend you create separate working directory outside swiss-maps directory.

Getting started section of swiss-maps contains all information you need to install and run it. Swiss-maps relies on GDAL, which you need to install. To generate TopoJSON files, run command inside swiss-maps directory:

make all

If everything is ok, in ~/projects/swiss-maps/topo/ folder, you’ll find TopoJSON files. Now generate NDJSON files for boundaries inside our working directory. Generate GeoJSON file for lakes:

topo2geo lakes=- < ../swiss-maps/topo/ch-municipalities-lakes.json \
> lakes.json

and for country boundaries:

topo2geo country=- < ../swiss-maps/topo/ch-country-lakes.json \
> country.json

Generate NDJSON for country boundaries:

ndjson-cat country.json | ndjson-split 'd.features' \
| ndjson-map 'd.properties = {"stroke": "#000", "stroke-opacity": 0.3}, d' \
> country.ndjson

and for lakes boundaries:

ndjson-split 'd.features' < lakes.json \
| ndjson-map 'd.properties.fill = "#6b9def", d' \
> lakes.ndjson

Draw boundaries of Switzerland and lakes:

cat country.ndjson lakes.ndjson \
| geo2svg -n -w 960 -h 500 \
> country-lakes.svg

You can see, we have a small problem. Would be nice to have intersection of Switzerland and lakes, b/c we don’t need on the map of Switzerland French part of Lake Geneva, Italian part of Lake Maggiore and German part of Lake Constance (Bodensee).

Quick and simple solution for me was to create a Python script, which uses already installed GDAL to intersect required polygons. I used Python wrapper, which is a bit tricky to install. You can play also with GDAL command line to get intersection of Switzerland and lakes polygons without using Python script.

The Python script (intersect.py) is very simple, it reads first geometry from STDIN and, for each next geometry read from STDIN, returns its intersection with first geometry:

#!/usr/bin/env python
from osgeo import ogr
from osgeo import gdal
import sys
if __name__=='__main__':
geoJsonMain = sys.stdin.readline()
geometryMain = ogr.CreateGeometryFromJson(geoJsonMain)
for geoJsonLine in sys.stdin:
geometryLine = ogr.CreateGeometryFromJson(geoJsonLine)
print geometryMain.Intersection(geometryLine).ExportToJson()

If i run script more than once, i need logging and input validation, but i don’t.

Python scripts works with pure geometries, for instance:

{
"type": "Polygon",
"coordinates": [
[
[
556.157854890176,
151.31341313413128
],
[
554.1445563131838,
148.27018270182697
]
]
]
}

but GeoJSON is a collection of geometries with additional information, for instance:

{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"id": 9040,
"properties": {},
"geometry": <Geometry goes here>
},
...
]
}

So we need extract geometries from GeoJSON, before we use python script for lakes. We can use jq command to extract lakes geometries:

cat lakes.json | jq -c '.features[] | [.geometry]' \
| ndjson-split > geometry-lakes.json

and country geometries:

cat country.json | jq -c '.features[0].geometry' \
> geometry-country.json

Run python script and add blue color to fill the lakes polygons:

cat geometry-country.json geometry-lakes.json \
| ./intersect.py \
| ndjson-map '{type: "Feature", properties: {fill: "#6b9def"}, geometry: d}' \
> lakes-intersected.ndjson

Draw lakes again:

cat \
<(cat lakes-intersected.ndjson) \
<(ndjson-split 'd.features' < country.json) \
| geo2svg -n -w 960 -h 500 \
> country-lakes-intersected.svg

Good. Now generate files for cantons boundaries:

topo2geo cantons=- < ../swiss-maps/topo/ch-cantons-lakes.json \
| ndjson-split 'd.features' \
| ndjson-map 'd.properties = {"stroke": "#000", "stroke-opacity": 0.3}, d' \
> cantons.ndjson

and municipalities:

topo2geo municipalities=- \
< ../swiss-maps/topo/ch-municipalities-lakes.json \
| ndjson-split 'd.features' \
> municipalities.ndjson

Now we use ndjson-join to join data from population-density.ndjson and municipalities.ndjson, using municipalities ids, presented in both files, and assign a color to each municipality’s polygon (use d3 module d3-scale-chromatic), depends on population density value:

ndjson-join 'd.id' municipalities.ndjson population-density.ndjson \
| ndjson-map -r d3 -r d3=d3-scale-chromatic 'z = d3.scaleThreshold().domain([1, 10, 50, 200, 500, 1000, 2000, 4000]).range(d3.schemeOrRd[9]),d[0].properties = { fill: z(d[1].density)}, d[0]' \
> merged-data.ndjson

Update from 30 August 2019: if you get an issue from previous command, try slightly changed version (-r d3-scale-chromatic is used instead of -r d3=d3-scale-chromatic):

ndjson-join 'd.id' municipalities.ndjson population-density.ndjson \
| ndjson-map -r d3 -r d3-scale-chromatic 'z = d3.scaleThreshold().domain([1, 10, 50, 200, 500, 1000, 2000, 4000]).range(d3.schemeOrRd[9]),d[0].properties = { fill: z(d[1].density)}, d[0]' \
> merged-data.ndjson

Finally just draw the map, you can see on top:

cat merged-data.ndjson lakes-intersected.ndjson \
cantons.ndjson country.ndjson \
| geo2svg --stroke none -n -w 960 -h 500 \
> swiss-population-density.svg

It was fun for me to create population density map of Switzerland in command-line (almost), that’s why i decided to share it here.

--

--