Source: main.js

// Define the dimensions of the visualization through a container.
var container = d3.select("div.svgContainer")
        .classed("svgContainer", true) //container class to make it responsive
        .append("svg");

var svg = d3.select("svg")
            .attr("id", "svg");

// links/nodes array
var links = [];
var nodes = [];
var radius = 0;

//global variables for metrics
/**
 * Indicates when the calculations are finished
 * @variable {boolean} global
 */
var calculationsReady = false;
/**
 * Array of all links in the graph
 * @variable {Array} global
 */
var allLinks = [];
/**
 * Array of all links which the user has selected
 * @variable {Array} global
 */
var userSelectedLinks = [];
/**
 * Size of the filter area for some Metrics
 * @variable {number} global
 */
var filterArea = 100;
/**
 * Weight of the Distance
 * @variable {number} global
 */
var alpha = 1/3;
/**
 * Weight of the edge length
 * @variable {number}
 */
var beta = 1/3;
/**
 * Weight of the parallelism
 * @variable {number} global
 */
var gamma = 1/3;
/**
 * Defines the radius of the marks on the Heatmap
 * @variable {number} global
 */
var radiusHeatmap = 0;
/**
 * Amount of communities
 * @variable {number} global
 */
var communityCount = 0;
/**
 * Array of all nodes and its communities
 * @variable {Array} global
 */
var community_result = [];
/**
 * Array of numbers which defines how many nodes of an community exists
 * @variable {Array} global
 */
var result_Array = [];
/**
 * Result of the autocorrelation metric
 * @variable {Array} global
 */
var community_autocorrelationMetric = [];
/**
 * Result of the entropy metric
 * @variable {Array} global
 */
var community_entropyMetric = [];
/**
 * Result of the metanode metric
 * @variable {Array} global
 */
var metanodes = [];
/**
 * Result of the EdgeBundling metric for every link
 * @variable {Array} global
 */
var EdgeBundling = [];

/**
 * Loads the .tsv data with d3 function
 * This Function is responsible for calculating the entire force-directed graph.
 * The File has to be in tsv form with two tabs "FromNodeId" and "ToNodeId".
 * The nodes and links get extracted from the file and are calculated with d3 to display a graph.
 */
d3.tsv("extern/Amazon0601.tsv", function(error, data)  {
    // This code is executed when the data.tsv file is loaded.
    if (error) throw error;

    // Store the links in global variable
    data.forEach(function(d) {
        links.push({source: d.FromNodeId, target: d.ToNodeId});
        //addNode(d.FromNodeId); // Adds the node if it is not contained yet
    })

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

    });

    var force = d3.layout.forceInABox()
       .groupBy("function (d) {return d.module}")
       .linkStrengthInterCluster(0.05)
       .gravityToFoci(0.2)
       .gravityOverall(0.05);

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

    // Draw the SVG lines
    var link = svg.selectAll(".link")
        .data(links)
        .enter().append("line")
        .attr("class", "link")
        .attr("id", function(d,i){ return "link" + i});
        //.attr("selected", false);

    // Draw node as a circle.
    var node = svg.selectAll(".node")
        .data(force.nodes())
        .enter().append("circle")
        .attr("class", "node")
        .attr("id", function(d,i){ return "node" + i});

    //wait until graph converges
    force.on("end", calculateVis);// layout is done

    // Resize the Layout through Window size
    resize();
    d3.select(window).on("resize", resize);

    force.start();

    function tick(e) {
        node.attr("cx", function(d) { return d.x = Math.max(radius, Math.min(width - radius, d.x)); })
            .attr("cy", function(d) { return d.y = Math.max(radius, Math.min(height - radius, d.y)); });
            //.call(force.drag);

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


    /**
     * The function resizes the force-directed graph and the svg which is displayed.
     * It calculates the properties by taking the sizes of the HTML Tags and adjust the graph.
     * During the calculation all other function are disabled (Heatmap, Metrics) and a Loading Symbol is displayed.
     */
    function resize() {
        // If a loading icon exists delte it
        destroyLoadingIcon();
        //Show Loading icon
        createLoadingIcon();

        //width = window.innerWidth, height = window.innerHeight;
        //width = document.getElementById('svgContainer').clientWidth;
        //height_header = document.getElementById('header').clientHeight;
        //height_footer = document.getElementById('footer').clientHeight;
        var footer = document.getElementById("footer");
        var mainContent = document.getElementById("mainContent");
        var wrapper = document.getElementById("wrapper");
        var leftMenue = document.getElementById("leftMenue");

        width = mainContent.clientWidth;
        height = window.innerHeight -(wrapper.clientHeight + footer.clientHeight);
        //height = leftMenue.clientHeight + (wrapper.clientHeight + footer.clientHeight);
        svg.attr("width", width).attr("height", height);
        force.size([width, height]).resume();
        var norm = Math.sqrt(Math.pow(width,2) + Math.pow(height,2));
        force.linkDistance(norm/30);
        norm = norm/300;
        if(norm > 7) norm = 7;
        radius = norm;
        node.attr("r", radius);

        // Make Slider as big as the biggest size (width,height)
        var FilterSize = document.getElementById('FilterSize');
        FilterSize.max = Math.max(width,height);
        FilterSize.value = Math.round(FilterSize.max/2);
        getFilterSizeValue();

        //if heatmap-canvas or statistic was drawn --> gets deleted before the new calculations
        deleteGraphANDStatistic();
        deleteCanvas();
        calculationsReady = false; // HeatMaps can not be used!
        //wait until graph converges and recalculate all metrics
        force.on("end", calculateVis);// layout is done

    }

    /**
     * The function calculates community structures and colors nodes accordingly
     * It should only be called after the graph has been finished drawing!
     * When this function is finished the graph is ready to be used for other computations and calculationsReady is set true.
     */
    function calculateVis()
    {
        //create community structure by Louvain
        var node_data = nodes.map(function (n){return n.id});
        var edge_data = links.map(function (n) {return {source: n.source.id, target: n.target.id, weigth: 1};});

        var community = jLouvain().nodes(node_data).edges(edge_data);
        var result  = community();
        community_result = result;

        // Map the Object to an Array (only the community number)
        result_Array = Object.keys(result).map(function(key) {
            return [result[key]];
        });

        communityCount = arrayMax(result_Array)+1; // Count all communities (starts at 0)

        var index = 0;
        nodes.forEach(function(n)
        {
            n.index = index;
            n.module = result[n.id];

            //get exact node positions of graph
            var positions = nodes.map(function(d) { return [d.x, d.y]; });
            n.x = positions[n.id][0];
            n.y = positions[n.id][1];
            index = index + 1;
        });

        //color nodes according to their cluster
        var color = d3.scale.category20();
        node.style("fill", function(d){return color(d.module);});


        radiusHeatmap = node.attr("r");

        // Calculation are done
        destroyLoadingIcon();

        LinkSelection(svg, link); // Starts the interaction
        allLinks = link[0];

        calculationsReady = true; // HeatMaps can be used!

    }
});

