Source: Metanodes.js

var nodesOld = []; //contains the original graph nodes
var linksOld = []; //contains the original graph links
var nodes_communitys; //contains all nodes with its community
var amountMetaN; //contains the overall number of communities

//contains the new meta-nodes
var newMetaNodes = [];
//contains the new meta-links
var newLinks = [];

var linksD = []; //contains the meta-links in a drawable form
var nodesD = []; //contains the meta-nodes in a drawable form

var dataset = []; //contains data for statistic

/**
 * This function creates metanodes out of nodes according to their community attribute.
 * This is done by creating for every community a node and the size of the node is determined by the number of associated
 * nodes. The strength of the links between these new nodes, which represent the metanodes, is determined by the number
 * of links between the original nodes. The more connections between a pair of communities exists, the bigger the link is
 * drawn.
 * @param {Array} nodesV contains all nodes of the original graph
 * @param {Array} linksV contains all links of the original graph
 * @param {Array} result contains the id's of all nodes with its community number
 * @param {number} communityCount contains the overall number of communities
 */
function createMetanodes(nodesV,linksV,result,communityCount)
{
    //initalize empty
    nodesOld = []; //contains the original graph nodes
    linksOld = []; //contains the original graph links
    nodes_communitys; //contains all nodes with its community
    amountMetaN; //contains the overall number of communities
    newMetaNodes = [];
    newLinks = [];
    linksD = []; //contains the meta-links in a drawable form
    nodesD = []; //contains the meta-nodes in a drawable form
    dataset = []; //contains data for statistic

    nodesOld = nodesV;
    linksOld = linksV;
    nodes_communitys = result;

    //need as many nodes as communities
    amountMetaN = communityCount;

    //for every community create metanode
    for(var i = 0; i < amountMetaN; i++)
    {
        var metaN = createMetaNode(i);
        newMetaNodes[i] = metaN;
    }

    //will contain the new links
    newLinks = createInterANDIntraNodes();


    //create dataset for the statistic
    var metaNodeNames = [];
    for (var key in newMetaNodes)
    {
        //name
        var name = "" + key;
        metaNodeNames.push(name);

        var dataP =
            {
                label: name,
                "Nodes": newMetaNodes[key].amountN,
                "IntraEdges": newLinks[key].intraEdges.length,
                "InterEdges": newLinks[key].interEdges.length
            }

        dataset.push(dataP);
    }

    //create nodes and links for drawing the meta-node graph
    var i = 0;
    for (var m = 0; m <newLinks.length; m++ )
    {
        for(var key in newLinks[m])
        {
            for(var n = 0; n <newLinks[m][key].length; n++ )
            {
                link = newLinks[m][key][n];
                linksD[i] = {source: link.source,
                    target: link.target
                };
                i = i+1;
            }
        }
    }

    // compute nodes from links data
    linksD.forEach(function(link) {
        link.source = nodesD[link.source] ||
            (nodesD[link.source] = {id: link.source});
        link.target = nodesD[link.target] ||
            (nodesD[link.target] = {id: link.target});

    });
}

/**
 * This function creates one metanode to the given community number.
 * For every metanode the amount of belonging nodes is calculated and their id's are stored.
 * @param {number} communityNr current community number which forms a metanode
 * @param {Array} nodes_communitys contains all nodes with its community
 * @return {Object} result which contains as properties the overall amount of belonging nodes and an array of ids
 * the belonging nodes
 */
function createMetaNode(communityNr)
{
    amountNode = 0;
    belongingNodes = [];

    var i = 0;
    //for all nodes which have the recent communityNr
    for(var nodeId in nodes_communitys) {

        var comm = nodes_communitys[nodeId];
        if(comm == communityNr)
        {
            //metanode
            belongingNodes.push(nodeId);
            amountNode = amountNode +1;
        }
        i++;
    }

    var result =
        { amountN: amountNode,
          belongingN: belongingNodes,
          id: communityNr
        };

    return result;
}

/**
 * This function creates inter- and intra-edges according to the current metanode.
 * The intra-edges are calculated for every metanode by using the links in the original node and looking through every stored
 * node of the metanode, if there is a connection to another node with the same community number. For inter-edges the same
 * is done, with the difference that connections to nodes with other communitiy numbers are sought.
 * @return {Array} nL is an array of objects which contains the inter- and intra-links of all metanodes.
 */
