workflow_visualization.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416
  1. /*
  2. * Example usage:
  3. *
  4. * var dv = new DagViewer('pig_5')
  5. * .setData(workflowData,jobsData)
  6. * .drawDag(svgWidth,svgHeight,nodeHeight);
  7. */
  8. function DagViewer(domId) {
  9. // initialize variables
  10. this._nodes = new Array();
  11. this._links = new Array();
  12. this._numNodes = 0;
  13. this._id = domId;
  14. this._SUCCESS = "SUCCESS";
  15. }
  16. // set workflow schema and job data
  17. DagViewer.prototype.setData = function (wfData, jobData) {
  18. // create map from entity names to nodes
  19. var existingNodes = new Array();
  20. var jobData = (jobData) ? jobData : new Array();
  21. var minStartTime = 0;
  22. if (jobData.length > 0)
  23. minStartTime = jobData[0].submitTime;
  24. var maxFinishTime = 0;
  25. // iterate through job data
  26. for (var i = 0; i < jobData.length; i++) {
  27. jobData[i].info = "jobId:"+jobData[i].name+" \n"+
  28. "nodeName:"+jobData[i].entityName+" \n"+
  29. "status:"+jobData[i].status+" \n"+
  30. "input:"+jobData[i].input+" \n"+
  31. "output:"+jobData[i].output+" \n"+
  32. "startTime:"+(new Date(jobData[i].submitTime).toString())+" \n"+
  33. "duration:"+DagViewer.formatDuration(jobData[i].elapsedTime);
  34. minStartTime = Math.min(minStartTime, jobData[i].submitTime);
  35. maxFinishTime = Math.max(maxFinishTime, jobData[i].submitTime + jobData[i].elapsedTime);
  36. this._addNode(existingNodes, jobData[i].entityName, jobData[i]);
  37. }
  38. this._minStartTime = minStartTime;
  39. this._maxFinishTime = maxFinishTime;
  40. var dag = eval('(' + wfData + ')').dag;
  41. this._sourceMarker = new Array();
  42. this._targetMarker = new Array();
  43. this._sourceMap = new Array();
  44. // for each source node in the context, create links between it and its target nodes
  45. for (var source in dag) {
  46. var sourceNode = null;
  47. if (source in existingNodes)
  48. sourceNode = existingNodes[source];
  49. for (var i = 0; i < dag[source].length; i++) {
  50. var targetNode = null;
  51. if (dag[source][i] in existingNodes)
  52. targetNode = existingNodes[dag[source][i]];
  53. this._addLink(sourceNode, targetNode);
  54. }
  55. }
  56. return this;
  57. }
  58. // add a node to the nodes array and to a provided map of entity names to nodes
  59. DagViewer.prototype._addNode = function (existingNodes, entityName, node) {
  60. existingNodes[entityName] = node;
  61. this._nodes.push(node);
  62. this._numNodes++;
  63. }
  64. // add a link between sourceNode and targetNode
  65. DagViewer.prototype._addLink = function (sourceNode, targetNode) {
  66. // if source or target is null, add marker indicating unsubmitted job and return
  67. if (sourceNode==null) {
  68. if (targetNode==null)
  69. return;
  70. this._sourceMarker.push(targetNode);
  71. return;
  72. }
  73. if (targetNode==null) {
  74. this._targetMarker.push(sourceNode);
  75. return;
  76. }
  77. // add link between nodes
  78. var status = false;
  79. if (sourceNode.status==this._SUCCESS && targetNode.status==this._SUCCESS)
  80. status = true;
  81. this._links.push({"source":sourceNode, "target":targetNode, "status":status, "value":sourceNode.output});
  82. // add source to map of targets to sources
  83. if (!(targetNode.name in this._sourceMap))
  84. this._sourceMap[targetNode.name] = new Array();
  85. this._sourceMap[targetNode.name].push(sourceNode);
  86. }
  87. // display the graph
  88. // rules of thumb: nodeHeight = 20, labelFontSize = 14, maxLabelWidth = 180
  89. // nodeHeight = 15, labelFontSize = 10, maxLabelWidth = 120
  90. // nodeHeight = 40, labelFontSize = 20, maxLabelWidth = 260
  91. // nodeHeight = 30, labelFontSize = 16
  92. DagViewer.prototype.drawDag = function (svgw, svgh, nodeHeight, labelFontSize, maxLabelWidth, axisPadding, numExtraSeries, extraSeriesSize) {
  93. this._addTimelineGraph(svgw, svgh, nodeHeight || 20, labelFontSize || 14, maxLabelWidth || 180, axisPadding || 30, numExtraSeries || 2, extraSeriesSize || 50);
  94. return this;
  95. }
  96. // draw timeline graph
  97. DagViewer.prototype._addTimelineGraph = function (svgw, svgh, nodeHeight, labelFontSize, maxLabelWidth, axisPadding, numExtraSeries, extraSeriesSize) {
  98. svgw = svgw;
  99. this._extraSeriesSize = extraSeriesSize;
  100. var margin = {"vertical":10, "horizontal":50};
  101. this._margin = margin;
  102. var w = svgw - 2*margin.horizontal;
  103. var startTime = this._minStartTime;
  104. var elapsedTime = this._maxFinishTime - this._minStartTime;
  105. var x = d3.time.scale.utc()
  106. .domain([startTime, startTime+elapsedTime])
  107. .range([0, w]);
  108. this._x = x;
  109. var xrel = d3.time.scale()
  110. .domain([0, elapsedTime])
  111. .range([0, w]);
  112. // process nodes and determine their x and y positions, width and height
  113. var minNodeSpacing = nodeHeight/2;
  114. var ends = new Array();
  115. var maxIndex = 0;
  116. this._nodes = this._nodes.sort(function(a,b){return a.name.localeCompare(b.name);});
  117. for (var i = 0; i < this._numNodes; i++) {
  118. var d = this._nodes[i];
  119. d.x = x(d.submitTime);
  120. d.w = x(d.elapsedTime+d.submitTime) - x(d.submitTime);
  121. if (d.w < nodeHeight/2) {
  122. d.w = nodeHeight/2;
  123. if (d.x + d.w > w)
  124. d.x = w - d.w;
  125. }
  126. var effectiveX = d.x
  127. var effectiveWidth = d.w;
  128. if (d.w < maxLabelWidth) {
  129. effectiveWidth = maxLabelWidth;
  130. if (d.x + effectiveWidth > w)
  131. effectiveX = w - effectiveWidth;
  132. else if (d.x > 0)
  133. effectiveX = d.x+(d.w-maxLabelWidth)/2;
  134. }
  135. // select "lane" (slot for y-position) for this node
  136. // starting at the slot above the node's closest source node (or 0, if none exists)
  137. // and moving down until a slot is found that has no nodes within minNodeSpacing of this node
  138. // excluding slots that contain more than one source of this node
  139. var index = 0;
  140. var rejectIndices = new Array();
  141. if (d.name in this._sourceMap) {
  142. var sources = this._sourceMap[d.name];
  143. var closestSource = sources[0];
  144. var indices = new Array();
  145. for (var j = 0; j < sources.length; j++) {
  146. if (sources[j].index in indices)
  147. rejectIndices[sources[j].index] = true;
  148. indices[sources[j].index] = true;
  149. if (sources[j].submitTime + sources[j].elapsedTime > closestSource.submitTime + closestSource.elapsedTime)
  150. closestSource = sources[j];
  151. }
  152. index = Math.max(0, closestSource.index-1);
  153. }
  154. while ((index in ends) && ((index in rejectIndices) || (ends[index]+minNodeSpacing >= effectiveX))) {
  155. index++
  156. }
  157. ends[index] = Math.max(effectiveX + effectiveWidth);
  158. maxIndex = Math.max(maxIndex, index);
  159. d.y = index*2*nodeHeight + axisPadding;
  160. d.h = nodeHeight;
  161. d.index = index;
  162. }
  163. var h = 2*axisPadding + 2*nodeHeight*(maxIndex+1);
  164. var realh = svgh - 2*margin.vertical - numExtraSeries*extraSeriesSize;
  165. var scale = 1;
  166. if (h > realh)
  167. scale = realh / h;
  168. svgh = Math.min(svgh, h + 2*margin.vertical + numExtraSeries*extraSeriesSize);
  169. this._extraSeriesOffset = h + margin.vertical;
  170. var svg = d3.select("div#" + this._id).append("svg:svg")
  171. .attr("width", svgw+"px")
  172. .attr("height", svgh+"px");
  173. var svgg = svg.append("g")
  174. .attr("transform", "translate("+margin.horizontal+","+margin.vertical+") scale("+scale+")");
  175. this._svgg = svgg;
  176. // add an untranslated white rectangle below everything
  177. // so mouse doesn't have to be over nodes for panning/zooming
  178. svgg.append("svg:rect")
  179. .attr("x", 0)
  180. .attr("y", 0)
  181. .attr("width", svgw)
  182. .attr("height", svgh/scale)
  183. .attr("style", "fill:white;stroke:none");
  184. // create axes
  185. var topAxis = d3.svg.axis()
  186. .scale(x)
  187. .orient("bottom");
  188. var bottomAxis = d3.svg.axis()
  189. .scale(xrel)
  190. .orient("top")
  191. .tickFormat(function(x) { return DagViewer.formatDuration(x.getTime()); });
  192. svgg.append("g")
  193. .attr("class", "x axis top")
  194. .call(topAxis);
  195. svgg.append("g")
  196. .attr("class", "x axis bottom")
  197. .call(bottomAxis)
  198. .attr("transform", "translate(0,"+h+")");
  199. // create a rectangle for each node
  200. var success = this._SUCCESS;
  201. var boxes = svgg.append("svg:g").selectAll("rect")
  202. .data(this._nodes)
  203. .enter().append("svg:rect")
  204. .attr("x", function(d) { return d.x; } )
  205. .attr("y", function(d) { return d.y; } )
  206. .attr("width", function(d) { return d.w; } )
  207. .attr("height", function(d) { return d.h; } )
  208. .attr("class", function (d) {
  209. return "node " + (d.status==success ? " finished" : "");
  210. })
  211. .attr("id", function (d) {
  212. return d.name;
  213. })
  214. .append("title")
  215. .text(function(d) { return d.info; });
  216. // defs for arrowheads marked as to whether they link finished jobs or not
  217. svgg.append("svg:defs").selectAll("arrowmarker")
  218. .data(["finished", "unfinished"])
  219. .enter().append("svg:marker")
  220. .attr("id", String)
  221. .attr("viewBox", "0 -5 10 10")
  222. .attr("markerWidth", 6)
  223. .attr("markerHeight", 6)
  224. .attr("orient", "auto")
  225. .append("svg:path")
  226. .attr("d", "M0,-3L8,0L0,3");
  227. // defs for unsubmitted node marker
  228. svgg.append("svg:defs").selectAll("circlemarker")
  229. .data(["circle"])
  230. .enter().append("svg:marker")
  231. .attr("id", String)
  232. .attr("viewBox", "-2 -2 18 18")
  233. .attr("markerWidth", 10)
  234. .attr("markerHeight", 10)
  235. .attr("refX", 10)
  236. .attr("refY", 5)
  237. .attr("orient", "auto")
  238. .append("svg:circle")
  239. .attr("cx", 5)
  240. .attr("cy", 5)
  241. .attr("r", 5);
  242. // create dangling links representing unsubmitted jobs
  243. var markerWidth = nodeHeight/2;
  244. var sourceMarker = svgg.append("svg:g").selectAll("line")
  245. .data(this._sourceMarker)
  246. .enter().append("svg:line")
  247. .attr("x1", function(d) { return d.x - markerWidth; } )
  248. .attr("x2", function(d) { return d.x; } )
  249. .attr("y1", function(d) { return d.y; } )
  250. .attr("y2", function(d) { return d.y + 3; } )
  251. .attr("class", "source mark")
  252. .attr("marker-start", "url(#circle)");
  253. var targetMarker = svgg.append("svg:g").selectAll("line")
  254. .data(this._targetMarker)
  255. .enter().append("svg:line")
  256. .attr("x1", function(d) { return d.x + d.w + markerWidth; } )
  257. .attr("x2", function(d) { return d.x + d.w; } )
  258. .attr("y1", function(d) { return d.y + d.h; } )
  259. .attr("y2", function(d) { return d.y + d.h - 3; } )
  260. .attr("class", "target mark")
  261. .attr("marker-start", "url(#circle)");
  262. // create links between the nodes
  263. var lines = svgg.append("svg:g").selectAll("path")
  264. .data(this._links)
  265. .enter().append("svg:path")
  266. .attr("d", function(d) {
  267. var s = d.source;
  268. var t = d.target;
  269. var x1 = s.x + s.w;
  270. var x2 = t.x;
  271. var y1 = s.y;
  272. var y2 = t.y;
  273. if (y1==y2) {
  274. y1 += s.h/2;
  275. y2 += t.h/2;
  276. } else if (y1 < y2) {
  277. y1 += s.h;
  278. } else {
  279. y2 += t.h;
  280. }
  281. return "M "+x1+" "+y1+" L "+((x2+x1)/2)+" "+((y2+y1)/2)+" L "+x2+" "+y2;
  282. } )
  283. .attr("class", function (d) {
  284. return "link" + (d.status ? " finished" : "");
  285. })
  286. .attr("marker-mid", function (d) {
  287. return "url(#" + (d.status ? "finished" : "unfinished") + ")";
  288. });
  289. // create text group for each node label
  290. var text = svgg.append("svg:g").selectAll("g")
  291. .data(this._nodes)
  292. .enter().append("svg:g");
  293. // add a shadow copy of the node label (will have a lighter color and thicker
  294. // stroke for legibility)
  295. text.append("svg:text")
  296. .attr("x", function(d) {
  297. var goal = d.x + d.w/2;
  298. var halfLabel = maxLabelWidth/2;
  299. if (goal < halfLabel) return halfLabel;
  300. else if (goal > w-halfLabel) return w-halfLabel;
  301. return goal;
  302. } )
  303. .attr("y", function(d) { return d.y + d.h + labelFontSize; } )
  304. .attr("class", "joblabel shadow")
  305. .attr("style", "font: "+labelFontSize+"px sans-serif")
  306. .text(function (d) {
  307. return d.name;
  308. });
  309. // add the main node label
  310. text.append("svg:text")
  311. .attr("x", function(d) {
  312. var goal = d.x + d.w/2;
  313. var halfLabel = maxLabelWidth/2;
  314. if (goal < halfLabel) return halfLabel;
  315. else if (goal > w-halfLabel) return w-halfLabel;
  316. return goal;
  317. } )
  318. .attr("y", function(d) { return d.y + d.h + labelFontSize; } )
  319. .attr("class", "joblabel")
  320. .attr("style", "font: "+labelFontSize+"px sans-serif")
  321. .text(function (d) {
  322. return d.name;
  323. });
  324. svg.call(d3.behavior.zoom().on("zoom", function() {
  325. var left = Math.min(Math.max(d3.event.translate[0]+margin.horizontal, margin.horizontal-w*d3.event.scale*scale), margin.horizontal+w);
  326. var top = Math.min(Math.max(d3.event.translate[1]+margin.vertical, margin.vertical-h*d3.event.scale*scale), margin.vertical+h);
  327. svgg.attr("transform", "translate("+left+","+top+") scale("+(d3.event.scale*scale)+")");
  328. }));
  329. }
  330. DagViewer.prototype.addTimeSeries = function (series, position, name) {
  331. var offset = this._extraSeriesOffset + this._extraSeriesSize*position;
  332. var x = this._x;
  333. var ymax = d3.max(series, function(d) {return d3.max(d.values, function(d) { return d.y;} ) } );
  334. var y = d3.scale.linear()
  335. .domain([0, ymax])
  336. .range([this._extraSeriesSize - this._margin.vertical, 0]);
  337. var yAxis = d3.svg.axis()
  338. .scale(y)
  339. .ticks(ymax < 4 ? ymax : 4)
  340. .orient("left");
  341. var line = d3.svg.line()
  342. .interpolate("linear")
  343. .x(function(d) { return x(d.x*1000); } )
  344. .y(function(d) { return y(d.y); } );
  345. this._svgg.append("svg:g")
  346. .attr("class", "y axis")
  347. .call(yAxis)
  348. .attr("transform", "translate(0,"+offset+")")
  349. .append("text")
  350. .attr("transform", "rotate(-90)")
  351. .attr("x", -(this._extraSeriesSize - this._margin.vertical)/2)
  352. .attr("y", -this._margin.horizontal + 11)
  353. .attr("class", "axislabel")
  354. .text(name);
  355. var lines = this._svgg.append("svg:g").selectAll("path")
  356. .data(series)
  357. .enter().append("svg:path")
  358. .attr("d", function(d) { return line(d.values);})
  359. .attr("class", function(d) { return d.name;})
  360. .attr("style", function(d) {
  361. if (d.name.substring(0,3)=="all")
  362. return "stroke:"+d3.interpolateRgb(d.color, 'black')(0.125)+";fill:white";
  363. else
  364. return "stroke:"+d3.interpolateRgb(d.color, 'black')(0.125)+";fill:"+d.color;
  365. })
  366. .attr("transform", "translate(0,"+offset+")");
  367. }
  368. DagViewer.formatDuration = function(d) {
  369. if (d==0) { return "0" }
  370. var seconds = Math.floor(parseInt(d) / 1000);
  371. if ( seconds < 60 )
  372. return seconds + "s";
  373. var minutes = Math.floor(seconds / 60);
  374. if ( minutes < 60 ) {
  375. var x = seconds - 60*minutes;
  376. return minutes + "m" + (x==0 ? "" : " " + x + "s");
  377. }
  378. var hours = Math.floor(minutes / 60);
  379. if ( hours < 24 ) {
  380. var x = minutes - 60*hours;
  381. return hours + "h" + (x==0 ? "" : " " + x + "m");
  382. }
  383. var days = Math.floor(hours / 24);
  384. if ( days < 7 ) {
  385. var x = hours - 24*days;
  386. return days + "d " + (x==0 ? "" : " " + x + "h");
  387. }
  388. var weeks = Math.floor(days / 7);
  389. var x = days - 7*weeks;
  390. return weeks + "w " + (x==0 ? "" : " " + x + "d");
  391. };