//###################### FUNCTIONS ######################

/**
 * Adds a Node to the node array if it is not contained already
 * @param {Object} node A node of the graph
 */
function addNode(node) {
    var found = nodes.some(function (el) {
        return el.id === node;
    });
    if (!found) {
        nodes.push({ id: node});
    }
};

/**
 * Returns the maximum value of an given array
 * @param {Array} arr Array of numbers
 * @returns {number} max Returns the maximum number from the array
 */
function arrayMax(arr) {
    var len = arr.length, max = -Infinity;
    while (len--) {
        if (Number(arr[len]) > max) {
            max = Number(arr[len]);
        }
    }
    return max;
};

/**
 * Function iterates over all nodes and their communities in the given array and adds up the amount for every community
 * Result is an Array of how many nodes are in the different communities
 * @param {Array} array Array of all nodes and an number to which community they belong
 * @returns {Array} occurrences Array with numbers which define how many nodes are in a community (0,..,n)
 */
function occurrence(array) {
    var occurrences = [];

    // Iterate over all nodes and their communities and add up the amount for every community
    array.forEach(function (nodes, i) {
        if (!occurrences[nodes]) {
            occurrences[nodes] = [i];
        } else {
            occurrences[nodes].push(i);
        }
    });
    Object.keys(occurrences).forEach(function (v) {
        occurrences[v] = [occurrences[v].length];
    });

    return occurrences;
};


//###################### HeatMap Buttons / Input ######################

/**
 * The function calculates the Autocorrelation Community Metric and draws the Heatmap for it.
 * The calculation only starts if the graph drawing is finished (calculationsReady = true).
 * Before drawing the new Heatmap, old canvas, statistics and other heatmaps are deleted.
 * @event onclick Activates after clicking onto the AutocorrelationBased Button
 */
function clickOn_AutocorrelationBased()
{
    if(calculationsReady){
        deleteGraphANDStatistic();
        deleteCanvas();
        community_autocorrelationMetric = Community_AutocorrelationBased(nodes, Object.keys(nodes).length, filterArea);
        drawHeatMap(community_autocorrelationMetric,radiusHeatmap*5);
    }

}

/**
 * The function calculates the EntropyBased Community Metric and draws the Heatmap for it.
 * The calculation only starts if the graph drawing is finished (calculationsReady = true).
 * Before drawing the new Heatmap, old canvas, statistics and other heatmaps are deleted.
 * @event onclick Activates after clicking onto the EntropyBased Button
 */
function clickOn_EntropyBased()
{
    if(calculationsReady) {
        deleteGraphANDStatistic();
        deleteCanvas();
        community_entropyMetric = Community_EntropyBased(Object.keys(nodes).length,communityCount,occurrence(result_Array), nodes, filterArea);
        drawHeatMap(community_entropyMetric, radiusHeatmap*5);
    }
}

/**
 * The function calculates the EdgeBundling Metric and draws the Heatmap for it.
 * The calculation only starts if the graph drawing is finished (calculationsReady = true) and the user has selected
 * at least 2 links on the graph (svg).
 * Before drawing the new Heatmap, old canvas, statistics and other heatmaps are deleted.
 * @event onclick Activates after clicking onto the EdgeBundling Button
 */
