/* * Example usage: * * var dv = new DagViewer('pig_5') * .setData(workflowData,jobsData) * .drawDag(svgWidth,svgHeight,nodeHeight); */ function DagViewer(domId) { // initialize variables this._nodes = new Array(); this._links = new Array(); this._numNodes = 0; this._id = domId; this._SUCCESS = "SUCCESS"; } // set workflow schema and job data DagViewer.prototype.setData = function (wfData, jobData) { // create map from entity names to nodes var existingNodes = new Array(); var jobData = (jobData) ? jobData : new Array(); var minStartTime = 0; if (jobData.length > 0) minStartTime = jobData[0].submitTime; var maxFinishTime = 0; // iterate through job data for (var i = 0; i < jobData.length; i++) { jobData[i].info = "jobId:"+jobData[i].name+" \n"+ "nodeName:"+jobData[i].entityName+" \n"+ "status:"+jobData[i].status+" \n"+ "input:"+jobData[i].input+" \n"+ "output:"+jobData[i].output+" \n"+ "startTime:"+(new Date(jobData[i].submitTime).toString())+" \n"+ "duration:"+DagViewer.formatDuration(jobData[i].elapsedTime); minStartTime = Math.min(minStartTime, jobData[i].submitTime); maxFinishTime = Math.max(maxFinishTime, jobData[i].submitTime + jobData[i].elapsedTime); this._addNode(existingNodes, jobData[i].entityName, jobData[i]); } this._minStartTime = minStartTime; this._maxFinishTime = maxFinishTime; var dag = eval('(' + wfData + ')').dag; this._sourceMarker = new Array(); this._targetMarker = new Array(); this._sourceMap = new Array(); // for each source node in the context, create links between it and its target nodes for (var source in dag) { var sourceNode = null; if (source in existingNodes) sourceNode = existingNodes[source]; for (var i = 0; i < dag[source].length; i++) { var targetNode = null; if (dag[source][i] in existingNodes) targetNode = existingNodes[dag[source][i]]; this._addLink(sourceNode, targetNode); } } return this; } // add a node to the nodes array and to a provided map of entity names to nodes DagViewer.prototype._addNode = function (existingNodes, entityName, node) { existingNodes[entityName] = node; this._nodes.push(node); this._numNodes++; } // add a link between sourceNode and targetNode DagViewer.prototype._addLink = function (sourceNode, targetNode) { // if source or target is null, add marker indicating unsubmitted job and return if (sourceNode==null) { if (targetNode==null) return; this._sourceMarker.push(targetNode); return; } if (targetNode==null) { this._targetMarker.push(sourceNode); return; } // add link between nodes var status = false; if (sourceNode.status==this._SUCCESS && targetNode.status==this._SUCCESS) status = true; this._links.push({"source":sourceNode, "target":targetNode, "status":status, "value":sourceNode.output}); // add source to map of targets to sources if (!(targetNode.name in this._sourceMap)) this._sourceMap[targetNode.name] = new Array(); this._sourceMap[targetNode.name].push(sourceNode); } // display the graph // rules of thumb: nodeHeight = 20, labelFontSize = 14, maxLabelWidth = 180 // nodeHeight = 15, labelFontSize = 10, maxLabelWidth = 120 // nodeHeight = 40, labelFontSize = 20, maxLabelWidth = 260 // nodeHeight = 30, labelFontSize = 16 DagViewer.prototype.drawDag = function (svgw, svgh, nodeHeight, labelFontSize, maxLabelWidth, axisPadding, numExtraSeries, extraSeriesSize) { this._addTimelineGraph(svgw, svgh, nodeHeight || 20, labelFontSize || 14, maxLabelWidth || 180, axisPadding || 30, numExtraSeries || 2, extraSeriesSize || 50); return this; } // draw timeline graph DagViewer.prototype._addTimelineGraph = function (svgw, svgh, nodeHeight, labelFontSize, maxLabelWidth, axisPadding, numExtraSeries, extraSeriesSize) { svgw = svgw; this._extraSeriesSize = extraSeriesSize; var margin = {"vertical":10, "horizontal":50}; this._margin = margin; var w = svgw - 2*margin.horizontal; var startTime = this._minStartTime; var elapsedTime = this._maxFinishTime - this._minStartTime; var x = d3.time.scale.utc() .domain([startTime, startTime+elapsedTime]) .range([0, w]); this._x = x; var xrel = d3.time.scale() .domain([0, elapsedTime]) .range([0, w]); // process nodes and determine their x and y positions, width and height var minNodeSpacing = nodeHeight/2; var ends = new Array(); var maxIndex = 0; this._nodes = this._nodes.sort(function(a,b){return a.name.localeCompare(b.name);}); for (var i = 0; i < this._numNodes; i++) { var d = this._nodes[i]; d.x = x(d.submitTime); d.w = x(d.elapsedTime+d.submitTime) - x(d.submitTime); if (d.w < nodeHeight/2) { d.w = nodeHeight/2; if (d.x + d.w > w) d.x = w - d.w; } var effectiveX = d.x var effectiveWidth = d.w; if (d.w < maxLabelWidth) { effectiveWidth = maxLabelWidth; if (d.x + effectiveWidth > w) effectiveX = w - effectiveWidth; else if (d.x > 0) effectiveX = d.x+(d.w-maxLabelWidth)/2; } // select "lane" (slot for y-position) for this node // starting at the slot above the node's closest source node (or 0, if none exists) // and moving down until a slot is found that has no nodes within minNodeSpacing of this node // excluding slots that contain more than one source of this node var index = 0; var rejectIndices = new Array(); if (d.name in this._sourceMap) { var sources = this._sourceMap[d.name]; var closestSource = sources[0]; var indices = new Array(); for (var j = 0; j < sources.length; j++) { if (sources[j].index in indices) rejectIndices[sources[j].index] = true; indices[sources[j].index] = true; if (sources[j].submitTime + sources[j].elapsedTime > closestSource.submitTime + closestSource.elapsedTime) closestSource = sources[j]; } index = Math.max(0, closestSource.index-1); } while ((index in ends) && ((index in rejectIndices) || (ends[index]+minNodeSpacing >= effectiveX))) { index++ } ends[index] = Math.max(effectiveX + effectiveWidth); maxIndex = Math.max(maxIndex, index); d.y = index*2*nodeHeight + axisPadding; d.h = nodeHeight; d.index = index; } var h = 2*axisPadding + 2*nodeHeight*(maxIndex+1); var realh = svgh - 2*margin.vertical - numExtraSeries*extraSeriesSize; var scale = 1; if (h > realh) scale = realh / h; svgh = Math.min(svgh, h + 2*margin.vertical + numExtraSeries*extraSeriesSize); this._extraSeriesOffset = h + margin.vertical; var svg = d3.select("div#" + this._id).append("svg:svg") .attr("width", svgw+"px") .attr("height", svgh+"px"); var svgg = svg.append("g") .attr("transform", "translate("+margin.horizontal+","+margin.vertical+") scale("+scale+")"); this._svgg = svgg; // add an untranslated white rectangle below everything // so mouse doesn't have to be over nodes for panning/zooming svgg.append("svg:rect") .attr("x", 0) .attr("y", 0) .attr("width", svgw) .attr("height", svgh/scale) .attr("style", "fill:white;stroke:none"); // create axes var topAxis = d3.svg.axis() .scale(x) .orient("bottom"); var bottomAxis = d3.svg.axis() .scale(xrel) .orient("top") .tickFormat(function(x) { return DagViewer.formatDuration(x.getTime()); }); svgg.append("g") .attr("class", "x axis top") .call(topAxis); svgg.append("g") .attr("class", "x axis bottom") .call(bottomAxis) .attr("transform", "translate(0,"+h+")"); // create a rectangle for each node var success = this._SUCCESS; var boxes = svgg.append("svg:g").selectAll("rect") .data(this._nodes) .enter().append("svg:rect") .attr("x", function(d) { return d.x; } ) .attr("y", function(d) { return d.y; } ) .attr("width", function(d) { return d.w; } ) .attr("height", function(d) { return d.h; } ) .attr("class", function (d) { return "node " + (d.status==success ? " finished" : ""); }) .attr("id", function (d) { return d.name; }) .append("title") .text(function(d) { return d.info; }); // defs for arrowheads marked as to whether they link finished jobs or not svgg.append("svg:defs").selectAll("arrowmarker") .data(["finished", "unfinished"]) .enter().append("svg:marker") .attr("id", String) .attr("viewBox", "0 -5 10 10") .attr("markerWidth", 6) .attr("markerHeight", 6) .attr("orient", "auto") .append("svg:path") .attr("d", "M0,-3L8,0L0,3"); // defs for unsubmitted node marker svgg.append("svg:defs").selectAll("circlemarker") .data(["circle"]) .enter().append("svg:marker") .attr("id", String) .attr("viewBox", "-2 -2 18 18") .attr("markerWidth", 10) .attr("markerHeight", 10) .attr("refX", 10) .attr("refY", 5) .attr("orient", "auto") .append("svg:circle") .attr("cx", 5) .attr("cy", 5) .attr("r", 5); // create dangling links representing unsubmitted jobs var markerWidth = nodeHeight/2; var sourceMarker = svgg.append("svg:g").selectAll("line") .data(this._sourceMarker) .enter().append("svg:line") .attr("x1", function(d) { return d.x - markerWidth; } ) .attr("x2", function(d) { return d.x; } ) .attr("y1", function(d) { return d.y; } ) .attr("y2", function(d) { return d.y + 3; } ) .attr("class", "source mark") .attr("marker-start", "url(#circle)"); var targetMarker = svgg.append("svg:g").selectAll("line") .data(this._targetMarker) .enter().append("svg:line") .attr("x1", function(d) { return d.x + d.w + markerWidth; } ) .attr("x2", function(d) { return d.x + d.w; } ) .attr("y1", function(d) { return d.y + d.h; } ) .attr("y2", function(d) { return d.y + d.h - 3; } ) .attr("class", "target mark") .attr("marker-start", "url(#circle)"); // create links between the nodes var lines = svgg.append("svg:g").selectAll("path") .data(this._links) .enter().append("svg:path") .attr("d", function(d) { var s = d.source; var t = d.target; var x1 = s.x + s.w; var x2 = t.x; var y1 = s.y; var y2 = t.y; if (y1==y2) { y1 += s.h/2; y2 += t.h/2; } else if (y1 < y2) { y1 += s.h; } else { y2 += t.h; } return "M "+x1+" "+y1+" L "+((x2+x1)/2)+" "+((y2+y1)/2)+" L "+x2+" "+y2; } ) .attr("class", function (d) { return "link" + (d.status ? " finished" : ""); }) .attr("marker-mid", function (d) { return "url(#" + (d.status ? "finished" : "unfinished") + ")"; }); // create text group for each node label var text = svgg.append("svg:g").selectAll("g") .data(this._nodes) .enter().append("svg:g"); // add a shadow copy of the node label (will have a lighter color and thicker // stroke for legibility) text.append("svg:text") .attr("x", function(d) { var goal = d.x + d.w/2; var halfLabel = maxLabelWidth/2; if (goal < halfLabel) return halfLabel; else if (goal > w-halfLabel) return w-halfLabel; return goal; } ) .attr("y", function(d) { return d.y + d.h + labelFontSize; } ) .attr("class", "joblabel shadow") .attr("style", "font: "+labelFontSize+"px sans-serif") .text(function (d) { return d.name; }); // add the main node label text.append("svg:text") .attr("x", function(d) { var goal = d.x + d.w/2; var halfLabel = maxLabelWidth/2; if (goal < halfLabel) return halfLabel; else if (goal > w-halfLabel) return w-halfLabel; return goal; } ) .attr("y", function(d) { return d.y + d.h + labelFontSize; } ) .attr("class", "joblabel") .attr("style", "font: "+labelFontSize+"px sans-serif") .text(function (d) { return d.name; }); svg.call(d3.behavior.zoom().on("zoom", function() { var left = Math.min(Math.max(d3.event.translate[0]+margin.horizontal, margin.horizontal-w*d3.event.scale*scale), margin.horizontal+w); var top = Math.min(Math.max(d3.event.translate[1]+margin.vertical, margin.vertical-h*d3.event.scale*scale), margin.vertical+h); svgg.attr("transform", "translate("+left+","+top+") scale("+(d3.event.scale*scale)+")"); })); } DagViewer.prototype.addTimeSeries = function (series, position, name) { var offset = this._extraSeriesOffset + this._extraSeriesSize*position; var x = this._x; var ymax = d3.max(series, function(d) {return d3.max(d.values, function(d) { return d.y;} ) } ); var y = d3.scale.linear() .domain([0, ymax]) .range([this._extraSeriesSize - this._margin.vertical, 0]); var yAxis = d3.svg.axis() .scale(y) .ticks(ymax < 4 ? ymax : 4) .orient("left"); var line = d3.svg.line() .interpolate("linear") .x(function(d) { return x(d.x*1000); } ) .y(function(d) { return y(d.y); } ); this._svgg.append("svg:g") .attr("class", "y axis") .call(yAxis) .attr("transform", "translate(0,"+offset+")") .append("text") .attr("transform", "rotate(-90)") .attr("x", -(this._extraSeriesSize - this._margin.vertical)/2) .attr("y", -this._margin.horizontal + 11) .attr("class", "axislabel") .text(name); var lines = this._svgg.append("svg:g").selectAll("path") .data(series) .enter().append("svg:path") .attr("d", function(d) { return line(d.values);}) .attr("class", function(d) { return d.name;}) .attr("style", function(d) { if (d.name.substring(0,3)=="all") return "stroke:"+d3.interpolateRgb(d.color, 'black')(0.125)+";fill:white"; else return "stroke:"+d3.interpolateRgb(d.color, 'black')(0.125)+";fill:"+d.color; }) .attr("transform", "translate(0,"+offset+")"); } DagViewer.formatDuration = function(d) { if (d==0) { return "0" } var seconds = Math.floor(parseInt(d) / 1000); if ( seconds < 60 ) return seconds + "s"; var minutes = Math.floor(seconds / 60); if ( minutes < 60 ) { var x = seconds - 60*minutes; return minutes + "m" + (x==0 ? "" : " " + x + "s"); } var hours = Math.floor(minutes / 60); if ( hours < 24 ) { var x = minutes - 60*hours; return hours + "h" + (x==0 ? "" : " " + x + "m"); } var days = Math.floor(hours / 24); if ( days < 7 ) { var x = hours - 24*days; return days + "d " + (x==0 ? "" : " " + x + "h"); } var weeks = Math.floor(days / 7); var x = days - 7*weeks; return weeks + "w " + (x==0 ? "" : " " + x + "d"); };