/**
* @typedef {number} Motif
**/
/**
*
* @enum {Motif}
*/
var MotifEnum = Object.freeze({"CLIQUE":1, "FAN":2, "CONNECTOR":3});
var cliquePolygonPoints = "0,-150 125,-200 200,-125 150,0 200,125 125,200 0,150 -125,200 -200,125 -150,0 -200,-125 -125,-200";
var connectorPolygonPoints = "0,-200 200,0 0,200 -200,0";
/**
* Represents the network.
* @constructor
* @param {Object} graph - Graph object containing the nodes and links of the graph as properties. Nodes and
* links are arrays of node/link objects.
* @param {Object} nxGraph - Data structure of the graph as needed for the networkx library to detect cliques.
* @param {d3.selection} svg - D3.selection containing the svg HTML-Element to draw the graph.
* @param {number} width - Width of svg.
* @param {number} height - Height of svg.
* @param {Function} labeling - Function returning the label of a node object.
*
*/
function Network(graph, nxGraph, svg, width, height, labeling){
this.nxGraph = nxGraph;
this.labeling = labeling; //Name of the node-attribute that should be displayed
var global_motif_index = 0;
var motifs = {};
var motif_properties = {};
var detectedNodes = [];
var cliques;
var fans;
var connector;
var graph = graph;
var svg = svg;
var svgW = width;
var svgH = height;
var prevScale = 1;
var prevTranslate = [0, 0];
var transformManual = false;
var appliedTransform;
var colorAttr;
var simulation;
var color;
var link;
var node;
var labels;
var legend = [];
var zoom;
/**
* Function to initialize the d3 force simulation and to create html-elements of the nodes, links
* and labels.
*/
this.setUpNetwork = function setUpNetwork() {
zoom = d3.zoom();
svg.call(zoom.on("zoom", zoomEvent));
color = d3.scaleOrdinal(d3.schemeCategory20);
simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) { return d.id; }))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(svgW / 2, svgH / 2))
.force("collide", d3.forceCollide().radius( function(d) {
if(d.radius){
return d.radius;
} else {
return 5;
}
})
.iterations(3)
.strength(0.1));
link = svg
.append("g")
.attr("class", "links")
.selectAll("line")
.data(graph.links)
.enter()
.append("line")
.attr("source_id", function(d) {
return "vertex_" + d.source;
})
.attr("target_id", function(d) {
return "vertex_" + d.target;
})
.attr("stroke-width", function(d) {
return Math.sqrt(d.value);
})
.attr("stroke", "#999");
var g_nodes = svg
.append("g")
.attr("class", "nodes")
.selectAll("g")
.data(graph.nodes)
.enter();
var g_vertex = g_nodes
.append("circle")
.attr("class", "vertex")
.attr("id", function(d) {
return "vertex_" + d.id;
})
.attr("r", 5)
.attr("fill", function(d) {
return color(d[colorAttr]);
})
.attr("stroke", "#fff")
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended))
.on("mouseover", this.showLabel)
.on("mouseout", this.hideLabel);
labels = svg
.append("g")
.attr("class", "labels")
.selectAll("text")
.data(graph.nodes)
.enter();
labels
.append("text")
.text(function(d) {
return this.labeling(d);
}.bind(this))
.attr("pointer-events", "none")
.attr("visibility", "hidden")
.attr("dx", "-30px")
.attr("id", function(d) {
return "label_" + d.id;
});
g_vertex
.append("title")
.text(function(d) {
return d.id;
});
node = d3.selectAll("circle");
labels = d3.selectAll("text");
simulation
.nodes(graph.nodes)
.on("tick", ticked)
.on("end", calculateScaleExtent);
simulation.force("link")
.links(graph.links);
};
/**
* Sets the visibility of the given label to visible.
* @param {Object} d - Label object.
*/
this.showLabel = function showLabel(d) {
//this is the current dom element
var id = this.getAttribute("id").substring(7);
d3.select("#label_" + id).attr("visibility", "visible");
};
/**
* Sets the visibility of the given label to hidden.
* @param {Object} d - Label object.
*/
this.hideLabel = function hideLabel(d) {
var id = this.getAttribute("id").substring(7);
d3.select("#label_" + id).attr("visibility", "hidden");
};
/**
* Sets the attribute according to which the nodes and glyphs will be colored and recolors
* the nodes and glyphs.
* Generates the color legend.
* @param {string} attribute - Name of the node attribute to color the nodes and glyphs.
*/
this.setColorAttribute = function setColorAttribute(attribute) {
legend = [];
colorAttr = attribute;
d3.selectAll("circle").attr("fill", function (d) {
if(!legend[d[colorAttr]]) {
legend[d[colorAttr]] = color(d[colorAttr]);
}
return color(d[colorAttr]);
});
d3.selectAll(".nodes g").select(function (d) {
var properties = calculateGlyphProperties(d.motif_id);
motif_properties[d.motif_id] = properties;
motif_properties[d.motif_id]["type"] = d.type;
if(this.childNodes.length > 1) {
this.childNodes[1].setAttribute("fill", properties["color"]);
} else {
this.childNodes[0].setAttribute("style", "fill:" + properties["color"] + ";stroke:white;stroke-width:10");
}
});
};
/**
* Stops the d3 simulation and d3 zoom listener of this network.
*/
this.clearNetwork = function clearNetwork() {
simulation.stop();
svg.call(zoom.on("zoom", null));
};
/**
* Returns the legend according to the current coloring.
* @returns {Array} Array where the indices represent the labels and the values the corresponding
* color.
*/
this.getLegend = function getLegend() {
return legend;
};
/**
* Function that detects motifs and replaces them by glyphs.
* @param {Motif} motifType - Type of motif to detect and replace. Either "CLIQUE", "FAN" or
* "CONNECTOR".
* @param {number} min - Minimum number of nodes that may be replaced by a glyph of type motifType.
* @param {number} max - Maximum number of nodes that may be replaced by a glyph of type motifType.
*/
this.collapseMotifs = function collapseMotifs(motifType, min, max) {
var tmp = this.initMotifDataStructure(motifType, min, max);
var collapse = tmp[0];
var motifs_tmp = tmp[1];
if(collapse) {
if(motifs_tmp) {
if (Object.keys(motifs_tmp.motifs).length > 0) {
Object.keys(motifs_tmp.motifs).forEach(function (key) {
motifs[global_motif_index] = {};
motifs[global_motif_index]["oldNodes"] = [];
motifs[global_motif_index]["motifNode"] = [];
motifs[global_motif_index]["motifLinks"] = [];
motifs[global_motif_index]["adjacentLinks"] = [];
motifs[global_motif_index]["type"] = motifType;
replaceMotif(motifType, motifs_tmp.motifs[key], global_motif_index);
motif_properties[global_motif_index] = calculateGlyphProperties(global_motif_index);
motif_properties[global_motif_index]["type"] = motifType;
motifs[global_motif_index]["motifNode"][0]["x"] = motif_properties[global_motif_index]["meanX"];
motifs[global_motif_index]["motifNode"][0]["y"] = motif_properties[global_motif_index]["meanY"];
motifs[global_motif_index]["motifNode"][0]["radius"] = motif_properties[global_motif_index]["radius"];
global_motif_index++;
});
var data = [];
Object.keys(motifs_tmp["motifs"]).forEach(function (value) {
var d = motifs[value]["motifNode"][0];
if (d) {
data.push(d);
}
});
appendGlyphs(motifType, data, motif_properties);
} else {
console.log("no motifs to collapse");
}
}
}
};
/**
* Inits the data structure containing the information of the motifs and removes previously
* detected glyphs if needed.
* @param {Motif} motifType - Type of motif to detect and replace. Either "CLIQUE", "FAN" or
* "CONNECTOR".
* @param {number} min - Minimum number of nodes that may be replaced by a glyph of type motifType.
* @param {number} max - Maximum number of nodes that may be replaced by a glyph of type motifType.
* @returns {Array} Array containing a boolean which is true if the given detection query
* is different to the previus query and false otherwise and the suitable data structure to
* store motifs according to motifType.
*/
this.initMotifDataStructure = function initMotifDataStructure(motifType, min, max){
var collapse = true;
var tmp;
var addMotif = true;
var index = 0;
switch(motifType) {
case MotifEnum.CLIQUE:
if(cliques) {
Object.keys(cliques.motifs).forEach(function (key) {
removeGlyph(motifs[key]["motifNode"][0].id);
delete motifs[key];
delete motif_properties[key];
});
if (min === -1 || max === -1) {
cliques = undefined;
collapse = false;
}
} else if (min === -1 || max === -1) {
collapse = false;
}
if(collapse) {
cliques = {};
cliques["min"] = min;
cliques["max"] = max;
cliques["index"] = motifs.length;
cliques["id"] = global_motif_index;
cliques["motifs"] = {};
addMotif = true;
index = 0;
tmp = detectCliques(this, min, max);
tmp.forEach(function (value) {
value.forEach(function (value2) {
if(detectedNodes.indexOf(value2) >= 0) {
addMotif = false;
}
});
if(addMotif) {
cliques["motifs"][global_motif_index + index] = value;
detectedNodes.push.apply(detectedNodes, value);
index++;
}
addMotif = true;
});
}
tmp = cliques;
break;
case MotifEnum.FAN:
if(fans) {
Object.keys(fans.motifs).forEach(function (key) {
removeGlyph(motifs[key]["motifNode"][0].id);
delete motifs[key];
delete motif_properties[key];
});
if( min === -1 || max === -1 ) {
fans = undefined;
collapse = false;
}
} else if(min === -1 || max === -1) {
collapse = false;
}
if(collapse){
fans = {};
fans["index"] = motifs.length;
fans["id"] = global_motif_index;
fans["motifs"] = {};
index = 0;
addMotif = true;
tmp = detectFans(this);
tmp.forEach(function (value) {
value.forEach(function (value2) {
if(detectedNodes.indexOf(value2) >= 0) {
addMotif = false;
}
});
if(addMotif) {
fans["motifs"][global_motif_index + index] = value;
detectedNodes.push.apply(detectedNodes, value);
index++;
}
addMotif = true;
});
tmp = fans;
}
break;
case MotifEnum.CONNECTOR:
if(connector) {
Object.keys(connector.motifs).forEach(function (key) {
removeGlyph(motifs[key]["motifNode"][0].id);
delete motifs[key];
delete motif_properties[key];
});
if (min === -1 || max === -1) {
connector = undefined;
collapse = false;
}
} else if( min === -1 || max === -1){
collapse = false;
}
if(collapse){
connector = {};
connector["min"] = min;
connector["max"] = max;
connector["index"] = motifs.length?motifs.length:0;
connector["id"] = global_motif_index;
connector["motifs"] = {};
index = 0;
addMotif = true;
tmp = detectConnectors(this, min, max);
tmp.forEach(function (value) {
value.forEach(function (value2) {
if(detectedNodes.indexOf(value2) >= 0) {
addMotif = false;
}
});
if(addMotif) {
connector["motifs"][global_motif_index + index] = value;
detectedNodes.push.apply(detectedNodes, value);
index++;
}
addMotif = true;
});
tmp = connector;
}
break;
default:
break;
}
return [collapse, tmp];
};
/**
* Replaces a motif by its corresponding glyph. Hides the nodes of the motifs and all links
* between these nodes and adds a new node which is the glyph and modifies incoming links of
* the nodes of the motif such that they are connected with the glyph.
* @param {Motif} motifType - Type of motif to detect and replace. Either "CLIQUE", "FAN" or
* "CONNECTOR".
* @param {Array} motif - Array containing the ids of the nodes of the motif.
* @param {number} motif_id - Id of the motif.
*/
function replaceMotif(motifType, motif, motif_id){
//find graph data of clique nodes
graph.nodes.forEach(function (value) {
if (motif.indexOf(value.id.toString()) >= 0) {
value["motif_id"] = motif_id;
motifs[motif_id]["oldNodes"].push(value);
}
});
//find graph data of clique links
graph.links.forEach(function (value) {
var sid = value.source.id.toString();
var tid = value.target.id.toString();
if (motif.indexOf(sid) >= 0 && motif.indexOf(tid) >= 0) {
motifs[motif_id]["motifLinks"].push(value);
} else if (motif.indexOf(sid) >= 0 || motif.indexOf(tid) >= 0) {
motifs[motif_id]["adjacentLinks"].push(value);
}
});
//hide links between nodes of the motif
d3.selectAll("line").select(function (d) {
if ((motif.indexOf(d.source.id.toString()) >= 0) && (motif.indexOf(d.target.id.toString()) >= 0)) {
this.setAttribute("class", "collapse");
return this;
}
}).attr("motif_id", "motif_" + motif_id);
//hide Nodes
motifs[motif_id]["oldNodes"].forEach(function (value) {
var nodeId = value["id"];
d3.select("[id='" + 'vertex_' + nodeId + "']")
.attr("class", function () {
return this.getAttribute("class") + " collapse";
})
.attr("motif_id", "motif_" + motif_id);
});
// add new node
var newNode = jQuery.extend(true, {}, motifs[motif_id]["oldNodes"][0]);
newNode["motif_id"] = motif_id;
newNode["radius"] = 10;
newNode["type"] = motifType;
var idx = graph.nodes.push(newNode) - 1;
graph.nodes[idx]["id"] = idx + "";
motifs[motif_id]["motifNode"].push(newNode);
// Modify links
motifs[motif_id]["adjacentLinks"].forEach(function (value) {
var sid = value.source.id.toString();
var tid = value.target.id.toString();
var mtf_idx;
var tmp;
if (!value["motif"] || value["motif"].length === 0) {
value["motif"] = [];
value["motif"][0] = newNode;
mtf_idx = 0;
} else {
value["motif"][1] = newNode;
mtf_idx = 1;
}
if (motif.indexOf(sid) >= 0) {
tmp = value.source;
value.source = value.motif[mtf_idx];
value.motif[mtf_idx] = tmp;
} else if (motif.indexOf(tid) >= 0) {
tmp = value.target;
value.target = value.motif[mtf_idx];
value.motif[mtf_idx] = tmp;
}
});
motifs[motif_id].tStatus = "active";
}
/**
* Scales and positions the network such that all components are visible within the svg.
*/
function fit() {
if(appliedTransform) {
scale = appliedTransform.k;
translate = [appliedTransform.x, appliedTransform.y];
} else {
var bbox = svg.node().getBBox();
var bboxW = bbox.width / (prevScale);
var bboxH = bbox.height / (prevScale);
var bboxX = (bbox.x - prevTranslate[0]) / prevScale;
var bboxY = (bbox.y - prevTranslate[1]) / prevScale;
var midX = bboxX + bboxW / 2;
var midY = bboxY + bboxH / 2;
if ((bboxW === 0) || (bboxH === 0)) {
// nothing to fit
return;
}
var scale = Math.sqrt(Math.min(svgW / bboxW, svgH / bboxH)) * 0.8;
scale = scale * scale;
var translate = [svgW / 2 - scale * midX, svgH / 2 - scale * midY];
zoom.scaleExtent([(scale) * 0.5, 5]);
prevScale = scale;
prevTranslate = translate;
}
transformManual = true;
svg.call(zoom.transform, d3.zoomIdentity.translate(translate[0], translate[1]).scale(scale));
}
/**
* Function that handles the ticks of the d3 force simulation. Updates the positions and
* scales of the html-elements.
*/
function ticked() {
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; });
node
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
node.selectAll("polygon").select(function () {
var parent = d3.select(this.parentNode)._groups[0][0];
var str = this.getAttribute("transform");
str = str.split("(")[2];
str = str.split(")")[0];
this.setAttribute("transform", "translate(" + parent.getAttribute("cx") + "," + parent.getAttribute("cy") + ")scale(" + str + ")");
});
node.selectAll("g path").select(function (d) {
var parent = d3.select(this.parentNode)._groups[0][0];
var str = this.getAttribute("transform");
str = str.split("(")[2];
str = str.split(")")[0];
this.setAttribute("transform", "translate(" + parent.getAttribute("cx") + "," + parent.getAttribute("cy") + ")scale(" + str + ")");
});
labels
.attr("x", function(d) { return d.x; })
.attr("y", function(d) { return d.y; });
fit();
}
/**
* Event handler for start of drag action.
* @param {Object} d - Node object that is dragged.
*/
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.15).restart();
d.fx = d.x;
d.fy = d.y;
}
/**
* Event handler for drag action.
* @param {Object} d - Node object that is dragged.
*/
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
/**
* Event handler for end of drag action.
* @param {Object} d - Node object that was dragged.
*/
function dragended(d) {
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
/**
* Event handler for zoom event.
* Applies zoom event to links, nodes and labels.
*/
function zoomEvent() {
if (!transformManual) {
appliedTransform = d3.event.transform;
}
transformManual = false;
svg.selectAll("line").attr("transform", d3.event.transform);
svg.selectAll(".vertex").attr("transform", d3.event.transform);
svg.selectAll(".labels").attr("transform", d3.event.transform);
}
this.toggleGlyph = function toggleGlyph(motif_id) {
if (motifs[motif_id].tStatus == undefined || motifs[motif_id].tStatus == "active") {
enlargeGlyph(motifs[motif_id].motifNode[0].id);
} else {
showGlyph(motifs[motif_id].motifNode[0].id);
}
};
function showGlyph(index) {
var glyph = graph.nodes[index];
var motif_id = glyph["motif_id"];
var motif = motifs[motif_id];
//hide links between nodes of the motif
d3.selectAll("line[motif_id='" + 'motif_' + motif_id + "']").attr("class", function () {
return this.getAttribute("class") + " collapse";
});
//hide Nodes
d3.selectAll("circle[motif_id='" + 'motif_' + motif_id + "']").attr("class", function () {
return this.getAttribute("class") + " collapse";
});
// Modify links
motif.adjacentLinks.forEach(function (value) {
var sid = value.source.id.toString();
var tid = value.target.id.toString();
var mtf_idx;
var tmp;
mtf_idx = value.motif.indexOf(motif.motifNode[0]);
if(value.source.motif_id === motif.motifNode[0].motif_id) {
tmp = value.source;
value.source = value.motif[mtf_idx];
value.motif[mtf_idx] = tmp;
} else if(value.target.motif_id === motif.motifNode[0].motif_id) {
tmp = value.target;
value.target = value.motif[mtf_idx];
value.motif[mtf_idx] = tmp;
}
});
//show glyph
d3.selectAll("g[motif_id='" + 'motif_' + motif.motifNode[0].motif_id + "']").attr("class", function () {
return "vertex";
});
motif.tStatus = "active";
restartSimulation(0.1);
}
/**
* Enlarges (hides) a glyph such that the underlying nodes and links are visible.
* @param {number} index - index of the glyph node inside the graph data structure.
*/
function enlargeGlyph(index){
var glyph = graph.nodes[index];
var gX = glyph.x;
var gY = glyph.y;
var motif_id = glyph["motif_id"];
var tmp;
console.log("enlarge glyph: " + index);
simulation.stop();
//modify Nodes
motifs[motif_id]["oldNodes"].forEach(function (value) {
value.x = value.x - (value.x - gX);
value.y = value.y - (value.y - gY);
});
//modify adjacent Links
motifs[motif_id]["adjacentLinks"].forEach(function (value) {
var sid = value.source.id.toString();
var tid = value.target.id.toString();
var mtf_idx = -1;
value.motif.forEach(function (value2, idx) {
if (((value2.motif_id === value.source.motif_id) && (sid === index.toString())) ||
((value2.motif_id === value.target.motif_id) && (tid === index.toString()))) {
mtf_idx = idx;
}
});
if(mtf_idx >= 0) {
if (sid && (sid === index.toString())) {
tmp = value.source;
value.source = value.motif[mtf_idx];
value.motif[mtf_idx] = tmp;
} else if (tid && (tid === index.toString())) {
tmp = value.target;
value.target = value.motif[mtf_idx];
value.motif[mtf_idx] = tmp;
}
} else {
console.log("something went wrong!");
}
});
motifs[motif_id].tStatus = "passive";
//hide glyph html
d3.selectAll(".nodes g[motif_id='" + 'motif_' + motif_id + "']").select(function (d) {
this.setAttribute("class", this.getAttribute("class") + " collapse");
return this;
});
//show Nodes and motifLinks
d3.selectAll(".nodes circle[motif_id='" + 'motif_' + motif_id + "']").select(function () {
this.setAttribute("class", "vertex");
this.setAttribute("stroke", "black");
return this;
});
d3.selectAll(".links [motif_id='" + 'motif_' + motif_id + "']").select(function () {
this.setAttribute("class", "");
this.setAttribute("stroke", "black");
return this;
});
restartSimulation(0.1);
}
/**
* Removes a glyph.
* @param {number} index - index of the glyph node inside the graph data structure.
*/
function removeGlyph(index) {
var glyph = graph.nodes[index];
var motif_id = glyph["motif_id"];
enlargeGlyph(index);
motifs[motif_id]["oldNodes"].forEach(function (value) {
detectedNodes.splice(detectedNodes.indexOf(value.id,1));
});
motifs[motif_id]["adjacentLinks"].forEach(function (value) {
var tmp = [];
value.motif.forEach(function (value2) {
if (value2.id.toString() !== index.toString()) {
tmp.push(value2);
}
});
value.motif = tmp;
});
//remove glyph HTMLElement
d3.selectAll(".nodes g[motif_id='" + 'motif_' + motif_id + "']").remove();
//show Nodes and motifLinks
d3.selectAll(".nodes circle[motif_id='" + 'motif_' + motif_id + "']").select(function () {
this.setAttribute("stroke", "white");
return this;
});
d3.selectAll(".links [motif_id='" + 'motif_' + motif_id + "']").select(function () {
this.setAttribute("stroke", "#999");
return this;
});
}
/**
* Calculates the scale extent of the graph according to its size.
*/
function calculateScaleExtent() {
var bbox = svg.node().getBBox();
var bboxW = bbox.width;
var bboxH = bbox.height;
if ((bboxW === 0) || (bboxH === 0)) {
return;
}
var scale = Math.sqrt(Math.min(svgW / bboxW, svgH / bboxH)) * 0.8;
scale = scale * scale;
if(appliedTransform) {
scale = scale * appliedTransform.k;
} else {
scale = scale * prevScale;
}
zoom.scaleExtent([(scale) * 0.5, 5]);
}
/**
* Restarts the d3 force simulation of the graph.
* @param {number} alpha - Parameter for d3 force simulation.
*/
function restartSimulation(alpha) {
node = svg.select(".nodes").selectAll("*").select( function () {
if(this.tagName === "circle" || this.tagName === "g" ) {
return this;
}
});
link = svg.selectAll("line");
simulation
.nodes(graph.nodes)
.on("tick", ticked)
.on("end", calculateScaleExtent);
simulation.force("link")
.links(graph.links);
if(!alpha || alpha < 0) {
simulation.restart();
} else {
simulation.alphaTarget(0);
simulation.alpha(alpha).restart();
}
}
/**
* Appends glyphs on the svg html-element.
* @param {Motif} motifType - Type of motif to detect and replace. Either "CLIQUE", "FAN" or
* "CONNECTOR".
* @param {Array} nodes - Array of node objects containing the data of the glyphs to append.
* @param {{}} properties - Array of properties (color, size, position,...) to visualize
* the glyph.
*/
function appendGlyphs(motifType, nodes, properties){
simulation.stop();
switch (motifType) {
case MotifEnum.CLIQUE:
appendClique(nodes, properties);
break;
case MotifEnum.FAN:
appendFan(nodes, properties);
break;
case MotifEnum.CONNECTOR:
appendConnector(nodes, properties);
break;
default:
break;
}
}
/**
* Appends clique glyphs on the svg html-element.
* @param {Array} nodes - Array of node objects containing the data of the glyphs to append.
* @param {{}} properties - Array of properties (color, size, position,...) to visualize
* the glyph.
*/
function appendClique(nodes, properties) {
var tmp = svg.selectAll(".nodes").selectAll("g")
.data(nodes, function(d){
if (d.type === 1) {
return d;
}
});
tmp
.enter()
.append("g")
.attr("class", "vertex")
.attr("id", function(d) {
return "vertex_" + d.id;
})
.attr("motif_id", function (d) {
return "motif_" + d.motif_id;
})
.attr("r", function (d) {
return properties[d.motif_id]["radius"];
})
.attr("transform",function (d) {
return properties[d.motif_id]["transform"];
})
.attr("cx", function (d) {
return properties[d.motif_id]["meanX"];
})
.attr("cy", function (d) {
return properties[d.motif_id]["meanY"];
})
.append("polygon")
.attr("points", cliquePolygonPoints)
.attr("style", function (d) {
return "fill:" + properties[d.motif_id]["color"] + ";stroke:white;stroke-width:10";
})
.attr("transform",function (d) {
var str = properties[d.motif_id]["transform"].split(")")[0];
return str + ")scale(" + properties[d.motif_id]["scale"] + ")";
})
.attr("type", function (d) {
return d.type;
})
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended))
.on("mouseover", this.showLabel)
.on("mouseout", this.hideLabel);
restartSimulation(0.1);
}
/**
* Appends fan glyphs on the svg html-element.
* @param {Array} nodes - Array of node objects containing the data of the glyphs to append.
* @param {{}} properties - Array of properties (color, size, position,...) to visualize
* the glyph.
*/
function appendFan(nodes, properties) {
var tmp = svg.selectAll(".nodes").selectAll("g")
.data(nodes, function(d){
if(d.type === 2) {
return d;
}
})
.enter()
.append("g")
.attr("class", "vertex")
.attr("id", function(d) {
return "vertex_" + d.id;
})
.attr("motif_id", function (d) {
return "motif_" + d.motif_id;
})
.attr("r", 30)
.attr("transform",function (d) {
return properties[d.motif_id]["transform"];
})
.attr("cx", function (d) {
return properties[d.motif_id]["meanX"];
})
.attr("cy", function (d) {
return properties[d.motif_id]["meanY"];
})
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended))
.on("mouseover", this.showLabel)
.on("mouseout", this.hideLabel);
tmp
.append("circle")
.attr("class", "vertex no-transform")
.attr("r", 5)
.attr("fill", function(d) {
var color = d3.color("white");
d3.selectAll("circle[id='" + 'vertex_' + motifs[d.motif_id]["oldNodes"][0].id + "']").select(function (d) {
color = this.getAttribute("fill");
});
return color;
})
.attr("stroke", "#fff")
.attr("type", function (d) {
return d.type;
});
tmp
.append("path")
.attr("d", function (d) {
return calculateFanShape(properties[d.motif_id]["scale"]);
})
.attr("fill", function(d) {
return properties[d.motif_id]["color"];
})
.attr("style", function (d) {
return "stroke:white;stroke-width:6";
})
.attr("transform",function (d) {
var str = properties[d.motif_id]["transform"].split(")")[0];
return str + ")scale(" + 0.25 + ")";
})
.attr("type", function (d) {
return d.type;
});
restartSimulation(0.1);
}
/**
* Appends connector glyphs on the svg html-element.
* @param {Array} nodes - Array of node objects containing the data of the glyphs to append.
* @param {{}} properties - Array of properties (color, size, position,...) to visualize
* the glyph.
*/
function appendConnector(nodes, properties) {
var tmp = svg.selectAll(".nodes").selectAll("g")
.data(nodes, function(d){
if(d.type === 3) {
return d;
}});
tmp
.enter()
.append("g")
.attr("class", "vertex")
.attr("id", function(d) {
return "vertex_" + d.id;
})
.attr("motif_id", function (d) {
return "motif_" + d.motif_id;
})
.attr("r", 30)
.append("polygon")
.attr("points", connectorPolygonPoints)
.attr("style", function (d) {
return "fill:" + properties[d.motif_id]["color"] + ";stroke:white;stroke-width:10";
})
.attr("transform",function (d) {
var str = properties[d.motif_id]["transform"].split(")")[0];
return str + ")scale(" + properties[d.motif_id]["scale"] + ")";
})
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended))
.on("mouseover", this.showLabel)
.on("mouseout", this.hideLabel);
restartSimulation(0.1);
}
/**
* Calculates the properties of a glyph based on the nodes which are represented by the glyph.
* @param {number} motif_id - Id of the motif.
* @returns {Object} Object containing the properties(meanX, meanY, scale, radius, color, transform)
* as attributes.
*/
function calculateGlyphProperties(motif_id) {
var properties = {};
var meanPos;
var meanColor = d3.color("white");
var width;
var height;
motifs[motif_id]["oldNodes"].forEach(function (value) {
if (!meanPos) {
meanPos = [];
meanPos[0] = value["x"];
meanPos[1] = value["y"];
meanColor = d3.rgb(color(value[colorAttr]));
} else {
meanPos[0] = (meanPos[0] + value["x"]);
meanPos[1] = (meanPos[1] + value["y"]);
var tmpColor = d3.rgb(color(value[colorAttr]));
meanColor.r = (meanColor.r + tmpColor.r);
meanColor.g = (meanColor.g + tmpColor.g);
meanColor.b = (meanColor.b + tmpColor.b);
}
});
meanPos /= motifs[motif_id]["oldNodes"].length;
meanColor.r /= motifs[motif_id]["oldNodes"].length;
meanColor.g /= motifs[motif_id]["oldNodes"].length;
meanColor.b /= motifs[motif_id]["oldNodes"].length;
if(meanPos) {
properties["meanX"] = meanPos[0];
properties["meanY"] = meanPos[1];
}
var scale = (motifs[motif_id]["oldNodes"].length / graph.nodes.length) * 2;
scale = scale < (5*3)/200 ? (5*3)/200 : scale;
if(motifs[motif_id]["type"] === 2) {
width = 400 * 0.25;
height = 400 * 0.25;
} else {
width = 400 * scale * 1.1;
height = 400 * scale * 1.1;
}
var r = Math.sqrt(width * width + height * height) / 2;
properties["scale"] = scale;
properties["radius"] = r;
properties["color"] = meanColor;
var transform = "";
d3.selectAll("[id='" + 'vertex_' + motifs[motif_id]["oldNodes"][0].id + "']").select(function () {
transform = this.getAttribute("transform");
return this;
});
properties["transform"] = transform;
return properties;
}
/**
* Converts polar coordinates to cartesian corrdinates of an arc.
* @param {number} centerX - X position of the center of the arc.
* @param {number} centerY - Y position of the center of the arc.
* @param {number} radius - Radius of the arc.
* @param {number} angleInDegrees - Angle of the arc in degrees.
* @returns {{x: *, y: *}} Object containing the cartesian x and y coordinates.
*/
function polarToCartesian(centerX, centerY, radius, angleInDegrees) {
var angleInRadians = (angleInDegrees-90) * Math.PI / 180.0;
return {
x: centerX + (radius * Math.cos(angleInRadians)),
y: centerY + (radius * Math.sin(angleInRadians))
};
}
/**
* Generates the string for a path html-element to describe an arc.
* @param {number} x - X coordinate of the center of the arc.
* @param {number} y - Y coordinate of the center of the arc.
* @param {number} radius - Radius of the arc.
* @param {number} startAngle - Degrees where to start the angle.
* @param {number} endAngle - Degrees where to end the angle.
* @returns {string} Description of the arc.
*/
function describeArc(x, y, radius, startAngle, endAngle){
var start = polarToCartesian(x, y, radius, endAngle);
var end = polarToCartesian(x, y, radius, startAngle);
var arcSweep = endAngle - startAngle <= 180 ? "0" : "1";
return [
"M", start.x, start.y,
"A", radius, radius, 0, arcSweep, 0, end.x, end.y,
"L", x,y,
"L", start.x, start.y
].join(" ");
}
/**
* Returns the description for a fan glyph according to the underlying nodes.
* @param {number} scale - Scale factor of the fan glyph.
* @returns {string} Description of the arc.
*/
function calculateFanShape(scale){
var radius = 200;
var angle = 180 * scale;
return describeArc(0,0,radius, 0, angle);
}
/**
* Returns all stored cliques
* @returns {Object} list of cliques
*/
this.getCliques = function getCliques() {
return cliques;
}
/**
* Returns all stored fans
* @returns {Object} list of fans
*/
this.getFans = function getFans() {
return fans;
}
/**
* Return all stored connectors
* @returns {Object} list of connectors
*/
this.getConnectors = function getConnectors() {
return connector;
}
/**
* Returns the label of a given node
* @param {string} nodeid - id of the required node
* @returns {string} label of the node
*/
this.getNodeLabel = function getNodeLabel(nodeid) {
//Node deren ID nodeid ist, nicht index
for (var i = 0; i < graph.nodes.length; i++) {
if (graph.nodes[i].id == nodeid) {
return labeling(graph.nodes[i]);
}
}
};
/**
* Returns the color of the glyph of the motif with the given id
* @param {number} motif_id - id of the required motif
* @returns {Object} d3-rgb-object resembling the color of the glyph
*/
this.getMotifColor = function getMotifColor(motif_id) {
return motif_properties[motif_id].color; //RGB-object
}
/**
* Highlights a glyph in the visualization
* @param {number} motif_id - id of the motif to highlight
*/
this.highlightMotif = function highlightMotif(motif_id) {
var nodeid = motifs[motif_id].motifNode[0].id;
if (motifs[motif_id].type == MotifEnum.FAN) {
$("#vertex_" + nodeid + " path").css("stroke", "black");
$("#vertex_" + nodeid + " path").css("fill", "#ddd");
} else {
$("#vertex_" + nodeid + " polygon").css("stroke", "black");
$("#vertex_" + nodeid + " polygon").css("fill", "#ddd");
}
}
/**
* Resets the color of a glyph to its original color
* @param {number} motif_id - id of the motif to color
*/
this.unlightMotif = function unlightMotif(motif_id) {
var nodeid = motifs[motif_id].motifNode[0].id;
var color = motif_properties[motif_id].color;
if (motifs[motif_id].type == MotifEnum.FAN) {
$("#vertex_" + nodeid + " path").css("stroke", "white");
$("#vertex_" + nodeid + " path").css("fill", color);
}
else {
$("#vertex_" + nodeid + " polygon").css("stroke", "white");
$("#vertex_" + nodeid + " polygon").css("fill", color);
}
}
}