function createInterANDIntraNodes()
{    //find according links
    var nL = [];

    for(var metaNodeId = 0; metaNodeId < newMetaNodes.length; metaNodeId++)
    {
        var intraEdges = []; //id of edges inside the same community
        var interEdges = []; //id of edges to other communities

        for(var i = 0; i < newMetaNodes[metaNodeId].belongingN.length; i++)
        {
            var currentN = newMetaNodes[metaNodeId].belongingN[i];
            for(var key in linksOld)
            {
                if (linksOld[key].source.id == currentN)
                {
                    var target = linksOld[key].target.id;
                    if(nodesOld[target].module == metaNodeId.toString() )
                    { //target node belongs to the same community
                        var edge = {
                            source: metaNodeId,
                            target: nodesOld[target].module
                        };
                        intraEdges.push(edge);

                    }else
                    { //target node belongs to another community
                        var edge = {
                            source: metaNodeId,
                            target: nodesOld[target].module
                        };
                        interEdges.push(edge);
                    }
                }
            }
        }

        nL[metaNodeId] = {
                            intraEdges: intraEdges,
                            interEdges: interEdges
                         };
    }
    return nL;
}

/**
 * This functions draws the metanode graph and the statistics. For the metanode graph a d3 force layout is created on a
 * svg. The statistic is drawn as grouped bar chart, which is also located on an svg. Also a close button is created to
 * delete these two svgs.
 */
function drawMetaNodeGraphAndStatistic()
{
    var svg = d3.select("svg");

    //metaNodeSVG is appended on original svg
    var metaNodeSVG = svg.append("svg")
        .attr("id", "metaNodeSVG")
        .attr("width", svg.attr("width")*0.6) //69%
        .attr("height", svg.attr("height"))
        .attr("x", svg.attr("width")*0.4)
        .classed("metaNodeContainer",true);

    //metaNodeSVG is appended on original svg
    var statisticSVG = svg.append("svg")
        .attr("id", "statisticSVG")
        .attr("width", svg.attr("width")*0.4)
        .attr("height", svg.attr("height"))
        .classed("statisticContainer",true);

    drawMetaNodes(metaNodeSVG);
    calculateStatistic(statisticSVG);

    // Close Button
    createMetaNodeCloseButton();
}

/**
 * This function uses a given svg to draw a grouped bar chart on it. Here for every metanode the amount of belonging nodes
 * is shown, as well as the amount of intra- and inter-edges.
 * @param {SVG}statisticSVG contains the SVG in which the grouped bar chart is drawn
 */
