Gravatar rfw.name

Visualizing Facebook Friends With D3.js or “How Wolfram|Alpha Does That Cool Friend Network Graph”

A while ago, Wolfram|Alpha got the ability to generate personal analytics based on your Facebook profile. It made some cool numbers and stuff, but the friend network graph was the most impressive:

My friend network

Wolfram|Alpha neatly separates your various social circles into clusters, based on proximity — with freaky accuracy.

With the awesome D3.js library, along with some gratuitous abuse of the Facebook API, we can make our own!

If you’re impatient, skip through all this text and check out the example or the screenshot!

Mining Friend Data

You’ll want the Javascript SDK for Facebook and do all the setup stuff for a Facebook application (in their Developers section).

First we want to log into Facebook with FB.login, then we want to make requests for two things about the user:

Getting the names and IDs of friends is simple — make the /me/friends request to the Graph API, and we get the data back in a handy JSON list:

var graph = {};

FB.api('/me/friends', function(response) {
    // Construct a mapping of IDs to friends (we might want this later).
    var friends = response.data.reduce(function(acc, x) {
        acc[x.id] = x.name;
        return acc;
    }, {});

    // Extract the list of friend IDs.
    var fids = Object.keys(friends);

    // Add some nodes to the graph.
    graph.nodes = fids.map(function(fid) {
        return {
            id:     fid,
            name:   friends[fid]
        }
    });

    // We're not done yet!
});

Now, for some FQL abuse:

SELECT uid1, uid2
FROM friend
WHERE uid1 IN (SELECT uid2 FROM friend WHERE uid1=me()) AND
      uid2 IN (SELECT uid2 FROM friend WHERE uid1=me())

The friends relation in FQL specifies the connection of friendship between two friends. We can do a query across all friends where one of the friends is the logged-in user, and as such we can find all mutual friends of the logged-in user’s friends. Neat!

FB.api('/fql?q=' + escape('SELECT uid1, uid2 ' + 
                          'FROM friend ' +
                          'WHERE uid1 IN (SELECT uid2 FROM friend WHERE uid1=me()) AND ' + 
                          '      uid2 IN (SELECT uid2 FROM friend WHERE uid1=me())'),
    function(response) {
        // Build a list of edges from the relations. D3.js needs us to store
        // them as indexes of nodes in the nodes list.
        graph.edges = response.data.map(function(rel) {
            return {
                source: fids.indexOf(rel.uid1),
                target: fids.indexOf(rel.uid2)
            };
        });
});

Drawing the Graph

Now that we have a graph, we can use D3.js to draw it. We want to use the force layout to render our graph — it’s a graph layout that pulls connected nodes together and pushes disconnected nodes apart, and which happens to be a great estimation of friend network clustering.

// Create a force layout to display nodes.
var force = d3.layout.force()
    .charge(-120)
    .linkDistance(40)
    .nodes(graph.nodes)
    .links(graph.edges)
    .size([parseInt(d3.select('#graph').style('width'), 10),
           parseInt(d3.select('#graph').style('height'), 10)]);

// Append an <svg> element to body for rendering (warning: SVG sucks and will
// probably hang Firefox, so use Chrome).
var svg = d3.select('body')
    .append('svg')
    .attr('width', parseInt(d3.select('#graph').style('width'), 10))
    .attr('height', parseInt(d3.select('#graph').style('height'), 10));

// Add the edges to the SVG.
var edge = svg.selectAll('line.edge')
    .data(graph.edges)
    .enter().append('line')
    .attr('class', 'edge')
    .style('stroke', 'rgba(200, 200, 200, 0.2)')
    .style('stroke-width', 0.5);

// Add the nodes to the SVG.
var node = svg.selectAll('circle.node')
    .data(graph.nodes)
    .enter().append('circle')
    .attr('class', 'node')
    .attr('r', 5)
    .style('fill', '#aaa')
    .style('cursor', 'pointer')
    .call(force.drag);

// Hook up some events to D3.js.
force.on('tick', function() {
        node.attr('cx', function(d) { return d.x; })
            .attr('cy', function(d) { return d.y; });

        edge.attr('x1', function(d) { return d.source.x; })
            .attr('y1', function(d) { return d.source.y; })
            .attr('x2', function(d) { return d.target.x; })
            .attr('y2', function(d) { return d.target.y; });
    });
});

// Start the visualization.
force.start();

If you don’t like giving apps your Facebook information, here’s a screenshot (of the example, which has slightly more functionality than the code above).

Screenshot!

Here’s the interactive example (don’t worry, I’m not spying on your Facebook data). There’s a little more code in the sample below but feel free to view its source. If you use Firefox, you’re gonna have a bad time.

(Facebook requires me to display a privacy policy for this.)

Ending Remarks

I haven’t figured out how to do node coloration yet. I think Wolfram|Alpha might use some network data (university, work, etc.) and then connectedness to determine what subgroup nodes are in. If you have any idea, hit me up in the comments.

comments powered by Disqus