/**
* @class PhraseNetTextMapper is a class to visualize a phrase-net
*
* @requires d3, cola.js
*
* @constructor
*
* @param {number} graphWidth The display with of the phrase-net
* @param {number} graphHeight The display height of the phrase-net
*/
function PhraseNetTextMapper(graphWidth, graphHeight)
{
this.graphWidth = graphWidth;
this.graphHeight = graphHeight;
var thisInstance = this;
// specify zooming behavior
this.zoom = d3.behavior.zoom()
.scaleExtent([0.1, 5])
.on("zoom", function() {thisInstance.container.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");});
// get the svg element an the graph container
this.svg = d3.select("#svg_phraseNet").call(thisInstance.zoom);;
this.container = d3.select("#svg_graph_container");
this.initialized = false;
}
/**
* clears the phrase net
*
* @requires d3, cola.js
*/
PhraseNetTextMapper.prototype.clearPhraseNet = function()
{
this.initialized = false;
// stop force
if (this.force != null) {
this.force.stop();
this.force = null;
}
// disable zoom and drag
this.zoom = null;
this.drag = null;
// remove all graph elements in the graph container
this.container.selectAll("*").remove();
this.links = null;
this.nodes = null;
this.svg_links = null;
this.svg_links_paths = null;
this.svg_links_arrows = null;
this.svg_nodes = null;
}
/**
* creates a phrase-net for the given node- and link-sets using the specified force-method and metric
*
* @requires d3, cola.js
*
* @param {arry} nodes An array of nodes
* @param {arry} links An array of links
* @param {string} method The used force method (d3 or cola)
* @param {object} metric The initial metric to set the graph elements' visual properties
*/
PhraseNetTextMapper.prototype.createPhraseNet = function(nodes, links, method, metric)
{
//////////////////////////////////////////////////////////////////
var time_prev;
var time_curr;
time_prev = new Date().getTime();
//////////////////////////////////////////////////////////////////
this.clearPhraseNet();
this.links = links;
this.nodes = nodes;
// reference to this object (to use in functions, where the scope changes)
var thisInstance = this;
// define a force for the node-link graph layout
if (method == "cola") {
this.force = cola.d3adaptor()
.nodes(d3.values(this.nodes))
.links(this.links)
.size([this.graphWidth, this.graphHeight])
.linkDistance(200)
.avoidOverlaps(true)
.convergenceThreshold(1e-9)
.handleDisconnected(true)
.on("tick", function() {thisInstance.tick()})
}
else if (method = "d3") {
this.force = d3.layout.force()
.nodes(d3.values(this.nodes))
.links(this.links)
.size([this.graphWidth, this.graphHeight])
.linkDistance(200)
.charge(-1200)
.on("tick", function() {thisInstance.tick()})
}
// create svg elements for the links
this.svg_links = this.container.selectAll(".link")
.data(this.links)
.enter().append("g")
.attr("class", "link")
.attr("marker-end", "url(#end)");
// create the a path for each link
this.svg_links_paths = this.svg_links.append("path")
.attr("class", "link");
// create the markers (arrows)
this.svg_links_arrows = this.container.append("defs").selectAll("marker")
.data(["end"])
.enter().append("marker")
.attr("id", String)
.attr("viewBox", "0 -5 10 10")
.attr("refX", 0.5)
.attr("refY", 0)
.attr("markerWidth", 2.5)
.attr("markerHeight", 2.5)
.attr("orient", "auto");
// create the arrow-heads
this.svg_links_arrows.append('path')
.attr('d', 'M 0, -5 L 10, 0 L 0, 5')
.attr("class", "marker");
// create svg elements for the nodes
this.svg_nodes = this.container.selectAll(".node")
.data(this.nodes)
.enter().append("g")
.attr("class", "node")
.call(this.force.drag);
// prevent panning when a node is dragged
this.svg_nodes.on("mousedown", function(node) {d3.event.stopPropagation();});
// apply metric (node-texts are set here)
this.applyMetric(metric, false);
// start the force simulation
if (method == "cola")
this.force.start(50, 0, 25);
else if (method = "d3")
this.force.start();
this.initialized = true;
//////////////////////////////////////////////////////////////////
time_curr = new Date().getTime();
console.log("INITIALIZING FORCE SIMULATION:" + "\n" + (time_curr - time_prev) + "ms ~ O(n)-O(n^2)");
time_prev = time_curr;
//////////////////////////////////////////////////////////////////
}
/**
* applies the given metric on the phrase-net
*
* @requires d3, cola.js
*
* @param {object} metric The metric to set the graph elements' visual properties
*/
PhraseNetTextMapper.prototype.applyMetric = function(metric, executeTick)
{
// set the metric's domains
metric.setDomains(this.nodes, this.links);
// delete old svg-elements
this.svg.selectAll("text.node").remove();
this.svg.selectAll("tspan.node").remove();
this.svg.selectAll("rect.node").remove();
// update links (set each link's width according to the defined metric)
this.svg_links.attr("stroke-width", function(link) {
link.width = metric.getLinkWidth(link);
return link.width + "px";
});
// create the text elements of all nodes (parent holding single t-spans (= lines))
this.svg_nodes.append("text")
.attr("text-align", "center")
.attr("class", "node")
.attr("id", function(node) {return node.id;})
// add t-spans to the text-element accordingly to it's inner subnodes
this.nodes.forEach(function(node) {
// get text element
var svg_text = d3.select("#" + node.id);
// create a t-spans for each subnode
// set the text-parameters (color and font-size) according to the defined metric
for (var i = 0; i < node.subnodes.length; i++) {
svg_text.append('tspan')
.data([node.subnodes[i]])
.attr('x', 0)
.attr('dy', "1em")
.attr("text-anchor", "middle")
.attr("font-size", function(subnode) {return metric.getFontSize(subnode) + "px"})
.attr("fill", function(subnode) {return metric.getFontColor(subnode)})
.text(function(subnode) {return subnode.name})
.attr("class", "node");
}
// move the text-element according to it's bounding box
var y = svg_text[0][0].getBBox().y + (svg_text[0][0].getBBox().height) / 2;
svg_text.attr("y", -y);
// save the bounding box of the text-element in the node's data (in order to access it from the links in the tick-function)
node.BBox_ = svg_text[0][0].getBBox();
// set the width and height of each node according to the text's bounding box (for collision)
var collision_Border = 95;
node.width = node.BBox_.width + collision_Border;
node.height = node.BBox_.height + collision_Border;
});
// add rectangle's serving as bounding boxes (when dragging nodes, etc.)
this.svg_nodes.append("rect")
.attr("class", "node")
.attr("transform", function(node) {return "translate(" + (-node.BBox_.width / 2) + "," + (-node.BBox_.height / 2) + ")";})
.attr("width", function(node) {return node.BBox_.width;})
.attr("height", function(node) {return node.BBox_.height;});
// call tick in the case the graph is currently not updating each frame
if (executeTick)
this.tick();
}
/**
* updates the phrase-net's elements
*
* @requires d3, cola.js
*/
PhraseNetTextMapper.prototype.tick = function()
{
// reference to this object (to use in functions, where the scope changes)
var thisInstance = this;
// update each node's position
this.svg_nodes.attr("transform", function(node) {
return "translate(" + node.x + "," + node.y + ")";
});
// update each link
this.svg_links_paths.attr("d", function(link) {
// check if link is a loop
if (link.target == link.source) {
var width = link.source.BBox_.width;
var height = link.source.BBox_.height;
// move the vector between the start- and end-point a bit to the right (by adding a factor of the normal vector)
var vec1 = {x : width/2,
y : -height};
// move the vector between the end- and start-point a bit to the right (by adding a factor of the normal vector)
var vec2 = {x : width,
y : -height/4};
// get the start-point's coordinates
var pos = {x : link.source.x,
y : link.source.y};
var startPoint = thisInstance.intersect(pos, vec1, link.source.BBox_);
var endPoint = thisInstance.intersect(pos, vec2, link.target.BBox_);
// move the end-point a little bit outwards (relative to the link's width (also relative to the arrow-head's size)) in order to prevent overlapping
var length = Math.sqrt(vec2.x * vec2.x + vec2.y * vec2.y) / link.width / 2.5;
endPoint.x += vec2.x / length;
endPoint.y += vec2.y / length;
xRotation = -45;
largeArc = 1;
sweep = 1;
drx = 40;
dry = 20;
return "M" + startPoint.x + "," + startPoint.y + "A" + drx + "," + dry + " " + xRotation + "," + largeArc + "," + sweep + " " + endPoint.x + "," + endPoint.y;
}
else {
var dx = link.target.x - link.source.x;
var dy = link.target.y - link.source.y;
// get the start-point's coordinates
var pos1 = {x : link.source.x,
y : link.source.y};
// get the end-point's coordinates
var pos2 = {x : link.target.x,
y : link.target.y};
// move the vector between the start- and end-point a bit to the right (by adding a factor of the normal vector)
var vec1 = {x : dx + dy/3.5,
y : dy - dx/3.5};
// move the vector between the end- and start-point a bit to the right (by adding a factor of the normal vector)
var vec2 = {x : -dx + dy/3.5,
y : -dy - dx/3.5};
var startPoint = thisInstance.intersect(pos1, vec1, link.source.BBox_);
var endPoint = thisInstance.intersect(pos2, vec2, link.target.BBox_);
// move the end-point a little bit outwards (relative to the link's width (also relative to the arrow-head's size)) in order to prevent overlapping
var length = Math.sqrt(vec2.x * vec2.x + vec2.y * vec2.y) / link.width / 2.5;
endPoint.x += vec2.x / length;
endPoint.y += vec2.y / length;
var dx_new = endPoint.x - startPoint.x;
var dy_new = endPoint.y - startPoint.y;
var dr = Math.sqrt(dx_new * dx_new + dy_new * dy_new) * 1.5;
// define the link's path
return "M" + startPoint.x + "," + startPoint.y + "A" + dr + "," + dr + " 0 0,1 " + endPoint.x + "," + endPoint.y;
}
});
}
/**
* Determines the intersection-point of a given vector at a given position with a given bounding box
*
* @param {array} pos A position
* @param {array} vec A direction vector
* @param {object} BBox A bounding box
*
* @returns the intersection point
*/
PhraseNetTextMapper.prototype.intersect = function(pos, vec, BBox)
{
factor_x = Math.abs(vec.x / (BBox.width / 2));
factor_y = Math.abs(vec.y / (BBox.height / 2));
if (factor_x > factor_y) {
var intersect_x = Math.sign(vec.x) * (BBox.width / 2);
var intersect_y = vec.y / factor_x;
}
else {
var intersect_x = vec.x / factor_y;
var intersect_y = Math.sign(vec.y) * (BBox.height / 2);
}
return {x : (pos.x + intersect_x),
y : (pos.y + intersect_y)};
};