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