function clickOn_EdgeBundling()
{
    if(calculationsReady) {

        deleteGraphANDStatistic();
        deleteCanvas();

        if(userSelectedLinks.length < 2){
            //Metric for Edge Bundling with all links
            //EdgeBundling =EdgeBundlingMetrics(allLinks,alpha,beta,gamma);
        }
        else{
            //Metric for Edge Bundling with all links
            EdgeBundling =EdgeBundlingMetrics(userSelectedLinks,alpha,beta,gamma);
            drawHeatMap(EdgeBundling, radiusHeatmap*2);
        }

    }
}

/**
 * The function calculates the Meta Nodes and draws the Statistic and Graph for it.
 * The calculation only starts if the graph drawing is finished (calculationsReady = true).
 * Before drawing the new Heatmap, old canvas, statistics and other heatmaps are deleted.
 * @event onclick Activates after clicking onto the Aggregate Button
 */
function clickOn_Aggregate()
{
    if(calculationsReady) {

        deleteGraphANDStatistic();
        deleteCanvas();
        metanodes = createMetanodes(nodes,links,community_result,communityCount);
        drawMetaNodeGraphAndStatistic();
    }
}

/**
 * Writes out the Range Slider Value to a label and the global variable "filterArea"
 * @event oninput Activates after changing the slider value
 */
function getFilterSizeValue()
{
    var FilterSize = document.getElementById('FilterSize');
    document.getElementById('FilterSize_Value').innerHTML = FilterSize.value;
    filterArea = parseFloat(FilterSize.value);
}

/**
 * Gets the Weight for the Distance from the input field and stores it in the global variable "alpha"
 * Calls @checkValidWeight function
 * @event oninput Activates after changing the Distance Weight value
 */
function getDistanceValue()
{
    var distanceInput = document.getElementById('Distance');
    alpha = parseFloat(distanceInput.value);
    checkValidWeight();
    distanceInput.reportValidity();
}

/**
 * Gets the Weight for the EdgeLength from the input field and stores it in the global variable "beta"
 * Calls @checkValidWeight function
 * @event oninput Activates after changing the EdgeLength Weight value
 */
function getEdgeLengthValue()
{
    var edgeLengthInput = document.getElementById('EdgeLength');
    beta = parseFloat(edgeLengthInput.value);
    checkValidWeight();
    edgeLengthInput.reportValidity();
}

/**
 * Gets the Weight for the Parallelism from the input field and stores it in the global variable "beta"
 * Calls @checkValidWeight function
 * @event oninput Activates after changing the Parallelism Weight value
 */
function getParallelismValue()
{
    var parallelismInput = document.getElementById('Parallelism');
    gamma = parseFloat(parallelismInput.value);
    checkValidWeight();
    parallelismInput.reportValidity();
}

/**
 * Checks the Validity of the three weight input fields.
 * All three values in the range of [0,1] should add up to 1.
 * Because of the precision of two digits the sum has to add up to 0.99, everything below is invalid.
 * Every value above 1 is also invalid. If the sum is invalid the three fields are marked red and a
 * error message is displayed.
 */
function checkValidWeight()
{
    var distanceInput = document.getElementById('Distance');
    var edgeLengthInput = document.getElementById('EdgeLength');
    var parallelismInput = document.getElementById('Parallelism');

    var sum = alpha + beta + gamma;

    if(sum > 1){
        distanceInput.setCustomValidity('Sum of weights > 1');
        edgeLengthInput.setCustomValidity('Sum of weights > 1');
        parallelismInput.setCustomValidity('Sum of weights > 1');
    }
    else if(sum < 0.99){
        distanceInput.setCustomValidity('Sum of weights < 1');
        edgeLengthInput.setCustomValidity('Sum of weights < 1');
        parallelismInput.setCustomValidity('Sum of weights < 1');
    }
    else{
        distanceInput.setCustomValidity('');
        edgeLengthInput.setCustomValidity('');
        parallelismInput.setCustomValidity('');
    }
}

/**
 * Displays an Loading Icon in the top-right edge of the svg
 */
function createLoadingIcon()
{
    var svgContainer = document.getElementById("svgContainer");

    var loadingIcon = document.createElement("SPAN");
    loadingIcon.classList.add("w3-container");
    loadingIcon.classList.add("w3-display-topright");
    loadingIcon.setAttribute("id", "loadingIcon");
    svgContainer.appendChild(loadingIcon);

    var icon = document.createElement("SPAN");
    icon.classList.add("fa");
    icon.classList.add("fa-spinner");
    icon.classList.add("fa-2x");
    icon.classList.add("w3-spin");
    loadingIcon.appendChild(icon);
}

/**
 * Destroys all Loading Icons which are placed in the svg
 */
function destroyLoadingIcon()
{
    var svgContainer = document.getElementById("svgContainer");
    for(var i= 0; i <svgContainer.children.length; i++){
        var loadingIcon = document.getElementById("loadingIcon");
        if(loadingIcon != null) svgContainer.removeChild(loadingIcon);
    }


}