function calculateStatistic(statisticSVG)
{
    //draw black border
    statisticSVG.append("rect")
        .attr("width", "100%")
        .attr("height", "100%")
        .attr("fill", "white")
        .attr("stroke", "#616161")
        .attr("stroke-width", 1)
        .attr("fill-opacity", 1)
        .attr("stroke-opacity",1);

    var attributeNames = d3.keys(dataset[0]).filter(function(key) { return key !== "label"; });

    dataset.forEach(function(d) {
        d.amounts = attributeNames.map(function(name) { return {name: name, value: +d[name]}; });
    });

    // drawing
    var widthP = statisticSVG.attr("width")/100;
    var heightP = statisticSVG.attr("height")/100;

    var margin = {top: 10*heightP, right: 10*widthP, bottom: 25*heightP, left: 10*widthP},
        width = statisticSVG.attr("width")- margin.left - margin.right,
        height = statisticSVG.attr("height") - margin.top - margin.bottom;

    var x0 = d3.scale.ordinal()
        .rangeRoundBands([margin.left*0.7, width], 0.1);

    var x1 = d3.scale.ordinal();

    var y = d3.scale.linear()
        .range([height, margin.bottom]);

    var xAxis = d3.svg.axis()
        .scale(x0)
        .tickSize(0)
        .orient("bottom");

    var yAxis = d3.svg.axis()
        .scale(y)
        .orient("right");

    var color = d3.scale.ordinal()
        .range(["#e41a1c","#377eb8","#4daf4a"]);

    //give names to chart
    x0.domain(dataset.map(function(d) { return d.label; }));
    x1.domain(attributeNames).rangeRoundBands([0, x0.rangeBand()])

    //give data to chart
    y.domain([0, d3.max(dataset, function(d) { return d3.max(d.amounts, function(d) { return d.value; }); })]);

    statisticSVG.append("g")
        .attr("class", "x axis")
        .attr("transform", "translate("+margin.left+"," + height + ")") //change here for bar width and location
        .call(xAxis);

    //create the y-axis and a y-axis text label
    statisticSVG.append("g")
        .attr("class", "y axis")
        .attr("transform", "translate(" + margin.left*0.5 + ", 0)") //change black y-axis here
        .call(yAxis)
        .append("text")
        .attr("x",-13*heightP)
        .attr("y", margin.left*0.5)
        .attr("transform", "rotate(-90)")
        .attr("dy", ".71em")
        .style("text-anchor", "end")
        .text("Amount");

    var bar = statisticSVG.selectAll(".bar")
        .data(dataset)
        .enter().append("g")
        .attr("class", "rect")
        .attr("transform", function(d) { return "translate(" +x0(d.label) + ",0)"; });

    bar.selectAll("rect")
        .data(function(d) { return d.amounts; })
        .enter().append("rect")
        .attr("width", x1.rangeBand())
        .attr("x", function(d) { return x1(d.name); })
        .attr("y", function(d) { return y(d.value); })
        .attr("value", function(d){return d.name;})
        .attr("height", function(d) { return height - y(d.value); })
        .style("fill", function(d) { return color(d.name); })
        .attr("transform", function(d) { return "translate(" +margin.left + ",0)"; });;


    //right legend
    var legend = statisticSVG.selectAll(".legend")
        .data(attributeNames.slice())
        .enter().append("g")
        .attr("class", "legend")
        .attr("transform", function(d, i) { return "translate(0," + i*20 + ")"; })

    legend.append("rect")
        .attr("x", width - 5*widthP)
        .attr("y",  6.5*heightP)
        .attr("width", 18)
        .attr("height", 18)
        .style("fill", function(d) { return color(d); });

    legend.append("text")
        .attr("x", width - 8*widthP)
        .attr("y", 8*heightP)
        .attr("dy", ".35em")
        .style("text-anchor", "end")
        .text(function(d) { return d; });

}

/**
 * This functions draws the metaNode graph on a delivered svg. For every community number a meta-node is drawn with its
 * related color. The size of the node is calculated according to the amount of nodes beloning to the certain community
 * The strength of the links is dependent on the number of inter-edges to the respective other communities.
 * @param {SVG} metaNodeSVG contains the SVG on which the graph will be drawn
 */
