workflow_visualization.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665
  1. /*
  2. * D3 Sankey diagram
  3. * another type to display graph
  4. */
  5. d3.sankey = function () {
  6. var sankey = {},
  7. nodeWidth = 24,
  8. nodePadding = 8,
  9. size = [1, 1],
  10. nodes = [],
  11. links = [],
  12. overlapLinksAtSources = false,
  13. overlapLinksAtTargets = false,
  14. minValue = 1;
  15. sankey.nodeWidth = function (_) {
  16. if (!arguments.length) return nodeWidth;
  17. nodeWidth = +_;
  18. return sankey;
  19. };
  20. sankey.nodePadding = function (_) {
  21. if (!arguments.length) return nodePadding;
  22. nodePadding = +_;
  23. return sankey;
  24. };
  25. sankey.nodes = function (_) {
  26. if (!arguments.length) return nodes;
  27. nodes = _;
  28. return sankey;
  29. };
  30. sankey.links = function (_) {
  31. if (!arguments.length) return links;
  32. links = _;
  33. return sankey;
  34. };
  35. sankey.size = function (_) {
  36. if (!arguments.length) return size;
  37. size = _;
  38. return sankey;
  39. };
  40. sankey.overlapLinksAtSources = function (_) {
  41. if (!arguments.length) return overlapLinksAtSources;
  42. overlapLinksAtSources = _;
  43. return sankey;
  44. };
  45. sankey.overlapLinksAtTargets = function (_) {
  46. if (!arguments.length) return overlapLinksAtTargets;
  47. overlapLinksAtTargets = _;
  48. return sankey;
  49. };
  50. sankey.minValue = function (_) {
  51. if (!arguments.length) return minValue;
  52. minValue = _;
  53. return sankey;
  54. };
  55. sankey.layout = function (iterations) {
  56. computeNodeLinks();
  57. computeNodeValues();
  58. computeNodeBreadths();
  59. computeNodeDepths(iterations);
  60. computeLinkDepths();
  61. return sankey;
  62. };
  63. sankey.relayout = function () {
  64. computeLinkDepths();
  65. return sankey;
  66. };
  67. sankey.link = function () {
  68. var curvature = .5;
  69. function link(d) {
  70. var x0 = d.source.x + d.source.dx,
  71. x1 = d.target.x,
  72. xi = d3.interpolateNumber(x0, x1),
  73. x2 = xi(curvature),
  74. x3 = xi(1 - curvature),
  75. y0 = d.source.y + (overlapLinksAtSources ? 0 : d.sy) + d.dy / 2,
  76. y1 = d.target.y + (overlapLinksAtTargets ? 0 : d.ty) + d.dy / 2;
  77. return "M" + x0 + "," + y0
  78. + "C" + x2 + "," + y0
  79. + " " + x3 + "," + y1
  80. + " " + x1 + "," + y1;
  81. }
  82. link.curvature = function (_) {
  83. if (!arguments.length) return curvature;
  84. curvature = +_;
  85. return link;
  86. };
  87. return link;
  88. };
  89. // Populate the sourceLinks and targetLinks for each node.
  90. // Also, if the source and target are not objects, assume they are indices.
  91. function computeNodeLinks() {
  92. nodes.forEach(function (node) {
  93. node.sourceLinks = [];
  94. node.targetLinks = [];
  95. });
  96. links.forEach(function (link) {
  97. var source = link.source,
  98. target = link.target;
  99. if (typeof source === "number") source = link.source = nodes[link.source];
  100. if (typeof target === "number") target = link.target = nodes[link.target];
  101. source.sourceLinks.push(link);
  102. target.targetLinks.push(link);
  103. if ("value" in link)
  104. link.value = Math.max(link.value, minValue);
  105. else
  106. link.value = minValue;
  107. });
  108. }
  109. // Compute the value (size) of each node by summing the associated links.
  110. function computeNodeValues() {
  111. nodes.forEach(function (node) {
  112. if ("value" in node)
  113. node.value = Math.max(node.value, minValue);
  114. else
  115. node.value = minValue;
  116. if (node.sourceLinks.length > 0) {
  117. if (overlapLinksAtSources)
  118. node.value = Math.max(node.value, d3.max(node.sourceLinks, value));
  119. else
  120. node.value = Math.max(node.value, d3.sum(node.sourceLinks, value));
  121. }
  122. if (node.targetLinks.length > 0) {
  123. if (overlapLinksAtTargets)
  124. node.value = Math.max(node.value, d3.max(node.targetLinks, value));
  125. else
  126. node.value = Math.max(node.value, d3.sum(node.targetLinks, value));
  127. }
  128. });
  129. }
  130. // Iteratively assign the breadth (x-position) for each node.
  131. // Nodes are assigned the maximum breadth of incoming neighbors plus one;
  132. // nodes with no incoming links are assigned breadth zero, while
  133. // nodes with no outgoing links are assigned the maximum breadth.
  134. function computeNodeBreadths() {
  135. var remainingNodes = nodes,
  136. nextNodes,
  137. x = 0;
  138. while (remainingNodes.length) {
  139. nextNodes = [];
  140. remainingNodes.forEach(function (node) {
  141. node.x = x;
  142. node.dx = nodeWidth;
  143. node.sourceLinks.forEach(function (link) {
  144. nextNodes.push(link.target);
  145. });
  146. });
  147. remainingNodes = nextNodes;
  148. ++x;
  149. }
  150. //
  151. moveSinksRight(x);
  152. scaleNodeBreadths((size[0] - nodeWidth) / (x - 1));
  153. }
  154. function moveSourcesRight() {
  155. nodes.forEach(function (node) {
  156. if (!node.targetLinks.length) {
  157. node.x = d3.min(node.sourceLinks, function (d) {
  158. return d.target.x;
  159. }) - 1;
  160. }
  161. });
  162. }
  163. function moveSinksRight(x) {
  164. nodes.forEach(function (node) {
  165. if (!node.sourceLinks.length) {
  166. node.x = x - 1;
  167. }
  168. });
  169. }
  170. function scaleNodeBreadths(kx) {
  171. nodes.forEach(function (node) {
  172. node.x *= kx;
  173. });
  174. }
  175. function computeNodeDepths(iterations) {
  176. var nodesByBreadth = d3.nest()
  177. .key(function (d) {
  178. return d.x;
  179. })
  180. .sortKeys(d3.ascending)
  181. .entries(nodes)
  182. .map(function (d) {
  183. return d.values;
  184. });
  185. //
  186. initializeNodeDepth();
  187. resolveCollisions();
  188. for (var alpha = 1; iterations > 0; --iterations) {
  189. relaxRightToLeft(alpha *= .99);
  190. resolveCollisions();
  191. relaxLeftToRight(alpha);
  192. resolveCollisions();
  193. }
  194. function initializeNodeDepth() {
  195. var ky = d3.min(nodesByBreadth, function (nodes) {
  196. return (size[1] - (nodes.length - 1) * nodePadding) / d3.sum(nodes, value);
  197. });
  198. nodesByBreadth.forEach(function (nodes) {
  199. nodes.forEach(function (node, i) {
  200. node.y = i;
  201. node.dy = node.value * ky;
  202. });
  203. });
  204. links.forEach(function (link) {
  205. link.dy = link.value * ky;
  206. });
  207. }
  208. function relaxLeftToRight(alpha) {
  209. nodesByBreadth.forEach(function (nodes, breadth) {
  210. nodes.forEach(function (node) {
  211. if (node.targetLinks.length) {
  212. var y = d3.sum(node.targetLinks, weightedSource) / d3.sum(node.targetLinks, value);
  213. node.y += (y - center(node)) * alpha;
  214. }
  215. });
  216. });
  217. function weightedSource(link) {
  218. return center(link.source) * link.value;
  219. }
  220. }
  221. function relaxRightToLeft(alpha) {
  222. nodesByBreadth.slice().reverse().forEach(function (nodes) {
  223. nodes.forEach(function (node) {
  224. if (node.sourceLinks.length) {
  225. var y = d3.sum(node.sourceLinks, weightedTarget) / d3.sum(node.sourceLinks, value);
  226. node.y += (y - center(node)) * alpha;
  227. }
  228. });
  229. });
  230. function weightedTarget(link) {
  231. return center(link.target) * link.value;
  232. }
  233. }
  234. function resolveCollisions() {
  235. nodesByBreadth.forEach(function (nodes) {
  236. var node,
  237. dy,
  238. y0 = 0,
  239. n = nodes.length,
  240. i;
  241. // Push any overlapping nodes down.
  242. nodes.sort(ascendingDepth);
  243. for (i = 0; i < n; ++i) {
  244. node = nodes[i];
  245. dy = y0 - node.y;
  246. if (dy > 0) node.y += dy;
  247. y0 = node.y + node.dy + nodePadding;
  248. }
  249. // If the bottommost node goes outside the bounds, push it back up.
  250. dy = y0 - nodePadding - size[1];
  251. if (dy > 0) {
  252. y0 = node.y -= dy;
  253. // Push any overlapping nodes back up.
  254. for (i = n - 2; i >= 0; --i) {
  255. node = nodes[i];
  256. dy = node.y + node.dy + nodePadding - y0;
  257. if (dy > 0) node.y -= dy;
  258. y0 = node.y;
  259. }
  260. }
  261. });
  262. }
  263. function ascendingDepth(a, b) {
  264. return a.y - b.y;
  265. }
  266. }
  267. function computeLinkDepths() {
  268. nodes.forEach(function (node) {
  269. node.sourceLinks.sort(ascendingTargetDepth);
  270. node.targetLinks.sort(ascendingSourceDepth);
  271. });
  272. nodes.forEach(function (node) {
  273. var sy = 0, ty = 0;
  274. node.sourceLinks.forEach(function (link) {
  275. link.sy = sy;
  276. sy += link.dy;
  277. });
  278. node.targetLinks.forEach(function (link) {
  279. link.ty = ty;
  280. ty += link.dy;
  281. });
  282. });
  283. function ascendingSourceDepth(a, b) {
  284. return a.source.y - b.source.y;
  285. }
  286. function ascendingTargetDepth(a, b) {
  287. return a.target.y - b.target.y;
  288. }
  289. }
  290. function center(node) {
  291. return node.y + node.dy / 2;
  292. }
  293. function value(link) {
  294. return link.value;
  295. }
  296. return sankey;
  297. };
  298. /*
  299. * Example usage:
  300. *
  301. * var dv = new DagViewer(false,'pig_5')
  302. * .setPhysicalParametrs(width,height[,charge,gravity])
  303. * .setData(dagSchema [,jobsData])
  304. * .drawDag([nodeSize,largeNodeSize,linkDistance]);
  305. */
  306. function DagViewer(type, domId) {
  307. // initialize variables and force layout
  308. this._nodes = new Array();
  309. this._links = new Array();
  310. this._numNodes = 0;
  311. this._type = type;
  312. this._id = domId;
  313. }
  314. DagViewer.prototype.setPhysicalParametrs = function (w, h, charge, gravity) {
  315. this._w = w;
  316. this._h = h;
  317. this._gravity = gravity || 0.1;
  318. this._charge = charge || -1000;
  319. this._force = d3.layout.force()
  320. .size([w, h])
  321. .gravity(this._gravity)
  322. .charge(this._charge);
  323. return this;
  324. }
  325. //set workflow schema
  326. DagViewer.prototype.setData = function (wfData, jobData) {
  327. // create map from entity names to nodes
  328. var existingNodes = new Array();
  329. var jobData = (jobData) ? jobData : new Array();
  330. // iterate through job data
  331. for (var i = 0; i < jobData.length; i++) {
  332. this._addNode(existingNodes, jobData[i].name, jobData[i]);
  333. }
  334. var dag = eval('(' + wfData + ')').dag;
  335. // for each source node in the context, create links between it and its target nodes
  336. for (var source in dag) {
  337. var sourceNode = this._getNode(source, existingNodes);
  338. for (var i = 0; i < dag[source].length; i++) {
  339. var targetNode = this._getNode(dag[source][i], existingNodes);
  340. this._addLink(sourceNode, targetNode);
  341. }
  342. }
  343. return this;
  344. }
  345. // add a node to the nodes array and to a provided map of entity names to nodes
  346. DagViewer.prototype._addNode = function (existingNodes, entityName, node) {
  347. existingNodes[entityName] = node;
  348. this._nodes.push(node);
  349. this._numNodes++;
  350. }
  351. // add a link between sourceNode and targetNode
  352. DagViewer.prototype._addLink = function (sourceNode, targetNode) {
  353. var status = false;
  354. if (sourceNode.status && targetNode.status)
  355. status = true;
  356. this._links.push({"source":sourceNode, "target":targetNode, "status":status, "value":sourceNode.output});
  357. }
  358. // get the node for an entity name, or add it if it doesn't exist
  359. // called after job nodes have all been added
  360. DagViewer.prototype._getNode = function (entityName, existingNodes) {
  361. if (!(entityName in existingNodes))
  362. this._addNode(existingNodes, entityName, { "name":entityName, "status":false, "input":1, "output":1});
  363. return existingNodes[entityName];
  364. }
  365. // display the graph
  366. DagViewer.prototype.drawDag = function (nodeSize, largeNodeSize, linkDistance) {
  367. this._nodeSize = nodeSize || 18;
  368. this._largeNodeSize = largeNodeSize || 30;
  369. this._linkDistance = linkDistance || 100;
  370. // add new display to specified div
  371. this._svg = d3.select("div#" + this._id).append("svg:svg")
  372. .attr("width", this._w)
  373. .attr("height", this._h);
  374. // add sankey diagram or graph depending on type
  375. if (this._type)
  376. this._addSankey();
  377. else
  378. this._addDag();
  379. return this;
  380. }
  381. //draw the graph
  382. DagViewer.prototype._addDag = function () {
  383. var w = this._w;
  384. var h = this._h;
  385. var nodeSize = this._nodeSize;
  386. var largeNodeSize = this._largeNodeSize;
  387. var linkDistance = this._linkDistance;
  388. // add nodes and links to force layout
  389. this._force.nodes(this._nodes)
  390. .links(this._links)
  391. .linkDistance(this._linkDistance);
  392. // defs for arrowheads marked as to whether they link finished jobs or not
  393. this._svg.append("svg:defs").selectAll("marker")
  394. .data(["finished", "unfinished"])
  395. .enter().append("svg:marker")
  396. .attr("id", String)
  397. .attr("viewBox", "0 -5 10 10")
  398. .attr("refX", nodeSize + 10)
  399. .attr("refY", 0)
  400. .attr("markerWidth", 6)
  401. .attr("markerHeight", 6)
  402. .attr("orient", "auto")
  403. .append("svg:path")
  404. .attr("d", "M0,-5L10,0L0,5");
  405. // create links between the nodes
  406. var lines = this._svg.append("svg:g").selectAll("line")
  407. .data(this._links)
  408. .enter().append("svg:line")
  409. .attr("class", function (d) {
  410. return "link" + (d.status ? " finished" : "");
  411. })
  412. .attr("marker-end", function (d) {
  413. return "url(#" + (d.status ? "finished" : "unfinished") + ")";
  414. });
  415. // create a circle for each node
  416. var circles = this._svg.append("svg:g").selectAll("circle")
  417. .data(this._nodes)
  418. .enter().append("svg:circle")
  419. .attr("r", nodeSize)
  420. .attr("class", function (d) {
  421. return "node " + (d.status ? " finished" : "");
  422. })
  423. .attr("id", function (d) {
  424. return d.name;
  425. })
  426. .on("dblclick", click)
  427. .call(this._force.drag);
  428. // create text group for each node label
  429. var text = this._svg.append("svg:g").selectAll("g")
  430. .data(this._nodes)
  431. .enter().append("svg:g");
  432. // add a shadow copy of the node label (will have a lighter color and thicker
  433. // stroke for legibility
  434. text.append("svg:text")
  435. .attr("x", nodeSize + 3)
  436. .attr("y", ".31em")
  437. .attr("class", "shadow")
  438. .text(function (d) {
  439. return d.name;
  440. });
  441. // add the main node label
  442. text.append("svg:text")
  443. .attr("x", nodeSize + 3)
  444. .attr("y", ".31em")
  445. .text(function (d) {
  446. return d.name;
  447. });
  448. // add mouseover actions
  449. this._addMouseoverSelection(circles);
  450. // start the force layout
  451. this._force.on("tick", tick)
  452. .start();
  453. // on force tick, adjust positions of nodes, links, and text
  454. function tick() {
  455. circles.attr("transform", function (d) {
  456. if (d.x < largeNodeSize) d.x = largeNodeSize;
  457. if (d.y < largeNodeSize) d.y = largeNodeSize;
  458. if (d.x > w - largeNodeSize) d.x = w - largeNodeSize;
  459. if (d.y > h - largeNodeSize) d.y = h - largeNodeSize;
  460. return "translate(" + d.x + "," + d.y + ")";
  461. });
  462. lines.attr("x1", function (d) {
  463. return d.source.x
  464. })
  465. .attr("y1", function (d) {
  466. return d.source.y
  467. })
  468. .attr("x2", function (d) {
  469. return d.target.x
  470. })
  471. .attr("y2", function (d) {
  472. return d.target.y
  473. });
  474. text.attr("transform", function (d) {
  475. return "translate(" + d.x + "," + d.y + ")";
  476. });
  477. }
  478. // on double click, fix node in place or release it
  479. function click() {
  480. d3.select(this).attr("fixed", function (d) {
  481. if (d.fixed) {
  482. d.fixed = false
  483. } else {
  484. d.fixed = true
  485. }
  486. return d.fixed;
  487. });
  488. }
  489. }
  490. //define mouseover action on nodes
  491. DagViewer.prototype._addMouseoverSelection = function (nodes) {
  492. var nodeSize = this._nodeSize;
  493. var largeNodeSize = this._largeNodeSize;
  494. // on mouseover, change size of node
  495. nodes.on("mouseover", function (d) {
  496. d3.select(this).transition().attr("r", largeNodeSize);
  497. })
  498. .on("mouseout", function (d) {
  499. d3.select(this).transition().attr("r", nodeSize);
  500. });
  501. }
  502. //draw Sankey diagram
  503. DagViewer.prototype._addSankey = function () {
  504. var w = this._w;
  505. var h = this._h;
  506. // add svg group
  507. var svgg = this._svg.append("g");
  508. var color = d3.scale.category20();
  509. // create sankey
  510. var sankey = d3.sankey()
  511. .nodeWidth(15)
  512. .nodePadding(10)
  513. .size([w, h * 0.67]);
  514. // get sankey links
  515. var spath = sankey.link();
  516. // set sankey nodes and links and calculate their positions and sizes
  517. sankey
  518. .nodes(this._nodes)
  519. .links(this._links)
  520. .overlapLinksAtSources(true)
  521. .layout(32);
  522. // create links and set their attributes
  523. var slink = svgg.append("g").selectAll(".link")
  524. .data(this._links)
  525. .enter().append("path")
  526. .attr("class", "slink")
  527. .attr("d", spath)
  528. .style("stroke-width", function (d) {
  529. return Math.max(1, d.dy);
  530. })
  531. .sort(function (a, b) {
  532. return b.dy - a.dy;
  533. });
  534. // add mouseover text to links
  535. slink.append("title")
  536. .text(function (d) {
  537. return d.source.name + " - " + d.target.name + ": " + d.value;
  538. });
  539. // create node groups, set their attributes, and enable vertical dragging
  540. var snode = svgg.append("g").selectAll(".node")
  541. .data(this._nodes)
  542. .enter().append("g")
  543. .attr("class", "snode")
  544. .attr("transform", function (d) {
  545. return "translate(" + d.x + "," + d.y + ")";
  546. })
  547. .call(d3.behavior.drag()
  548. .origin(function (d) {
  549. return d;
  550. })
  551. .on("dragstart", function () {
  552. this.parentNode.appendChild(this);
  553. })
  554. .on("drag", dragmove));
  555. // add rectangles to node groups
  556. snode.append("rect")
  557. .attr("height", function (d) {
  558. return d.dy;
  559. })
  560. .attr("width", sankey.nodeWidth())
  561. .style("fill", function (d) {
  562. return d.color = color(d.name.replace(/ .*/, ""));
  563. })
  564. .style("stroke", function (d) {
  565. return d3.rgb(d.color).darker(2);
  566. })
  567. .append("title")
  568. .text(function (d) {
  569. return "info" in d ? d.info.join("\n") : d.name;
  570. });
  571. // add node labels
  572. snode.append("text")
  573. .attr("x", -6)
  574. .attr("y", function (d) {
  575. return d.dy / 2;
  576. })
  577. .attr("dy", ".35em")
  578. .attr("text-anchor", "end")
  579. .attr("transform", null)
  580. .text(function (d) {
  581. return d.name;
  582. })
  583. .filter(function (d) {
  584. return d.x < w / 2;
  585. })
  586. .attr("x", 6 + sankey.nodeWidth())
  587. .attr("text-anchor", "start");
  588. // add mouseover actions
  589. this._addMouseoverSelection(snode);
  590. // enable vertical dragging with recalculation of link placement
  591. function dragmove(d) {
  592. d3.select(this).attr("transform", "translate(" + d.x + "," + (d.y = Math.max(0, Math.min(h - d.dy, d3.event.y))) + ")");
  593. sankey.relayout();
  594. slink.attr("d", spath);
  595. }
  596. }