## Wednesday, August 14, 2013

### Representing population density with D3js

Choropleth maps represent variables colouring (or filling with a pattern) areas in the map.
We were discussing a map with the local OGGeo group in Barcelona, and we all noticed that it had a problem.
The map was about the percentage of people in Catalonia whose home language was Catalan.It showed most of the areas coloured as mostly Catalan-speakers zone, but since the greatest share of the population in Catalonia is around the city of Barcelona, where Spanish is more spoken as primary language, the map could give a wrong impression to people that didn't know the population distribution in Catalonia.

Cartograms are thought to solve this problem, but the author of the map didn't like them, because if the population is very unevenly distributed, the result is really difficult to understand Map taken from the Wikipedia's cartogram entry

So I tried a pair of things to see if the result was better. The examples code are in a GIST at GitHUB, so they can be visualized at http://bl.ocks.org:

# Resizing polygons according to the population density

Every comarca (the regions Catalonia is divided in) is coloured according to the percentage of people able to speak Catalan (yes, I couldn't find the number of people who has Catalan as their first language). When the button is pressed, the coloured polygons shrink according to the population density of the region:
The only comarca that stays with the same size is Barcelonès, since is the one with the highest population density. You can see that the dark-green zones are low populated, so they shrink a lot.
It's very difficult to me to see the color when the polygon gets too small, even though I didn't shrink them linearly, but smoothing it with a square root, so I think that it's not the best solution.
Since the code is very similar to the one at Mike Bostok's example I won't comment it.

# Filling with points according to the population density

This example at Kartograph web page gave me the idea for the second example, although I changed it a little using colors.
In this case, every polygon is filled with circles. The color of the circle represents the magnitude we are showing (% of people able to speak Catalan, in this case), and the size represents the population density.
I think that the result is clearer in this case. The separation between circles and the maximum circle radius can be adjusted to get better results.
I will comment the code, since I had to write it from the scratch.

Basically, for each polygon, the bounding box is calculated, and the region filled with dots. Then, the polygon is used to clip the rectangle of circles.

Point density according to the population density

.background {
fill: None;
stroke: #444;
stroke-width: 1;
}

var scaleColors = ['#7308a5','#0000ff', '#00aeae', '#007900'];
var pointSeparation = 15;
var maxPointSize = 25;
var width = 960,
height = 500;

var projection = d3.geo.mercator()
.center([2.1,41.7])
.scale(9000)
.translate([width / 2, height / 2]);

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

var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);

d3.csv("comarques.csv", function(data_values) {
max_density = 0;
max_value = 0;
min_value = 100;
for (i=0; i<data_values.length; i++){
if(parseFloat(data_values[i].Density) > max_density){
max_density = parseFloat(data_values[i].Density);
}
if(parseFloat(data_values[i].Catalan) > max_value){
max_value = parseFloat(data_values[i].Catalan);
}
if(parseFloat(data_values[i].Catalan) < min_value){
min_value = parseFloat(data_values[i].Catalan);
}
}

color = d3.scale.quantize(45).domain([min_value,max_value]).range(scaleColors);

d3.json("comarques.topo.json", function(error, us) {

.data(topojson.feature(us, us.objects.comarques).features)
.enter()
.append("clipPath")
.attr("id",function(d){return d.properties.comarca;})
.append("path")
.attr("d",path);

svg.selectAll(".points")
.data(topojson.feature(us, us.objects.comarques).features)
.enter()
.append("g")
.attr("class","points")
.attr("clip-path", function(d){return "url(#"+d.properties.comarca+")";})
.each(draw_circles);

function draw_circles(d) {
pointSize = maxPointSize * Math.sqrt(data_values[parseInt(d.properties.comarca, 10)-1].Density/max_density);
bounds = path.bounds(d);
centroid = path.centroid(d);;
width_d = bounds - bounds;
height_d = bounds - bounds;
color_d = color(parseFloat(data_values[parseInt(d.properties.comarca, 10)-1].Catalan));
for (i_w = 0; i_w< Math.ceil(width_d); i_w = i_w + pointSeparation){
for (j_h = 0; j_h< Math.ceil(height_d); j_h = j_h + pointSeparation){
t_w = i_w-Math.ceil(width_d/2) + centroid;
t_h = j_h-Math.ceil(height_d/2) + centroid;
d3.select(this).append("circle")
.attr("r", pointSize)
.attr("cx",t_w)
.attr("cy",t_h)
.style("fill", color_d);
}
}

}

svg.append("g")
.attr("id","limits")
.append("path")
.datum(topojson.feature(us, us.objects.comarques))
.attr("class", "background")
.attr("d", path);

scale = svg.append("g");
scale.append("text")
.attr("x",width - 400 )
.attr("y",height - 50)
.text("Can speak Catalan");
for (i=0; i< scaleColors.length; i++){
scale.append("rect")
.attr("width", 30)
.attr("height",15)
.attr("x",width - 400 + 37*i )
.attr("y",height - 40)
.style("fill",scaleColors[i])
.style("stroke","#000");
scale.append("text")
.attr("x",width - 400 + 37*i )
.attr("y",height - 10)
.text(function(){
return Math.round(min_value + i*(max_value - min_value)/scaleColors.length)+"%";
});

}

});
});



•  The first lines set the pointSeparation and maxPointSize and set the color scale.
• Lines 20 to 31 set the projection and the SVG element.
• Lines 32 to 47 get the data about the number of speakers and population, and set the maximum and minimum values to be able to adjust colors and circle sizes.
• Line 49 sets the color scale
• Lines 53 to 60 sets the masks for each polygon by appending a clipPath
• Then, from 63 to 69, the points are drawn by calling the function draw_circles for each polygon. To call the function, the each method is used. The clip-path attribute links the masks set before.
• Lines 74 to 93 contain the function draw circles.
• The bounding box and centroid are nneded. The first one to get the width and height, and the second to move it at he region center.
• Once the size is set, the grid is drawn with the two loops
• lines 95 to 100 draw the borders
• lines 102 to 120 draw the legend.

## Notes

I created a GIST to store all the data for both examples, since It's easier to re-use it.