function drawMetaNodes(metaNodeSVG)
{
    var widthP = metaNodeSVG.attr("width")/100;
    var heightP = metaNodeSVG.attr("height")/100;

    var margin = {top: 0*heightP, right: 0*widthP, bottom: 0*heightP, left: 0*widthP};
    var widthM = metaNodeSVG.attr("width")- margin.left - margin.right;
    var heightM = metaNodeSVG.attr("height") - margin.top - margin.bottom;

    //set background color of metaNodeSVG
    metaNodeSVG.append("rect")
        .attr("width", "100%")
        .attr("height", "100%")
        .attr("fill", "white");

    //define layout for metaNode-graph
    var force = d3.layout.forceInABox()
        .groupBy("function (d) {return d.id}")
        .gravityOverall(0.5)
        .size([widthM, heightM])
        .linkStrengthInterCluster(0.5)
        .charge(-3000);

    force
        .nodes(d3.values(nodesD))
        .links(linksD)
        .on("tick", tick);

    // Draw the graph lines
    var link = metaNodeSVG.selectAll(".link")
        .data(linksD)
        .enter().append("line")
        .attr("class", "link")
        .attr("id", function(d,i){ return "link" + i});

    // Draw node as a circle.
    var node = metaNodeSVG.selectAll(".node")
        .data(force.nodes())
        .enter().append("g")

    var circle = node.append("circle")
        .attr("class", "node")
        .attr("id", function(d,i){ return "node" + i});


    //calculate attributes of links
    var norm = Math.sqrt(Math.pow(width,2) + Math.pow(height,2));
    //force.linkDistance(norm/10);
    force.linkDistance(norm/7);
    //calculate links according to amount of links
    link.style("stroke-width", function (d)
                                {
                                    var percent = 100/linksD.length;
                                    var interEdges =  newLinks[d.source.id].interEdges;

                                    var size = 0;
                                    for(var i = 0; i < interEdges.length; i++)
                                    {
                                        if(interEdges[i].target == d.target.id)
                                        {
                                            size = size+1;
                                        }
                                    }
                                    size = size*percent;
                                    return 1+(2*size); //without additional factor
                                });

    //calculate origin radius of nodes
    norm = norm/300;
    if(norm > 7) norm = 7;
    var radius = norm;

    //calculate nodes according to amount of nodes in metaNode
    var color = d3.scale.category20();//color nodes according to their cluster
    circle.attr("r", function(d)
                    {
                        var percent = 100/amountMetaN;
                        var size = (newMetaNodes[d.id].amountN*percent/100); //size is between [0,1]

                        //calculate radius according to amount of nodes
                        //return radius + (5*size) //with additional factor
                        var r = radius + (2*size);
                        newMetaNodes[d.id]["radius"] = r;

                        return  r;
                    });

    circle.style("fill", function(d){return color(d.id);})
    var nodeName = node.append("svg:text")
        .attr("class", "text")
        .attr("dy", ".35em")
        .text(function(d) {return d.id});

    force.start();


    function tick(e) {

        circle.attr("cx", function(d) {
            return d.x = Math.max(newMetaNodes[d.id].radius, Math.min(widthM - newMetaNodes[d.id].radius, d.x));
        })
            .attr("cy", function(d) {
                return d.y = Math.max(newMetaNodes[d.id].radius, Math.min(heightM - newMetaNodes[d.id].radius, d.y)); });


        nodeName.attr("x", function (d) {return d.x;})
                .attr("y", function (d) {return d.y - 4*heightP;});

        link.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; });

    }

}

/**
 * This function deletes the svgs which store the meta-graph and the statistic. Therefore all existing svgs are located and
 * the svgs with the id's "metaNodeSVG" and "metaNodeSVG" are removed. Also the close button is removed.
 */
function deleteGraphANDStatistic()
{
    var svgs = d3.selectAll("svg")[0];
    var flagDelete = [];

    for(var i = 0; i <svgs.length; i++)
    {
        if((svgs[i].id == "metaNodeSVG") || (svgs[i].id == "statisticSVG")) {
            flagDelete[i] = 1;
        } else
        {
            flagDelete[i] = 0;
        }
    }

    for(var i = 0; i <svgs.length; i++)
    {
        if(flagDelete[i] == 1)
        {
            svgs[i].remove();
        }
    }

    //remove close button
    var con = document.getElementById("svgContainer");
    var children = con.childNodes;

    for(var c = 0; c < children.length; c++) {
        if (children[c].id == "closeButton") {
            con.removeChild(children[c]);
        }
    }
}

/**
 * This function adds a close button to the container holding the svgs which contain the metanode graph and the statistic.
 * This is done by searching this container and appending a button on it. When this button is clicked, the meta-node
 * force layout and the grouped bar chart representing the statistic are deleted.
  */
function createMetaNodeCloseButton()
{
    var MetaOverlay = document.getElementById("svgContainer");
    // Button
    var closeButton = document.createElement("SPAN");
    closeButton.classList.add("w3-button");
    closeButton.classList.add("w3-theme-l2");
    closeButton.classList.add("w3-display-topright");
    closeButton.setAttribute("id", "closeButton");
    MetaOverlay.appendChild(closeButton);
    // Icon
    var closeIcon = document.createElement("SPAN");
    closeIcon.classList.add("fa");
    closeIcon.classList.add("fa-remove");
    closeIcon.classList.add("fa-lg");
    closeButton.appendChild(closeIcon);
    // Close function
    closeButton.addEventListener("click", function() {deleteGraphANDStatistic() });
}