John Trengrove

home posts about

Tor relay d3.js map

14 Jun 2014

Tor is a program for anonymising one's internet traffic. It does this through funnelling traffic through a number of Tor relays. Data is based on a snapshot from Onionoo on the 23rd of May.

I wanted to have a look at these relays and map out their geographic distribution. Using d3.js and the d3.js hexbin plugin, I grouped the relays by location, with the size of the hexbin representing the number or relays. I then coloured the relays by speed, with redder hexbin representing groups with faster relays.

As slow Tor relays are generally due to people hosting them on their own home network, a dark red cluster would generally represent a set of dedicated servers running Tor. It is interesting to see that Athens runs mainly fast Tor relays, and also the speed and number of relays in Denver.

var width = 740,
  height = 550;

var projection = d3.geo.mercator()
  .translate([375, 330])
  .scale(160)

var color = d3.scale.linear()
  .domain([0, 1500000])
  .range(["#ffcc00", "red"])
  .interpolate(d3.interpolateLab);

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

var radius = d3.scale.sqrt()
  .domain([0, 100])
  .range([0, 2]);

var hexbin = d3.hexbin()
  .size([width, height])
  .radius(2);

var graticule = d3.geo.graticule();

var svg = d3.select("svg")
  .style("display", "block")
  .attr("width", width)
  .attr("height", height)
  .call(d3.behavior.zoom()
  .on("zoom", redraw))
  .append("g")

function redraw() {
  svg.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
}

queue()
  .defer(d3.json, "/data/world-50m.json")
  .defer(d3.json, "/data/relays-23-may.json")
  .await(ready);

svg.append("path")
  .datum(graticule)
  .attr("class", "graticule")
  .attr("d", path);

function ready(error, world, details) {
  var relays = details.relays;
  var ry = []
  relays.forEach(function(r) {
    r.proj = projection([r[0],r[1]]);
    ry.push({
      "0": r.proj[0],
      "1": r.proj[1],
      "observed_bandwidth": r[2]
    })
  });

  svg.insert("path", ".graticule")
    .datum(topojson.feature(world, world.objects.land))
    .attr("class", "land")
    .attr("d", path);

  svg.insert("path", ".graticule")
    .datum(topojson.mesh(world, world.objects.countries, function(a, b) { return a !== b; }))
    .attr("class", "boundary")
    .attr("d", path);

  svg.append("g")
    .attr("class", "hexagons")
    .selectAll("path")
    .data(hexbin(ry))
    .enter().append("path")
    .attr("d",function(d) { return hexbin.hexagon(radius(d.length)); })
    .attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; })
    .style("fill", function(d) { return color(d3.median(d, function(d) { return d.observed_bandwidth})); })

}