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