Source: TextMapper.js

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