From d8c1350ba16973d902e28e553b629bfbc72683ef Mon Sep 17 00:00:00 2001 From: osvalds Date: Thu, 28 Sep 2023 21:32:35 +0300 Subject: [PATCH] Add support for `labelheight` for clusters --- bower.json | 2 +- dist/dagre.js | 45 ++++++++++++++++++++++++++++++++------ dist/dagre.min.js | 19 +++++++++++----- lib/layout.js | 4 ++-- lib/nesting-graph.js | 5 +++-- lib/position/index.js | 19 ++++++++++++++-- lib/util.js | 15 +++++++++++++ package-lock.json | 8 +++---- package.json | 7 ++++-- test/nesting-graph-test.js | 2 +- 10 files changed, 99 insertions(+), 27 deletions(-) diff --git a/bower.json b/bower.json index 8e3110d5..b57bb603 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "dagre", - "version": "1.0.4", + "version": "1.0.5-pre", "main": [ "dist/dagre.core.js" ], diff --git a/dist/dagre.js b/dist/dagre.js index fa10bb1a..80d95e1d 100644 --- a/dist/dagre.js +++ b/dist/dagre.js @@ -532,8 +532,8 @@ function updateInputGraph(inputGraph, layoutGraph) { let graphNumAttrs = ["nodesep", "edgesep", "ranksep", "marginx", "marginy"]; let graphDefaults = { ranksep: 50, edgesep: 20, nodesep: 50, rankdir: "tb" }; let graphAttrs = ["acyclicer", "ranker", "rankdir", "align"]; -let nodeNumAttrs = ["width", "height"]; -let nodeDefaults = { width: 0, height: 0 }; +let nodeNumAttrs = ["width", "height", "labelheight"]; +let nodeDefaults = { width: 0, height: 0, labelheight: 0 }; let edgeNumAttrs = ["minlen", "weight", "width", "height", "labeloffset"]; let edgeDefaults = { minlen: 1, weight: 1, width: 0, height: 0, @@ -901,9 +901,10 @@ function dfs(g, root, nodeSep, weight, height, depths, v) { return; } - let top = util.addBorderNode(g, "_bt"); - let bottom = util.addBorderNode(g, "_bb"); let label = g.node(v); + // Adding custom border node to help with cluster label offset + let top = util.addWhimBorderNode(g, "_bt", label.labelheight); + let bottom = util.addBorderNode(g, "_bb"); g.setParent(top, v); label.borderTop = top; @@ -2184,8 +2185,23 @@ function positionY(g) { return height; } }, 0); - layer.forEach(v => g.node(v).y = prevY + maxHeight / 2); - prevY += maxHeight + rankSep; + let borderTopSeen = false; + let labelheight = 0; + layer.forEach(v => { + let node = g.node(v); + if (node.dummy === "border" && node.whimNode) { + borderTopSeen = true; + labelheight = node.labelheight; + } + node.y = prevY + maxHeight / 2 + }); + + if (borderTopSeen) { + // hard coding padding for cluster labels + prevY += maxHeight + labelheight + 24; + } else { + prevY += maxHeight + rankSep; + } }); } @@ -2648,6 +2664,7 @@ let Graph = require("@dagrejs/graphlib").Graph; module.exports = { addBorderNode, + addWhimBorderNode, addDummyNode, asNonCompoundGraph, buildLayerMatrix, @@ -2847,6 +2864,20 @@ function addBorderNode(g, prefix, rank, order) { return addDummyNode(g, "border", node, prefix); } +// labelHeight is a custom property to be used to offset cluster children nodes +// by the height of the cluster label +function addWhimBorderNode(g, prefix, labelHeight) { + const node = { + width: 0, + height: 0, + whimNode: true, + labelheight: labelHeight + }; + + return addDummyNode(g, "border", node, prefix); +} + + function maxRank(g) { return Math.max(...g.nodes().map(v => { let rank = g.node(v).rank; @@ -2948,7 +2979,7 @@ function zipObject(props, values) { } },{"@dagrejs/graphlib":29}],28:[function(require,module,exports){ -module.exports = "1.0.4"; +module.exports = "1.0.5-pre"; },{}],29:[function(require,module,exports){ /** diff --git a/dist/dagre.min.js b/dist/dagre.min.js index 4e08965e..f3bd25a0 100644 --- a/dist/dagre.min.js +++ b/dist/dagre.min.js @@ -44,7 +44,7 @@ g.edges().forEach(e=>{let prevWeight=fasGraph.edge(e.v,e.w)||0;let weight=weight * graph. This process only copies whitelisted attributes from the layout graph * to the input graph, so it serves as a good place to determine what * attributes can influence layout. - */function updateInputGraph(inputGraph,layoutGraph){inputGraph.nodes().forEach(v=>{let inputLabel=inputGraph.node(v);let layoutLabel=layoutGraph.node(v);if(inputLabel){inputLabel.x=layoutLabel.x;inputLabel.y=layoutLabel.y;inputLabel.rank=layoutLabel.rank;if(layoutGraph.children(v).length){inputLabel.width=layoutLabel.width;inputLabel.height=layoutLabel.height}}});inputGraph.edges().forEach(e=>{let inputLabel=inputGraph.edge(e);let layoutLabel=layoutGraph.edge(e);inputLabel.points=layoutLabel.points;if(layoutLabel.hasOwnProperty("x")){inputLabel.x=layoutLabel.x;inputLabel.y=layoutLabel.y}});inputGraph.graph().width=layoutGraph.graph().width;inputGraph.graph().height=layoutGraph.graph().height}let graphNumAttrs=["nodesep","edgesep","ranksep","marginx","marginy"];let graphDefaults={ranksep:50,edgesep:20,nodesep:50,rankdir:"tb"};let graphAttrs=["acyclicer","ranker","rankdir","align"];let nodeNumAttrs=["width","height"];let nodeDefaults={width:0,height:0};let edgeNumAttrs=["minlen","weight","width","height","labeloffset"];let edgeDefaults={minlen:1,weight:1,width:0,height:0,labeloffset:10,labelpos:"r"};let edgeAttrs=["labelpos"]; + */function updateInputGraph(inputGraph,layoutGraph){inputGraph.nodes().forEach(v=>{let inputLabel=inputGraph.node(v);let layoutLabel=layoutGraph.node(v);if(inputLabel){inputLabel.x=layoutLabel.x;inputLabel.y=layoutLabel.y;inputLabel.rank=layoutLabel.rank;if(layoutGraph.children(v).length){inputLabel.width=layoutLabel.width;inputLabel.height=layoutLabel.height}}});inputGraph.edges().forEach(e=>{let inputLabel=inputGraph.edge(e);let layoutLabel=layoutGraph.edge(e);inputLabel.points=layoutLabel.points;if(layoutLabel.hasOwnProperty("x")){inputLabel.x=layoutLabel.x;inputLabel.y=layoutLabel.y}});inputGraph.graph().width=layoutGraph.graph().width;inputGraph.graph().height=layoutGraph.graph().height}let graphNumAttrs=["nodesep","edgesep","ranksep","marginx","marginy"];let graphDefaults={ranksep:50,edgesep:20,nodesep:50,rankdir:"tb"};let graphAttrs=["acyclicer","ranker","rankdir","align"];let nodeNumAttrs=["width","height","labelheight"];let nodeDefaults={width:0,height:0,labelheight:0};let edgeNumAttrs=["minlen","weight","width","height","labeloffset"];let edgeDefaults={minlen:1,weight:1,width:0,height:0,labeloffset:10,labelpos:"r"};let edgeAttrs=["labelpos"]; /* * Constructs a new graph from the input graph, which can be used for layout. * This process copies only whitelisted attributes from the input graph to the @@ -97,7 +97,9 @@ let weight=sumWeights(g)+1; g.children().forEach(child=>dfs(g,root,nodeSep,weight,height,depths,child)); // Save the multiplier for node layers for later removal of empty border // layers. -g.graph().nodeRankFactor=nodeSep}function dfs(g,root,nodeSep,weight,height,depths,v){let children=g.children(v);if(!children.length){if(v!==root){g.setEdge(root,v,{weight:0,minlen:nodeSep})}return}let top=util.addBorderNode(g,"_bt");let bottom=util.addBorderNode(g,"_bb");let label=g.node(v);g.setParent(top,v);label.borderTop=top;g.setParent(bottom,v);label.borderBottom=bottom;children.forEach(child=>{dfs(g,root,nodeSep,weight,height,depths,child);let childNode=g.node(child);let childTop=childNode.borderTop?childNode.borderTop:child;let childBottom=childNode.borderBottom?childNode.borderBottom:child;let thisWeight=childNode.borderTop?weight:2*weight;let minlen=childTop!==childBottom?1:height-depths[v]+1;g.setEdge(top,childTop,{weight:thisWeight,minlen:minlen,nestingEdge:true});g.setEdge(childBottom,bottom,{weight:thisWeight,minlen:minlen,nestingEdge:true})});if(!g.parent(v)){g.setEdge(root,top,{weight:0,minlen:height+depths[v]})}}function treeDepths(g){var depths={};function dfs(v,depth){var children=g.children(v);if(children&&children.length){children.forEach(child=>dfs(child,depth+1))}depths[v]=depth}g.children().forEach(v=>dfs(v,1));return depths}function sumWeights(g){return g.edges().reduce((acc,e)=>acc+g.edge(e).weight,0)}function cleanup(g){var graphLabel=g.graph();g.removeNode(graphLabel.nestingRoot);delete graphLabel.nestingRoot;g.edges().forEach(e=>{var edge=g.edge(e);if(edge.nestingEdge){g.removeEdge(e)}})}},{"./util":27}],10:[function(require,module,exports){"use strict";let util=require("./util");module.exports={run:run,undo:undo}; +g.graph().nodeRankFactor=nodeSep}function dfs(g,root,nodeSep,weight,height,depths,v){let children=g.children(v);if(!children.length){if(v!==root){g.setEdge(root,v,{weight:0,minlen:nodeSep})}return}let label=g.node(v); +// Adding custom border node to help with cluster label offset +let top=util.addWhimBorderNode(g,"_bt",label.labelheight);let bottom=util.addBorderNode(g,"_bb");g.setParent(top,v);label.borderTop=top;g.setParent(bottom,v);label.borderBottom=bottom;children.forEach(child=>{dfs(g,root,nodeSep,weight,height,depths,child);let childNode=g.node(child);let childTop=childNode.borderTop?childNode.borderTop:child;let childBottom=childNode.borderBottom?childNode.borderBottom:child;let thisWeight=childNode.borderTop?weight:2*weight;let minlen=childTop!==childBottom?1:height-depths[v]+1;g.setEdge(top,childTop,{weight:thisWeight,minlen:minlen,nestingEdge:true});g.setEdge(childBottom,bottom,{weight:thisWeight,minlen:minlen,nestingEdge:true})});if(!g.parent(v)){g.setEdge(root,top,{weight:0,minlen:height+depths[v]})}}function treeDepths(g){var depths={};function dfs(v,depth){var children=g.children(v);if(children&&children.length){children.forEach(child=>dfs(child,depth+1))}depths[v]=depth}g.children().forEach(v=>dfs(v,1));return depths}function sumWeights(g){return g.edges().reduce((acc,e)=>acc+g.edge(e).weight,0)}function cleanup(g){var graphLabel=g.graph();g.removeNode(graphLabel.nestingRoot);delete graphLabel.nestingRoot;g.edges().forEach(e=>{var edge=g.edge(e);if(edge.nestingEdge){g.removeEdge(e)}})}},{"./util":27}],10:[function(require,module,exports){"use strict";let util=require("./util");module.exports={run:run,undo:undo}; /* * Breaks any long edges in the graph into short segments that span 1 layer * each. This operation is undoable with the denormalize function. @@ -311,7 +313,9 @@ Object.keys(align).forEach(v=>xs[v]=xs[root[v]]);return xs}function buildBlockGr * the minimum coordinate of the smallest width alignment and right-biased * alignments have their maximum coordinate at the same point as the maximum * coordinate of the smallest width alignment. - */function alignCoordinates(xss,alignTo){let alignToVals=Object.values(alignTo),alignToMin=Math.min(...alignToVals),alignToMax=Math.max(...alignToVals);["u","d"].forEach(vert=>{["l","r"].forEach(horiz=>{let alignment=vert+horiz,xs=xss[alignment];if(xs===alignTo)return;let xsVals=Object.values(xs);let delta=alignToMin-Math.min(...xsVals);if(horiz!=="l"){delta=alignToMax-Math.max(...xsVals)}if(delta){xss[alignment]=util.mapValues(xs,x=>x+delta)}})})}function balance(xss,align){return util.mapValues(xss.ul,(num,v)=>{if(align){return xss[align.toLowerCase()][v]}else{let xs=Object.values(xss).map(xs=>xs[v]).sort((a,b)=>a-b);return(xs[1]+xs[2])/2}})}function positionX(g){let layering=util.buildLayerMatrix(g);let conflicts=Object.assign(findType1Conflicts(g,layering),findType2Conflicts(g,layering));let xss={};let adjustedLayering;["u","d"].forEach(vert=>{adjustedLayering=vert==="u"?layering:Object.values(layering).reverse();["l","r"].forEach(horiz=>{if(horiz==="r"){adjustedLayering=adjustedLayering.map(inner=>{return Object.values(inner).reverse()})}let neighborFn=(vert==="u"?g.predecessors:g.successors).bind(g);let align=verticalAlignment(g,adjustedLayering,conflicts,neighborFn);let xs=horizontalCompaction(g,adjustedLayering,align.root,align.align,horiz==="r");if(horiz==="r"){xs=util.mapValues(xs,x=>-x)}xss[vert+horiz]=xs})});let smallestWidth=findSmallestWidthAlignment(g,xss);alignCoordinates(xss,smallestWidth);return balance(xss,g.graph().align)}function sep(nodeSep,edgeSep,reverseSep){return(g,v,w)=>{let vLabel=g.node(v);let wLabel=g.node(w);let sum=0;let delta;sum+=vLabel.width/2;if(vLabel.hasOwnProperty("labelpos")){switch(vLabel.labelpos.toLowerCase()){case"l":delta=-vLabel.width/2;break;case"r":delta=vLabel.width/2;break}}if(delta){sum+=reverseSep?delta:-delta}delta=0;sum+=(vLabel.dummy?edgeSep:nodeSep)/2;sum+=(wLabel.dummy?edgeSep:nodeSep)/2;sum+=wLabel.width/2;if(wLabel.hasOwnProperty("labelpos")){switch(wLabel.labelpos.toLowerCase()){case"l":delta=wLabel.width/2;break;case"r":delta=-wLabel.width/2;break}}if(delta){sum+=reverseSep?delta:-delta}delta=0;return sum}}function width(g,v){return g.node(v).width}},{"../util":27,"@dagrejs/graphlib":29}],22:[function(require,module,exports){"use strict";let util=require("../util");let positionX=require("./bk").positionX;module.exports=position;function position(g){g=util.asNonCompoundGraph(g);positionY(g);Object.entries(positionX(g)).forEach(([v,x])=>g.node(v).x=x)}function positionY(g){let layering=util.buildLayerMatrix(g);let rankSep=g.graph().ranksep;let prevY=0;layering.forEach(layer=>{const maxHeight=layer.reduce((acc,v)=>{const height=g.node(v).height;if(acc>height){return acc}else{return height}},0);layer.forEach(v=>g.node(v).y=prevY+maxHeight/2);prevY+=maxHeight+rankSep})}},{"../util":27,"./bk":21}],23:[function(require,module,exports){"use strict";var Graph=require("@dagrejs/graphlib").Graph;var slack=require("./util").slack;module.exports=feasibleTree; + */function alignCoordinates(xss,alignTo){let alignToVals=Object.values(alignTo),alignToMin=Math.min(...alignToVals),alignToMax=Math.max(...alignToVals);["u","d"].forEach(vert=>{["l","r"].forEach(horiz=>{let alignment=vert+horiz,xs=xss[alignment];if(xs===alignTo)return;let xsVals=Object.values(xs);let delta=alignToMin-Math.min(...xsVals);if(horiz!=="l"){delta=alignToMax-Math.max(...xsVals)}if(delta){xss[alignment]=util.mapValues(xs,x=>x+delta)}})})}function balance(xss,align){return util.mapValues(xss.ul,(num,v)=>{if(align){return xss[align.toLowerCase()][v]}else{let xs=Object.values(xss).map(xs=>xs[v]).sort((a,b)=>a-b);return(xs[1]+xs[2])/2}})}function positionX(g){let layering=util.buildLayerMatrix(g);let conflicts=Object.assign(findType1Conflicts(g,layering),findType2Conflicts(g,layering));let xss={};let adjustedLayering;["u","d"].forEach(vert=>{adjustedLayering=vert==="u"?layering:Object.values(layering).reverse();["l","r"].forEach(horiz=>{if(horiz==="r"){adjustedLayering=adjustedLayering.map(inner=>{return Object.values(inner).reverse()})}let neighborFn=(vert==="u"?g.predecessors:g.successors).bind(g);let align=verticalAlignment(g,adjustedLayering,conflicts,neighborFn);let xs=horizontalCompaction(g,adjustedLayering,align.root,align.align,horiz==="r");if(horiz==="r"){xs=util.mapValues(xs,x=>-x)}xss[vert+horiz]=xs})});let smallestWidth=findSmallestWidthAlignment(g,xss);alignCoordinates(xss,smallestWidth);return balance(xss,g.graph().align)}function sep(nodeSep,edgeSep,reverseSep){return(g,v,w)=>{let vLabel=g.node(v);let wLabel=g.node(w);let sum=0;let delta;sum+=vLabel.width/2;if(vLabel.hasOwnProperty("labelpos")){switch(vLabel.labelpos.toLowerCase()){case"l":delta=-vLabel.width/2;break;case"r":delta=vLabel.width/2;break}}if(delta){sum+=reverseSep?delta:-delta}delta=0;sum+=(vLabel.dummy?edgeSep:nodeSep)/2;sum+=(wLabel.dummy?edgeSep:nodeSep)/2;sum+=wLabel.width/2;if(wLabel.hasOwnProperty("labelpos")){switch(wLabel.labelpos.toLowerCase()){case"l":delta=wLabel.width/2;break;case"r":delta=-wLabel.width/2;break}}if(delta){sum+=reverseSep?delta:-delta}delta=0;return sum}}function width(g,v){return g.node(v).width}},{"../util":27,"@dagrejs/graphlib":29}],22:[function(require,module,exports){"use strict";let util=require("../util");let positionX=require("./bk").positionX;module.exports=position;function position(g){g=util.asNonCompoundGraph(g);positionY(g);Object.entries(positionX(g)).forEach(([v,x])=>g.node(v).x=x)}function positionY(g){let layering=util.buildLayerMatrix(g);let rankSep=g.graph().ranksep;let prevY=0;layering.forEach(layer=>{const maxHeight=layer.reduce((acc,v)=>{const height=g.node(v).height;if(acc>height){return acc}else{return height}},0);let borderTopSeen=false;let labelheight=0;layer.forEach(v=>{let node=g.node(v);if(node.dummy==="border"&&node.whimNode){borderTopSeen=true;labelheight=node.labelheight}node.y=prevY+maxHeight/2});if(borderTopSeen){ +// hard coding padding for cluster labels +prevY+=maxHeight+labelheight+24}else{prevY+=maxHeight+rankSep}})}},{"../util":27,"./bk":21}],23:[function(require,module,exports){"use strict";var Graph=require("@dagrejs/graphlib").Graph;var slack=require("./util").slack;module.exports=feasibleTree; /* * Constructs a spanning tree with tight edges and adjusted the input node's * ranks to achieve this. A tight edge is one that is has a length that matches @@ -458,7 +462,7 @@ if(vLabel.lim>wLabel.lim){tailLabel=wLabel;flip=true}var candidates=g.edges().fi * difference between the length of the edge and its minimum length. */function slack(g,e){return g.node(e.w).rank-g.node(e.v).rank-g.edge(e).minlen}},{}],27:[function(require,module,exports){ /* eslint "no-console": off */ -"use strict";let Graph=require("@dagrejs/graphlib").Graph;module.exports={addBorderNode:addBorderNode,addDummyNode:addDummyNode,asNonCompoundGraph:asNonCompoundGraph,buildLayerMatrix:buildLayerMatrix,intersectRect:intersectRect,mapValues:mapValues,maxRank:maxRank,normalizeRanks:normalizeRanks,notime:notime,partition:partition,pick:pick,predecessorWeights:predecessorWeights,range:range,removeEmptyRanks:removeEmptyRanks,simplify:simplify,successorWeights:successorWeights,time:time,uniqueId:uniqueId,zipObject:zipObject}; +"use strict";let Graph=require("@dagrejs/graphlib").Graph;module.exports={addBorderNode:addBorderNode,addWhimBorderNode:addWhimBorderNode,addDummyNode:addDummyNode,asNonCompoundGraph:asNonCompoundGraph,buildLayerMatrix:buildLayerMatrix,intersectRect:intersectRect,mapValues:mapValues,maxRank:maxRank,normalizeRanks:normalizeRanks,notime:notime,partition:partition,pick:pick,predecessorWeights:predecessorWeights,range:range,removeEmptyRanks:removeEmptyRanks,simplify:simplify,successorWeights:successorWeights,time:time,uniqueId:uniqueId,zipObject:zipObject}; /* * Adds a dummy node to the graph and return v. */function addDummyNode(g,type,attrs,name){let v;do{v=uniqueId(name)}while(g.hasNode(v));attrs.dummy=type;g.setNode(v,attrs);return v} @@ -486,7 +490,10 @@ if(dx<0){w=-w}sx=w;sy=w*dy/dx}return{x:x+sx,y:y+sy}} * rank(v) >= 0 and at least one node w has rank(w) = 0. */function normalizeRanks(g){let min=Math.min(...g.nodes().map(v=>{let rank=g.node(v).rank;if(rank===undefined){return Number.MAX_VALUE}return rank}));g.nodes().forEach(v=>{let node=g.node(v);if(node.hasOwnProperty("rank")){node.rank-=min}})}function removeEmptyRanks(g){ // Ranks may not start at 0, so we need to offset them -let offset=Math.min(...g.nodes().map(v=>g.node(v).rank));let layers=[];g.nodes().forEach(v=>{let rank=g.node(v).rank-offset;if(!layers[rank]){layers[rank]=[]}layers[rank].push(v)});let delta=0;let nodeRankFactor=g.graph().nodeRankFactor;Array.from(layers).forEach((vs,i)=>{if(vs===undefined&&i%nodeRankFactor!==0){--delta}else if(vs!==undefined&&delta){vs.forEach(v=>g.node(v).rank+=delta)}})}function addBorderNode(g,prefix,rank,order){let node={width:0,height:0};if(arguments.length>=4){node.rank=rank;node.order=order}return addDummyNode(g,"border",node,prefix)}function maxRank(g){return Math.max(...g.nodes().map(v=>{let rank=g.node(v).rank;if(rank===undefined){return Number.MIN_VALUE}return rank}))} +let offset=Math.min(...g.nodes().map(v=>g.node(v).rank));let layers=[];g.nodes().forEach(v=>{let rank=g.node(v).rank-offset;if(!layers[rank]){layers[rank]=[]}layers[rank].push(v)});let delta=0;let nodeRankFactor=g.graph().nodeRankFactor;Array.from(layers).forEach((vs,i)=>{if(vs===undefined&&i%nodeRankFactor!==0){--delta}else if(vs!==undefined&&delta){vs.forEach(v=>g.node(v).rank+=delta)}})}function addBorderNode(g,prefix,rank,order){let node={width:0,height:0};if(arguments.length>=4){node.rank=rank;node.order=order}return addDummyNode(g,"border",node,prefix)} +// labelHeight is a custom property to be used to offset cluster children nodes +// by the height of the cluster label +function addWhimBorderNode(g,prefix,labelHeight){const node={width:0,height:0,whimNode:true,labelheight:labelHeight};return addDummyNode(g,"border",node,prefix)}function maxRank(g){return Math.max(...g.nodes().map(v=>{let rank=g.node(v).rank;if(rank===undefined){return Number.MIN_VALUE}return rank}))} /* * Partition a collection into two groups: `lhs` and `rhs`. If the supplied * function returns true for an entry it goes into `lhs`. Otherwise it goes @@ -495,7 +502,7 @@ let offset=Math.min(...g.nodes().map(v=>g.node(v).rank));let layers=[];g.nodes() /* * Returns a new function that wraps `fn` with a timer. The wrapper logs the * time it takes to execute the function. - */function time(name,fn){let start=Date.now();try{return fn()}finally{console.log(name+" time: "+(Date.now()-start)+"ms")}}function notime(name,fn){return fn()}let idCounter=0;function uniqueId(prefix){var id=++idCounter;return toString(prefix)+id}function range(start,limit,step=1){if(limit==null){limit=start;start=0}let endCon=i=>ilimitval[funcOrProp]}return Object.entries(obj).reduce((acc,[k,v])=>{acc[k]=func(v,k);return acc},{})}function zipObject(props,values){return props.reduce((acc,key,i)=>{acc[key]=values[i];return acc},{})}},{"@dagrejs/graphlib":29}],28:[function(require,module,exports){module.exports="1.0.4"},{}],29:[function(require,module,exports){ + */function time(name,fn){let start=Date.now();try{return fn()}finally{console.log(name+" time: "+(Date.now()-start)+"ms")}}function notime(name,fn){return fn()}let idCounter=0;function uniqueId(prefix){var id=++idCounter;return toString(prefix)+id}function range(start,limit,step=1){if(limit==null){limit=start;start=0}let endCon=i=>ilimitval[funcOrProp]}return Object.entries(obj).reduce((acc,[k,v])=>{acc[k]=func(v,k);return acc},{})}function zipObject(props,values){return props.reduce((acc,key,i)=>{acc[key]=values[i];return acc},{})}},{"@dagrejs/graphlib":29}],28:[function(require,module,exports){module.exports="1.0.5-pre"},{}],29:[function(require,module,exports){ /** * Copyright (c) 2014, Chris Pettitt * All rights reserved. diff --git a/lib/layout.js b/lib/layout.js index dfccf301..c792437a 100644 --- a/lib/layout.js +++ b/lib/layout.js @@ -97,8 +97,8 @@ function updateInputGraph(inputGraph, layoutGraph) { let graphNumAttrs = ["nodesep", "edgesep", "ranksep", "marginx", "marginy"]; let graphDefaults = { ranksep: 50, edgesep: 20, nodesep: 50, rankdir: "tb" }; let graphAttrs = ["acyclicer", "ranker", "rankdir", "align"]; -let nodeNumAttrs = ["width", "height"]; -let nodeDefaults = { width: 0, height: 0 }; +let nodeNumAttrs = ["width", "height", "labelheight"]; +let nodeDefaults = { width: 0, height: 0, labelheight: 0 }; let edgeNumAttrs = ["minlen", "weight", "width", "height", "labeloffset"]; let edgeDefaults = { minlen: 1, weight: 1, width: 0, height: 0, diff --git a/lib/nesting-graph.js b/lib/nesting-graph.js index f5927bdf..df97d5f7 100644 --- a/lib/nesting-graph.js +++ b/lib/nesting-graph.js @@ -59,9 +59,10 @@ function dfs(g, root, nodeSep, weight, height, depths, v) { return; } - let top = util.addBorderNode(g, "_bt"); - let bottom = util.addBorderNode(g, "_bb"); let label = g.node(v); + // Adding custom border node to help with cluster label offset + let top = util.addWhimBorderNode(g, "_bt", label.labelheight); + let bottom = util.addBorderNode(g, "_bb"); g.setParent(top, v); label.borderTop = top; diff --git a/lib/position/index.js b/lib/position/index.js index 2258081c..04694ee9 100644 --- a/lib/position/index.js +++ b/lib/position/index.js @@ -25,8 +25,23 @@ function positionY(g) { return height; } }, 0); - layer.forEach(v => g.node(v).y = prevY + maxHeight / 2); - prevY += maxHeight + rankSep; + let borderTopSeen = false; + let labelheight = 0; + layer.forEach(v => { + let node = g.node(v); + if (node.dummy === "border" && node.whimNode) { + borderTopSeen = true; + labelheight = node.labelheight; + } + node.y = prevY + maxHeight / 2; + }); + + if (borderTopSeen) { + // hard coding padding for cluster labels + prevY += maxHeight + labelheight + 24; + } else { + prevY += maxHeight + rankSep; + } }); } diff --git a/lib/util.js b/lib/util.js index 807da23a..4deea110 100644 --- a/lib/util.js +++ b/lib/util.js @@ -6,6 +6,7 @@ let Graph = require("@dagrejs/graphlib").Graph; module.exports = { addBorderNode, + addWhimBorderNode, addDummyNode, asNonCompoundGraph, buildLayerMatrix, @@ -205,6 +206,20 @@ function addBorderNode(g, prefix, rank, order) { return addDummyNode(g, "border", node, prefix); } +// labelHeight is a custom property to be used to offset cluster children nodes +// by the height of the cluster label +function addWhimBorderNode(g, prefix, labelHeight) { + const node = { + width: 0, + height: 0, + whimNode: true, + labelheight: labelHeight + }; + + return addDummyNode(g, "border", node, prefix); +} + + function maxRank(g) { return Math.max(...g.nodes().map(v => { let rank = g.node(v).rank; diff --git a/package-lock.json b/package-lock.json index cc3eb4f1..fd360945 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { - "name": "@dagrejs/dagre", - "version": "1.0.4", + "name": "@whimsicalcode/dagre", + "version": "1.0.5-pre", "lockfileVersion": 2, "requires": true, "packages": { "": { - "name": "@dagrejs/dagre", - "version": "1.0.4", + "name": "@whimsicalcode/dagre", + "version": "1.0.5-pre", "license": "MIT", "dependencies": { "@dagrejs/graphlib": "2.1.13" diff --git a/package.json b/package.json index 16c89d43..9f5ed8d7 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,14 @@ { - "name": "@dagrejs/dagre", + "name": "@whimsicalcode/dagre", "version": "1.0.5-pre", "description": "Graph layout for JavaScript", "author": "Chris Pettitt ", "contributors": [ "Matthew Dahl (https://github.com/sandersky)" ], + "publishConfig": { + "registry":"https://npm.pkg.github.com" + }, "license": "MIT", "main": "index.js", "scripts": { @@ -48,6 +51,6 @@ }, "repository": { "type": "git", - "url": "https://github.com/dagrejs/dagre.git" + "url": "https://github.com/whimsicalcode/dagre.git" } } \ No newline at end of file diff --git a/test/nesting-graph-test.js b/test/nesting-graph-test.js index e685982d..8e62c76f 100644 --- a/test/nesting-graph-test.js +++ b/test/nesting-graph-test.js @@ -37,7 +37,7 @@ describe("rank/nestingGraph", () => { expect(g.edge(g.outEdges(borderTop, "a")[0]).minlen).equals(1); expect(g.outEdges("a", borderBottom)).to.have.length(1); expect(g.edge(g.outEdges("a", borderBottom)[0]).minlen).equals(1); - expect(g.node(borderTop)).eqls({ width: 0, height: 0, dummy: "border" }); + expect(g.node(borderTop)).eqls({ width: 0, height: 0, whimNode: true, labelheight: undefined, dummy: "border" }); expect(g.node(borderBottom)).eqls({ width: 0, height: 0, dummy: "border" }); });