HiveBrain v1.2.0
Get Started
← Back to all entries
patternjavascriptMinor

D3 visualization of a Facebook friend graph

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
friendvisualizationgraphfacebook

Problem

I've been learning Python for about a year and now I'm trying to improve my JavaScript. I wrote this simple d3 visualization that shows your Facebook friends as a force-directed graph. Here's a live version and here's the source on GitHub. It works well, but it can be slow.

I understand that the number of edges to calculate grows \$O(n^2)\$ with nodes, so complexity increases quickly for users with lots of friends, but I'm wondering if:

-
there are any obvious optimizations I could make

-
what bad habits I might be importing from Python

-
how I might tweak the layout parameters to reach equilibrium faster

I'm pretty new to programming and very new to JavaScript, so I'm sure I'm making some mistakes.

Content of file friendgraph.js:

```
// Facebook SDK

// Initialize the Facebook SDK
window.fbAsyncInit = function () {
FB.init({
appId: '341827359241906', // App ID
channelUrl: 'channel.html', // Path to your Channel File
status: true, // check login status
cookie: true, // enable cookies to allow the server to access the session
xfbml: true // parse XFBML
});

// Listen for and handle auth.statusChange events
FB.Event.subscribe('auth.statusChange', function(response) {
if (response.authResponse) {
// On login...
FB.api('/me', function(me) {
if (me.name) {
// Display user name
document.getElementById('auth-displayname').innerHTML = me.name;
// Retrieve friends API object
FB.api('/me/friends', getFriends);
}
})
document.getElementById('auth-loggedout').style.display = 'none';
document.getElementById('auth-loggedin').style.display = 'block';
} else {
// User has not authorized your app or isn't logged in
document.getElementById('auth-loggedout').style.display = 'block';
document.getElementById('auth-loggedin').style.display = 'none';
}
});

// Respon

Solution

I couldn't test out your application since I don't have a facebook account but here are some tips:

1) Use dot notation instead of bracket notation to access known property names.

Old Code:

friends[sourceIndex]['value'];


New Code:

friends[sourceIndex].value;


2) Keep the operations peformed within a loop to the bare minimum.

For performance, it's best to avoid nested loops and function calls within a loop.

From your code:

//...
function indexWithAttribute(array, attr, value) {
    for (var i = 0; i < array.length; i++) {
        if (array[i][attr] === value) {
            return i;
        }
    }
}
//...
function getLinks(response, id, friends, friendlinks) {
//...
    for (i = 0; i < mutualFriends.length; i++) {
        friends[sourceIndex]['value'] = mutualFriends.length;
        targetIndex = indexWithAttribute(friends, 'id', mutualFriends[i]['id']);
//...


The code above is bad for performance because the for loop contains calls to indexWithAttribute(), which contains another for loop.
The deeply nested iterations will result in O(N^2) completion time.

One possible solution for this problem would be to create a hash table for the ids to index relationship. Hash tables take O(1) to find a value but require more memory, unlike a for loop O(n/2).

Code:

/**
* Returns a lookup table for the relationship (key)attribute to (value)index from an array of objects.
* This function expects that all referenced attributes will have a unique value.
* 
* @author Larry Battle 
* @param [Array] arr - An array of objects.
* @param [String] attr - A common property name amoung all the objects in `arr`.
* @returns [Object]
* @example

    var arr = [
        { id: 2 },
        { id: 12 },
        { id: 89 }
    ];
    var hash = createAttributeToIndexTable( arr, "id" );
    console.log( JSON.stringify( hash ) = '{"2":0,"12":1,"89":2}' );

**/
var createAttributeToIndexTable = function( arr, attr ){
    var hash = {};
    for( var i = 0, len = arr.length; i < len; i++ ){
        hash[ arr[i][attr] ] = i;
    }
    return hash;
};


Old code:

function getLinks(response, id, friends, friendlinks) {
    var mutualFriends = response['data'];
    var sourceIndex = indexWithAttribute(friends, 'id', id);
    var completed = Math.round(100*(sourceIndex/friends.length));

    document.getElementById('load-status').innerHTML = 'Calculating mutual friend links: ' + completed + '%'    
    for (i=0; i< mutualFriends.length; i++) {
            friends[sourceIndex]['value'] = mutualFriends.length;
            targetIndex = indexWithAttribute(friends, 'id', mutualFriends[i]['id']);
            friendlinks.push({'source':sourceIndex, 
                              'target':targetIndex,
                              'value':mutualFriends.length });
    }       
    if (sourceIndex === friends.length - 1) { 
        graphFriends(friends, friendlinks); 
    }        
}


New code:

var createAttributeToIndexTable = function( arr, attr ){
    var hash = {};
    for( var i = 0, len = arr.length; i < len; i++ ){
        hash[ arr[i][attr] ] = i;
    }
    return hash;
};
function getLinks(response, id, friends, friendlinks) {
    var mutualFriends = response.data;
    var idToIndexTable = createAttributeToIndexTable( friends, 'id' );
    var sourceIndex = idToIndexTable[id];
    var completed = Math.round(100 * (sourceIndex / friends.length));
    document.getElementById('load-status').innerHTML = 'Calculating mutual friend links: ' + completed + '%'
        for (i = 0; i < mutualFriends.length; i++) {
            friends[sourceIndex].value = mutualFriends.length;
            targetIndex = idToIndexTable[ mutualFriends[i].id ];
            friendlinks.push({
                'source' : sourceIndex,
                'target' : targetIndex,
                'value' : mutualFriends.length
            });
        }
        if (sourceIndex === friends.length - 1) {
            graphFriends(friends, friendlinks);
        }
}


3) Add a small delay to avoid locking up the webpage. Or use webworkers.

Old Code:

function getMutualFriends(id, friends, friendlinks) {
    FB.api('/me/mutualfriends/' + id, function (response) {
        getLinks(response, id, friends, friendlinks);
    });
}


New Code:

function getMutualFriends(id, friends, friendlinks) {
    FB.api('/me/mutualfriends/' + id, function (response) {
        setTimeout(function(){
            getLinks(response, id, friends, friendlinks);
        }, 100);
    });
}


Note:

Find a way to simplify this section. I think there might be a way to append a clone of a line without recreating it everytime.

Code:

```
force.nodes(friends).links(friendlinks).start();
var link = svg.selectAll("line.link").data(friendlinks).enter().append("line").attr("class", "link").style("stroke", "#eee").style("stroke-width", 1);
var node = svg.selectAll("circle.node").data(friends).enter().append("circle").attr("class", "node").attr("r", function

Code Snippets

friends[sourceIndex]['value'];
friends[sourceIndex].value;
//...
function indexWithAttribute(array, attr, value) {
    for (var i = 0; i < array.length; i++) {
        if (array[i][attr] === value) {
            return i;
        }
    }
}
//...
function getLinks(response, id, friends, friendlinks) {
//...
    for (i = 0; i < mutualFriends.length; i++) {
        friends[sourceIndex]['value'] = mutualFriends.length;
        targetIndex = indexWithAttribute(friends, 'id', mutualFriends[i]['id']);
//...
/**
* Returns a lookup table for the relationship (key)attribute to (value)index from an array of objects.
* This function expects that all referenced attributes will have a unique value.
* 
* @author Larry Battle <bateru.com/news>
* @param [Array] arr - An array of objects.
* @param [String] attr - A common property name amoung all the objects in `arr`.
* @returns [Object]
* @example

    var arr = [
        { id: 2 },
        { id: 12 },
        { id: 89 }
    ];
    var hash = createAttributeToIndexTable( arr, "id" );
    console.log( JSON.stringify( hash ) = '{"2":0,"12":1,"89":2}' );

**/
var createAttributeToIndexTable = function( arr, attr ){
    var hash = {};
    for( var i = 0, len = arr.length; i < len; i++ ){
        hash[ arr[i][attr] ] = i;
    }
    return hash;
};
function getLinks(response, id, friends, friendlinks) {
    var mutualFriends = response['data'];
    var sourceIndex = indexWithAttribute(friends, 'id', id);
    var completed = Math.round(100*(sourceIndex/friends.length));

    document.getElementById('load-status').innerHTML = 'Calculating mutual friend links: ' + completed + '%'    
    for (i=0; i< mutualFriends.length; i++) {
            friends[sourceIndex]['value'] = mutualFriends.length;
            targetIndex = indexWithAttribute(friends, 'id', mutualFriends[i]['id']);
            friendlinks.push({'source':sourceIndex, 
                              'target':targetIndex,
                              'value':mutualFriends.length });
    }       
    if (sourceIndex === friends.length - 1) { 
        graphFriends(friends, friendlinks); 
    }        
}

Context

StackExchange Code Review Q#15396, answer score: 9

Revisions (0)

No revisions yet.