Source: TextMapper.js

  1. /**
  2. * @class PhraseNetTextMapper is a class to visualize a phrase-net
  3. *
  4. * @requires d3, cola.js
  5. *
  6. * @constructor
  7. *
  8. * @param {number} graphWidth The display with of the phrase-net
  9. * @param {number} graphHeight The display height of the phrase-net
  10. */
  11. function PhraseNetTextMapper(graphWidth, graphHeight)
  12. {
  13. this.graphWidth = graphWidth;
  14. this.graphHeight = graphHeight;
  15. var thisInstance = this;
  16. // specify zooming behavior
  17. this.zoom = d3.behavior.zoom()
  18. .scaleExtent([0.1, 5])
  19. .on("zoom", function() {thisInstance.container.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");});
  20. // get the svg element an the graph container
  21. this.svg = d3.select("#svg_phraseNet").call(thisInstance.zoom);;
  22. this.container = d3.select("#svg_graph_container");
  23. this.initialized = false;
  24. }
  25. /**
  26. * clears the phrase net
  27. *
  28. * @requires d3, cola.js
  29. */
  30. PhraseNetTextMapper.prototype.clearPhraseNet = function()
  31. {
  32. this.initialized = false;
  33. // stop force
  34. if (this.force != null) {
  35. this.force.stop();
  36. this.force = null;
  37. }
  38. // disable zoom and drag
  39. this.zoom = null;
  40. this.drag = null;
  41. // remove all graph elements in the graph container
  42. this.container.selectAll("*").remove();
  43. this.links = null;
  44. this.nodes = null;
  45. this.svg_links = null;
  46. this.svg_links_paths = null;
  47. this.svg_links_arrows = null;
  48. this.svg_nodes = null;
  49. }
  50. /**
  51. * creates a phrase-net for the given node- and link-sets using the specified force-method and metric
  52. *
  53. * @requires d3, cola.js
  54. *
  55. * @param {arry} nodes An array of nodes
  56. * @param {arry} links An array of links
  57. * @param {string} method The used force method (d3 or cola)
  58. * @param {object} metric The initial metric to set the graph elements' visual properties
  59. */
  60. PhraseNetTextMapper.prototype.createPhraseNet = function(nodes, links, method, metric)
  61. {
  62. //////////////////////////////////////////////////////////////////
  63. var time_prev;
  64. var time_curr;
  65. time_prev = new Date().getTime();
  66. //////////////////////////////////////////////////////////////////
  67. this.clearPhraseNet();
  68. this.links = links;
  69. this.nodes = nodes;
  70. // reference to this object (to use in functions, where the scope changes)
  71. var thisInstance = this;
  72. // define a force for the node-link graph layout
  73. if (method == "cola") {
  74. this.force = cola.d3adaptor()
  75. .nodes(d3.values(this.nodes))
  76. .links(this.links)
  77. .size([this.graphWidth, this.graphHeight])
  78. .linkDistance(200)
  79. .avoidOverlaps(true)
  80. .convergenceThreshold(1e-9)
  81. .handleDisconnected(true)
  82. .on("tick", function() {thisInstance.tick()})
  83. }
  84. else if (method = "d3") {
  85. this.force = d3.layout.force()
  86. .nodes(d3.values(this.nodes))
  87. .links(this.links)
  88. .size([this.graphWidth, this.graphHeight])
  89. .linkDistance(200)
  90. .charge(-1200)
  91. .on("tick", function() {thisInstance.tick()})
  92. }
  93. // create svg elements for the links
  94. this.svg_links = this.container.selectAll(".link")
  95. .data(this.links)
  96. .enter().append("g")
  97. .attr("class", "link")
  98. .attr("marker-end", "url(#end)");
  99. // create the a path for each link
  100. this.svg_links_paths = this.svg_links.append("path")
  101. .attr("class", "link");
  102. // create the markers (arrows)
  103. this.svg_links_arrows = this.container.append("defs").selectAll("marker")
  104. .data(["end"])
  105. .enter().append("marker")
  106. .attr("id", String)
  107. .attr("viewBox", "0 -5 10 10")
  108. .attr("refX", 0.5)
  109. .attr("refY", 0)
  110. .attr("markerWidth", 2.5)
  111. .attr("markerHeight", 2.5)
  112. .attr("orient", "auto");
  113. // create the arrow-heads
  114. this.svg_links_arrows.append('path')
  115. .attr('d', 'M 0, -5 L 10, 0 L 0, 5')
  116. .attr("class", "marker");
  117. // create svg elements for the nodes
  118. this.svg_nodes = this.container.selectAll(".node")
  119. .data(this.nodes)
  120. .enter().append("g")
  121. .attr("class", "node")
  122. .call(this.force.drag);
  123. // prevent panning when a node is dragged
  124. this.svg_nodes.on("mousedown", function(node) {d3.event.stopPropagation();});
  125. // apply metric (node-texts are set here)
  126. this.applyMetric(metric, false);
  127. // start the force simulation
  128. if (method == "cola")
  129. this.force.start(50, 0, 25);
  130. else if (method = "d3")
  131. this.force.start();
  132. this.initialized = true;
  133. //////////////////////////////////////////////////////////////////
  134. time_curr = new Date().getTime();
  135. console.log("INITIALIZING FORCE SIMULATION:" + "\n" + (time_curr - time_prev) + "ms ~ O(n)-O(n^2)");
  136. time_prev = time_curr;
  137. //////////////////////////////////////////////////////////////////
  138. }
  139. /**
  140. * applies the given metric on the phrase-net
  141. *
  142. * @requires d3, cola.js
  143. *
  144. * @param {object} metric The metric to set the graph elements' visual properties
  145. */
  146. PhraseNetTextMapper.prototype.applyMetric = function(metric, executeTick)
  147. {
  148. // set the metric's domains
  149. metric.setDomains(this.nodes, this.links);
  150. // delete old svg-elements
  151. this.svg.selectAll("text.node").remove();
  152. this.svg.selectAll("tspan.node").remove();
  153. this.svg.selectAll("rect.node").remove();
  154. // update links (set each link's width according to the defined metric)
  155. this.svg_links.attr("stroke-width", function(link) {
  156. link.width = metric.getLinkWidth(link);
  157. return link.width + "px";
  158. });
  159. // create the text elements of all nodes (parent holding single t-spans (= lines))
  160. this.svg_nodes.append("text")
  161. .attr("text-align", "center")
  162. .attr("class", "node")
  163. .attr("id", function(node) {return node.id;})
  164. // add t-spans to the text-element accordingly to it's inner subnodes
  165. this.nodes.forEach(function(node) {
  166. // get text element
  167. var svg_text = d3.select("#" + node.id);
  168. // create a t-spans for each subnode
  169. // set the text-parameters (color and font-size) according to the defined metric
  170. for (var i = 0; i < node.subnodes.length; i++) {
  171. svg_text.append('tspan')
  172. .data([node.subnodes[i]])
  173. .attr('x', 0)
  174. .attr('dy', "1em")
  175. .attr("text-anchor", "middle")
  176. .attr("font-size", function(subnode) {return metric.getFontSize(subnode) + "px"})
  177. .attr("fill", function(subnode) {return metric.getFontColor(subnode)})
  178. .text(function(subnode) {return subnode.name})
  179. .attr("class", "node");
  180. }
  181. // move the text-element according to it's bounding box
  182. var y = svg_text[0][0].getBBox().y + (svg_text[0][0].getBBox().height) / 2;
  183. svg_text.attr("y", -y);
  184. // 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)
  185. node.BBox_ = svg_text[0][0].getBBox();
  186. // set the width and height of each node according to the text's bounding box (for collision)
  187. var collision_Border = 95;
  188. node.width = node.BBox_.width + collision_Border;
  189. node.height = node.BBox_.height + collision_Border;
  190. });
  191. // add rectangle's serving as bounding boxes (when dragging nodes, etc.)
  192. this.svg_nodes.append("rect")
  193. .attr("class", "node")
  194. .attr("transform", function(node) {return "translate(" + (-node.BBox_.width / 2) + "," + (-node.BBox_.height / 2) + ")";})
  195. .attr("width", function(node) {return node.BBox_.width;})
  196. .attr("height", function(node) {return node.BBox_.height;});
  197. // call tick in the case the graph is currently not updating each frame
  198. if (executeTick)
  199. this.tick();
  200. }
  201. /**
  202. * updates the phrase-net's elements
  203. *
  204. * @requires d3, cola.js
  205. */
  206. PhraseNetTextMapper.prototype.tick = function()
  207. {
  208. // reference to this object (to use in functions, where the scope changes)
  209. var thisInstance = this;
  210. // update each node's position
  211. this.svg_nodes.attr("transform", function(node) {
  212. return "translate(" + node.x + "," + node.y + ")";
  213. });
  214. // update each link
  215. this.svg_links_paths.attr("d", function(link) {
  216. // check if link is a loop
  217. if (link.target == link.source) {
  218. var width = link.source.BBox_.width;
  219. var height = link.source.BBox_.height;
  220. // move the vector between the start- and end-point a bit to the right (by adding a factor of the normal vector)
  221. var vec1 = {x : width/2,
  222. y : -height};
  223. // move the vector between the end- and start-point a bit to the right (by adding a factor of the normal vector)
  224. var vec2 = {x : width,
  225. y : -height/4};
  226. // get the start-point's coordinates
  227. var pos = {x : link.source.x,
  228. y : link.source.y};
  229. var startPoint = thisInstance.intersect(pos, vec1, link.source.BBox_);
  230. var endPoint = thisInstance.intersect(pos, vec2, link.target.BBox_);
  231. // 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
  232. var length = Math.sqrt(vec2.x * vec2.x + vec2.y * vec2.y) / link.width / 2.5;
  233. endPoint.x += vec2.x / length;
  234. endPoint.y += vec2.y / length;
  235. xRotation = -45;
  236. largeArc = 1;
  237. sweep = 1;
  238. drx = 40;
  239. dry = 20;
  240. return "M" + startPoint.x + "," + startPoint.y + "A" + drx + "," + dry + " " + xRotation + "," + largeArc + "," + sweep + " " + endPoint.x + "," + endPoint.y;
  241. }
  242. else {
  243. var dx = link.target.x - link.source.x;
  244. var dy = link.target.y - link.source.y;
  245. // get the start-point's coordinates
  246. var pos1 = {x : link.source.x,
  247. y : link.source.y};
  248. // get the end-point's coordinates
  249. var pos2 = {x : link.target.x,
  250. y : link.target.y};
  251. // move the vector between the start- and end-point a bit to the right (by adding a factor of the normal vector)
  252. var vec1 = {x : dx + dy/3.5,
  253. y : dy - dx/3.5};
  254. // move the vector between the end- and start-point a bit to the right (by adding a factor of the normal vector)
  255. var vec2 = {x : -dx + dy/3.5,
  256. y : -dy - dx/3.5};
  257. var startPoint = thisInstance.intersect(pos1, vec1, link.source.BBox_);
  258. var endPoint = thisInstance.intersect(pos2, vec2, link.target.BBox_);
  259. // 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
  260. var length = Math.sqrt(vec2.x * vec2.x + vec2.y * vec2.y) / link.width / 2.5;
  261. endPoint.x += vec2.x / length;
  262. endPoint.y += vec2.y / length;
  263. var dx_new = endPoint.x - startPoint.x;
  264. var dy_new = endPoint.y - startPoint.y;
  265. var dr = Math.sqrt(dx_new * dx_new + dy_new * dy_new) * 1.5;
  266. // define the link's path
  267. return "M" + startPoint.x + "," + startPoint.y + "A" + dr + "," + dr + " 0 0,1 " + endPoint.x + "," + endPoint.y;
  268. }
  269. });
  270. }
  271. /**
  272. * Determines the intersection-point of a given vector at a given position with a given bounding box
  273. *
  274. * @param {array} pos A position
  275. * @param {array} vec A direction vector
  276. * @param {object} BBox A bounding box
  277. *
  278. * @returns the intersection point
  279. */
  280. PhraseNetTextMapper.prototype.intersect = function(pos, vec, BBox)
  281. {
  282. factor_x = Math.abs(vec.x / (BBox.width / 2));
  283. factor_y = Math.abs(vec.y / (BBox.height / 2));
  284. if (factor_x > factor_y) {
  285. var intersect_x = Math.sign(vec.x) * (BBox.width / 2);
  286. var intersect_y = vec.y / factor_x;
  287. }
  288. else {
  289. var intersect_x = vec.x / factor_y;
  290. var intersect_y = Math.sign(vec.y) * (BBox.height / 2);
  291. }
  292. return {x : (pos.x + intersect_x),
  293. y : (pos.y + intersect_y)};
  294. };