├── .eslintrc.json ├── .gitignore ├── .jshintrc ├── .npmignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── assets ├── edgeLabelSpace.jpg └── noEdgeLabelSpace.jpg ├── bower.json ├── dist-origin └── dagre.js ├── dist ├── dagre.core.js ├── dagre.core.min.js ├── dagre.js └── dagre.min.js ├── examples ├── add-subgraph │ ├── index.html │ └── index.js ├── assign-layer │ ├── index.html │ └── index.js ├── d3.v7.min.js ├── elkData.json ├── flip │ ├── index.html │ └── index.js ├── keep-data-order │ ├── index.html │ └── index.js ├── manual-order │ ├── index.html │ └── index.js └── test │ ├── index.html │ └── index.js ├── index.d.ts ├── index.js ├── karma.conf.js ├── karma.core.conf.js ├── lib ├── acyclic.js ├── add-border-segments.js ├── coordinate-system.js ├── data │ └── list.js ├── debug.js ├── graphlib.js ├── greedy-fas.js ├── layout.js ├── lodash.js ├── nesting-graph.js ├── normalize.js ├── order │ ├── add-subgraph-constraints.js │ ├── barycenter.js │ ├── build-layer-graph.js │ ├── cross-count.js │ ├── index.js │ ├── init-data-order.js │ ├── init-order.js │ ├── resolve-conflicts.js │ ├── sort-subgraph.js │ └── sort.js ├── parent-dummy-chains.js ├── position │ ├── bk.js │ └── index.js ├── rank │ ├── feasible-tree.js │ ├── index.js │ ├── network-simplex.js │ └── util.js ├── util.js └── version.js ├── package-lock.json ├── package.json ├── src ├── bench.js └── release │ ├── bump-version.js │ ├── check-version.js │ ├── make-bower.json.js │ ├── make-version.js │ └── release.sh └── test ├── acyclic-test.js ├── add-border-segments-test.js ├── bundle-test.js ├── chai.js ├── console.html ├── coordinate-system-test.js ├── data └── list-test.js ├── greedy-fas-test.js ├── layout-test.js ├── nesting-graph-test.js ├── normalize-test.js ├── order ├── add-subgraph-constraints-test.js ├── barycenter-test.js ├── build-layer-graph-test.js ├── cross-count-test.js ├── init-order-test.js ├── order-test.js ├── resolve-conflicts-test.js ├── sort-subgraph-test.js └── sort-test.js ├── parent-dummy-chains-test.js ├── position-test.js ├── position └── bk-test.js ├── rank ├── feasible-tree-layer-test.js ├── feasible-tree-test.js ├── network-simplex-test.js ├── rank-test.js └── util-test.js ├── util-test.js └── version-test.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true, 5 | "mocha": true 6 | }, 7 | "extends": "eslint:recommended", 8 | "rules": { 9 | "indent": [ "error", 2 ], 10 | "linebreak-style": [ "error", "unix" ], 11 | "semi": [ "error", "always" ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | node_modules/ 4 | tmp/ 5 | 6 | .DS_Store -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "camelcase": true, 3 | "eqeqeq": true, 4 | "expr": true, 5 | "freeze": true, 6 | "immed": true, 7 | "newcap": true, 8 | "noarg": true, 9 | "quotmark": "double", 10 | "trailing": true, 11 | "undef": true, 12 | "unused": true, 13 | 14 | "laxbreak": true, 15 | 16 | "node": true, 17 | 18 | "globals": { 19 | "afterEach": false, 20 | "beforeEach": false, 21 | "describe": false, 22 | "it": false 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /bench 2 | /build 3 | /Makefile 4 | /node_modules 5 | /src 6 | /test 7 | /tmp 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "lts/*" 4 | script: KARMA_OPTS="--browsers Firefox,PhantomJS" make -e test 5 | before_script: 6 | - export DISPLAY=:99.0 7 | services: 8 | - xvfb 9 | addons: 10 | firefox: latest 11 | script: KARMA_OPTS="--browsers Firefox" make -e test 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2014 Chris Pettitt 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | MOD = dagre 2 | 3 | NPM = npm 4 | BROWSERIFY = ./node_modules/browserify/bin/cmd.js 5 | JSHINT = ./node_modules/jshint/bin/jshint 6 | ESLINT = ./node_modules/eslint/bin/eslint.js 7 | KARMA = ./node_modules/karma/bin/karma 8 | MOCHA = ./node_modules/mocha/bin/_mocha 9 | UGLIFY = ./node_modules/uglify-js/bin/uglifyjs 10 | 11 | JSHINT_OPTS = --reporter node_modules/jshint-stylish/index.js 12 | MOCHA_OPTS = -R dot 13 | 14 | BUILD_DIR = build 15 | COVERAGE_DIR = $(BUILD_DIR)/cov 16 | DIST_DIR = dist 17 | 18 | SRC_FILES = index.js lib/version.js $(shell find lib -type f -name '*.js') 19 | TEST_FILES = $(shell find test -type f -name '*.js' | grep -v 'bundle-test.js') 20 | BUILD_FILES = $(addprefix $(BUILD_DIR)/, \ 21 | $(MOD).js $(MOD).min.js \ 22 | $(MOD).core.js $(MOD).core.min.js) 23 | 24 | DIRS = $(BUILD_DIR) 25 | 26 | .PHONY: all bench clean browser-test unit-test test dist 27 | 28 | all: unit-test lint 29 | 30 | bench: test 31 | @src/bench.js 32 | 33 | lib/version.js: package.json 34 | @src/release/make-version.js > $@ 35 | 36 | $(DIRS): 37 | @mkdir -p $@ 38 | 39 | test: unit-test browser-test 40 | 41 | unit-test: $(SRC_FILES) $(TEST_FILES) node_modules | $(BUILD_DIR) 42 | @$(MOCHA) --dir $(COVERAGE_DIR) -- $(MOCHA_OPTS) $(TEST_FILES) || $(MOCHA) $(MOCHA_OPTS) $(TEST_FILES) 43 | 44 | browser-test: $(BUILD_DIR)/$(MOD).js $(BUILD_DIR)/$(MOD).core.js 45 | $(KARMA) start --single-run $(KARMA_OPTS) 46 | $(KARMA) start karma.core.conf.js --single-run $(KARMA_OPTS) 47 | 48 | bower.json: package.json src/release/make-bower.json.js 49 | @src/release/make-bower.json.js > $@ 50 | 51 | lint: 52 | @$(JSHINT) $(JSHINT_OPTS) $(filter-out node_modules, $?) 53 | @$(ESLINT) $(SRC_FILES) $(TEST_FILES) 54 | 55 | # $(BUILD_DIR)/$(MOD).js: index.js $(SRC_FILES) | unit-test 56 | $(BUILD_DIR)/$(MOD).js: index.js $(SRC_FILES) 57 | @$(BROWSERIFY) $< > $@ -s dagre 58 | 59 | $(BUILD_DIR)/$(MOD).min.js: $(BUILD_DIR)/$(MOD).js 60 | @$(UGLIFY) $< --comments '@license' > $@ 61 | 62 | # $(BUILD_DIR)/$(MOD).core.js: index.js $(SRC_FILES) | unit-test 63 | $(BUILD_DIR)/$(MOD).core.js: index.js $(SRC_FILES) 64 | @$(BROWSERIFY) $< > $@ --no-bundle-external -s dagre 65 | 66 | $(BUILD_DIR)/$(MOD).core.min.js: $(BUILD_DIR)/$(MOD).core.js 67 | @$(UGLIFY) $< --comments '@license' > $@ 68 | 69 | # dist: $(BUILD_FILES) | bower.json test 70 | dist: $(BUILD_FILES) 71 | @rm -rf $@ 72 | @mkdir -p $@ 73 | @cp $^ dist 74 | 75 | release: dist 76 | @echo 77 | @echo Starting release... 78 | @echo 79 | @src/release/release.sh $(MOD) dist 80 | 81 | clean: 82 | rm -rf $(BUILD_DIR) 83 | 84 | node_modules: package.json 85 | @$(NPM) install 86 | @touch $@ 87 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dagrejs - Layered layout for directed acyclic graph 2 | 3 | **This project is a fork from [dagre](https://github.com/dagrejs/dagre). For more information prelase refer to origin project.** 4 | 5 | ## Enhanced features 6 | 7 | New features: 8 | * support specify layer(rank) for certain node 9 | * support manually control nodes' order 10 | * support keep origin layout when re-layout with nodes added 11 | 12 | Optimizations: 13 | * rewrite rank algorithm to support assign layer 14 | * consider previous iteration result at node-ordering step 15 | * support generate `edgeLabelSpacing` or not, which controls generate dummy node between nodes 16 | 17 | ## Usage 18 | 19 | > For full usage please refer to dagre's documentation: https://github.com/dagrejs/dagre/wiki. 20 | 21 | ### edgeLabelSpace 22 | 23 | Default dagre always generate dummy node for every edge, which can be used for edge's curve drawing, etc. If you do not need it, disable it in layout's options: 24 | 25 | ```js 26 | dagre.layout(g, { 27 | edgeLabelSpace: false 28 | }) 29 | ``` 30 | 31 | Bellow shows graph with or without `edgeLabelSpace`: 32 | 33 | ![](./assets/edgeLabelSpace.jpg) 34 | ![](./assets/noEdgeLabelSpace.jpg) 35 | 36 | ### Specify layer 37 | 38 | Now you can manually specify node's layer(rank) by add layer in node's attribute: 39 | 40 | ```js 41 | const data = { 42 | nodes: [ 43 | { id: '0' }, 44 | { id: '1', layer: 1 }, 45 | { id: '2', layer: 3 }, 46 | { id: '3' }, 47 | ], 48 | // edges: [...] 49 | } 50 | 51 | data.nodes.forEach((n) => { 52 | g.setNode(n.id, n); 53 | }); 54 | ``` 55 | 56 | Caution: 57 | * layer is **0-indexed**, which means the root node's layer is 0 58 | * manual layer **should not** violate DAG's properties (e.g. You cannot assign a layer value for a target node greater or equal to cresponding source node.) 59 | 60 | ### Control nodes' order 61 | 62 | Sometimes we want to manually control nodes' order in every layer in case of unexpected result caused by alogrithm. Now we can also configurate in options. 63 | 64 | ```js 65 | dagre.layout(g, { 66 | keepNodeOrder: true, 67 | nodeOrder: ['3', '2', '1', '0'] // an array of nodes's ID. 68 | }); 69 | ``` 70 | 71 | A common usage is keeping data's order: 72 | ```js 73 | const data = { 74 | nodes: [ 75 | { id: '0' }, 76 | { id: '2' }, 77 | { id: '3' }, 78 | { id: '1' }, 79 | ], 80 | // edges: [...] 81 | } 82 | 83 | dagre.layout(g, { 84 | keepNodeOrder: true, 85 | nodeOrder: data.nodes.map(n => n.id) 86 | }); 87 | ``` 88 | 89 | Caution: 90 | * The order only work at same layer ordering step. It does not affect the layer assignment step. 91 | * Like specifying layer, internally the library added `fixorder` attribute for each node. Of cause you can manually set this attribute, but it introduces ambiguity. 92 | 93 | ### Keep origin layout 94 | 95 | When re-layout with small modification, we may want to keep origin layout result. Now we can pass the origin graph to new layout function: 96 | 97 | ```js 98 | dagre.layout(originGraph) // layout() will internally modify originGraph 99 | dagre.layout( 100 | g, 101 | { 102 | prevGraph: originGraph // pass originGraph to new function 103 | } 104 | ); 105 | ``` 106 | 107 | For full example please refer to `add-subgraph` example in examples folder. -------------------------------------------------------------------------------- /assets/edgeLabelSpace.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brickmaker/dagre/3613385c9462511c3c7e95cc7fdaf9c1472e867a/assets/edgeLabelSpace.jpg -------------------------------------------------------------------------------- /assets/noEdgeLabelSpace.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brickmaker/dagre/3613385c9462511c3c7e95cc7fdaf9c1472e867a/assets/noEdgeLabelSpace.jpg -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dagre", 3 | "version": "0.8.5", 4 | "main": [ 5 | "dist/dagre.core.js" 6 | ], 7 | "ignore": [ 8 | ".*", 9 | "README.md", 10 | "CHANGELOG.md", 11 | "Makefile", 12 | "browser.js", 13 | "dist/dagre.js", 14 | "dist/dagre.min.js", 15 | "index.js", 16 | "karma*", 17 | "lib/**", 18 | "package.json", 19 | "src/**", 20 | "test/**" 21 | ], 22 | "dependencies": { 23 | "graphlib": "^2.1.8", 24 | "lodash": "^4.17.15" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/add-subgraph/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dagre test 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/add-subgraph/index.js: -------------------------------------------------------------------------------- 1 | const simpleGraph1 = { 2 | nodes: [ 3 | { 4 | id: "0", 5 | width: 30, 6 | height: 20, 7 | color: "#a6cee3", 8 | }, 9 | { 10 | id: "1", 11 | width: 30, 12 | height: 20, 13 | color: "#1f78b4", 14 | }, 15 | { 16 | id: "2", 17 | width: 30, 18 | height: 20, 19 | color: "#b2df8a", 20 | }, 21 | { 22 | id: "3", 23 | width: 30, 24 | height: 20, 25 | color: "#33a02c", 26 | }, 27 | { 28 | id: "4", 29 | width: 30, 30 | height: 20, 31 | color: "#fb9a99", 32 | }, 33 | { 34 | id: "5", 35 | width: 30, 36 | height: 20, 37 | color: "#ff7f00", 38 | }, 39 | { 40 | id: "6", 41 | width: 30, 42 | height: 20, 43 | color: "#6a3d9a", 44 | }, 45 | ], 46 | edges: [ 47 | { 48 | source: "0", 49 | target: "1", 50 | }, 51 | { 52 | source: "0", 53 | target: "2", 54 | }, 55 | { 56 | source: "0", 57 | target: "3", 58 | }, 59 | { 60 | source: "1", 61 | target: "4", 62 | }, 63 | { 64 | source: "2", 65 | target: "5", 66 | }, 67 | { 68 | source: "3", 69 | target: "6", 70 | }, 71 | ], 72 | }; 73 | 74 | const simpleGraph2 = { 75 | nodes: [ 76 | { 77 | id: "7", 78 | width: 30, 79 | height: 20, 80 | color: "#e31a1c", 81 | }, 82 | ], 83 | edges: [ 84 | { 85 | source: "0", 86 | target: "7", 87 | }, 88 | { 89 | source: "7", 90 | target: "4", 91 | }, 92 | { 93 | source: "7", 94 | target: "6", 95 | }, 96 | ], 97 | }; 98 | 99 | const issueGraph1 = { 100 | nodes: [ 101 | { 102 | id: "k79zNA0TkCwQPQWw4yn", 103 | label: "ETL数据流", 104 | color: "#a6cee3", 105 | }, 106 | { 107 | id: "GWMF0chbHRKDkENg1hS", 108 | label: "ETL数据流2", 109 | color: "#1f78b4", 110 | }, 111 | { 112 | id: "xCzXirgILRm9fF7gjeb", 113 | label: "报告", 114 | color: "#b2df8a", 115 | }, 116 | { 117 | id: "GxZeEGkky88xKxq1r22", 118 | label: "工厂输出表", 119 | color: "#33a02c", 120 | }, 121 | { 122 | id: "a", 123 | label: "a", 124 | color: "#fb9a99", 125 | }, 126 | { 127 | id: "b", 128 | label: "b", 129 | color: "#ff7f00", 130 | }, 131 | { 132 | id: "c", 133 | label: "c", 134 | color: "#6a3d9a", 135 | }, 136 | { 137 | id: "AKl8iaVQamqiMaMCF7E", 138 | label: "csv数据源", 139 | color: "#2a9d9a", 140 | }, 141 | ], 142 | edges: [ 143 | // { 144 | // source: "9RQmLGueOikkikLvHVO", 145 | // target: "I2Msu7qhDMQPmGLOduP", 146 | // }, 147 | { 148 | source: "k79zNA0TkCwQPQWw4yn", 149 | target: "GxZeEGkky88xKxq1r22", 150 | }, 151 | // { 152 | // source: "I2Msu7qhDMQPmGLOduP", 153 | // target: "k79zNA0TkCwQPQWw4yn", 154 | // }, 155 | // { 156 | // source: "QUCo43VpL9LaPT4QVx0", 157 | // target: "k79zNA0TkCwQPQWw4yn", 158 | // }, 159 | { 160 | source: "GxZeEGkky88xKxq1r22", 161 | target: "xCzXirgILRm9fF7gjeb", 162 | }, 163 | { 164 | source: "xCzXirgILRm9fF7gjeb", 165 | target: "b", 166 | }, 167 | { 168 | source: "xCzXirgILRm9fF7gjeb", 169 | target: "c", 170 | }, 171 | { 172 | source: "AKl8iaVQamqiMaMCF7E", 173 | target: "xCzXirgILRm9fF7gjeb", 174 | }, 175 | { 176 | source: "GxZeEGkky88xKxq1r22", 177 | target: "GWMF0chbHRKDkENg1hS", 178 | }, 179 | { 180 | source: "GWMF0chbHRKDkENg1hS", 181 | target: "a", 182 | }, 183 | ], 184 | }; 185 | 186 | const issueGraph2 = { 187 | nodes: [ 188 | { 189 | id: "vm1234", 190 | label: "新增报告", 191 | }, 192 | ], 193 | edges: [ 194 | { 195 | source: "a", 196 | target: "vm1234", 197 | }, 198 | ], 199 | }; 200 | 201 | // const data1 = simpleGraph1; 202 | // const data2 = simpleGraph2; 203 | const data1 = issueGraph1; 204 | const data2 = issueGraph2; 205 | 206 | const data1Copy = JSON.parse(JSON.stringify(data1)); 207 | const data2Copy = JSON.parse(JSON.stringify(data2)); 208 | 209 | const div = document.createElement("div"); 210 | document.body.appendChild(div); 211 | const svg1 = d3 212 | .select(div) 213 | .append("svg") 214 | .style("margin", 40) 215 | .style("overflow", "visible") 216 | .attr("width", 500) 217 | .attr("height", 300); 218 | 219 | const svg2 = d3 220 | .select(div) 221 | .append("svg") 222 | .style("margin", 40) 223 | .style("overflow", "visible") 224 | .attr("width", 500) 225 | .attr("height", 300); 226 | 227 | const originGraph = createGraph(data1); 228 | originGraph.setGraph({ 229 | rankdir: "LR", 230 | }); 231 | dagre.layout(originGraph, { 232 | edgeLabelSpace: true, 233 | }); 234 | console.log(originGraph); 235 | const originGraphCopy = createGraph(data1Copy); 236 | originGraphCopy.setGraph({ 237 | rankdir: "LR", 238 | }); 239 | dagre.layout(originGraphCopy, { 240 | edgeLabelSpace: true, 241 | }); 242 | 243 | drawGraph(originGraph, svg1); 244 | drawGraph(originGraphCopy, svg2); 245 | 246 | const g1 = createGraph({ 247 | nodes: [...data1.nodes, ...data2.nodes], 248 | edges: [...data1.edges, ...data2.edges], 249 | }); 250 | 251 | g1.setGraph({ 252 | rankdir: "LR", 253 | }); 254 | 255 | dagre.layout(g1, { 256 | edgeLabelSpace: true, 257 | }); 258 | 259 | const g2 = createGraph({ 260 | nodes: [...data1Copy.nodes, ...data2Copy.nodes], 261 | edges: [...data1Copy.edges, ...data2Copy.edges], 262 | }); 263 | g2.setGraph({ 264 | rankdir: "LR", 265 | }); 266 | dagre.layout( 267 | g2, 268 | { 269 | edgeLabelSpace: true, 270 | prevGraph: originGraphCopy 271 | } 272 | ); 273 | 274 | function addSubGraph() { 275 | drawGraph(g1, svg1); 276 | drawGraph(g2, svg2); 277 | } 278 | 279 | d3.select("body") 280 | .append("button") 281 | .text("添加子图") 282 | .on("click", () => { 283 | addSubGraph(); 284 | }); 285 | 286 | function createGraph(data) { 287 | // Create a new directed graph 288 | const g = new dagre.graphlib.Graph(); 289 | 290 | // Set an object for the graph label 291 | g.setGraph({ 292 | // ranker: "longest-path", 293 | ranker: "tight-tree", 294 | // ranker: "network-complex", 295 | }); 296 | 297 | // Default to assigning a new object as a label for each new edge. 298 | g.setDefaultEdgeLabel(function () { 299 | return {}; 300 | }); 301 | 302 | // Add nodes to the graph. The first argument is the node id. The second is 303 | // metadata about the node. In this case we're going to add labels to each of 304 | // our nodes. 305 | data.nodes.forEach((n) => { 306 | g.setNode(n.id, n); 307 | }); 308 | 309 | // Add edges to the graph. 310 | data.edges.forEach((e) => { 311 | g.setEdge(e.source, e.target); 312 | }); 313 | 314 | return g; 315 | } 316 | 317 | function drawGraph(g, svg) { 318 | const nodes = g.nodes().map((n) => g.node(n)); 319 | const edges = g.edges().map((e) => { 320 | const res = g.edge(e); 321 | res.source = g.node(e.v); 322 | res.target = g.node(e.w); 323 | return res; 324 | }); 325 | 326 | const link = svg.selectAll(".edge").data(edges); 327 | 328 | const easeFunc = d3.easeElasticOut.amplitude(1).period(0.9); 329 | 330 | link 331 | .enter() 332 | .append("line") 333 | .transition() 334 | .duration(1000) 335 | .ease(easeFunc) 336 | .attr("class", "edge") 337 | .attr("stroke", "black") 338 | .attr("x1", (d) => d.source.x) 339 | .attr("y1", (d) => d.source.y) 340 | .attr("x2", (d) => d.target.x) 341 | .attr("y2", (d) => d.target.y); 342 | // .append("polyline") 343 | // .attr("class", "edge") 344 | // .attr("fill", "none") 345 | // .attr("stroke", "black") 346 | // .attr("points", (d) => { 347 | // return `${d.source.x}, ${d.source.y} ${d.points 348 | // .map((p) => `${p.x},${p.y}`) 349 | // .join(" ")} ${d.target.x}, ${d.target.y}`; 350 | // }); 351 | 352 | link 353 | .transition() 354 | .duration(1000) 355 | .ease(easeFunc) 356 | .attr("x1", (d) => d.source.x) 357 | .attr("y1", (d) => d.source.y) 358 | .attr("x2", (d) => d.target.x) 359 | .attr("y2", (d) => d.target.y); 360 | // .attr("points", (d) => { 361 | // return `${d.source.x}, ${d.source.y} ${d.points 362 | // .map((p) => `${p.x},${p.y}`) 363 | // .join(" ")} ${d.target.x}, ${d.target.y}`; 364 | // }); 365 | 366 | link.exit().transition().duration(1000).remove(); 367 | 368 | const node = svg.selectAll(".node").data(nodes); 369 | 370 | node 371 | .enter() 372 | .append("rect") 373 | .transition() 374 | .duration(1000) 375 | .ease(easeFunc) 376 | .style("fill", (d) => d.color) 377 | .attr("rx", 5) 378 | .attr("class", "node") 379 | .attr("width", (d) => d.width ?? 20) 380 | .attr("height", (d) => d.height ?? 20) 381 | .attr("x", (d) => d.x - (d.width ?? 20) / 2) 382 | .attr("y", (d) => d.y - (d.height ?? 20) / 2); 383 | 384 | node 385 | .raise() 386 | .transition() 387 | .duration(1000) 388 | .ease(easeFunc) 389 | .attr("x", (d) => d.x - (d.width ?? 20) / 2) 390 | .attr("y", (d) => d.y - (d.height ?? 20) / 2); 391 | 392 | node.exit().remove(); 393 | 394 | // node.append("title").text((d) => d.id); 395 | } 396 | -------------------------------------------------------------------------------- /examples/assign-layer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dagre test 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/assign-layer/index.js: -------------------------------------------------------------------------------- 1 | const data = { 2 | nodes: [ 3 | { 4 | id: "0", 5 | label: "0", 6 | x: 200, 7 | y: 500 8 | }, 9 | { 10 | id: "1", 11 | label: "1" 12 | }, 13 | { 14 | id: "2", 15 | label: "2", 16 | layer: 2, 17 | }, 18 | { 19 | id: "3", 20 | label: "3" 21 | }, 22 | { 23 | id: "4", 24 | label: "4" 25 | }, 26 | { 27 | id: "5", 28 | label: "5" 29 | }, 30 | { 31 | id: "6", 32 | label: "6" 33 | }, 34 | { 35 | id: "7", 36 | label: "7" 37 | }, 38 | { 39 | id: "8", 40 | label: "8" 41 | }, 42 | { 43 | id: "9", 44 | label: "9" 45 | } 46 | ], 47 | edges: [ 48 | { 49 | source: "0", 50 | target: "1" 51 | }, 52 | { 53 | source: "0", 54 | target: "2" 55 | }, 56 | { 57 | source: "1", 58 | target: "4" 59 | }, 60 | { 61 | source: "0", 62 | target: "3" 63 | }, 64 | { 65 | source: "3", 66 | target: "4" 67 | }, 68 | { 69 | source: "4", 70 | target: "5" 71 | }, 72 | { 73 | source: "4", 74 | target: "6" 75 | }, 76 | { 77 | source: "5", 78 | target: "7" 79 | }, 80 | { 81 | source: "5", 82 | target: "8" 83 | }, 84 | { 85 | source: "8", 86 | target: "9" 87 | }, 88 | { 89 | source: "2", 90 | target: "9" 91 | }, 92 | { 93 | source: "3", 94 | target: "9" 95 | } 96 | ] 97 | }; 98 | 99 | data.nodes.forEach((n) => { 100 | n.width = 20; 101 | n.height = 20; 102 | }); 103 | 104 | const g = createGraph(data); 105 | 106 | // Set an object for the graph label 107 | g.setGraph({ 108 | // ranker: "longest-path", 109 | // ranker: "tight-tree", 110 | ranker: "network-complex", 111 | rankdir: 'LR', 112 | align: 'UL' 113 | }); 114 | 115 | dagre.layout(g, { 116 | edgeLabelSpace: true, 117 | }); 118 | 119 | g.nodes().forEach(function (v) { 120 | console.log("Node " + v + ": " + JSON.stringify(g.node(v))); 121 | }); 122 | g.edges().forEach(function (e) { 123 | console.log("Edge " + e.v + " -> " + e.w + ": " + JSON.stringify(g.edge(e))); 124 | }); 125 | 126 | const div = document.createElement("div"); 127 | document.body.appendChild(div); 128 | drawGraph(g, div); 129 | 130 | function createGraph(data) { 131 | // Create a new directed graph 132 | const g = new dagre.graphlib.Graph(); 133 | 134 | // Default to assigning a new object as a label for each new edge. 135 | g.setDefaultEdgeLabel(function () { 136 | return {}; 137 | }); 138 | 139 | // Add nodes to the graph. The first argument is the node id. The second is 140 | // metadata about the node. In this case we're going to add labels to each of 141 | // our nodes. 142 | data.nodes.forEach((n) => { 143 | g.setNode(n.id, n); 144 | }); 145 | 146 | // Add edges to the graph. 147 | data.edges.forEach((e) => { 148 | g.setEdge(e.source, e.target); 149 | }); 150 | 151 | return g; 152 | } 153 | 154 | function drawGraph(g, container) { 155 | const svg = d3 156 | .select(container) 157 | .append("svg") 158 | .attr("width", 1800) 159 | .attr("height", 2400); 160 | const nodes = g.nodes().map((n) => g.node(n)); 161 | const edges = g.edges().map((e) => { 162 | const res = g.edge(e); 163 | res.source = g.node(e.v); 164 | res.target = g.node(e.w); 165 | return res; 166 | }); 167 | 168 | svg 169 | .selectAll(".edge") 170 | .data(edges) 171 | .enter() 172 | .append("polyline") 173 | .attr("class", "edge") 174 | .attr("fill", "none") 175 | .attr("stroke", "black") 176 | .attr("points", (d) => { 177 | return `${d.source.x}, ${d.source.y} ${d.points 178 | .map((p) => `${p.x},${p.y}`) 179 | .join(" ")} ${d.target.x}, ${d.target.y}`; 180 | }); 181 | 182 | const node = svg 183 | .selectAll(".node") 184 | .data(nodes) 185 | .enter() 186 | .append("rect") 187 | .style("fill", "#aaaaaa") 188 | .attr("class", "node") 189 | .attr("x", (d) => d.x - (d.width ?? 20) / 2) 190 | .attr("y", (d) => d.y - (d.height ?? 20) / 2) 191 | .attr("width", (d) => d.width ?? 20) 192 | .attr("height", (d) => d.height ?? 20); 193 | 194 | /* 195 | const label = svg 196 | .selectAll(".label") 197 | .data(nodes) 198 | .enter() 199 | .append("text") 200 | .attr("transform", (d) => `translate(${d.x},${d.y}) rotate(20) `) 201 | // .attr("x", (d) => d.x) 202 | // .attr("y", (d) => d.y) 203 | .text((d) => d.id); 204 | */ 205 | 206 | node.append("title").text((d) => d.id); 207 | } 208 | -------------------------------------------------------------------------------- /examples/flip/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dagre test 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/flip/index.js: -------------------------------------------------------------------------------- 1 | const data = { 2 | nodes: [ 3 | // { 4 | // id: "9RQmLGueOikkikLvHVO", 5 | // label: "Mysql连接账户", 6 | // }, 7 | { 8 | id: "k79zNA0TkCwQPQWw4yn", 9 | label: "ETL数据流", 10 | }, 11 | { 12 | id: "GWMF0chbHRKDkENg1hS", 13 | label: "ETL数据流2", 14 | }, 15 | { 16 | id: "xCzXirgILRm9fF7gjeb", 17 | label: "报告", 18 | }, 19 | // { 20 | // id: "I2Msu7qhDMQPmGLOduP", 21 | // label: "Mysql数据源", 22 | // }, 23 | // { 24 | // id: "QUCo43VpL9LaPT4QVx0", 25 | // label: "Excel数据源", 26 | // }, 27 | { 28 | id: "GxZeEGkky88xKxq1r22", 29 | label: "工厂输出表", 30 | }, 31 | { 32 | id: "a", 33 | label: "a", 34 | }, 35 | { 36 | id: "b", 37 | label: "b", 38 | }, 39 | { 40 | id: "c", 41 | label: "c", 42 | }, 43 | { 44 | id: "AKl8iaVQamqiMaMCF7E", 45 | label: "csv数据源", 46 | }, 47 | ], 48 | edges: [ 49 | // { 50 | // source: "9RQmLGueOikkikLvHVO", 51 | // target: "I2Msu7qhDMQPmGLOduP", 52 | // }, 53 | { 54 | source: "k79zNA0TkCwQPQWw4yn", 55 | target: "GxZeEGkky88xKxq1r22", 56 | }, 57 | // { 58 | // source: "I2Msu7qhDMQPmGLOduP", 59 | // target: "k79zNA0TkCwQPQWw4yn", 60 | // }, 61 | // { 62 | // source: "QUCo43VpL9LaPT4QVx0", 63 | // target: "k79zNA0TkCwQPQWw4yn", 64 | // }, 65 | { 66 | source: "GxZeEGkky88xKxq1r22", 67 | target: "xCzXirgILRm9fF7gjeb", 68 | }, 69 | { 70 | source: "xCzXirgILRm9fF7gjeb", 71 | target: "b", 72 | }, 73 | { 74 | source: "xCzXirgILRm9fF7gjeb", 75 | target: "c", 76 | }, 77 | { 78 | source: "AKl8iaVQamqiMaMCF7E", 79 | target: "xCzXirgILRm9fF7gjeb", 80 | }, 81 | { 82 | source: "GxZeEGkky88xKxq1r22", 83 | target: "GWMF0chbHRKDkENg1hS", 84 | }, 85 | { 86 | source: "GWMF0chbHRKDkENg1hS", 87 | target: "a", 88 | }, 89 | ], 90 | }; 91 | 92 | const addGraph = { 93 | nodes: [ 94 | { 95 | id: "vm1234", 96 | label: "新增报告", 97 | }, 98 | ], 99 | edges: [ 100 | { 101 | source: "a", 102 | target: "vm1234", 103 | }, 104 | ], 105 | }; 106 | 107 | const data1 = { 108 | nodes: [...data.nodes, ...addGraph.nodes], 109 | edges: [...data.edges, ...addGraph.edges], 110 | }; 111 | 112 | data.nodes.forEach((n) => { 113 | n.width = 20; 114 | n.height = 20; 115 | }); 116 | 117 | const g = createGraph(data); 118 | // const g = createGraph(data1); 119 | 120 | // Set an object for the graph label 121 | g.setGraph({ 122 | // ranker: "longest-path", 123 | ranker: "tight-tree", 124 | // ranker: "network-complex", 125 | rankdir: "LR", 126 | }); 127 | 128 | dagre.layout(g, { 129 | // edgeLabelSpace: false, 130 | }); 131 | 132 | g.nodes().forEach(function (v) { 133 | console.log("Node " + v + ": " + JSON.stringify(g.node(v))); 134 | }); 135 | g.edges().forEach(function (e) { 136 | console.log("Edge " + e.v + " -> " + e.w + ": " + JSON.stringify(g.edge(e))); 137 | }); 138 | 139 | const div = document.createElement("div"); 140 | document.body.appendChild(div); 141 | drawGraph(g, div); 142 | 143 | function createGraph(data) { 144 | // Create a new directed graph 145 | const g = new dagre.graphlib.Graph(); 146 | 147 | // Default to assigning a new object as a label for each new edge. 148 | g.setDefaultEdgeLabel(function () { 149 | return {}; 150 | }); 151 | 152 | // Add nodes to the graph. The first argument is the node id. The second is 153 | // metadata about the node. In this case we're going to add labels to each of 154 | // our nodes. 155 | data.nodes.forEach((n) => { 156 | g.setNode(n.id, n); 157 | }); 158 | 159 | // Add edges to the graph. 160 | data.edges.forEach((e) => { 161 | g.setEdge(e.source, e.target); 162 | }); 163 | 164 | return g; 165 | } 166 | 167 | function drawGraph(g, container) { 168 | const svg = d3 169 | .select(container) 170 | .append("svg") 171 | .attr("width", 1800) 172 | .attr("height", 2400); 173 | const nodes = g.nodes().map((n) => g.node(n)); 174 | const edges = g.edges().map((e) => { 175 | const res = g.edge(e); 176 | res.source = g.node(e.v); 177 | res.target = g.node(e.w); 178 | return res; 179 | }); 180 | 181 | svg 182 | .selectAll(".edge") 183 | .data(edges) 184 | .enter() 185 | .append("polyline") 186 | .attr("class", "edge") 187 | .attr("fill", "none") 188 | .attr("stroke", "black") 189 | .attr("points", (d) => { 190 | return `${d.source.x}, ${d.source.y} ${d.points 191 | .map((p) => `${p.x},${p.y}`) 192 | .join(" ")} ${d.target.x}, ${d.target.y}`; 193 | }); 194 | 195 | const node = svg 196 | .selectAll(".node") 197 | .data(nodes) 198 | .enter() 199 | .append("rect") 200 | .style("fill", "#aaaaaa") 201 | .attr("class", "node") 202 | .attr("x", (d) => d.x - (d.width ?? 20) / 2) 203 | .attr("y", (d) => d.y - (d.height ?? 20) / 2) 204 | .attr("width", (d) => d.width ?? 20) 205 | .attr("height", (d) => d.height ?? 20); 206 | 207 | /* 208 | const label = svg 209 | .selectAll(".label") 210 | .data(nodes) 211 | .enter() 212 | .append("text") 213 | .attr("transform", (d) => `translate(${d.x},${d.y}) rotate(20) `) 214 | // .attr("x", (d) => d.x) 215 | // .attr("y", (d) => d.y) 216 | .text((d) => d.label); 217 | */ 218 | 219 | node.append("title").text((d) => d.id); 220 | } 221 | 222 | function removeBranch(g, node) { 223 | nodeMap = {}; 224 | edgeMap = {}; 225 | for (const node of g.nodes) { 226 | nodeMap[node.id] = node; 227 | } 228 | for (const edge of g.edges) { 229 | if (!edgeMap[edge.source]) { 230 | edgeMap[edge.source] = []; 231 | } 232 | edgeMap[edge.source].push(edge.target); 233 | } 234 | 235 | let removeSet = new Set([node]); 236 | let q = new Set([node]); 237 | while (q.size) { 238 | let qq = new Set(); 239 | for (const s of q) { 240 | if (!edgeMap[s]) continue; 241 | for (const t of edgeMap[s]) { 242 | removeSet.add(t); 243 | qq.add(t); 244 | } 245 | } 246 | q = qq; 247 | } 248 | 249 | const newG = {}; 250 | newG.nodes = g.nodes.filter((n) => !removeSet.has(n.id)); 251 | // newG.nodes = g.nodes; 252 | newG.edges = g.edges.filter((e) => !removeSet.has(e.target)); 253 | return newG; 254 | } 255 | -------------------------------------------------------------------------------- /examples/keep-data-order/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dagre test 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/keep-data-order/index.js: -------------------------------------------------------------------------------- 1 | const data = { 2 | nodes: [ 3 | { 4 | id: "0", 5 | label: "0", 6 | }, 7 | { 8 | id: "1", 9 | label: "1" 10 | }, 11 | { 12 | id: "2", 13 | label: "2", 14 | }, 15 | { 16 | id: "4", 17 | label: "4" 18 | }, 19 | { 20 | id: "3", 21 | label: "3" 22 | }, 23 | { 24 | id: "5", 25 | label: "5" 26 | }, 27 | { 28 | id: "6", 29 | label: "6" 30 | }, 31 | ], 32 | edges: [ 33 | { 34 | source: "0", 35 | target: "1" 36 | }, 37 | { 38 | source: "0", 39 | target: "2" 40 | }, 41 | { 42 | source: "1", 43 | target: "3" 44 | }, 45 | { 46 | source: "2", 47 | target: "4" 48 | }, 49 | { 50 | source: "3", 51 | target: "5" 52 | }, 53 | { 54 | source: "4", 55 | target: "6" 56 | }, 57 | ] 58 | }; 59 | 60 | data.nodes.forEach((n) => { 61 | n.width = 20; 62 | n.height = 20; 63 | }); 64 | 65 | const g = createGraph(data); 66 | 67 | // Set an object for the graph label 68 | g.setGraph({ 69 | // ranker: "longest-path", 70 | // ranker: "tight-tree", 71 | // ranker: "network-complex", 72 | rankdir: 'LR', 73 | }); 74 | 75 | dagre.layout(g, { 76 | edgeLabelSpace: false, 77 | keepNodeOrder: true, 78 | nodeOrder: data.nodes.map(n => n.id) 79 | }); 80 | 81 | g.nodes().forEach(function (v) { 82 | console.log("Node " + v + ": " + JSON.stringify(g.node(v))); 83 | }); 84 | g.edges().forEach(function (e) { 85 | console.log("Edge " + e.v + " -> " + e.w + ": " + JSON.stringify(g.edge(e))); 86 | }); 87 | 88 | const div = document.createElement("div"); 89 | document.body.appendChild(div); 90 | drawGraph(g, div); 91 | 92 | function createGraph(data) { 93 | // Create a new directed graph 94 | const g = new dagre.graphlib.Graph(); 95 | 96 | // Default to assigning a new object as a label for each new edge. 97 | g.setDefaultEdgeLabel(function () { 98 | return {}; 99 | }); 100 | 101 | // Add nodes to the graph. The first argument is the node id. The second is 102 | // metadata about the node. In this case we're going to add labels to each of 103 | // our nodes. 104 | data.nodes.forEach((n) => { 105 | g.setNode(n.id, n); 106 | }); 107 | 108 | // Add edges to the graph. 109 | data.edges.forEach((e) => { 110 | g.setEdge(e.source, e.target); 111 | }); 112 | 113 | return g; 114 | } 115 | 116 | function drawGraph(g, container) { 117 | const svg = d3 118 | .select(container) 119 | .append("svg") 120 | .attr("width", 1800) 121 | .attr("height", 2400); 122 | const nodes = g.nodes().map((n) => g.node(n)); 123 | const edges = g.edges().map((e) => { 124 | const res = g.edge(e); 125 | res.source = g.node(e.v); 126 | res.target = g.node(e.w); 127 | return res; 128 | }); 129 | 130 | svg 131 | .selectAll(".edge") 132 | .data(edges) 133 | .enter() 134 | .append("polyline") 135 | .attr("class", "edge") 136 | .attr("fill", "none") 137 | .attr("stroke", "black") 138 | .attr("points", (d) => { 139 | return `${d.source.x}, ${d.source.y} ${d.points 140 | .map((p) => `${p.x},${p.y}`) 141 | .join(" ")} ${d.target.x}, ${d.target.y}`; 142 | }); 143 | 144 | const node = svg 145 | .selectAll(".node") 146 | .data(nodes) 147 | .enter() 148 | .append("rect") 149 | .style("fill", "#aaaaaa") 150 | .attr("class", "node") 151 | .attr("x", (d) => d.x - (d.width ?? 20) / 2) 152 | .attr("y", (d) => d.y - (d.height ?? 20) / 2) 153 | .attr("width", (d) => d.width ?? 20) 154 | .attr("height", (d) => d.height ?? 20); 155 | 156 | /* 157 | const label = svg 158 | .selectAll(".label") 159 | .data(nodes) 160 | .enter() 161 | .append("text") 162 | .attr("transform", (d) => `translate(${d.x},${d.y}) rotate(20) `) 163 | // .attr("x", (d) => d.x) 164 | // .attr("y", (d) => d.y) 165 | .text((d) => d.id); 166 | */ 167 | 168 | node.append("title").text((d) => d.id); 169 | } 170 | -------------------------------------------------------------------------------- /examples/manual-order/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dagre test 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dagre test 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/test/index.js: -------------------------------------------------------------------------------- 1 | const Graph = dagre.graphlib.Graph; 2 | var g = new Graph(); 3 | g.setDefaultEdgeLabel(function () { 4 | return {}; 5 | }); 6 | g.setGraph({ 7 | ranker: 'tight-tree', 8 | }); 9 | g.setNode("a", { id: "a", width: 20, height: 20 }) 10 | .setNode("b", { id: "b", width: 20, height: 20, layer: 2 }) 11 | .setNode("c", { id: "c", width: 20, height: 20 }) 12 | // .setNode("d", { width: 20, height: 20 }) 13 | .setEdge("a", "b", { minlen: 1 }) 14 | .setEdge("a", "c", { minlen: 1 }); 15 | // .setEdge("b", "d"); 16 | 17 | // feasibleTree(g); 18 | dagre.layout(g); 19 | 20 | g.nodes().forEach(function (v) { 21 | console.log("Node " + v + ": " + JSON.stringify(g.node(v))); 22 | }); 23 | g.edges().forEach(function (e) { 24 | console.log("Edge " + e.v + " -> " + e.w + ": " + JSON.stringify(g.edge(e))); 25 | }); 26 | 27 | const div = document.createElement("div"); 28 | document.body.appendChild(div); 29 | drawGraph(g, div); 30 | 31 | function createGraph(data) { 32 | // Create a new directed graph 33 | const g = new dagre.graphlib.Graph(); 34 | 35 | // Default to assigning a new object as a label for each new edge. 36 | g.setDefaultEdgeLabel(function () { 37 | return {}; 38 | }); 39 | 40 | // Add nodes to the graph. The first argument is the node id. The second is 41 | // metadata about the node. In this case we're going to add labels to each of 42 | // our nodes. 43 | data.nodes.forEach((n) => { 44 | g.setNode(n.id, n); 45 | }); 46 | 47 | // Add edges to the graph. 48 | data.edges.forEach((e) => { 49 | g.setEdge(e.source, e.target); 50 | }); 51 | 52 | return g; 53 | } 54 | 55 | function drawGraph(g, container) { 56 | const svg = d3 57 | .select(container) 58 | .append("svg") 59 | .attr("width", 1800) 60 | .attr("height", 2400); 61 | const nodes = g.nodes().map((n) => g.node(n)); 62 | const edges = g.edges().map((e) => { 63 | const res = g.edge(e); 64 | res.source = g.node(e.v); 65 | res.target = g.node(e.w); 66 | return res; 67 | }); 68 | 69 | svg 70 | .selectAll(".edge") 71 | .data(edges) 72 | .enter() 73 | .append("polyline") 74 | .attr("class", "edge") 75 | .attr("fill", "none") 76 | .attr("stroke", "black") 77 | .attr("points", (d) => { 78 | return `${d.source.x}, ${d.source.y} ${d.points 79 | .map((p) => `${p.x},${p.y}`) 80 | .join(" ")} ${d.target.x}, ${d.target.y}`; 81 | }); 82 | 83 | const node = svg 84 | .selectAll(".node") 85 | .data(nodes) 86 | .enter() 87 | .append("rect") 88 | .style("fill", "#aaaaaa") 89 | .attr("class", "node") 90 | .attr("x", (d) => d.x - (d.width ?? 20) / 2) 91 | .attr("y", (d) => d.y - (d.height ?? 20) / 2) 92 | .attr("width", (d) => d.width ?? 20) 93 | .attr("height", (d) => d.height ?? 20); 94 | 95 | /* 96 | const label = svg 97 | .selectAll(".label") 98 | .data(nodes) 99 | .enter() 100 | .append("text") 101 | .attr("transform", (d) => `translate(${d.x},${d.y}) rotate(20) `) 102 | // .attr("x", (d) => d.x) 103 | // .attr("y", (d) => d.y) 104 | .text((d) => d.id); 105 | */ 106 | 107 | node.append("title").text((d) => d.id); 108 | } 109 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | // refer to: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/dagre/index.d.ts 2 | // with small addition 3 | 4 | // Type definitions for dagre 0.7 5 | // Project: https://github.com/dagrejs/dagre 6 | // Definitions by: Qinfeng Chen 7 | // Lisa Vallfors 8 | // Pete Vilter 9 | // David Newell 10 | // Graham Lea 11 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped 12 | // TypeScript Version: 2.2 13 | 14 | 15 | export as namespace dagre; 16 | 17 | export namespace graphlib { 18 | class Graph { 19 | constructor(opt?: { directed?: boolean | undefined; multigraph?: boolean | undefined; compound?: boolean | undefined }); 20 | 21 | graph(): GraphLabel; 22 | isDirected(): boolean; 23 | isMultiGraph(): boolean; 24 | setGraph(label: GraphLabel): Graph; 25 | 26 | edge(edgeObj: Edge): GraphEdge; 27 | edge(outNodeName: string, inNodeName: string, name?: string): GraphEdge; 28 | edgeCount(): number; 29 | edges(): Edge[]; 30 | hasEdge(edgeObj: Edge): boolean; 31 | hasEdge(outNodeName: string, inNodeName: string, name?: string): boolean; 32 | inEdges(inNodeName: string, outNodeName?: string): Edge[] | undefined; 33 | outEdges(outNodeName: string, inNodeName?: string): Edge[] | undefined; 34 | removeEdge(outNodeName: string, inNodeName: string): Graph; 35 | setDefaultEdgeLabel(callback: string | ((v: string, w: string, name?: string) => string | Label)): Graph; 36 | setEdge(params: Edge, value?: string | { [key: string]: any }): Graph; 37 | setEdge(sourceId: string, targetId: string, value?: string | Label, name?: string): Graph; 38 | 39 | children(parentName: string): string | undefined; 40 | hasNode(name: string): boolean; 41 | neighbors(name: string): Array> | undefined; 42 | node(id: string | Label): Node; 43 | nodeCount(): number; 44 | nodes(): string[]; 45 | parent(childName: string): string | undefined; 46 | predecessors(name: string): Array> | undefined; 47 | removeNode(name: string): Graph; 48 | filterNodes(callback: (nodeId: string) => boolean): Graph; 49 | setDefaultNodeLabel(callback: string | ((nodeId: string) => string | Label)): Graph; 50 | setNode(name: string, label: string | Label): Graph; 51 | setParent(childName: string, parentName: string): void; 52 | sinks(): Array>; 53 | sources(): Array>; 54 | successors(name: string): Array> | undefined; 55 | } 56 | 57 | namespace json { 58 | function read(graph: any): Graph; 59 | function write(graph: Graph): any; 60 | } 61 | 62 | namespace alg { 63 | function components(graph: Graph): string[][]; 64 | function dijkstra(graph: Graph, source: string, weightFn?: WeightFn, edgeFn?: EdgeFn): any; 65 | function dijkstraAll(graph: Graph, weightFn?: WeightFn, edgeFn?: EdgeFn): any; 66 | function findCycles(graph: Graph): string[][]; 67 | function floydWarchall(graph: Graph, weightFn?: WeightFn, edgeFn?: EdgeFn): any; 68 | function isAcyclic(graph: Graph): boolean; 69 | function postorder(graph: Graph, nodeNames: string | string[]): string[]; 70 | function preorder(graph: Graph, nodeNames: string | string[]): string[]; 71 | function prim(graph: Graph, weightFn?: WeightFn): Graph; 72 | function tarjam(graph: Graph): string[][]; 73 | function topsort(graph: Graph): string[]; 74 | } 75 | } 76 | 77 | export interface Label { 78 | [key: string]: any; 79 | } 80 | export type WeightFn = (edge: Edge) => number; 81 | export type EdgeFn = (outNodeName: string) => GraphEdge[]; 82 | 83 | export interface GraphLabel { 84 | width?: number | undefined; 85 | height?: number | undefined; 86 | compound?: boolean | undefined; 87 | rankdir?: string | undefined; 88 | align?: string | undefined; 89 | nodesep?: number | undefined; 90 | edgesep?: number | undefined; 91 | ranksep?: number | undefined; 92 | marginx?: number | undefined; 93 | marginy?: number | undefined; 94 | acyclicer?: string | undefined; 95 | ranker?: string | undefined; 96 | } 97 | 98 | export interface NodeConfig { 99 | width?: number | undefined; 100 | height?: number | undefined; 101 | } 102 | 103 | export interface EdgeConfig { 104 | minlen?: number | undefined; 105 | weight?: number | undefined; 106 | width?: number | undefined; 107 | height?: number | undefined; 108 | lablepos?: 'l' | 'c' | 'r' | undefined; 109 | labeloffest?: number | undefined; 110 | } 111 | 112 | export interface CustomConfig { 113 | edgeLabelSpace?: boolean | undefined; 114 | keepNodeOrder?: boolean | undefined; 115 | nodeOrder?: string[] | undefined; 116 | prevGraph?: graphlib.Graph | undefined; 117 | } 118 | 119 | export function layout(graph: graphlib.Graph, layout?: GraphLabel & NodeConfig & EdgeConfig & CustomConfig): void; 120 | 121 | export interface Edge { 122 | v: string; 123 | w: string; 124 | name?: string | undefined; 125 | } 126 | 127 | export interface GraphEdge { 128 | points: Array<{ x: number; y: number }>; 129 | [key: string]: any; 130 | } 131 | 132 | export type Node = T & { 133 | x: number; 134 | y: number; 135 | width: number; 136 | height: number; 137 | class?: string | undefined; 138 | label?: string | undefined; 139 | padding?: number | undefined; 140 | paddingX?: number | undefined; 141 | paddingY?: number | undefined; 142 | rx?: number | undefined; 143 | ry?: number | undefined; 144 | shape?: string | undefined; 145 | }; -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2012-2014 Chris Pettitt 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | module.exports = { 24 | graphlib: require("./lib/graphlib"), 25 | 26 | layout: require("./lib/layout"), 27 | debug: require("./lib/debug"), 28 | util: { 29 | time: require("./lib/util").time, 30 | notime: require("./lib/util").notime 31 | }, 32 | version: require("./lib/version") 33 | }; 34 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Sat Oct 18 2014 17:38:05 GMT-0700 (PDT) 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | 7 | // base path that will be used to resolve all patterns (eg. files, exclude) 8 | basePath: '', 9 | 10 | 11 | // frameworks to use 12 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 13 | frameworks: ['mocha'], 14 | 15 | 16 | // list of files / patterns to load in the browser 17 | files: [ 18 | 'build/dagre.js', 19 | 20 | 'node_modules/chai/chai.js', 21 | 'test/bundle-test.js' 22 | ], 23 | 24 | 25 | // list of files to exclude 26 | exclude: [ 27 | ], 28 | 29 | 30 | // preprocess matching files before serving them to the browser 31 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 32 | preprocessors: { 33 | }, 34 | 35 | 36 | // test results reporter to use 37 | // possible values: 'dots', 'progress' 38 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 39 | reporters: ['progress'], 40 | 41 | 42 | // web server port 43 | port: 9876, 44 | 45 | 46 | // enable / disable colors in the output (reporters and logs) 47 | colors: true, 48 | 49 | 50 | // level of logging 51 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 52 | logLevel: config.LOG_INFO, 53 | 54 | 55 | // enable / disable watching file and executing tests whenever any file changes 56 | autoWatch: true, 57 | 58 | 59 | // start these browsers 60 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 61 | browsers: ['Chrome', 'Firefox', 'PhantomJS'], 62 | 63 | 64 | // Continuous Integration mode 65 | // if true, Karma captures browsers, runs the tests and exits 66 | singleRun: false 67 | }); 68 | }; 69 | -------------------------------------------------------------------------------- /karma.core.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Sat Oct 18 2014 17:38:05 GMT-0700 (PDT) 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | 7 | // base path that will be used to resolve all patterns (eg. files, exclude) 8 | basePath: '', 9 | 10 | 11 | // frameworks to use 12 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 13 | frameworks: ['mocha'], 14 | 15 | 16 | // list of files / patterns to load in the browser 17 | files: [ 18 | 'node_modules/lodash/lodash.js', 19 | 'node_modules/graphlib/dist/graphlib.core.js', 20 | 'build/dagre.core.js', 21 | 22 | 'node_modules/chai/chai.js', 23 | 'test/bundle-test.js' 24 | ], 25 | 26 | 27 | // list of files to exclude 28 | exclude: [ 29 | ], 30 | 31 | 32 | // preprocess matching files before serving them to the browser 33 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 34 | preprocessors: { 35 | }, 36 | 37 | 38 | // test results reporter to use 39 | // possible values: 'dots', 'progress' 40 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 41 | reporters: ['progress'], 42 | 43 | 44 | // web server port 45 | port: 9876, 46 | 47 | 48 | // enable / disable colors in the output (reporters and logs) 49 | colors: true, 50 | 51 | 52 | // level of logging 53 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 54 | logLevel: config.LOG_INFO, 55 | 56 | 57 | // enable / disable watching file and executing tests whenever any file changes 58 | autoWatch: true, 59 | 60 | 61 | // start these browsers 62 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 63 | browsers: ['Chrome', 'Firefox', 'PhantomJS'], 64 | 65 | 66 | // Continuous Integration mode 67 | // if true, Karma captures browsers, runs the tests and exits 68 | singleRun: false 69 | }); 70 | }; 71 | -------------------------------------------------------------------------------- /lib/acyclic.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _ = require("./lodash"); 4 | var greedyFAS = require("./greedy-fas"); 5 | 6 | module.exports = { 7 | run: run, 8 | undo: undo 9 | }; 10 | 11 | function run(g) { 12 | var fas = (g.graph().acyclicer === "greedy" 13 | ? greedyFAS(g, weightFn(g)) 14 | : dfsFAS(g)); 15 | _.forEach(fas, function(e) { 16 | var label = g.edge(e); 17 | g.removeEdge(e); 18 | label.forwardName = e.name; 19 | label.reversed = true; 20 | g.setEdge(e.w, e.v, label, _.uniqueId("rev")); 21 | }); 22 | 23 | function weightFn(g) { 24 | return function(e) { 25 | return g.edge(e).weight; 26 | }; 27 | } 28 | } 29 | 30 | function dfsFAS(g) { 31 | var fas = []; 32 | var stack = {}; 33 | var visited = {}; 34 | 35 | function dfs(v) { 36 | if (_.has(visited, v)) { 37 | return; 38 | } 39 | visited[v] = true; 40 | stack[v] = true; 41 | _.forEach(g.outEdges(v), function(e) { 42 | if (_.has(stack, e.w)) { 43 | fas.push(e); 44 | } else { 45 | dfs(e.w); 46 | } 47 | }); 48 | delete stack[v]; 49 | } 50 | 51 | _.forEach(g.nodes(), dfs); 52 | return fas; 53 | } 54 | 55 | function undo(g) { 56 | _.forEach(g.edges(), function(e) { 57 | var label = g.edge(e); 58 | if (label.reversed) { 59 | g.removeEdge(e); 60 | 61 | var forwardName = label.forwardName; 62 | delete label.reversed; 63 | delete label.forwardName; 64 | g.setEdge(e.w, e.v, label, forwardName); 65 | } 66 | }); 67 | } 68 | -------------------------------------------------------------------------------- /lib/add-border-segments.js: -------------------------------------------------------------------------------- 1 | var _ = require("./lodash"); 2 | var util = require("./util"); 3 | 4 | module.exports = addBorderSegments; 5 | 6 | function addBorderSegments(g) { 7 | function dfs(v) { 8 | var children = g.children(v); 9 | var node = g.node(v); 10 | if (children.length) { 11 | _.forEach(children, dfs); 12 | } 13 | 14 | if (_.has(node, "minRank")) { 15 | node.borderLeft = []; 16 | node.borderRight = []; 17 | for (var rank = node.minRank, maxRank = node.maxRank + 1; 18 | rank < maxRank; 19 | ++rank) { 20 | addBorderNode(g, "borderLeft", "_bl", v, node, rank); 21 | addBorderNode(g, "borderRight", "_br", v, node, rank); 22 | } 23 | } 24 | } 25 | 26 | _.forEach(g.children(), dfs); 27 | } 28 | 29 | function addBorderNode(g, prop, prefix, sg, sgNode, rank) { 30 | var label = { width: 0, height: 0, rank: rank, borderType: prop }; 31 | var prev = sgNode[prop][rank - 1]; 32 | var curr = util.addDummyNode(g, "border", label, prefix); 33 | sgNode[prop][rank] = curr; 34 | g.setParent(curr, sg); 35 | if (prev) { 36 | g.setEdge(prev, curr, { weight: 1 }); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/coordinate-system.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _ = require("./lodash"); 4 | 5 | module.exports = { 6 | adjust: adjust, 7 | undo: undo 8 | }; 9 | 10 | function adjust(g) { 11 | var rankDir = g.graph().rankdir.toLowerCase(); 12 | if (rankDir === "lr" || rankDir === "rl") { 13 | swapWidthHeight(g); 14 | } 15 | } 16 | 17 | function undo(g) { 18 | var rankDir = g.graph().rankdir.toLowerCase(); 19 | if (rankDir === "bt" || rankDir === "rl") { 20 | reverseY(g); 21 | } 22 | 23 | if (rankDir === "lr" || rankDir === "rl") { 24 | swapXY(g); 25 | swapWidthHeight(g); 26 | } 27 | } 28 | 29 | function swapWidthHeight(g) { 30 | _.forEach(g.nodes(), function(v) { swapWidthHeightOne(g.node(v)); }); 31 | _.forEach(g.edges(), function(e) { swapWidthHeightOne(g.edge(e)); }); 32 | } 33 | 34 | function swapWidthHeightOne(attrs) { 35 | var w = attrs.width; 36 | attrs.width = attrs.height; 37 | attrs.height = w; 38 | } 39 | 40 | function reverseY(g) { 41 | _.forEach(g.nodes(), function(v) { reverseYOne(g.node(v)); }); 42 | 43 | _.forEach(g.edges(), function(e) { 44 | var edge = g.edge(e); 45 | _.forEach(edge.points, reverseYOne); 46 | if (_.has(edge, "y")) { 47 | reverseYOne(edge); 48 | } 49 | }); 50 | } 51 | 52 | function reverseYOne(attrs) { 53 | attrs.y = -attrs.y; 54 | } 55 | 56 | function swapXY(g) { 57 | _.forEach(g.nodes(), function(v) { swapXYOne(g.node(v)); }); 58 | 59 | _.forEach(g.edges(), function(e) { 60 | var edge = g.edge(e); 61 | _.forEach(edge.points, swapXYOne); 62 | if (_.has(edge, "x")) { 63 | swapXYOne(edge); 64 | } 65 | }); 66 | } 67 | 68 | function swapXYOne(attrs) { 69 | var x = attrs.x; 70 | attrs.x = attrs.y; 71 | attrs.y = x; 72 | } 73 | -------------------------------------------------------------------------------- /lib/data/list.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Simple doubly linked list implementation derived from Cormen, et al., 3 | * "Introduction to Algorithms". 4 | */ 5 | 6 | module.exports = List; 7 | 8 | function List() { 9 | var sentinel = {}; 10 | sentinel._next = sentinel._prev = sentinel; 11 | this._sentinel = sentinel; 12 | } 13 | 14 | List.prototype.dequeue = function() { 15 | var sentinel = this._sentinel; 16 | var entry = sentinel._prev; 17 | if (entry !== sentinel) { 18 | unlink(entry); 19 | return entry; 20 | } 21 | }; 22 | 23 | List.prototype.enqueue = function(entry) { 24 | var sentinel = this._sentinel; 25 | if (entry._prev && entry._next) { 26 | unlink(entry); 27 | } 28 | entry._next = sentinel._next; 29 | sentinel._next._prev = entry; 30 | sentinel._next = entry; 31 | entry._prev = sentinel; 32 | }; 33 | 34 | List.prototype.toString = function() { 35 | var strs = []; 36 | var sentinel = this._sentinel; 37 | var curr = sentinel._prev; 38 | while (curr !== sentinel) { 39 | strs.push(JSON.stringify(curr, filterOutLinks)); 40 | curr = curr._prev; 41 | } 42 | return "[" + strs.join(", ") + "]"; 43 | }; 44 | 45 | function unlink(entry) { 46 | entry._prev._next = entry._next; 47 | entry._next._prev = entry._prev; 48 | delete entry._next; 49 | delete entry._prev; 50 | } 51 | 52 | function filterOutLinks(k, v) { 53 | if (k !== "_next" && k !== "_prev") { 54 | return v; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/debug.js: -------------------------------------------------------------------------------- 1 | var _ = require("./lodash"); 2 | var util = require("./util"); 3 | var Graph = require("./graphlib").Graph; 4 | 5 | module.exports = { 6 | debugOrdering: debugOrdering 7 | }; 8 | 9 | /* istanbul ignore next */ 10 | function debugOrdering(g) { 11 | var layerMatrix = util.buildLayerMatrix(g); 12 | 13 | var h = new Graph({ compound: true, multigraph: true }).setGraph({}); 14 | 15 | _.forEach(g.nodes(), function(v) { 16 | h.setNode(v, { label: v }); 17 | h.setParent(v, "layer" + g.node(v).rank); 18 | }); 19 | 20 | _.forEach(g.edges(), function(e) { 21 | h.setEdge(e.v, e.w, {}, e.name); 22 | }); 23 | 24 | _.forEach(layerMatrix, function(layer, i) { 25 | var layerV = "layer" + i; 26 | h.setNode(layerV, { rank: "same" }); 27 | _.reduce(layer, function(u, v) { 28 | h.setEdge(u, v, { style: "invis" }); 29 | return v; 30 | }); 31 | }); 32 | 33 | return h; 34 | } 35 | -------------------------------------------------------------------------------- /lib/graphlib.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-redeclare 2 | /* global window */ 3 | 4 | var graphlib; 5 | 6 | if (typeof require === "function") { 7 | try { 8 | graphlib = require("graphlib"); 9 | } catch (e) { 10 | // continue regardless of error 11 | } 12 | } 13 | 14 | if (!graphlib) { 15 | graphlib = window.graphlib; 16 | } 17 | 18 | module.exports = graphlib; 19 | -------------------------------------------------------------------------------- /lib/greedy-fas.js: -------------------------------------------------------------------------------- 1 | var _ = require("./lodash"); 2 | var Graph = require("./graphlib").Graph; 3 | var List = require("./data/list"); 4 | 5 | /* 6 | * A greedy heuristic for finding a feedback arc set for a graph. A feedback 7 | * arc set is a set of edges that can be removed to make a graph acyclic. 8 | * The algorithm comes from: P. Eades, X. Lin, and W. F. Smyth, "A fast and 9 | * effective heuristic for the feedback arc set problem." This implementation 10 | * adjusts that from the paper to allow for weighted edges. 11 | */ 12 | module.exports = greedyFAS; 13 | 14 | var DEFAULT_WEIGHT_FN = _.constant(1); 15 | 16 | function greedyFAS(g, weightFn) { 17 | if (g.nodeCount() <= 1) { 18 | return []; 19 | } 20 | var state = buildState(g, weightFn || DEFAULT_WEIGHT_FN); 21 | var results = doGreedyFAS(state.graph, state.buckets, state.zeroIdx); 22 | 23 | // Expand multi-edges 24 | return _.flatten(_.map(results, function(e) { 25 | return g.outEdges(e.v, e.w); 26 | }), true); 27 | } 28 | 29 | function doGreedyFAS(g, buckets, zeroIdx) { 30 | var results = []; 31 | var sources = buckets[buckets.length - 1]; 32 | var sinks = buckets[0]; 33 | 34 | var entry; 35 | while (g.nodeCount()) { 36 | while ((entry = sinks.dequeue())) { removeNode(g, buckets, zeroIdx, entry); } 37 | while ((entry = sources.dequeue())) { removeNode(g, buckets, zeroIdx, entry); } 38 | if (g.nodeCount()) { 39 | for (var i = buckets.length - 2; i > 0; --i) { 40 | entry = buckets[i].dequeue(); 41 | if (entry) { 42 | results = results.concat(removeNode(g, buckets, zeroIdx, entry, true)); 43 | break; 44 | } 45 | } 46 | } 47 | } 48 | 49 | return results; 50 | } 51 | 52 | function removeNode(g, buckets, zeroIdx, entry, collectPredecessors) { 53 | var results = collectPredecessors ? [] : undefined; 54 | 55 | _.forEach(g.inEdges(entry.v), function(edge) { 56 | var weight = g.edge(edge); 57 | var uEntry = g.node(edge.v); 58 | 59 | if (collectPredecessors) { 60 | results.push({ v: edge.v, w: edge.w }); 61 | } 62 | 63 | uEntry.out -= weight; 64 | assignBucket(buckets, zeroIdx, uEntry); 65 | }); 66 | 67 | _.forEach(g.outEdges(entry.v), function(edge) { 68 | var weight = g.edge(edge); 69 | var w = edge.w; 70 | var wEntry = g.node(w); 71 | wEntry["in"] -= weight; 72 | assignBucket(buckets, zeroIdx, wEntry); 73 | }); 74 | 75 | g.removeNode(entry.v); 76 | 77 | return results; 78 | } 79 | 80 | function buildState(g, weightFn) { 81 | var fasGraph = new Graph(); 82 | var maxIn = 0; 83 | var maxOut = 0; 84 | 85 | _.forEach(g.nodes(), function(v) { 86 | fasGraph.setNode(v, { v: v, "in": 0, out: 0 }); 87 | }); 88 | 89 | // Aggregate weights on nodes, but also sum the weights across multi-edges 90 | // into a single edge for the fasGraph. 91 | _.forEach(g.edges(), function(e) { 92 | var prevWeight = fasGraph.edge(e.v, e.w) || 0; 93 | var weight = weightFn(e); 94 | var edgeWeight = prevWeight + weight; 95 | fasGraph.setEdge(e.v, e.w, edgeWeight); 96 | maxOut = Math.max(maxOut, fasGraph.node(e.v).out += weight); 97 | maxIn = Math.max(maxIn, fasGraph.node(e.w)["in"] += weight); 98 | }); 99 | 100 | var buckets = _.range(maxOut + maxIn + 3).map(function() { return new List(); }); 101 | var zeroIdx = maxIn + 1; 102 | 103 | _.forEach(fasGraph.nodes(), function(v) { 104 | assignBucket(buckets, zeroIdx, fasGraph.node(v)); 105 | }); 106 | 107 | return { graph: fasGraph, buckets: buckets, zeroIdx: zeroIdx }; 108 | } 109 | 110 | function assignBucket(buckets, zeroIdx, entry) { 111 | if (!entry.out) { 112 | buckets[0].enqueue(entry); 113 | } else if (!entry["in"]) { 114 | buckets[buckets.length - 1].enqueue(entry); 115 | } else { 116 | buckets[entry.out - entry["in"] + zeroIdx].enqueue(entry); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /lib/lodash.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-redeclare 2 | /* global window */ 3 | 4 | var lodash; 5 | 6 | if (typeof require === "function") { 7 | try { 8 | lodash = { 9 | cloneDeep: require("lodash/cloneDeep"), 10 | constant: require("lodash/constant"), 11 | defaults: require("lodash/defaults"), 12 | each: require("lodash/each"), 13 | filter: require("lodash/filter"), 14 | find: require("lodash/find"), 15 | flatten: require("lodash/flatten"), 16 | forEach: require("lodash/forEach"), 17 | forIn: require("lodash/forIn"), 18 | has: require("lodash/has"), 19 | isUndefined: require("lodash/isUndefined"), 20 | last: require("lodash/last"), 21 | map: require("lodash/map"), 22 | mapValues: require("lodash/mapValues"), 23 | max: require("lodash/max"), 24 | merge: require("lodash/merge"), 25 | min: require("lodash/min"), 26 | minBy: require("lodash/minBy"), 27 | now: require("lodash/now"), 28 | pick: require("lodash/pick"), 29 | range: require("lodash/range"), 30 | reduce: require("lodash/reduce"), 31 | sortBy: require("lodash/sortBy"), 32 | uniqueId: require("lodash/uniqueId"), 33 | values: require("lodash/values"), 34 | zipObject: require("lodash/zipObject"), 35 | }; 36 | } catch (e) { 37 | // continue regardless of error 38 | } 39 | } 40 | 41 | if (!lodash) { 42 | lodash = window._; 43 | } 44 | 45 | module.exports = lodash; 46 | -------------------------------------------------------------------------------- /lib/nesting-graph.js: -------------------------------------------------------------------------------- 1 | var _ = require("./lodash"); 2 | var util = require("./util"); 3 | 4 | module.exports = { 5 | run: run, 6 | cleanup: cleanup 7 | }; 8 | 9 | /* 10 | * A nesting graph creates dummy nodes for the tops and bottoms of subgraphs, 11 | * adds appropriate edges to ensure that all cluster nodes are placed between 12 | * these boundries, and ensures that the graph is connected. 13 | * 14 | * In addition we ensure, through the use of the minlen property, that nodes 15 | * and subgraph border nodes to not end up on the same rank. 16 | * 17 | * Preconditions: 18 | * 19 | * 1. Input graph is a DAG 20 | * 2. Nodes in the input graph has a minlen attribute 21 | * 22 | * Postconditions: 23 | * 24 | * 1. Input graph is connected. 25 | * 2. Dummy nodes are added for the tops and bottoms of subgraphs. 26 | * 3. The minlen attribute for nodes is adjusted to ensure nodes do not 27 | * get placed on the same rank as subgraph border nodes. 28 | * 29 | * The nesting graph idea comes from Sander, "Layout of Compound Directed 30 | * Graphs." 31 | */ 32 | function run(g) { 33 | var root = util.addDummyNode(g, "root", {}, "_root"); 34 | var depths = treeDepths(g); 35 | var height = _.max(_.values(depths)) - 1; // Note: depths is an Object not an array 36 | var nodeSep = 2 * height + 1; 37 | 38 | g.graph().nestingRoot = root; 39 | 40 | // Multiply minlen by nodeSep to align nodes on non-border ranks. 41 | _.forEach(g.edges(), function(e) { g.edge(e).minlen *= nodeSep; }); 42 | 43 | // Calculate a weight that is sufficient to keep subgraphs vertically compact 44 | var weight = sumWeights(g) + 1; 45 | 46 | // Create border nodes and link them up 47 | _.forEach(g.children(), function(child) { 48 | dfs(g, root, nodeSep, weight, height, depths, child); 49 | }); 50 | 51 | // Save the multiplier for node layers for later removal of empty border 52 | // layers. 53 | g.graph().nodeRankFactor = nodeSep; 54 | } 55 | 56 | function dfs(g, root, nodeSep, weight, height, depths, v) { 57 | var children = g.children(v); 58 | if (!children.length) { 59 | if (v !== root) { 60 | g.setEdge(root, v, { weight: 0, minlen: nodeSep }); 61 | } 62 | return; 63 | } 64 | 65 | var top = util.addBorderNode(g, "_bt"); 66 | var bottom = util.addBorderNode(g, "_bb"); 67 | var label = g.node(v); 68 | 69 | g.setParent(top, v); 70 | label.borderTop = top; 71 | g.setParent(bottom, v); 72 | label.borderBottom = bottom; 73 | 74 | _.forEach(children, function(child) { 75 | dfs(g, root, nodeSep, weight, height, depths, child); 76 | 77 | var childNode = g.node(child); 78 | var childTop = childNode.borderTop ? childNode.borderTop : child; 79 | var childBottom = childNode.borderBottom ? childNode.borderBottom : child; 80 | var thisWeight = childNode.borderTop ? weight : 2 * weight; 81 | var minlen = childTop !== childBottom ? 1 : height - depths[v] + 1; 82 | 83 | g.setEdge(top, childTop, { 84 | weight: thisWeight, 85 | minlen: minlen, 86 | nestingEdge: true 87 | }); 88 | 89 | g.setEdge(childBottom, bottom, { 90 | weight: thisWeight, 91 | minlen: minlen, 92 | nestingEdge: true 93 | }); 94 | }); 95 | 96 | if (!g.parent(v)) { 97 | g.setEdge(root, top, { weight: 0, minlen: height + depths[v] }); 98 | } 99 | } 100 | 101 | function treeDepths(g) { 102 | var depths = {}; 103 | function dfs(v, depth) { 104 | var children = g.children(v); 105 | if (children && children.length) { 106 | _.forEach(children, function(child) { 107 | dfs(child, depth + 1); 108 | }); 109 | } 110 | depths[v] = depth; 111 | } 112 | _.forEach(g.children(), function(v) { dfs(v, 1); }); 113 | return depths; 114 | } 115 | 116 | function sumWeights(g) { 117 | return _.reduce(g.edges(), function(acc, e) { 118 | return acc + g.edge(e).weight; 119 | }, 0); 120 | } 121 | 122 | function cleanup(g) { 123 | var graphLabel = g.graph(); 124 | g.removeNode(graphLabel.nestingRoot); 125 | delete graphLabel.nestingRoot; 126 | _.forEach(g.edges(), function(e) { 127 | var edge = g.edge(e); 128 | if (edge.nestingEdge) { 129 | g.removeEdge(e); 130 | } 131 | }); 132 | } 133 | -------------------------------------------------------------------------------- /lib/normalize.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _ = require("./lodash"); 4 | var util = require("./util"); 5 | 6 | module.exports = { 7 | run: run, 8 | undo: undo 9 | }; 10 | 11 | /* 12 | * Breaks any long edges in the graph into short segments that span 1 layer 13 | * each. This operation is undoable with the denormalize function. 14 | * 15 | * Pre-conditions: 16 | * 17 | * 1. The input graph is a DAG. 18 | * 2. Each node in the graph has a "rank" property. 19 | * 20 | * Post-condition: 21 | * 22 | * 1. All edges in the graph have a length of 1. 23 | * 2. Dummy nodes are added where edges have been split into segments. 24 | * 3. The graph is augmented with a "dummyChains" attribute which contains 25 | * the first dummy in each chain of dummy nodes produced. 26 | */ 27 | function run(g) { 28 | g.graph().dummyChains = []; 29 | _.forEach(g.edges(), function(edge) { normalizeEdge(g, edge); }); 30 | } 31 | 32 | function normalizeEdge(g, e) { 33 | var v = e.v; 34 | var vRank = g.node(v).rank; 35 | var w = e.w; 36 | var wRank = g.node(w).rank; 37 | var name = e.name; 38 | var edgeLabel = g.edge(e); 39 | var labelRank = edgeLabel.labelRank; 40 | 41 | if (wRank === vRank + 1) return; 42 | 43 | g.removeEdge(e); 44 | 45 | var dummy, attrs, i; 46 | for (i = 0, ++vRank; vRank < wRank; ++i, ++vRank) { 47 | edgeLabel.points = []; 48 | attrs = { 49 | width: 0, height: 0, 50 | edgeLabel: edgeLabel, edgeObj: e, 51 | rank: vRank 52 | }; 53 | dummy = util.addDummyNode(g, "edge", attrs, "_d"); 54 | if (vRank === labelRank) { 55 | attrs.width = edgeLabel.width; 56 | attrs.height = edgeLabel.height; 57 | attrs.dummy = "edge-label"; 58 | attrs.labelpos = edgeLabel.labelpos; 59 | } 60 | g.setEdge(v, dummy, { weight: edgeLabel.weight }, name); 61 | if (i === 0) { 62 | g.graph().dummyChains.push(dummy); 63 | } 64 | v = dummy; 65 | } 66 | 67 | g.setEdge(v, w, { weight: edgeLabel.weight }, name); 68 | } 69 | 70 | function undo(g) { 71 | _.forEach(g.graph().dummyChains, function(v) { 72 | var node = g.node(v); 73 | var origLabel = node.edgeLabel; 74 | var w; 75 | g.setEdge(node.edgeObj, origLabel); 76 | while (node.dummy) { 77 | w = g.successors(v)[0]; 78 | g.removeNode(v); 79 | origLabel.points.push({ x: node.x, y: node.y }); 80 | if (node.dummy === "edge-label") { 81 | origLabel.x = node.x; 82 | origLabel.y = node.y; 83 | origLabel.width = node.width; 84 | origLabel.height = node.height; 85 | } 86 | v = w; 87 | node = g.node(v); 88 | } 89 | }); 90 | } 91 | -------------------------------------------------------------------------------- /lib/order/add-subgraph-constraints.js: -------------------------------------------------------------------------------- 1 | var _ = require("../lodash"); 2 | 3 | module.exports = addSubgraphConstraints; 4 | 5 | function addSubgraphConstraints(g, cg, vs) { 6 | var prev = {}, 7 | rootPrev; 8 | 9 | _.forEach(vs, function(v) { 10 | var child = g.parent(v), 11 | parent, 12 | prevChild; 13 | while (child) { 14 | parent = g.parent(child); 15 | if (parent) { 16 | prevChild = prev[parent]; 17 | prev[parent] = child; 18 | } else { 19 | prevChild = rootPrev; 20 | rootPrev = child; 21 | } 22 | if (prevChild && prevChild !== child) { 23 | cg.setEdge(prevChild, child); 24 | return; 25 | } 26 | child = parent; 27 | } 28 | }); 29 | 30 | /* 31 | function dfs(v) { 32 | var children = v ? g.children(v) : g.children(); 33 | if (children.length) { 34 | var min = Number.POSITIVE_INFINITY, 35 | subgraphs = []; 36 | _.each(children, function(child) { 37 | var childMin = dfs(child); 38 | if (g.children(child).length) { 39 | subgraphs.push({ v: child, order: childMin }); 40 | } 41 | min = Math.min(min, childMin); 42 | }); 43 | _.reduce(_.sortBy(subgraphs, "order"), function(prev, curr) { 44 | cg.setEdge(prev.v, curr.v); 45 | return curr; 46 | }); 47 | return min; 48 | } 49 | return g.node(v).order; 50 | } 51 | dfs(undefined); 52 | */ 53 | } 54 | -------------------------------------------------------------------------------- /lib/order/barycenter.js: -------------------------------------------------------------------------------- 1 | var _ = require("../lodash"); 2 | 3 | module.exports = barycenter; 4 | 5 | function barycenter(g, movable) { 6 | return _.map(movable, function(v) { 7 | var inV = g.inEdges(v); 8 | if (!inV.length) { 9 | return { v: v }; 10 | } else { 11 | var result = _.reduce(inV, function(acc, e) { 12 | var edge = g.edge(e), 13 | nodeU = g.node(e.v); 14 | return { 15 | sum: acc.sum + (edge.weight * nodeU.order), 16 | weight: acc.weight + edge.weight 17 | }; 18 | }, { sum: 0, weight: 0 }); 19 | 20 | return { 21 | v: v, 22 | barycenter: result.sum / result.weight, 23 | weight: result.weight 24 | }; 25 | } 26 | }); 27 | } 28 | 29 | -------------------------------------------------------------------------------- /lib/order/build-layer-graph.js: -------------------------------------------------------------------------------- 1 | var _ = require("../lodash"); 2 | var Graph = require("../graphlib").Graph; 3 | 4 | module.exports = buildLayerGraph; 5 | 6 | /* 7 | * Constructs a graph that can be used to sort a layer of nodes. The graph will 8 | * contain all base and subgraph nodes from the request layer in their original 9 | * hierarchy and any edges that are incident on these nodes and are of the type 10 | * requested by the "relationship" parameter. 11 | * 12 | * Nodes from the requested rank that do not have parents are assigned a root 13 | * node in the output graph, which is set in the root graph attribute. This 14 | * makes it easy to walk the hierarchy of movable nodes during ordering. 15 | * 16 | * Pre-conditions: 17 | * 18 | * 1. Input graph is a DAG 19 | * 2. Base nodes in the input graph have a rank attribute 20 | * 3. Subgraph nodes in the input graph has minRank and maxRank attributes 21 | * 4. Edges have an assigned weight 22 | * 23 | * Post-conditions: 24 | * 25 | * 1. Output graph has all nodes in the movable rank with preserved 26 | * hierarchy. 27 | * 2. Root nodes in the movable layer are made children of the node 28 | * indicated by the root attribute of the graph. 29 | * 3. Non-movable nodes incident on movable nodes, selected by the 30 | * relationship parameter, are included in the graph (without hierarchy). 31 | * 4. Edges incident on movable nodes, selected by the relationship 32 | * parameter, are added to the output graph. 33 | * 5. The weights for copied edges are aggregated as need, since the output 34 | * graph is not a multi-graph. 35 | */ 36 | function buildLayerGraph(g, rank, relationship) { 37 | var root = createRootNode(g), 38 | result = new Graph({ compound: true }).setGraph({ root: root }) 39 | .setDefaultNodeLabel(function(v) { return g.node(v); }); 40 | 41 | _.forEach(g.nodes(), function(v) { 42 | var node = g.node(v), 43 | parent = g.parent(v); 44 | 45 | if (node.rank === rank || node.minRank <= rank && rank <= node.maxRank) { 46 | result.setNode(v); 47 | result.setParent(v, parent || root); 48 | 49 | // This assumes we have only short edges! 50 | _.forEach(g[relationship](v), function(e) { 51 | var u = e.v === v ? e.w : e.v, 52 | edge = result.edge(u, v), 53 | weight = !_.isUndefined(edge) ? edge.weight : 0; 54 | result.setEdge(u, v, { weight: g.edge(e).weight + weight }); 55 | }); 56 | 57 | if (_.has(node, "minRank")) { 58 | result.setNode(v, { 59 | borderLeft: node.borderLeft[rank], 60 | borderRight: node.borderRight[rank] 61 | }); 62 | } 63 | } 64 | }); 65 | 66 | return result; 67 | } 68 | 69 | function createRootNode(g) { 70 | var v; 71 | while (g.hasNode((v = _.uniqueId("_root")))); 72 | return v; 73 | } 74 | -------------------------------------------------------------------------------- /lib/order/cross-count.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _ = require("../lodash"); 4 | 5 | module.exports = crossCount; 6 | 7 | /* 8 | * A function that takes a layering (an array of layers, each with an array of 9 | * ordererd nodes) and a graph and returns a weighted crossing count. 10 | * 11 | * Pre-conditions: 12 | * 13 | * 1. Input graph must be simple (not a multigraph), directed, and include 14 | * only simple edges. 15 | * 2. Edges in the input graph must have assigned weights. 16 | * 17 | * Post-conditions: 18 | * 19 | * 1. The graph and layering matrix are left unchanged. 20 | * 21 | * This algorithm is derived from Barth, et al., "Bilayer Cross Counting." 22 | */ 23 | function crossCount(g, layering) { 24 | var cc = 0; 25 | for (var i = 1; i < layering.length; ++i) { 26 | cc += twoLayerCrossCount(g, layering[i-1], layering[i]); 27 | } 28 | return cc; 29 | } 30 | 31 | function twoLayerCrossCount(g, northLayer, southLayer) { 32 | // Sort all of the edges between the north and south layers by their position 33 | // in the north layer and then the south. Map these edges to the position of 34 | // their head in the south layer. 35 | var southPos = _.zipObject(southLayer, 36 | _.map(southLayer, function (v, i) { return i; })); 37 | var southEntries = _.flatten(_.map(northLayer, function(v) { 38 | return _.sortBy(_.map(g.outEdges(v), function(e) { 39 | return { pos: southPos[e.w], weight: g.edge(e).weight }; 40 | }), "pos"); 41 | }), true); 42 | 43 | // Build the accumulator tree 44 | var firstIndex = 1; 45 | while (firstIndex < southLayer.length) firstIndex <<= 1; 46 | var treeSize = 2 * firstIndex - 1; 47 | firstIndex -= 1; 48 | var tree = _.map(new Array(treeSize), function() { return 0; }); 49 | 50 | // Calculate the weighted crossings 51 | var cc = 0; 52 | _.forEach(southEntries.forEach(function(entry) { 53 | var index = entry.pos + firstIndex; 54 | tree[index] += entry.weight; 55 | var weightSum = 0; 56 | while (index > 0) { 57 | if (index % 2) { 58 | weightSum += tree[index + 1]; 59 | } 60 | index = (index - 1) >> 1; 61 | tree[index] += entry.weight; 62 | } 63 | cc += entry.weight * weightSum; 64 | })); 65 | 66 | return cc; 67 | } 68 | -------------------------------------------------------------------------------- /lib/order/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _ = require("../lodash"); 4 | var initOrder = require("./init-order"); 5 | var crossCount = require("./cross-count"); 6 | var sortSubgraph = require("./sort-subgraph"); 7 | var buildLayerGraph = require("./build-layer-graph"); 8 | var addSubgraphConstraints = require("./add-subgraph-constraints"); 9 | var Graph = require("../graphlib").Graph; 10 | var util = require("../util"); 11 | 12 | module.exports = order; 13 | 14 | /* 15 | * Applies heuristics to minimize edge crossings in the graph and sets the best 16 | * order solution as an order attribute on each node. 17 | * 18 | * Pre-conditions: 19 | * 20 | * 1. Graph must be DAG 21 | * 2. Graph nodes must be objects with a "rank" attribute 22 | * 3. Graph edges must have the "weight" attribute 23 | * 24 | * Post-conditions: 25 | * 26 | * 1. Graph nodes will have an "order" attribute based on the results of the 27 | * algorithm. 28 | */ 29 | function order(g) { 30 | var maxRank = util.maxRank(g), 31 | downLayerGraphs = buildLayerGraphs(g, _.range(1, maxRank + 1), "inEdges"), 32 | upLayerGraphs = buildLayerGraphs(g, _.range(maxRank - 1, -1, -1), "outEdges"); 33 | 34 | var layering = initOrder(g); 35 | assignOrder(g, layering); 36 | 37 | var bestCC = Number.POSITIVE_INFINITY, 38 | best; 39 | 40 | for (var i = 0, lastBest = 0; lastBest < 4; ++i, ++lastBest) { 41 | sweepLayerGraphs(i % 2 ? downLayerGraphs : upLayerGraphs, i % 4 >= 2); 42 | 43 | layering = util.buildLayerMatrix(g); 44 | var cc = crossCount(g, layering); 45 | if (cc < bestCC) { 46 | lastBest = 0; 47 | best = _.cloneDeep(layering); 48 | bestCC = cc; 49 | } 50 | } 51 | 52 | // consider use previous result, maybe somewhat reduendant 53 | layering = initOrder(g); 54 | assignOrder(g, layering); 55 | for (i = 0, lastBest = 0; lastBest < 4; ++i, ++lastBest) { 56 | sweepLayerGraphs(i % 2 ? downLayerGraphs : upLayerGraphs, i % 4 >= 2, true); 57 | 58 | layering = util.buildLayerMatrix(g); 59 | cc = crossCount(g, layering); 60 | if (cc < bestCC) { 61 | lastBest = 0; 62 | best = _.cloneDeep(layering); 63 | bestCC = cc; 64 | } 65 | } 66 | assignOrder(g, best); 67 | } 68 | 69 | function buildLayerGraphs(g, ranks, relationship) { 70 | return _.map(ranks, function(rank) { 71 | return buildLayerGraph(g, rank, relationship); 72 | }); 73 | } 74 | 75 | function sweepLayerGraphs(layerGraphs, biasRight, usePrev) { 76 | var cg = new Graph(); 77 | _.forEach(layerGraphs, function(lg) { 78 | var root = lg.graph().root; 79 | var sorted = sortSubgraph(lg, root, cg, biasRight, usePrev); 80 | _.forEach(sorted.vs, function(v, i) { 81 | lg.node(v).order = i; 82 | }); 83 | addSubgraphConstraints(lg, cg, sorted.vs); 84 | }); 85 | } 86 | 87 | function assignOrder(g, layering) { 88 | _.forEach(layering, function(layer) { 89 | _.forEach(layer, function(v, i) { 90 | g.node(v).order = i; 91 | }); 92 | }); 93 | } 94 | -------------------------------------------------------------------------------- /lib/order/init-data-order.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _ = require("../lodash"); 4 | 5 | module.exports = initDataOrder; 6 | 7 | 8 | /** 9 | * 按照数据中的结果设置fixorder 10 | */ 11 | function initDataOrder(g, nodeOrder) { 12 | var simpleNodes = _.filter(g.nodes(), function(v) { 13 | return !g.children(v).length; 14 | }); 15 | var maxRank = _.max(_.map(simpleNodes, function(v) { return g.node(v).rank; })); 16 | var layers = _.map(_.range(maxRank + 1), function() { return []; }); 17 | _.forEach(nodeOrder, function(n) { 18 | var node = g.node(n); 19 | // 只考虑原有节点,dummy节点需要按照后续算法排出 20 | if (node.dummy) { 21 | return; 22 | } 23 | node.fixorder = layers[node.rank].length; // 设置fixorder为当层的顺序 24 | layers[node.rank].push(n); 25 | }); 26 | } -------------------------------------------------------------------------------- /lib/order/init-order.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _ = require("../lodash"); 4 | 5 | module.exports = initOrder; 6 | 7 | /* 8 | * Assigns an initial order value for each node by performing a DFS search 9 | * starting from nodes in the first rank. Nodes are assigned an order in their 10 | * rank as they are first visited. 11 | * 12 | * This approach comes from Gansner, et al., "A Technique for Drawing Directed 13 | * Graphs." 14 | * 15 | * Returns a layering matrix with an array per layer and each layer sorted by 16 | * the order of its nodes. 17 | */ 18 | function initOrder(g) { 19 | var visited = {}; 20 | var simpleNodes = _.filter(g.nodes(), function(v) { 21 | return !g.children(v).length; 22 | }); 23 | var maxRank = _.max(_.map(simpleNodes, function(v) { return g.node(v).rank; })); 24 | var layers = _.map(_.range(maxRank + 1), function() { return []; }); 25 | 26 | function dfs(v) { 27 | if (_.has(visited, v)) return; 28 | visited[v] = true; 29 | var node = g.node(v); 30 | layers[node.rank].push(v); 31 | _.forEach(g.successors(v), dfs); 32 | } 33 | 34 | var orderedVs = _.sortBy(simpleNodes, function(v) { return g.node(v).rank; }); 35 | 36 | // 有fixOrder的,直接排序好放进去 37 | var fixOrderNodes = _.sortBy(_.filter(orderedVs, function (n) { 38 | return g.node(n).fixorder !== undefined; 39 | }), function(n) { 40 | return g.node(n).fixorder; 41 | }); 42 | 43 | _.forEach(fixOrderNodes, function(n) { 44 | layers[g.node(n).rank].push(n); 45 | visited[n] = true; 46 | }); 47 | 48 | _.forEach(orderedVs, dfs); 49 | 50 | return layers; 51 | } 52 | -------------------------------------------------------------------------------- /lib/order/resolve-conflicts.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _ = require("../lodash"); 4 | 5 | module.exports = resolveConflicts; 6 | 7 | /* 8 | * Given a list of entries of the form {v, barycenter, weight} and a 9 | * constraint graph this function will resolve any conflicts between the 10 | * constraint graph and the barycenters for the entries. If the barycenters for 11 | * an entry would violate a constraint in the constraint graph then we coalesce 12 | * the nodes in the conflict into a new node that respects the contraint and 13 | * aggregates barycenter and weight information. 14 | * 15 | * This implementation is based on the description in Forster, "A Fast and 16 | * Simple Hueristic for Constrained Two-Level Crossing Reduction," thought it 17 | * differs in some specific details. 18 | * 19 | * Pre-conditions: 20 | * 21 | * 1. Each entry has the form {v, barycenter, weight}, or if the node has 22 | * no barycenter, then {v}. 23 | * 24 | * Returns: 25 | * 26 | * A new list of entries of the form {vs, i, barycenter, weight}. The list 27 | * `vs` may either be a singleton or it may be an aggregation of nodes 28 | * ordered such that they do not violate constraints from the constraint 29 | * graph. The property `i` is the lowest original index of any of the 30 | * elements in `vs`. 31 | */ 32 | function resolveConflicts(entries, cg) { 33 | var mappedEntries = {}; 34 | _.forEach(entries, function(entry, i) { 35 | var tmp = mappedEntries[entry.v] = { 36 | indegree: 0, 37 | "in": [], 38 | out: [], 39 | vs: [entry.v], 40 | i: i 41 | }; 42 | if (!_.isUndefined(entry.barycenter)) { 43 | tmp.barycenter = entry.barycenter; 44 | tmp.weight = entry.weight; 45 | } 46 | }); 47 | 48 | _.forEach(cg.edges(), function(e) { 49 | var entryV = mappedEntries[e.v]; 50 | var entryW = mappedEntries[e.w]; 51 | if (!_.isUndefined(entryV) && !_.isUndefined(entryW)) { 52 | entryW.indegree++; 53 | entryV.out.push(mappedEntries[e.w]); 54 | } 55 | }); 56 | 57 | var sourceSet = _.filter(mappedEntries, function(entry) { 58 | return !entry.indegree; 59 | }); 60 | 61 | return doResolveConflicts(sourceSet); 62 | } 63 | 64 | function doResolveConflicts(sourceSet) { 65 | var entries = []; 66 | 67 | function handleIn(vEntry) { 68 | return function(uEntry) { 69 | if (uEntry.merged) { 70 | return; 71 | } 72 | if (_.isUndefined(uEntry.barycenter) || 73 | _.isUndefined(vEntry.barycenter) || 74 | uEntry.barycenter >= vEntry.barycenter) { 75 | mergeEntries(vEntry, uEntry); 76 | } 77 | }; 78 | } 79 | 80 | function handleOut(vEntry) { 81 | return function(wEntry) { 82 | wEntry["in"].push(vEntry); 83 | if (--wEntry.indegree === 0) { 84 | sourceSet.push(wEntry); 85 | } 86 | }; 87 | } 88 | 89 | while (sourceSet.length) { 90 | var entry = sourceSet.pop(); 91 | entries.push(entry); 92 | _.forEach(entry["in"].reverse(), handleIn(entry)); 93 | _.forEach(entry.out, handleOut(entry)); 94 | } 95 | 96 | return _.map(_.filter(entries, function(entry) { return !entry.merged; }), 97 | function(entry) { 98 | return _.pick(entry, ["vs", "i", "barycenter", "weight"]); 99 | }); 100 | 101 | } 102 | 103 | function mergeEntries(target, source) { 104 | var sum = 0; 105 | var weight = 0; 106 | 107 | if (target.weight) { 108 | sum += target.barycenter * target.weight; 109 | weight += target.weight; 110 | } 111 | 112 | if (source.weight) { 113 | sum += source.barycenter * source.weight; 114 | weight += source.weight; 115 | } 116 | 117 | target.vs = source.vs.concat(target.vs); 118 | target.barycenter = sum / weight; 119 | target.weight = weight; 120 | target.i = Math.min(source.i, target.i); 121 | source.merged = true; 122 | } 123 | -------------------------------------------------------------------------------- /lib/order/sort-subgraph.js: -------------------------------------------------------------------------------- 1 | var _ = require("../lodash"); 2 | var barycenter = require("./barycenter"); 3 | var resolveConflicts = require("./resolve-conflicts"); 4 | var sort = require("./sort"); 5 | 6 | module.exports = sortSubgraph; 7 | 8 | function sortSubgraph(g, v, cg, biasRight, usePrev) { 9 | var movable = g.children(v); 10 | // fixorder的点不参与排序(这个方案不合适,只排了新增节点,和原来的分离) 11 | // var movable = _.filter(g.children(v), function(v) { return g.node(v).fixorder === undefined; }); 12 | var node = g.node(v); 13 | var bl = node ? node.borderLeft : undefined; 14 | var br = node ? node.borderRight: undefined; 15 | var subgraphs = {}; 16 | 17 | if (bl) { 18 | movable = _.filter(movable, function(w) { 19 | return w !== bl && w !== br; 20 | }); 21 | } 22 | 23 | var barycenters = barycenter(g, movable); 24 | _.forEach(barycenters, function(entry) { 25 | if (g.children(entry.v).length) { 26 | var subgraphResult = sortSubgraph(g, entry.v, cg, biasRight); 27 | subgraphs[entry.v] = subgraphResult; 28 | if (_.has(subgraphResult, "barycenter")) { 29 | mergeBarycenters(entry, subgraphResult); 30 | } 31 | } 32 | }); 33 | 34 | var entries = resolveConflicts(barycenters, cg); 35 | expandSubgraphs(entries, subgraphs); 36 | 37 | // 添加fixorder信息到entries里边 38 | // TODO: 不考虑复合情况,只用第一个点的fixorder信息,后续考虑更完备的实现 39 | _.forEach(entries, function (e) { 40 | var node = g.node(e.vs[0]); 41 | e.fixorder = node.fixorder; 42 | e.order = node.order; 43 | }); 44 | 45 | var result = sort(entries, biasRight, usePrev); 46 | 47 | if (bl) { 48 | result.vs = _.flatten([bl, result.vs, br], true); 49 | if (g.predecessors(bl).length) { 50 | var blPred = g.node(g.predecessors(bl)[0]), 51 | brPred = g.node(g.predecessors(br)[0]); 52 | if (!_.has(result, "barycenter")) { 53 | result.barycenter = 0; 54 | result.weight = 0; 55 | } 56 | result.barycenter = (result.barycenter * result.weight + 57 | blPred.order + brPred.order) / (result.weight + 2); 58 | result.weight += 2; 59 | } 60 | } 61 | 62 | return result; 63 | } 64 | 65 | function expandSubgraphs(entries, subgraphs) { 66 | _.forEach(entries, function(entry) { 67 | entry.vs = _.flatten(entry.vs.map(function(v) { 68 | if (subgraphs[v]) { 69 | return subgraphs[v].vs; 70 | } 71 | return v; 72 | }), true); 73 | }); 74 | } 75 | 76 | function mergeBarycenters(target, other) { 77 | if (!_.isUndefined(target.barycenter)) { 78 | target.barycenter = (target.barycenter * target.weight + 79 | other.barycenter * other.weight) / 80 | (target.weight + other.weight); 81 | target.weight += other.weight; 82 | } else { 83 | target.barycenter = other.barycenter; 84 | target.weight = other.weight; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /lib/order/sort.js: -------------------------------------------------------------------------------- 1 | var _ = require("../lodash"); 2 | var util = require("../util"); 3 | 4 | module.exports = sort; 5 | 6 | function sort(entries, biasRight, usePrev) { 7 | var parts = util.partition(entries, function(entry) { 8 | // NOTE: 有fixorder的也可以排 9 | return (_.has(entry, "fixorder") && !isNaN(entry.fixorder)) || _.has(entry, "barycenter"); 10 | }); 11 | var sortable = parts.lhs, 12 | unsortable = _.sortBy(parts.rhs, function(entry) { return -entry.i; }), 13 | vs = [], 14 | sum = 0, 15 | weight = 0, 16 | vsIndex = 0; 17 | 18 | sortable.sort(compareWithBias(!!biasRight, !!usePrev)); 19 | 20 | vsIndex = consumeUnsortable(vs, unsortable, vsIndex); 21 | 22 | _.forEach(sortable, function (entry) { 23 | vsIndex += entry.vs.length; 24 | vs.push(entry.vs); 25 | sum += entry.barycenter * entry.weight; 26 | weight += entry.weight; 27 | vsIndex = consumeUnsortable(vs, unsortable, vsIndex); 28 | }); 29 | 30 | var result = { vs: _.flatten(vs, true) }; 31 | if (weight) { 32 | result.barycenter = sum / weight; 33 | result.weight = weight; 34 | } 35 | return result; 36 | } 37 | 38 | function consumeUnsortable(vs, unsortable, index) { 39 | var last; 40 | while (unsortable.length && (last = _.last(unsortable)).i <= index) { 41 | unsortable.pop(); 42 | vs.push(last.vs); 43 | index++; 44 | } 45 | return index; 46 | } 47 | 48 | /** 49 | * 配置是否考虑使用之前的布局结果 50 | */ 51 | function compareWithBias(bias, usePrev) { 52 | return function(entryV, entryW) { 53 | // 排序的时候先判断fixorder,不行再判断重心 54 | if (entryV.fixorder !== undefined && entryW.fixorder !== undefined) { 55 | return entryV.fixorder - entryW.fixorder; 56 | } 57 | if (entryV.barycenter < entryW.barycenter) { 58 | return -1; 59 | } else if (entryV.barycenter > entryW.barycenter) { 60 | return 1; 61 | } 62 | // 重心相同,考虑之前排好的顺序 63 | if (usePrev && entryV.order !== undefined && entryW.order !== undefined) { 64 | if (entryV.order < entryW.order) { 65 | return -1; 66 | } else if (entryV.order > entryW.order) { 67 | return 1; 68 | } 69 | } 70 | 71 | return !bias ? entryV.i - entryW.i : entryW.i - entryV.i; 72 | }; 73 | } 74 | -------------------------------------------------------------------------------- /lib/parent-dummy-chains.js: -------------------------------------------------------------------------------- 1 | var _ = require("./lodash"); 2 | 3 | module.exports = parentDummyChains; 4 | 5 | function parentDummyChains(g) { 6 | var postorderNums = postorder(g); 7 | 8 | _.forEach(g.graph().dummyChains, function(v) { 9 | var node = g.node(v); 10 | var edgeObj = node.edgeObj; 11 | var pathData = findPath(g, postorderNums, edgeObj.v, edgeObj.w); 12 | var path = pathData.path; 13 | var lca = pathData.lca; 14 | var pathIdx = 0; 15 | var pathV = path[pathIdx]; 16 | var ascending = true; 17 | 18 | while (v !== edgeObj.w) { 19 | node = g.node(v); 20 | 21 | if (ascending) { 22 | while ((pathV = path[pathIdx]) !== lca && 23 | g.node(pathV).maxRank < node.rank) { 24 | pathIdx++; 25 | } 26 | 27 | if (pathV === lca) { 28 | ascending = false; 29 | } 30 | } 31 | 32 | if (!ascending) { 33 | while (pathIdx < path.length - 1 && 34 | g.node(pathV = path[pathIdx + 1]).minRank <= node.rank) { 35 | pathIdx++; 36 | } 37 | pathV = path[pathIdx]; 38 | } 39 | 40 | g.setParent(v, pathV); 41 | v = g.successors(v)[0]; 42 | } 43 | }); 44 | } 45 | 46 | // Find a path from v to w through the lowest common ancestor (LCA). Return the 47 | // full path and the LCA. 48 | function findPath(g, postorderNums, v, w) { 49 | var vPath = []; 50 | var wPath = []; 51 | var low = Math.min(postorderNums[v].low, postorderNums[w].low); 52 | var lim = Math.max(postorderNums[v].lim, postorderNums[w].lim); 53 | var parent; 54 | var lca; 55 | 56 | // Traverse up from v to find the LCA 57 | parent = v; 58 | do { 59 | parent = g.parent(parent); 60 | vPath.push(parent); 61 | } while (parent && 62 | (postorderNums[parent].low > low || lim > postorderNums[parent].lim)); 63 | lca = parent; 64 | 65 | // Traverse from w to LCA 66 | parent = w; 67 | while ((parent = g.parent(parent)) !== lca) { 68 | wPath.push(parent); 69 | } 70 | 71 | return { path: vPath.concat(wPath.reverse()), lca: lca }; 72 | } 73 | 74 | function postorder(g) { 75 | var result = {}; 76 | var lim = 0; 77 | 78 | function dfs(v) { 79 | var low = lim; 80 | _.forEach(g.children(v), dfs); 81 | result[v] = { low: low, lim: lim++ }; 82 | } 83 | _.forEach(g.children(), dfs); 84 | 85 | return result; 86 | } 87 | -------------------------------------------------------------------------------- /lib/position/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _ = require("../lodash"); 4 | var util = require("../util"); 5 | var positionX = require("./bk").positionX; 6 | 7 | module.exports = position; 8 | 9 | function position(g) { 10 | g = util.asNonCompoundGraph(g); 11 | 12 | positionY(g); 13 | _.forEach(positionX(g), function(x, v) { 14 | g.node(v).x = x; 15 | }); 16 | } 17 | 18 | function positionY(g) { 19 | var layering = util.buildLayerMatrix(g); 20 | var rankSep = g.graph().ranksep; 21 | var prevY = 0; 22 | _.forEach(layering, function(layer) { 23 | var maxHeight = _.max(_.map(layer, function(v) { return g.node(v).height; })); 24 | _.forEach(layer, function(v) { 25 | g.node(v).y = prevY + maxHeight / 2; 26 | }); 27 | prevY += maxHeight + rankSep; 28 | }); 29 | } 30 | 31 | -------------------------------------------------------------------------------- /lib/rank/feasible-tree.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _ = require("../lodash"); 4 | var Graph = require("../graphlib").Graph; 5 | var slack = require("./util").slack; 6 | 7 | // module.exports = feasibleTree; 8 | module.exports = { 9 | feasibleTree: feasibleTree, 10 | feasibleTreeWithLayer: feasibleTreeWithLayer 11 | }; 12 | 13 | /* 14 | * Constructs a spanning tree with tight edges and adjusted the input node's 15 | * ranks to achieve this. A tight edge is one that is has a length that matches 16 | * its "minlen" attribute. 17 | * 18 | * The basic structure for this function is derived from Gansner, et al., "A 19 | * Technique for Drawing Directed Graphs." 20 | * 21 | * Pre-conditions: 22 | * 23 | * 1. Graph must be a DAG. 24 | * 2. Graph must be connected. 25 | * 3. Graph must have at least one node. 26 | * 5. Graph nodes must have been previously assigned a "rank" property that 27 | * respects the "minlen" property of incident edges. 28 | * 6. Graph edges must have a "minlen" property. 29 | * 30 | * Post-conditions: 31 | * 32 | * - Graph nodes will have their rank adjusted to ensure that all edges are 33 | * tight. 34 | * 35 | * Returns a tree (undirected graph) that is constructed using only "tight" 36 | * edges. 37 | */ 38 | function feasibleTree(g) { 39 | var t = new Graph({ directed: false }); 40 | 41 | // Choose arbitrary node from which to start our tree 42 | var start = g.nodes()[0]; 43 | var size = g.nodeCount(); 44 | t.setNode(start, {}); 45 | 46 | var edge, delta; 47 | while (tightTree(t, g) < size) { 48 | edge = findMinSlackEdge(t, g); 49 | delta = t.hasNode(edge.v) ? slack(g, edge) : -slack(g, edge); 50 | shiftRanks(t, g, delta); 51 | } 52 | 53 | return t; 54 | } 55 | 56 | /* 57 | * Finds a maximal tree of tight edges and returns the number of nodes in the 58 | * tree. 59 | */ 60 | function tightTree(t, g) { 61 | function dfs(v) { 62 | _.forEach(g.nodeEdges(v), function(e) { 63 | var edgeV = e.v, 64 | w = (v === edgeV) ? e.w : edgeV; 65 | if (!t.hasNode(w) && !slack(g, e)) { 66 | t.setNode(w, {}); 67 | t.setEdge(v, w, {}); 68 | dfs(w); 69 | } 70 | }); 71 | } 72 | 73 | _.forEach(t.nodes(), dfs); 74 | return t.nodeCount(); 75 | } 76 | 77 | /* 78 | * Constructs a spanning tree with tight edges and adjusted the input node's 79 | * ranks to achieve this. A tight edge is one that is has a length that matches 80 | * its "minlen" attribute. 81 | * 82 | * The basic structure for this function is derived from Gansner, et al., "A 83 | * Technique for Drawing Directed Graphs." 84 | * 85 | * Pre-conditions: 86 | * 87 | * 1. Graph must be a DAG. 88 | * 2. Graph must be connected. 89 | * 3. Graph must have at least one node. 90 | * 5. Graph nodes must have been previously assigned a "rank" property that 91 | * respects the "minlen" property of incident edges. 92 | * 6. Graph edges must have a "minlen" property. 93 | * 94 | * Post-conditions: 95 | * 96 | * - Graph nodes will have their rank adjusted to ensure that all edges are 97 | * tight. 98 | * 99 | * Returns a tree (undirected graph) that is constructed using only "tight" 100 | * edges. 101 | */ 102 | function feasibleTreeWithLayer(g) { 103 | var t = new Graph({ directed: false }); 104 | 105 | // Choose arbitrary node from which to start our tree 106 | var start = g.nodes()[0]; 107 | var size = g.nodeCount(); 108 | t.setNode(start, {}); 109 | 110 | var edge, delta; 111 | while (tightTreeWithLayer(t, g) < size) { 112 | edge = findMinSlackEdge(t, g); 113 | delta = t.hasNode(edge.v) ? slack(g, edge) : -slack(g, edge); 114 | shiftRanks(t, g, delta); 115 | } 116 | 117 | return t; 118 | } 119 | 120 | 121 | /* 122 | * Finds a maximal tree of tight edges and returns the number of nodes in the 123 | * tree. 124 | */ 125 | function tightTreeWithLayer(t, g) { 126 | function dfs(v) { 127 | _.forEach(g.nodeEdges(v), function(e) { 128 | var edgeV = e.v, 129 | w = (v === edgeV) ? e.w : edgeV; 130 | // 对于指定layer的,直接加入tight-tree,不参与调整 131 | if (!t.hasNode(w) && (g.node(w).layer !== undefined || !slack(g, e))) { 132 | t.setNode(w, {}); 133 | t.setEdge(v, w, {}); 134 | dfs(w); 135 | } 136 | }); 137 | } 138 | 139 | _.forEach(t.nodes(), dfs); 140 | return t.nodeCount(); 141 | } 142 | 143 | /* 144 | * Finds the edge with the smallest slack that is incident on tree and returns 145 | * it. 146 | */ 147 | function findMinSlackEdge(t, g) { 148 | return _.minBy(g.edges(), function(e) { 149 | if (t.hasNode(e.v) !== t.hasNode(e.w)) { 150 | return slack(g, e); 151 | } 152 | }); 153 | } 154 | 155 | function shiftRanks(t, g, delta) { 156 | _.forEach(t.nodes(), function(v) { 157 | g.node(v).rank += delta; 158 | }); 159 | } 160 | -------------------------------------------------------------------------------- /lib/rank/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var rankUtil = require("./util"); 4 | var longestPath = rankUtil.longestPathWithLayer; 5 | var feasibleTree = require("./feasible-tree").feasibleTreeWithLayer; 6 | var networkSimplex = require("./network-simplex"); 7 | 8 | module.exports = rank; 9 | 10 | /* 11 | * Assigns a rank to each node in the input graph that respects the "minlen" 12 | * constraint specified on edges between nodes. 13 | * 14 | * This basic structure is derived from Gansner, et al., "A Technique for 15 | * Drawing Directed Graphs." 16 | * 17 | * Pre-conditions: 18 | * 19 | * 1. Graph must be a connected DAG 20 | * 2. Graph nodes must be objects 21 | * 3. Graph edges must have "weight" and "minlen" attributes 22 | * 23 | * Post-conditions: 24 | * 25 | * 1. Graph nodes will have a "rank" attribute based on the results of the 26 | * algorithm. Ranks can start at any index (including negative), we'll 27 | * fix them up later. 28 | */ 29 | function rank(g) { 30 | switch(g.graph().ranker) { 31 | case "network-simplex": networkSimplexRanker(g); break; 32 | case "tight-tree": tightTreeRanker(g); break; 33 | case "longest-path": longestPathRanker(g); break; 34 | // default: networkSimplexRanker(g); 35 | default: tightTreeRanker(g); 36 | } 37 | } 38 | 39 | // A fast and simple ranker, but results are far from optimal. 40 | var longestPathRanker = longestPath; 41 | 42 | function tightTreeRanker(g) { 43 | longestPath(g); 44 | feasibleTree(g); 45 | } 46 | 47 | function networkSimplexRanker(g) { 48 | networkSimplex(g); 49 | } 50 | -------------------------------------------------------------------------------- /lib/rank/network-simplex.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _ = require("../lodash"); 4 | var feasibleTree = require("./feasible-tree").feasibleTree; 5 | var slack = require("./util").slack; 6 | var initRank = require("./util").longestPath; 7 | var preorder = require("../graphlib").alg.preorder; 8 | var postorder = require("../graphlib").alg.postorder; 9 | var simplify = require("../util").simplify; 10 | 11 | module.exports = networkSimplex; 12 | 13 | // Expose some internals for testing purposes 14 | networkSimplex.initLowLimValues = initLowLimValues; 15 | networkSimplex.initCutValues = initCutValues; 16 | networkSimplex.calcCutValue = calcCutValue; 17 | networkSimplex.leaveEdge = leaveEdge; 18 | networkSimplex.enterEdge = enterEdge; 19 | networkSimplex.exchangeEdges = exchangeEdges; 20 | 21 | /* 22 | * The network simplex algorithm assigns ranks to each node in the input graph 23 | * and iteratively improves the ranking to reduce the length of edges. 24 | * 25 | * Preconditions: 26 | * 27 | * 1. The input graph must be a DAG. 28 | * 2. All nodes in the graph must have an object value. 29 | * 3. All edges in the graph must have "minlen" and "weight" attributes. 30 | * 31 | * Postconditions: 32 | * 33 | * 1. All nodes in the graph will have an assigned "rank" attribute that has 34 | * been optimized by the network simplex algorithm. Ranks start at 0. 35 | * 36 | * 37 | * A rough sketch of the algorithm is as follows: 38 | * 39 | * 1. Assign initial ranks to each node. We use the longest path algorithm, 40 | * which assigns ranks to the lowest position possible. In general this 41 | * leads to very wide bottom ranks and unnecessarily long edges. 42 | * 2. Construct a feasible tight tree. A tight tree is one such that all 43 | * edges in the tree have no slack (difference between length of edge 44 | * and minlen for the edge). This by itself greatly improves the assigned 45 | * rankings by shorting edges. 46 | * 3. Iteratively find edges that have negative cut values. Generally a 47 | * negative cut value indicates that the edge could be removed and a new 48 | * tree edge could be added to produce a more compact graph. 49 | * 50 | * Much of the algorithms here are derived from Gansner, et al., "A Technique 51 | * for Drawing Directed Graphs." The structure of the file roughly follows the 52 | * structure of the overall algorithm. 53 | */ 54 | function networkSimplex(g) { 55 | g = simplify(g); 56 | initRank(g); 57 | var t = feasibleTree(g); 58 | initLowLimValues(t); 59 | initCutValues(t, g); 60 | 61 | var e, f; 62 | while ((e = leaveEdge(t))) { 63 | f = enterEdge(t, g, e); 64 | exchangeEdges(t, g, e, f); 65 | } 66 | } 67 | 68 | /* 69 | * Initializes cut values for all edges in the tree. 70 | */ 71 | function initCutValues(t, g) { 72 | var vs = postorder(t, t.nodes()); 73 | vs = vs.slice(0, vs.length - 1); 74 | _.forEach(vs, function(v) { 75 | assignCutValue(t, g, v); 76 | }); 77 | } 78 | 79 | function assignCutValue(t, g, child) { 80 | var childLab = t.node(child); 81 | var parent = childLab.parent; 82 | t.edge(child, parent).cutvalue = calcCutValue(t, g, child); 83 | } 84 | 85 | /* 86 | * Given the tight tree, its graph, and a child in the graph calculate and 87 | * return the cut value for the edge between the child and its parent. 88 | */ 89 | function calcCutValue(t, g, child) { 90 | var childLab = t.node(child); 91 | var parent = childLab.parent; 92 | // True if the child is on the tail end of the edge in the directed graph 93 | var childIsTail = true; 94 | // The graph's view of the tree edge we're inspecting 95 | var graphEdge = g.edge(child, parent); 96 | // The accumulated cut value for the edge between this node and its parent 97 | var cutValue = 0; 98 | 99 | if (!graphEdge) { 100 | childIsTail = false; 101 | graphEdge = g.edge(parent, child); 102 | } 103 | 104 | cutValue = graphEdge.weight; 105 | 106 | _.forEach(g.nodeEdges(child), function(e) { 107 | var isOutEdge = e.v === child, 108 | other = isOutEdge ? e.w : e.v; 109 | 110 | if (other !== parent) { 111 | var pointsToHead = isOutEdge === childIsTail, 112 | otherWeight = g.edge(e).weight; 113 | 114 | cutValue += pointsToHead ? otherWeight : -otherWeight; 115 | if (isTreeEdge(t, child, other)) { 116 | var otherCutValue = t.edge(child, other).cutvalue; 117 | cutValue += pointsToHead ? -otherCutValue : otherCutValue; 118 | } 119 | } 120 | }); 121 | 122 | return cutValue; 123 | } 124 | 125 | function initLowLimValues(tree, root) { 126 | if (arguments.length < 2) { 127 | root = tree.nodes()[0]; 128 | } 129 | dfsAssignLowLim(tree, {}, 1, root); 130 | } 131 | 132 | function dfsAssignLowLim(tree, visited, nextLim, v, parent) { 133 | var low = nextLim; 134 | var label = tree.node(v); 135 | 136 | visited[v] = true; 137 | _.forEach(tree.neighbors(v), function(w) { 138 | if (!_.has(visited, w)) { 139 | nextLim = dfsAssignLowLim(tree, visited, nextLim, w, v); 140 | } 141 | }); 142 | 143 | label.low = low; 144 | label.lim = nextLim++; 145 | if (parent) { 146 | label.parent = parent; 147 | } else { 148 | // TODO should be able to remove this when we incrementally update low lim 149 | delete label.parent; 150 | } 151 | 152 | return nextLim; 153 | } 154 | 155 | function leaveEdge(tree) { 156 | return _.find(tree.edges(), function(e) { 157 | return tree.edge(e).cutvalue < 0; 158 | }); 159 | } 160 | 161 | function enterEdge(t, g, edge) { 162 | var v = edge.v; 163 | var w = edge.w; 164 | 165 | // For the rest of this function we assume that v is the tail and w is the 166 | // head, so if we don't have this edge in the graph we should flip it to 167 | // match the correct orientation. 168 | if (!g.hasEdge(v, w)) { 169 | v = edge.w; 170 | w = edge.v; 171 | } 172 | 173 | var vLabel = t.node(v); 174 | var wLabel = t.node(w); 175 | var tailLabel = vLabel; 176 | var flip = false; 177 | 178 | // If the root is in the tail of the edge then we need to flip the logic that 179 | // checks for the head and tail nodes in the candidates function below. 180 | if (vLabel.lim > wLabel.lim) { 181 | tailLabel = wLabel; 182 | flip = true; 183 | } 184 | 185 | var candidates = _.filter(g.edges(), function(edge) { 186 | return flip === isDescendant(t, t.node(edge.v), tailLabel) && 187 | flip !== isDescendant(t, t.node(edge.w), tailLabel); 188 | }); 189 | 190 | return _.minBy(candidates, function(edge) { return slack(g, edge); }); 191 | } 192 | 193 | function exchangeEdges(t, g, e, f) { 194 | var v = e.v; 195 | var w = e.w; 196 | t.removeEdge(v, w); 197 | t.setEdge(f.v, f.w, {}); 198 | initLowLimValues(t); 199 | initCutValues(t, g); 200 | updateRanks(t, g); 201 | } 202 | 203 | function updateRanks(t, g) { 204 | var root = _.find(t.nodes(), function(v) { return !g.node(v).parent; }); 205 | var vs = preorder(t, root); 206 | vs = vs.slice(1); 207 | _.forEach(vs, function(v) { 208 | var parent = t.node(v).parent, 209 | edge = g.edge(v, parent), 210 | flipped = false; 211 | 212 | if (!edge) { 213 | edge = g.edge(parent, v); 214 | flipped = true; 215 | } 216 | 217 | g.node(v).rank = g.node(parent).rank + (flipped ? edge.minlen : -edge.minlen); 218 | }); 219 | } 220 | 221 | /* 222 | * Returns true if the edge is in the tree. 223 | */ 224 | function isTreeEdge(tree, u, v) { 225 | return tree.hasEdge(u, v); 226 | } 227 | 228 | /* 229 | * Returns true if the specified node is descendant of the root node per the 230 | * assigned low and lim attributes in the tree. 231 | */ 232 | function isDescendant(tree, vLabel, rootLabel) { 233 | return rootLabel.low <= vLabel.lim && vLabel.lim <= rootLabel.lim; 234 | } 235 | -------------------------------------------------------------------------------- /lib/rank/util.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _ = require("../lodash"); 4 | 5 | module.exports = { 6 | longestPath: longestPath, 7 | longestPathWithLayer: longestPathWithLayer, 8 | slack: slack, 9 | }; 10 | 11 | /* 12 | * Initializes ranks for the input graph using the longest path algorithm. This 13 | * algorithm scales well and is fast in practice, it yields rather poor 14 | * solutions. Nodes are pushed to the lowest layer possible, leaving the bottom 15 | * ranks wide and leaving edges longer than necessary. However, due to its 16 | * speed, this algorithm is good for getting an initial ranking that can be fed 17 | * into other algorithms. 18 | * 19 | * This algorithm does not normalize layers because it will be used by other 20 | * algorithms in most cases. If using this algorithm directly, be sure to 21 | * run normalize at the end. 22 | * 23 | * Pre-conditions: 24 | * 25 | * 1. Input graph is a DAG. 26 | * 2. Input graph node labels can be assigned properties. 27 | * 28 | * Post-conditions: 29 | * 30 | * 1. Each node will be assign an (unnormalized) "rank" property. 31 | */ 32 | function longestPath(g) { 33 | var visited = {}; 34 | 35 | function dfs(v) { 36 | var label = g.node(v); 37 | if (_.has(visited, v)) { 38 | return label.rank; 39 | } 40 | visited[v] = true; 41 | 42 | var rank = _.min(_.map(g.outEdges(v), function(e) { 43 | return dfs(e.w) - g.edge(e).minlen; 44 | })); 45 | 46 | if (rank === Number.POSITIVE_INFINITY || // return value of _.map([]) for Lodash 3 47 | rank === undefined || // return value of _.map([]) for Lodash 4 48 | rank === null) { // return value of _.map([null]) 49 | rank = 0; 50 | } 51 | 52 | return (label.rank = rank); 53 | } 54 | 55 | _.forEach(g.sources(), dfs); 56 | } 57 | 58 | function longestPathWithLayer(g) { 59 | // 用longest path,找出最深的点 60 | var visited = {}; 61 | var minRank = 0; 62 | 63 | function dfs(v) { 64 | var label = g.node(v); 65 | if (_.has(visited, v)) { 66 | return label.rank; 67 | } 68 | visited[v] = true; 69 | 70 | var rank = _.min(_.map(g.outEdges(v), function(e) { 71 | return dfs(e.w) - g.edge(e).minlen; 72 | })); 73 | 74 | if (rank === Number.POSITIVE_INFINITY || // return value of _.map([]) for Lodash 3 75 | rank === undefined || // return value of _.map([]) for Lodash 4 76 | rank === null) { // return value of _.map([null]) 77 | rank = 0; 78 | } 79 | 80 | label.rank = rank; 81 | minRank = Math.min(label.rank, minRank); 82 | return label.rank; 83 | } 84 | 85 | _.forEach(g.sources(), dfs); 86 | 87 | minRank += 1; // NOTE: 最小的层级是dummy root,+1 88 | 89 | // forward一遍,赋值层级 90 | function dfsForward(v, nextRank) { 91 | var label = g.node(v); 92 | 93 | var currRank = !isNaN(label.layer) ? label.layer : nextRank; 94 | 95 | // 没有指定,取最大值 96 | if (label.rank === undefined || label.rank < currRank) { 97 | label.rank = currRank; 98 | } 99 | 100 | // DFS遍历子节点 101 | _.map(g.outEdges(v), function (e) { 102 | dfsForward(e.w, currRank + g.edge(e).minlen); 103 | }); 104 | } 105 | 106 | // 指定层级的,更新下游 107 | g.nodes().forEach(function (n) { 108 | var label = g.node(n); 109 | if (!isNaN(label.layer)) { 110 | dfsForward(n, label.layer); // 默认的dummy root所在层的rank是-1 111 | } else { 112 | label.rank -= minRank; 113 | } 114 | }); 115 | 116 | // g.sources().forEach(function (root) { 117 | // dfsForward(root, -1); // 默认的dummy root所在层的rank是-1 118 | // }); 119 | 120 | // 不这样做了,赋值的层级只影响下游 121 | /* 122 | // backward一遍,把父节点收紧 123 | function dfsBackward(v) { 124 | var label = g.node(v); 125 | 126 | // 有指定layer,不改动 127 | if (!isNaN(label.layer)) { 128 | label.rank = label.layer; 129 | return label.rank; 130 | } 131 | 132 | // 其它 133 | var rank = _.min(_.map(g.outEdges(v), function(e) { 134 | return dfsBackward(e.w) - g.edge(e).minlen; 135 | })); 136 | 137 | if (!isNaN(rank)) { 138 | label.rank = rank; 139 | } 140 | 141 | return label.rank; 142 | } 143 | 144 | _.forEach(g.sources(), dfsBackward); 145 | */ 146 | } 147 | 148 | /* 149 | * Returns the amount of slack for the given edge. The slack is defined as the 150 | * difference between the length of the edge and its minimum length. 151 | */ 152 | function slack(g, e) { 153 | return g.node(e.w).rank - g.node(e.v).rank - g.edge(e).minlen; 154 | } 155 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | /* eslint "no-console": off */ 2 | 3 | "use strict"; 4 | 5 | var _ = require("./lodash"); 6 | var Graph = require("./graphlib").Graph; 7 | 8 | module.exports = { 9 | addDummyNode: addDummyNode, 10 | simplify: simplify, 11 | asNonCompoundGraph: asNonCompoundGraph, 12 | successorWeights: successorWeights, 13 | predecessorWeights: predecessorWeights, 14 | intersectRect: intersectRect, 15 | buildLayerMatrix: buildLayerMatrix, 16 | normalizeRanks: normalizeRanks, 17 | removeEmptyRanks: removeEmptyRanks, 18 | addBorderNode: addBorderNode, 19 | maxRank: maxRank, 20 | partition: partition, 21 | time: time, 22 | notime: notime 23 | }; 24 | 25 | /* 26 | * Adds a dummy node to the graph and return v. 27 | */ 28 | function addDummyNode(g, type, attrs, name) { 29 | var v; 30 | do { 31 | v = _.uniqueId(name); 32 | } while (g.hasNode(v)); 33 | 34 | attrs.dummy = type; 35 | g.setNode(v, attrs); 36 | return v; 37 | } 38 | 39 | /* 40 | * Returns a new graph with only simple edges. Handles aggregation of data 41 | * associated with multi-edges. 42 | */ 43 | function simplify(g) { 44 | var simplified = new Graph().setGraph(g.graph()); 45 | _.forEach(g.nodes(), function(v) { simplified.setNode(v, g.node(v)); }); 46 | _.forEach(g.edges(), function(e) { 47 | var simpleLabel = simplified.edge(e.v, e.w) || { weight: 0, minlen: 1 }; 48 | var label = g.edge(e); 49 | simplified.setEdge(e.v, e.w, { 50 | weight: simpleLabel.weight + label.weight, 51 | minlen: Math.max(simpleLabel.minlen, label.minlen) 52 | }); 53 | }); 54 | return simplified; 55 | } 56 | 57 | function asNonCompoundGraph(g) { 58 | var simplified = new Graph({ multigraph: g.isMultigraph() }).setGraph(g.graph()); 59 | _.forEach(g.nodes(), function(v) { 60 | if (!g.children(v).length) { 61 | simplified.setNode(v, g.node(v)); 62 | } 63 | }); 64 | _.forEach(g.edges(), function(e) { 65 | simplified.setEdge(e, g.edge(e)); 66 | }); 67 | return simplified; 68 | } 69 | 70 | function successorWeights(g) { 71 | var weightMap = _.map(g.nodes(), function(v) { 72 | var sucs = {}; 73 | _.forEach(g.outEdges(v), function(e) { 74 | sucs[e.w] = (sucs[e.w] || 0) + g.edge(e).weight; 75 | }); 76 | return sucs; 77 | }); 78 | return _.zipObject(g.nodes(), weightMap); 79 | } 80 | 81 | function predecessorWeights(g) { 82 | var weightMap = _.map(g.nodes(), function(v) { 83 | var preds = {}; 84 | _.forEach(g.inEdges(v), function(e) { 85 | preds[e.v] = (preds[e.v] || 0) + g.edge(e).weight; 86 | }); 87 | return preds; 88 | }); 89 | return _.zipObject(g.nodes(), weightMap); 90 | } 91 | 92 | /* 93 | * Finds where a line starting at point ({x, y}) would intersect a rectangle 94 | * ({x, y, width, height}) if it were pointing at the rectangle's center. 95 | */ 96 | function intersectRect(rect, point) { 97 | var x = rect.x; 98 | var y = rect.y; 99 | 100 | // Rectangle intersection algorithm from: 101 | // http://math.stackexchange.com/questions/108113/find-edge-between-two-boxes 102 | var dx = point.x - x; 103 | var dy = point.y - y; 104 | var w = rect.width / 2; 105 | var h = rect.height / 2; 106 | 107 | if (!dx && !dy) { 108 | throw new Error("Not possible to find intersection inside of the rectangle"); 109 | } 110 | 111 | var sx, sy; 112 | if (Math.abs(dy) * w > Math.abs(dx) * h) { 113 | // Intersection is top or bottom of rect. 114 | if (dy < 0) { 115 | h = -h; 116 | } 117 | sx = h * dx / dy; 118 | sy = h; 119 | } else { 120 | // Intersection is left or right of rect. 121 | if (dx < 0) { 122 | w = -w; 123 | } 124 | sx = w; 125 | sy = w * dy / dx; 126 | } 127 | 128 | return { x: x + sx, y: y + sy }; 129 | } 130 | 131 | /* 132 | * Given a DAG with each node assigned "rank" and "order" properties, this 133 | * function will produce a matrix with the ids of each node. 134 | */ 135 | function buildLayerMatrix(g) { 136 | var layering = _.map(_.range(maxRank(g) + 1), function() { return []; }); 137 | _.forEach(g.nodes(), function(v) { 138 | var node = g.node(v); 139 | var rank = node.rank; 140 | if (!_.isUndefined(rank)) { 141 | layering[rank][node.order] = v; 142 | } 143 | }); 144 | return layering; 145 | } 146 | 147 | /* 148 | * Adjusts the ranks for all nodes in the graph such that all nodes v have 149 | * rank(v) >= 0 and at least one node w has rank(w) = 0. 150 | */ 151 | function normalizeRanks(g) { 152 | var min = _.min(_.map(g.nodes(), function(v) { return g.node(v).rank; })); 153 | _.forEach(g.nodes(), function(v) { 154 | var node = g.node(v); 155 | if (_.has(node, "rank")) { 156 | node.rank -= min; 157 | } 158 | }); 159 | } 160 | 161 | function removeEmptyRanks(g) { 162 | // Ranks may not start at 0, so we need to offset them 163 | var offset = _.min(_.map(g.nodes(), function(v) { return g.node(v).rank; })); 164 | 165 | var layers = []; 166 | _.forEach(g.nodes(), function(v) { 167 | var rank = g.node(v).rank - offset; 168 | if (!layers[rank]) { 169 | layers[rank] = []; 170 | } 171 | layers[rank].push(v); 172 | }); 173 | 174 | var delta = 0; 175 | var nodeRankFactor = g.graph().nodeRankFactor; 176 | _.forEach(layers, function(vs, i) { 177 | if (_.isUndefined(vs) && i % nodeRankFactor !== 0) { 178 | --delta; 179 | } else if (delta) { 180 | _.forEach(vs, function(v) { g.node(v).rank += delta; }); 181 | } 182 | }); 183 | } 184 | 185 | function addBorderNode(g, prefix, rank, order) { 186 | var node = { 187 | width: 0, 188 | height: 0 189 | }; 190 | if (arguments.length >= 4) { 191 | node.rank = rank; 192 | node.order = order; 193 | } 194 | return addDummyNode(g, "border", node, prefix); 195 | } 196 | 197 | function maxRank(g) { 198 | return _.max(_.map(g.nodes(), function(v) { 199 | var rank = g.node(v).rank; 200 | if (!_.isUndefined(rank)) { 201 | return rank; 202 | } 203 | })); 204 | } 205 | 206 | /* 207 | * Partition a collection into two groups: `lhs` and `rhs`. If the supplied 208 | * function returns true for an entry it goes into `lhs`. Otherwise it goes 209 | * into `rhs. 210 | */ 211 | function partition(collection, fn) { 212 | var result = { lhs: [], rhs: [] }; 213 | _.forEach(collection, function(value) { 214 | if (fn(value)) { 215 | result.lhs.push(value); 216 | } else { 217 | result.rhs.push(value); 218 | } 219 | }); 220 | return result; 221 | } 222 | 223 | /* 224 | * Returns a new function that wraps `fn` with a timer. The wrapper logs the 225 | * time it takes to execute the function. 226 | */ 227 | function time(name, fn) { 228 | var start = _.now(); 229 | try { 230 | return fn(); 231 | } finally { 232 | console.log(name + " time: " + (_.now() - start) + "ms"); 233 | } 234 | } 235 | 236 | function notime(name, fn) { 237 | return fn(); 238 | } 239 | -------------------------------------------------------------------------------- /lib/version.js: -------------------------------------------------------------------------------- 1 | module.exports = "0.1.1"; 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dagrejs", 3 | "version": "0.2.1", 4 | "description": "Layered layout for directed acyclic graph", 5 | "license": "MIT", 6 | "main": "index.js", 7 | "keywords": [ 8 | "graph", 9 | "layout" 10 | ], 11 | "dependencies": { 12 | "graphlib": "^2.1.8", 13 | "lodash": "^4.17.19" 14 | }, 15 | "devDependencies": { 16 | "benchmark": "2.1.4", 17 | "browserify": "16.5.1", 18 | "chai": "4.2.0", 19 | "eslint": "7.4.0", 20 | "jshint": "2.11.1", 21 | "jshint-stylish": "2.2.1", 22 | "karma": "5.1.0", 23 | "karma-chrome-launcher": "3.1.0", 24 | "karma-firefox-launcher": "1.3.0", 25 | "karma-mocha": "2.0.1", 26 | "karma-phantomjs-launcher": "1.0.4", 27 | "karma-requirejs": "1.1.0", 28 | "karma-safari-launcher": "1.0.0", 29 | "mocha": "8.2.0", 30 | "phantomjs-prebuilt": "2.1.16", 31 | "requirejs": "2.3.6", 32 | "semver": "7.3.2", 33 | "sprintf": "0.1.5", 34 | "uglify-js": "3.10.0" 35 | }, 36 | "repository": { 37 | "type": "git", 38 | "url": "https://github.com/brickmaker/dagre.git" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/bench.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var Benchmark = require("benchmark"), 4 | sprintf = require("sprintf").sprintf; 5 | 6 | var Graph = require("graphlib").Graph, 7 | rank = require("../lib/rank"), 8 | layout = require("..").layout; 9 | 10 | function runBenchmark(name, fn) { 11 | var options = {}; 12 | options.onComplete = function(bench) { 13 | var target = bench.target, 14 | hz = target.hz, 15 | stats = target.stats, 16 | rme = stats.rme, 17 | samples = stats.sample.length, 18 | msg = sprintf(" %25s: %13s ops/sec \xb1 %s%% (%3d run(s) sampled)", 19 | target.name, 20 | Benchmark.formatNumber(hz.toFixed(2)), 21 | rme.toFixed(2), 22 | samples); 23 | console.log(msg); 24 | }; 25 | options.onError = function(bench) { 26 | console.error(" " + bench.target.error); 27 | }; 28 | options.setup = function() { 29 | this.count = Math.random() * 1000; 30 | this.nextInt = function(range) { 31 | return Math.floor(this.count++ % range ); 32 | }; 33 | }; 34 | new Benchmark(name, fn, options).run(); 35 | } 36 | 37 | var g = new Graph() 38 | .setGraph({}) 39 | .setDefaultNodeLabel(function() { return { width: 1, height: 1}; }) 40 | .setDefaultEdgeLabel(function() { return { minlen: 1, weight: 1 }; }) 41 | .setPath(["a", "b", "c", "d", "h"]) 42 | .setPath(["a", "e", "g", "h"]) 43 | .setPath(["a", "f", "g"]); 44 | 45 | runBenchmark("longest-path ranker", function() { 46 | g.graph().ranker = "longest-path"; 47 | rank(g); 48 | }); 49 | 50 | runBenchmark("tight-tree ranker", function() { 51 | g.graph().ranker = "tight-tree"; 52 | rank(g); 53 | }); 54 | 55 | runBenchmark("network-simplex ranker", function() { 56 | g.graph().ranker = "network-simplex"; 57 | rank(g); 58 | }); 59 | 60 | runBenchmark("layout", function() { 61 | delete g.graph().ranker; 62 | layout(g); 63 | }); 64 | -------------------------------------------------------------------------------- /src/release/bump-version.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* 4 | * Bumps the minor version and sets the prelease tag. 5 | */ 6 | 7 | var fs = require("fs"), 8 | semver = require("semver"); 9 | 10 | var packageFile = fs.readFileSync("package.json"); 11 | var packageJson = JSON.parse(packageFile); 12 | 13 | if (!("version" in packageJson)) { 14 | bail("ERROR: Could not find version in package.json"); 15 | } 16 | 17 | var ver = semver.parse(packageJson.version); 18 | packageJson.version = ver.inc("patch").toString() + "-pre"; 19 | 20 | fs.writeFileSync("package.json", JSON.stringify(packageJson, undefined, 2)); 21 | 22 | // Write an error message to stderr and then exit immediately with an error. 23 | function bail(msg) { 24 | stderr.write(msg + "\n"); 25 | process.exit(1); 26 | } 27 | -------------------------------------------------------------------------------- /src/release/check-version.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* 4 | * Prints the current version from the specified package-file to stdout or 5 | * fails with an error if either the version cannot be determined or it is 6 | * a pre-release. 7 | */ 8 | 9 | var fs = require("fs"), 10 | semver = require("semver"); 11 | 12 | var packageFile = fs.readFileSync("package.json"); 13 | var packageJson = JSON.parse(packageFile); 14 | 15 | if (!("version" in packageJson)) { 16 | bail("ERROR: Could not find version in package.json"); 17 | } 18 | 19 | var ver = semver.parse(packageJson.version), 20 | preRelease = process.env.PRE_RELEASE; 21 | 22 | if (ver.prerelease.length > 0 && !preRelease) { 23 | bail("ERROR: version is a pre-release: " + ver); 24 | } 25 | 26 | console.log(ver.toString()); 27 | 28 | // Write an error message to stderr and then exit immediately with an error. 29 | function bail(msg) { 30 | process.stderr.write(msg + "\n"); 31 | process.exit(1); 32 | } 33 | -------------------------------------------------------------------------------- /src/release/make-bower.json.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // Renders the bower.json template and prints it to stdout 4 | 5 | var packageJson = require("../../package.json"); 6 | var packageNameParts = packageJson.name.split("/"); 7 | var packageName = packageNameParts[packageNameParts.length - 1]; 8 | 9 | var template = { 10 | name: packageName, 11 | version: packageJson.version, 12 | main: ["dist/" + packageName + ".core.js"], 13 | ignore: [ 14 | ".*", 15 | "README.md", 16 | "CHANGELOG.md", 17 | "Makefile", 18 | "browser.js", 19 | "dist/" + packageName + ".js", 20 | "dist/" + packageName + ".min.js", 21 | "index.js", 22 | "karma*", 23 | "lib/**", 24 | "package.json", 25 | "src/**", 26 | "test/**" 27 | ], 28 | dependencies: packageJson.dependencies 29 | }; 30 | 31 | console.log(JSON.stringify(template, null, 2)); 32 | -------------------------------------------------------------------------------- /src/release/make-version.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var package = require("../../package.json"); 4 | console.log("module.exports = \"" + package.version + "\";"); 5 | -------------------------------------------------------------------------------- /src/release/release.sh: -------------------------------------------------------------------------------- 1 | # Fail on error 2 | set -e 3 | [ -n "$DEBUG"] && set -x 4 | 5 | bail() { 6 | echo $1 >&2 7 | exit 1 8 | } 9 | 10 | # Initial config 11 | PROJECT=$1 12 | PROJECT_ROOT=`pwd` 13 | PAGES_DIR=/tmp/$PROJECT-pages 14 | DIST_DIR=$2 15 | 16 | # Check version. Is this a release? If not abort 17 | VERSION=$(./src/release/check-version.js) 18 | SHORT_VERSION=$(echo $VERSION | cut -f1 -d-) 19 | 20 | echo Attemping to publish version: $VERSION 21 | 22 | # Preflight checks 23 | [ -n "$PROJECT" ] || bail "No project name was specified." 24 | [ -n "$DIST_DIR" ] || bail "No dist dir was specified." 25 | [ -z "`git tag -l v$VERSION`" ] || bail "Version already published. Skipping publish." 26 | [ "`git rev-parse HEAD`" = "`git rev-parse master`" ] || [ -n "$PRE_RELEASE" ] || bail "ERROR: You must release from the master branch" 27 | [ -z "`git status --porcelain`" ] || bail "ERROR: Dirty index on working tree. Use git status to check" 28 | 29 | # Publish to pages 30 | rm -rf $PAGES_DIR 31 | git clone git@github.com:dagrejs/dagrejs.github.io.git $PAGES_DIR 32 | 33 | TMP_TARGET=$PAGES_DIR/project/$PROJECT/latest 34 | rm -rf $TMP_TARGET 35 | mkdir -p $TMP_TARGET 36 | cp -r $DIST_DIR/*.js $TMP_TARGET 37 | 38 | TMP_TARGET=$PAGES_DIR/project/$PROJECT/v$VERSION 39 | rm -rf $TMP_TARGET 40 | mkdir -p $TMP_TARGET 41 | cp -r $DIST_DIR/*.js $TMP_TARGET 42 | 43 | cd $PAGES_DIR/project/$PROJECT 44 | git add -A 45 | git commit -m "Publishing $PROJECT v$VERSION" 46 | git push -f origin master 47 | cd $PROJECT_ROOT 48 | echo "Published $PROJECT to pages" 49 | 50 | # Publish tag 51 | git tag v$VERSION 52 | git push origin 53 | git push origin v$VERSION 54 | echo Published $PROJECT v$VERSION 55 | 56 | # Publish to npm 57 | npm publish --access=public 58 | echo Published to npm 59 | 60 | # Update patch level version + commit 61 | ./src/release/bump-version.js 62 | make lib/version.js 63 | git commit package.json lib/version.js -m "Bump version and set as pre-release" 64 | git push origin 65 | echo Updated patch version 66 | 67 | echo Release complete! 68 | -------------------------------------------------------------------------------- /test/acyclic-test.js: -------------------------------------------------------------------------------- 1 | var _ = require("lodash"); 2 | var expect = require("./chai").expect; 3 | var acyclic = require("../lib/acyclic"); 4 | var Graph = require("../lib/graphlib").Graph; 5 | var findCycles = require("../lib/graphlib").alg.findCycles; 6 | 7 | describe("acyclic", function() { 8 | var ACYCLICERS = [ 9 | "greedy", 10 | "dfs", 11 | "unknown-should-still-work" 12 | ]; 13 | var g; 14 | 15 | beforeEach(function() { 16 | g = new Graph({ multigraph: true }) 17 | .setDefaultEdgeLabel(function() { return { minlen: 1, weight: 1 }; }); 18 | }); 19 | 20 | _.forEach(ACYCLICERS, function(acyclicer) { 21 | describe(acyclicer, function() { 22 | beforeEach(function() { 23 | g.setGraph({ acyclicer: acyclicer }); 24 | }); 25 | 26 | describe("run", function() { 27 | it("does not change an already acyclic graph", function() { 28 | g.setPath(["a", "b", "d"]); 29 | g.setPath(["a", "c", "d"]); 30 | acyclic.run(g); 31 | var results = _.map(g.edges(), stripLabel); 32 | expect(_.sortBy(results, ["v", "w"])).to.eql([ 33 | { v: "a", w: "b" }, 34 | { v: "a", w: "c" }, 35 | { v: "b", w: "d" }, 36 | { v: "c", w: "d" } 37 | ]); 38 | }); 39 | 40 | it("breaks cycles in the input graph", function() { 41 | g.setPath(["a", "b", "c", "d", "a"]); 42 | acyclic.run(g); 43 | expect(findCycles(g)).to.eql([]); 44 | }); 45 | 46 | it("creates a multi-edge where necessary", function() { 47 | g.setPath(["a", "b", "a"]); 48 | acyclic.run(g); 49 | expect(findCycles(g)).to.eql([]); 50 | if (g.hasEdge("a", "b")) { 51 | expect(g.outEdges("a", "b")).to.have.length(2); 52 | } else { 53 | expect(g.outEdges("b", "a")).to.have.length(2); 54 | } 55 | expect(g.edgeCount()).to.equal(2); 56 | }); 57 | }); 58 | 59 | describe("undo", function() { 60 | it("does not change edges where the original graph was acyclic", function() { 61 | g.setEdge("a", "b", { minlen: 2, weight: 3 }); 62 | acyclic.run(g); 63 | acyclic.undo(g); 64 | expect(g.edge("a", "b")).to.eql({ minlen: 2, weight: 3 }); 65 | expect(g.edges()).to.have.length(1); 66 | }); 67 | 68 | it("can restore previosuly reversed edges", function() { 69 | g.setEdge("a", "b", { minlen: 2, weight: 3 }); 70 | g.setEdge("b", "a", { minlen: 3, weight: 4 }); 71 | acyclic.run(g); 72 | acyclic.undo(g); 73 | expect(g.edge("a", "b")).to.eql({ minlen: 2, weight: 3 }); 74 | expect(g.edge("b", "a")).to.eql({ minlen: 3, weight: 4 }); 75 | expect(g.edges()).to.have.length(2); 76 | }); 77 | }); 78 | }); 79 | }); 80 | 81 | describe("greedy-specific functionality", function() { 82 | it("prefers to break cycles at low-weight edges", function() { 83 | g.setGraph({ acyclicer: "greedy" }); 84 | g.setDefaultEdgeLabel(function() { return { minlen: 1, weight: 2 }; }); 85 | g.setPath(["a", "b", "c", "d", "a"]); 86 | g.setEdge("c", "d", { weight: 1 }); 87 | acyclic.run(g); 88 | expect(findCycles(g)).to.eql([]); 89 | expect(g.hasEdge("c", "d")).to.be.false; 90 | }); 91 | }); 92 | }); 93 | 94 | function stripLabel(edge) { 95 | var c = _.clone(edge); 96 | delete c.label; 97 | return c; 98 | } 99 | -------------------------------------------------------------------------------- /test/add-border-segments-test.js: -------------------------------------------------------------------------------- 1 | var expect = require("./chai").expect; 2 | var addBorderSegments = require("../lib/add-border-segments"); 3 | var Graph = require("../lib/graphlib").Graph; 4 | 5 | describe("addBorderSegments", function() { 6 | var g; 7 | 8 | beforeEach(function() { 9 | g = new Graph({ compound: true }); 10 | }); 11 | 12 | it("does not add border nodes for a non-compound graph", function() { 13 | var g = new Graph(); 14 | g.setNode("a", { rank: 0 }); 15 | addBorderSegments(g); 16 | expect(g.nodeCount()).to.equal(1); 17 | expect(g.node("a")).to.eql({ rank: 0 }); 18 | }); 19 | 20 | it("does not add border nodes for a graph with no clusters", function() { 21 | g.setNode("a", { rank: 0 }); 22 | addBorderSegments(g); 23 | expect(g.nodeCount()).to.equal(1); 24 | expect(g.node("a")).to.eql({ rank: 0 }); 25 | }); 26 | 27 | it("adds a border for a single-rank subgraph", function() { 28 | g.setNode("sg", { minRank: 1, maxRank: 1 }); 29 | addBorderSegments(g); 30 | 31 | var bl = g.node("sg").borderLeft[1]; 32 | var br = g.node("sg").borderRight[1]; 33 | expect(g.node(bl)).eqls({ 34 | dummy: "border", borderType: "borderLeft", 35 | rank: 1, width: 0, height: 0 }); 36 | expect(g.parent(bl)).equals("sg"); 37 | expect(g.node(br)).eqls({ 38 | dummy: "border", borderType: "borderRight", 39 | rank: 1, width: 0, height: 0 }); 40 | expect(g.parent(br)).equals("sg"); 41 | }); 42 | 43 | it("adds a border for a multi-rank subgraph", function() { 44 | g.setNode("sg", { minRank: 1, maxRank: 2 }); 45 | addBorderSegments(g); 46 | 47 | var sgNode = g.node("sg"); 48 | var bl2 = sgNode.borderLeft[1]; 49 | var br2 = sgNode.borderRight[1]; 50 | expect(g.node(bl2)).eqls({ 51 | dummy: "border", borderType: "borderLeft", 52 | rank: 1, width: 0, height: 0 }); 53 | expect(g.parent(bl2)).equals("sg"); 54 | expect(g.node(br2)).eqls({ 55 | dummy: "border", borderType: "borderRight", 56 | rank: 1, width: 0, height: 0 }); 57 | expect(g.parent(br2)).equals("sg"); 58 | 59 | var bl1 = sgNode.borderLeft[2]; 60 | var br1 = sgNode.borderRight[2]; 61 | expect(g.node(bl1)).eqls({ 62 | dummy: "border", borderType: "borderLeft", 63 | rank: 2, width: 0, height: 0 }); 64 | expect(g.parent(bl1)).equals("sg"); 65 | expect(g.node(br1)).eqls({ 66 | dummy: "border", borderType: "borderRight", 67 | rank: 2, width: 0, height: 0 }); 68 | expect(g.parent(br1)).equals("sg"); 69 | 70 | expect(g.hasEdge(sgNode.borderLeft[1], sgNode.borderLeft[2])).to.be.true; 71 | expect(g.hasEdge(sgNode.borderRight[1], sgNode.borderRight[2])).to.be.true; 72 | }); 73 | 74 | it("adds borders for nested subgraphs", function() { 75 | g.setNode("sg1", { minRank: 1, maxRank: 1 }); 76 | g.setNode("sg2", { minRank: 1, maxRank: 1 }); 77 | g.setParent("sg2", "sg1"); 78 | addBorderSegments(g); 79 | 80 | var bl1 = g.node("sg1").borderLeft[1]; 81 | var br1 = g.node("sg1").borderRight[1]; 82 | expect(g.node(bl1)).eqls({ 83 | dummy: "border", borderType: "borderLeft", 84 | rank: 1, width: 0, height: 0 }); 85 | expect(g.parent(bl1)).equals("sg1"); 86 | expect(g.node(br1)).eqls({ 87 | dummy: "border", borderType: "borderRight", 88 | rank: 1, width: 0, height: 0 }); 89 | expect(g.parent(br1)).equals("sg1"); 90 | 91 | var bl2 = g.node("sg2").borderLeft[1]; 92 | var br2 = g.node("sg2").borderRight[1]; 93 | expect(g.node(bl2)).eqls({ 94 | dummy: "border", borderType: "borderLeft", 95 | rank: 1, width: 0, height: 0 }); 96 | expect(g.parent(bl2)).equals("sg2"); 97 | expect(g.node(br2)).eqls({ 98 | dummy: "border", borderType: "borderRight", 99 | rank: 1, width: 0, height: 0 }); 100 | expect(g.parent(br2)).equals("sg2"); 101 | }); 102 | }); 103 | -------------------------------------------------------------------------------- /test/bundle-test.js: -------------------------------------------------------------------------------- 1 | /* global chai, dagre */ 2 | 3 | // These are smoke tests to make sure the bundles look like they are working 4 | // correctly. 5 | 6 | var expect = chai.expect; 7 | var graphlib = dagre.graphlib; 8 | 9 | describe("bundle", function() { 10 | it("exports dagre", function() { 11 | expect(dagre).to.be.an("object"); 12 | expect(dagre.graphlib).to.be.an("object"); 13 | expect(dagre.layout).to.be.a("function"); 14 | expect(dagre.util).to.be.an("object"); 15 | expect(dagre.version).to.be.a("string"); 16 | }); 17 | 18 | it("can do trivial layout", function() { 19 | var g = new graphlib.Graph().setGraph({}); 20 | g.setNode("a", { label: "a", width: 50, height: 100 }); 21 | g.setNode("b", { label: "b", width: 50, height: 100 }); 22 | g.setEdge("a", "b", { label: "ab", width: 50, height: 100 }); 23 | 24 | dagre.layout(g); 25 | expect(g.node("a")).to.have.property("x"); 26 | expect(g.node("a")).to.have.property("y"); 27 | expect(g.node("a").x).to.be.gte(0); 28 | expect(g.node("a").y).to.be.gte(0); 29 | expect(g.edge("a", "b")).to.have.property("x"); 30 | expect(g.edge("a", "b")).to.have.property("y"); 31 | expect(g.edge("a", "b").x).to.be.gte(0); 32 | expect(g.edge("a", "b").y).to.be.gte(0); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /test/chai.js: -------------------------------------------------------------------------------- 1 | var chai = require("chai"); 2 | 3 | module.exports = chai; 4 | 5 | chai.config.includeStack = true; 6 | 7 | /* 8 | * Fix Chai"s `notProperty` which passes when an object has a property but its 9 | * value is undefined. 10 | */ 11 | chai.assert.notProperty = function(obj, prop) { 12 | chai.assert(!(prop in obj), "Found prop " + prop + " in " + obj + " with value " + obj[prop]); 13 | }; 14 | -------------------------------------------------------------------------------- /test/console.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dagre Debug Console 5 | 6 | 7 | 8 | 9 | 10 | 11 | 46 | 47 | 68 | 69 |
70 |

Dagre Debug Console

71 | 72 |
73 | 74 | Enable timing instrumentation 75 |
76 | 77 |
78 | 79 | 80 | 83 | 84 | 85 | 86 | 87 |
88 |
89 | 90 | 245 | -------------------------------------------------------------------------------- /test/coordinate-system-test.js: -------------------------------------------------------------------------------- 1 | var Graph = require("../lib/graphlib").Graph; 2 | var coordinateSystem = require("../lib/coordinate-system"); 3 | var expect = require("./chai").expect; 4 | 5 | describe("coordinateSystem", function() { 6 | var g; 7 | 8 | beforeEach(function() { 9 | g = new Graph(); 10 | }); 11 | 12 | describe("coordinateSystem.adjust", function() { 13 | beforeEach(function() { 14 | g.setNode("a", { width: 100, height: 200 }); 15 | }); 16 | 17 | it("does nothing to node dimensions with rankdir = TB", function() { 18 | g.setGraph({ rankdir: "TB" }); 19 | coordinateSystem.adjust(g); 20 | expect(g.node("a")).eqls({ width: 100, height: 200 }); 21 | }); 22 | 23 | it("does nothing to node dimensions with rankdir = BT", function() { 24 | g.setGraph({ rankdir: "BT" }); 25 | coordinateSystem.adjust(g); 26 | expect(g.node("a")).eqls({ width: 100, height: 200 }); 27 | }); 28 | 29 | it("swaps width and height for nodes with rankdir = LR", function() { 30 | g.setGraph({ rankdir: "LR" }); 31 | coordinateSystem.adjust(g); 32 | expect(g.node("a")).eqls({ width: 200, height: 100 }); 33 | }); 34 | 35 | it("swaps width and height for nodes with rankdir = RL", function() { 36 | g.setGraph({ rankdir: "RL" }); 37 | coordinateSystem.adjust(g); 38 | expect(g.node("a")).eqls({ width: 200, height: 100 }); 39 | }); 40 | }); 41 | 42 | describe("coordinateSystem.undo", function() { 43 | beforeEach(function() { 44 | g.setNode("a", { width: 100, height: 200, x: 20, y: 40 }); 45 | }); 46 | 47 | it("does nothing to points with rankdir = TB", function() { 48 | g.setGraph({ rankdir: "TB" }); 49 | coordinateSystem.undo(g); 50 | expect(g.node("a")).eqls({ x: 20, y: 40, width: 100, height: 200 }); 51 | }); 52 | 53 | it("flips the y coordinate for points with rankdir = BT", function() { 54 | g.setGraph({ rankdir: "BT" }); 55 | coordinateSystem.undo(g); 56 | expect(g.node("a")).eqls({ x: 20, y: -40, width: 100, height: 200 }); 57 | }); 58 | 59 | it("swaps dimensions and coordinates for points with rankdir = LR", function() { 60 | g.setGraph({ rankdir: "LR" }); 61 | coordinateSystem.undo(g); 62 | expect(g.node("a")).eqls({ x: 40, y: 20, width: 200, height: 100 }); 63 | }); 64 | 65 | it("swaps dims and coords and flips x for points with rankdir = RL", function() { 66 | g.setGraph({ rankdir: "RL" }); 67 | coordinateSystem.undo(g); 68 | expect(g.node("a")).eqls({ x: -40, y: 20, width: 200, height: 100 }); 69 | }); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /test/data/list-test.js: -------------------------------------------------------------------------------- 1 | var expect = require("../chai").expect; 2 | var List = require("../../lib/data/list"); 3 | 4 | describe("data.List", function() { 5 | var list; 6 | 7 | beforeEach(function() { 8 | list = new List(); 9 | }); 10 | 11 | describe("dequeue", function() { 12 | it("returns undefined with an empty list", function() { 13 | expect(list.dequeue()).to.be.undefined; 14 | }); 15 | 16 | it("unlinks and returns the first entry", function() { 17 | var obj = {}; 18 | list.enqueue(obj); 19 | expect(list.dequeue()).to.equal(obj); 20 | }); 21 | 22 | it("unlinks and returns multiple entries in FIFO order", function() { 23 | var obj1 = {}; 24 | var obj2 = {}; 25 | list.enqueue(obj1); 26 | list.enqueue(obj2); 27 | 28 | expect(list.dequeue()).to.equal(obj1); 29 | expect(list.dequeue()).to.equal(obj2); 30 | }); 31 | 32 | it("unlinks and relinks an entry if it is re-enqueued", function() { 33 | var obj1 = {}; 34 | var obj2 = {}; 35 | list.enqueue(obj1); 36 | list.enqueue(obj2); 37 | list.enqueue(obj1); 38 | 39 | expect(list.dequeue()).to.equal(obj2); 40 | expect(list.dequeue()).to.equal(obj1); 41 | }); 42 | 43 | it("unlinks and relinks an entry if it is enqueued on another list", function() { 44 | var obj = {}; 45 | var list2 = new List(); 46 | list.enqueue(obj); 47 | list2.enqueue(obj); 48 | 49 | expect(list.dequeue()).to.be.undefined; 50 | expect(list2.dequeue()).to.equal(obj); 51 | }); 52 | 53 | it("can return a string representation", function() { 54 | list.enqueue({ entry: 1 }); 55 | list.enqueue({ entry: 2 }); 56 | 57 | expect(list.toString()).to.equal("[{\"entry\":1}, {\"entry\":2}]"); 58 | }); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /test/greedy-fas-test.js: -------------------------------------------------------------------------------- 1 | var _ = require("lodash"); 2 | var expect = require("./chai").expect; 3 | var Graph = require("../lib/graphlib").Graph; 4 | var findCycles = require("../lib/graphlib").alg.findCycles; 5 | var greedyFAS = require("../lib/greedy-fas"); 6 | 7 | describe("greedyFAS", function() { 8 | var g; 9 | 10 | beforeEach(function() { 11 | g = new Graph(); 12 | }); 13 | 14 | it("returns the empty set for empty graphs", function() { 15 | expect(greedyFAS(g)).to.eql([]); 16 | }); 17 | 18 | it("returns the empty set for single-node graphs", function() { 19 | g.setNode("a"); 20 | expect(greedyFAS(g)).to.eql([]); 21 | }); 22 | 23 | it("returns an empty set if the input graph is acyclic", function() { 24 | var g = new Graph(); 25 | g.setEdge("a", "b"); 26 | g.setEdge("b", "c"); 27 | g.setEdge("b", "d"); 28 | g.setEdge("a", "e"); 29 | expect(greedyFAS(g)).to.eql([]); 30 | }); 31 | 32 | it("returns a single edge with a simple cycle", function() { 33 | var g = new Graph(); 34 | g.setEdge("a", "b"); 35 | g.setEdge("b", "a"); 36 | checkFAS(g, greedyFAS(g)); 37 | }); 38 | 39 | it("returns a single edge in a 4-node cycle", function() { 40 | var g = new Graph(); 41 | g.setEdge("n1", "n2"); 42 | g.setPath(["n2", "n3", "n4", "n5", "n2"]); 43 | g.setEdge("n3", "n5"); 44 | g.setEdge("n4", "n2"); 45 | g.setEdge("n4", "n6"); 46 | checkFAS(g, greedyFAS(g)); 47 | }); 48 | 49 | it("returns two edges for two 4-node cycles", function() { 50 | var g = new Graph(); 51 | g.setEdge("n1", "n2"); 52 | g.setPath(["n2", "n3", "n4", "n5", "n2"]); 53 | g.setEdge("n3", "n5"); 54 | g.setEdge("n4", "n2"); 55 | g.setEdge("n4", "n6"); 56 | g.setPath(["n6", "n7", "n8", "n9", "n6"]); 57 | g.setEdge("n7", "n9"); 58 | g.setEdge("n8", "n6"); 59 | g.setEdge("n8", "n10"); 60 | checkFAS(g, greedyFAS(g)); 61 | }); 62 | 63 | it("works with arbitrarily weighted edges", function() { 64 | // Our algorithm should also work for graphs with multi-edges, a graph 65 | // where more than one edge can be pointing in the same direction between 66 | // the same pair of incident nodes. We try this by assigning weights to 67 | // our edges representing the number of edges from one node to the other. 68 | 69 | var g1 = new Graph(); 70 | g1.setEdge("n1", "n2", 2); 71 | g1.setEdge("n2", "n1", 1); 72 | expect(greedyFAS(g1, weightFn(g1))).to.eql([{v: "n2", w: "n1"}]); 73 | 74 | var g2 = new Graph(); 75 | g2.setEdge("n1", "n2", 1); 76 | g2.setEdge("n2", "n1", 2); 77 | expect(greedyFAS(g2, weightFn(g2))).to.eql([{v: "n1", w: "n2"}]); 78 | }); 79 | 80 | it("works for multigraphs", function() { 81 | var g = new Graph({ multigraph: true }); 82 | g.setEdge("a", "b", 5, "foo"); 83 | g.setEdge("b", "a", 2, "bar"); 84 | g.setEdge("b", "a", 2, "baz"); 85 | expect(_.sortBy(greedyFAS(g, weightFn(g)), "name")).to.eql([ 86 | { v: "b", w: "a", name: "bar" }, 87 | { v: "b", w: "a", name: "baz" } 88 | ]); 89 | }); 90 | }); 91 | 92 | function checkFAS(g, fas) { 93 | var n = g.nodeCount(); 94 | var m = g.edgeCount(); 95 | _.forEach(fas, function(edge) { 96 | g.removeEdge(edge.v, edge.w); 97 | }); 98 | expect(findCycles(g)).to.eql([]); 99 | // The more direct m/2 - n/6 fails for the simple cycle A <-> B, where one 100 | // edge must be reversed, but the performance bound implies that only 2/3rds 101 | // of an edge can be reversed. I'm using floors to acount for this. 102 | expect(fas.length).to.be.lte(Math.floor(m/2) - Math.floor(n/6)); 103 | } 104 | 105 | function weightFn(g) { 106 | return function(e) { 107 | return g.edge(e); 108 | }; 109 | } 110 | -------------------------------------------------------------------------------- /test/nesting-graph-test.js: -------------------------------------------------------------------------------- 1 | var expect = require("./chai").expect; 2 | var Graph = require("../lib/graphlib").Graph; 3 | var components = require("../lib/graphlib").alg.components; 4 | var nestingGraph = require("../lib/nesting-graph"); 5 | 6 | describe("rank/nestingGraph", function() { 7 | var g; 8 | 9 | beforeEach(function() { 10 | g = new Graph({ compound: true }) 11 | .setGraph({}) 12 | .setDefaultNodeLabel(function() { return {}; }); 13 | }); 14 | 15 | describe("run", function() { 16 | it("connects a disconnected graph", function() { 17 | g.setNode("a"); 18 | g.setNode("b"); 19 | expect(components(g)).to.have.length(2); 20 | nestingGraph.run(g); 21 | expect(components(g)).to.have.length(1); 22 | expect(g.hasNode("a")); 23 | expect(g.hasNode("b")); 24 | }); 25 | 26 | it("adds border nodes to the top and bottom of a subgraph", function() { 27 | g.setParent("a", "sg1"); 28 | nestingGraph.run(g); 29 | 30 | var borderTop = g.node("sg1").borderTop; 31 | var borderBottom = g.node("sg1").borderBottom; 32 | expect(borderTop).to.exist; 33 | expect(borderBottom).to.exist; 34 | expect(g.parent(borderTop)).to.equal("sg1"); 35 | expect(g.parent(borderBottom)).to.equal("sg1"); 36 | expect(g.outEdges(borderTop, "a")).to.have.length(1); 37 | expect(g.edge(g.outEdges(borderTop, "a")[0]).minlen).equals(1); 38 | expect(g.outEdges("a", borderBottom)).to.have.length(1); 39 | expect(g.edge(g.outEdges("a", borderBottom)[0]).minlen).equals(1); 40 | expect(g.node(borderTop)).eqls({ width: 0, height: 0, dummy: "border" }); 41 | expect(g.node(borderBottom)).eqls({ width: 0, height: 0, dummy: "border" }); 42 | }); 43 | 44 | it("adds edges between borders of nested subgraphs", function() { 45 | g.setParent("sg2", "sg1"); 46 | g.setParent("a", "sg2"); 47 | nestingGraph.run(g); 48 | 49 | var sg1Top = g.node("sg1").borderTop; 50 | var sg1Bottom = g.node("sg1").borderBottom; 51 | var sg2Top = g.node("sg2").borderTop; 52 | var sg2Bottom = g.node("sg2").borderBottom; 53 | expect(sg1Top).to.exist; 54 | expect(sg1Bottom).to.exist; 55 | expect(sg2Top).to.exist; 56 | expect(sg2Bottom).to.exist; 57 | expect(g.outEdges(sg1Top, sg2Top)).to.have.length(1); 58 | expect(g.edge(g.outEdges(sg1Top, sg2Top)[0]).minlen).equals(1); 59 | expect(g.outEdges(sg2Bottom, sg1Bottom)).to.have.length(1); 60 | expect(g.edge(g.outEdges(sg2Bottom, sg1Bottom)[0]).minlen).equals(1); 61 | }); 62 | 63 | it("adds sufficient weight to border to node edges", function() { 64 | // We want to keep subgraphs tight, so we should ensure that the weight for 65 | // the edge between the top (and bottom) border nodes and nodes in the 66 | // subgraph have weights exceeding anything in the graph. 67 | g.setParent("x", "sg"); 68 | g.setEdge("a", "x", { weight: 100 }); 69 | g.setEdge("x", "b", { weight: 200 }); 70 | nestingGraph.run(g); 71 | 72 | var top = g.node("sg").borderTop; 73 | var bot = g.node("sg").borderBottom; 74 | expect(g.edge(top, "x").weight).to.be.gt(300); 75 | expect(g.edge("x", bot).weight).to.be.gt(300); 76 | }); 77 | 78 | it("adds an edge from the root to the tops of top-level subgraphs", function() { 79 | g.setParent("a", "sg1"); 80 | nestingGraph.run(g); 81 | 82 | var root = g.graph().nestingRoot; 83 | var borderTop = g.node("sg1").borderTop; 84 | expect(root).to.exist; 85 | expect(borderTop).to.exist; 86 | expect(g.outEdges(root, borderTop)).to.have.length(1); 87 | expect(g.hasEdge(g.outEdges(root, borderTop)[0])).to.be.true; 88 | }); 89 | 90 | it("adds an edge from root to each node with the correct minlen #1", function() { 91 | g.setNode("a"); 92 | nestingGraph.run(g); 93 | 94 | var root = g.graph().nestingRoot; 95 | expect(root).to.exist; 96 | expect(g.outEdges(root, "a")).to.have.length(1); 97 | expect(g.edge(g.outEdges(root, "a")[0])).eqls({ weight: 0, minlen: 1 }); 98 | }); 99 | 100 | it("adds an edge from root to each node with the correct minlen #2", function() { 101 | g.setParent("a", "sg1"); 102 | nestingGraph.run(g); 103 | 104 | var root = g.graph().nestingRoot; 105 | expect(root).to.exist; 106 | expect(g.outEdges(root, "a")).to.have.length(1); 107 | expect(g.edge(g.outEdges(root, "a")[0])).eqls({ weight: 0, minlen: 3 }); 108 | }); 109 | 110 | it("adds an edge from root to each node with the correct minlen #3", function() { 111 | g.setParent("sg2", "sg1"); 112 | g.setParent("a", "sg2"); 113 | nestingGraph.run(g); 114 | 115 | var root = g.graph().nestingRoot; 116 | expect(root).to.exist; 117 | expect(g.outEdges(root, "a")).to.have.length(1); 118 | expect(g.edge(g.outEdges(root, "a")[0])).eqls({ weight: 0, minlen: 5 }); 119 | }); 120 | 121 | it("does not add an edge from the root to itself", function() { 122 | g.setNode("a"); 123 | nestingGraph.run(g); 124 | 125 | var root = g.graph().nestingRoot; 126 | expect(g.outEdges(root, root)).eqls([]); 127 | }); 128 | 129 | it("expands inter-node edges to separate SG border and nodes #1", function() { 130 | g.setEdge("a", "b", { minlen: 1 }); 131 | nestingGraph.run(g); 132 | expect(g.edge("a", "b").minlen).equals(1); 133 | }); 134 | 135 | it("expands inter-node edges to separate SG border and nodes #2", function() { 136 | g.setParent("a", "sg1"); 137 | g.setEdge("a", "b", { minlen: 1 }); 138 | nestingGraph.run(g); 139 | expect(g.edge("a", "b").minlen).equals(3); 140 | }); 141 | 142 | it("expands inter-node edges to separate SG border and nodes #3", function() { 143 | g.setParent("sg2", "sg1"); 144 | g.setParent("a", "sg2"); 145 | g.setEdge("a", "b", { minlen: 1 }); 146 | nestingGraph.run(g); 147 | expect(g.edge("a", "b").minlen).equals(5); 148 | }); 149 | 150 | it("sets minlen correctly for nested SG boder to children", function() { 151 | g.setParent("a", "sg1"); 152 | g.setParent("sg2", "sg1"); 153 | g.setParent("b", "sg2"); 154 | nestingGraph.run(g); 155 | 156 | // We expect the following layering: 157 | // 158 | // 0: root 159 | // 1: empty (close sg2) 160 | // 2: empty (close sg1) 161 | // 3: open sg1 162 | // 4: open sg2 163 | // 5: a, b 164 | // 6: close sg2 165 | // 7: close sg1 166 | 167 | var root = g.graph().nestingRoot; 168 | var sg1Top = g.node("sg1").borderTop; 169 | var sg1Bot = g.node("sg1").borderBottom; 170 | var sg2Top = g.node("sg2").borderTop; 171 | var sg2Bot = g.node("sg2").borderBottom; 172 | 173 | expect(g.edge(root, sg1Top).minlen).equals(3); 174 | expect(g.edge(sg1Top, sg2Top).minlen).equals(1); 175 | expect(g.edge(sg1Top, "a").minlen).equals(2); 176 | expect(g.edge("a", sg1Bot).minlen).equals(2); 177 | expect(g.edge(sg2Top, "b").minlen).equals(1); 178 | expect(g.edge("b", sg2Bot).minlen).equals(1); 179 | expect(g.edge(sg2Bot, sg1Bot).minlen).equals(1); 180 | }); 181 | }); 182 | 183 | describe("cleanup", function() { 184 | it("removes nesting graph edges", function() { 185 | g.setParent("a", "sg1"); 186 | g.setEdge("a", "b", { minlen: 1 }); 187 | nestingGraph.run(g); 188 | nestingGraph.cleanup(g); 189 | expect(g.successors("a")).eqls(["b"]); 190 | }); 191 | 192 | it("removes the root node", function() { 193 | g.setParent("a", "sg1"); 194 | nestingGraph.run(g); 195 | nestingGraph.cleanup(g); 196 | expect(g.nodeCount()).to.equal(4); // sg1 + sg1Top + sg1Bottom + "a" 197 | }); 198 | }); 199 | }); 200 | -------------------------------------------------------------------------------- /test/normalize-test.js: -------------------------------------------------------------------------------- 1 | var _ = require("lodash"); 2 | var expect = require("./chai").expect; 3 | var normalize = require("../lib/normalize"); 4 | var Graph = require("../lib/graphlib").Graph; 5 | 6 | describe("normalize", function() { 7 | var g; 8 | 9 | beforeEach(function() { 10 | g = new Graph({ multigraph: true, compound: true }).setGraph({}); 11 | }); 12 | 13 | describe("run", function() { 14 | it("does not change a short edge", function() { 15 | g.setNode("a", { rank: 0 }); 16 | g.setNode("b", { rank: 1 }); 17 | g.setEdge("a", "b", {}); 18 | 19 | normalize.run(g); 20 | 21 | expect(_.map(g.edges(), incidentNodes)).to.eql([{ v: "a", w: "b" }]); 22 | expect(g.node("a").rank).to.equal(0); 23 | expect(g.node("b").rank).to.equal(1); 24 | }); 25 | 26 | it("splits a two layer edge into two segments", function() { 27 | g.setNode("a", { rank: 0 }); 28 | g.setNode("b", { rank: 2 }); 29 | g.setEdge("a", "b", {}); 30 | 31 | normalize.run(g); 32 | 33 | expect(g.successors("a")).to.have.length(1); 34 | var successor = g.successors("a")[0]; 35 | expect(g.node(successor).dummy).to.equal("edge"); 36 | expect(g.node(successor).rank).to.equal(1); 37 | expect(g.successors(successor)).to.eql(["b"]); 38 | expect(g.node("a").rank).to.equal(0); 39 | expect(g.node("b").rank).to.equal(2); 40 | 41 | expect(g.graph().dummyChains).to.have.length(1); 42 | expect(g.graph().dummyChains[0]).to.equal(successor); 43 | }); 44 | 45 | it("assigns width = 0, height = 0 to dummy nodes by default", function() { 46 | g.setNode("a", { rank: 0 }); 47 | g.setNode("b", { rank: 2 }); 48 | g.setEdge("a", "b", { width: 10, height: 10 }); 49 | 50 | normalize.run(g); 51 | 52 | expect(g.successors("a")).to.have.length(1); 53 | var successor = g.successors("a")[0]; 54 | expect(g.node(successor).width).to.equal(0); 55 | expect(g.node(successor).height).to.equal(0); 56 | }); 57 | 58 | it("assigns width and height from the edge for the node on labelRank", function() { 59 | g.setNode("a", { rank: 0 }); 60 | g.setNode("b", { rank: 4 }); 61 | g.setEdge("a", "b", { width: 20, height: 10, labelRank: 2 }); 62 | 63 | normalize.run(g); 64 | 65 | var labelV = g.successors(g.successors("a")[0])[0]; 66 | var labelNode = g.node(labelV); 67 | expect(labelNode.width).to.equal(20); 68 | expect(labelNode.height).to.equal(10); 69 | }); 70 | 71 | it("preserves the weight for the edge", function() { 72 | g.setNode("a", { rank: 0 }); 73 | g.setNode("b", { rank: 2 }); 74 | g.setEdge("a", "b", { weight: 2 }); 75 | 76 | normalize.run(g); 77 | 78 | expect(g.successors("a")).to.have.length(1); 79 | expect(g.edge("a", g.successors("a")[0]).weight).to.equal(2); 80 | }); 81 | }); 82 | 83 | describe("undo", function() { 84 | it("reverses the run operation", function() { 85 | g.setNode("a", { rank: 0 }); 86 | g.setNode("b", { rank: 2 }); 87 | g.setEdge("a", "b", {}); 88 | 89 | normalize.run(g); 90 | normalize.undo(g); 91 | 92 | expect(_.map(g.edges(), incidentNodes)).to.eql([{ v: "a", w: "b" }]); 93 | expect(g.node("a").rank).to.equal(0); 94 | expect(g.node("b").rank).to.equal(2); 95 | }); 96 | 97 | it("restores previous edge labels", function() { 98 | g.setNode("a", { rank: 0 }); 99 | g.setNode("b", { rank: 2 }); 100 | g.setEdge("a", "b", { foo: "bar" }); 101 | 102 | normalize.run(g); 103 | normalize.undo(g); 104 | 105 | expect(g.edge("a", "b").foo).equals("bar"); 106 | }); 107 | 108 | it("collects assigned coordinates into the 'points' attribute", function() { 109 | g.setNode("a", { rank: 0 }); 110 | g.setNode("b", { rank: 2 }); 111 | g.setEdge("a", "b", {}); 112 | 113 | normalize.run(g); 114 | 115 | var dummyLabel = g.node(g.neighbors("a")[0]); 116 | dummyLabel.x = 5; 117 | dummyLabel.y = 10; 118 | 119 | normalize.undo(g); 120 | 121 | expect(g.edge("a", "b").points).eqls([{ x: 5, y: 10 }]); 122 | }); 123 | 124 | it("merges assigned coordinates into the 'points' attribute", function() { 125 | g.setNode("a", { rank: 0 }); 126 | g.setNode("b", { rank: 4 }); 127 | g.setEdge("a", "b", {}); 128 | 129 | normalize.run(g); 130 | 131 | var aSucLabel = g.node(g.neighbors("a")[0]); 132 | aSucLabel.x = 5; 133 | aSucLabel.y = 10; 134 | 135 | var midLabel = g.node(g.successors(g.successors("a")[0])[0]); 136 | midLabel.x = 20; 137 | midLabel.y = 25; 138 | 139 | var bPredLabel = g.node(g.neighbors("b")[0]); 140 | bPredLabel.x = 100; 141 | bPredLabel.y = 200; 142 | 143 | normalize.undo(g); 144 | 145 | expect(g.edge("a", "b").points) 146 | .eqls([{ x: 5, y: 10 }, { x: 20, y: 25 }, { x: 100, y: 200 }]); 147 | }); 148 | 149 | it("sets coords and dims for the label, if the edge has one", function() { 150 | g.setNode("a", { rank: 0 }); 151 | g.setNode("b", { rank: 2 }); 152 | g.setEdge("a", "b", { width: 10, height: 20, labelRank: 1 }); 153 | 154 | normalize.run(g); 155 | 156 | var labelNode = g.node(g.successors("a")[0]); 157 | labelNode.x = 50; 158 | labelNode.y = 60; 159 | labelNode.width = 20; 160 | labelNode.height = 10; 161 | 162 | normalize.undo(g); 163 | 164 | expect(_.pick(g.edge("a", "b"), ["x", "y", "width", "height"])).eqls({ 165 | x: 50, y: 60, width: 20, height: 10 166 | }); 167 | }); 168 | 169 | it("sets coords and dims for the label, if the long edge has one", function() { 170 | g.setNode("a", { rank: 0 }); 171 | g.setNode("b", { rank: 4 }); 172 | g.setEdge("a", "b", { width: 10, height: 20, labelRank: 2 }); 173 | 174 | normalize.run(g); 175 | 176 | var labelNode = g.node(g.successors(g.successors("a")[0])[0]); 177 | labelNode.x = 50; 178 | labelNode.y = 60; 179 | labelNode.width = 20; 180 | labelNode.height = 10; 181 | 182 | normalize.undo(g); 183 | 184 | expect(_.pick(g.edge("a", "b"), ["x", "y", "width", "height"])).eqls({ 185 | x: 50, y: 60, width: 20, height: 10 186 | }); 187 | }); 188 | 189 | it("restores multi-edges", function() { 190 | g.setNode("a", { rank: 0 }); 191 | g.setNode("b", { rank: 2 }); 192 | g.setEdge("a", "b", {}, "bar"); 193 | g.setEdge("a", "b", {}, "foo"); 194 | 195 | normalize.run(g); 196 | 197 | var outEdges = _.sortBy(g.outEdges("a"), "name"); 198 | expect(outEdges).to.have.length(2); 199 | 200 | var barDummy = g.node(outEdges[0].w); 201 | barDummy.x = 5; 202 | barDummy.y = 10; 203 | 204 | var fooDummy = g.node(outEdges[1].w); 205 | fooDummy.x = 15; 206 | fooDummy.y = 20; 207 | 208 | normalize.undo(g); 209 | 210 | expect(g.hasEdge("a", "b")).to.be.false; 211 | expect(g.edge("a", "b", "bar").points).eqls([{ x: 5, y: 10 }]); 212 | expect(g.edge("a", "b", "foo").points).eqls([{ x: 15, y: 20 }]); 213 | }); 214 | }); 215 | }); 216 | 217 | function incidentNodes(edge) { 218 | return { v: edge.v, w: edge.w }; 219 | } 220 | -------------------------------------------------------------------------------- /test/order/add-subgraph-constraints-test.js: -------------------------------------------------------------------------------- 1 | var _ = require("lodash"); 2 | var expect = require("../chai").expect; 3 | var Graph = require("../../lib/graphlib").Graph; 4 | var addSubgraphConstraints = require("../../lib/order/add-subgraph-constraints"); 5 | 6 | describe("order/addSubgraphConstraints", function() { 7 | var g, cg; 8 | 9 | beforeEach(function() { 10 | g = new Graph({ compound: true }); 11 | cg = new Graph(); 12 | }); 13 | 14 | it("does not change CG for a flat set of nodes", function() { 15 | var vs = ["a", "b", "c", "d"]; 16 | _.forEach(vs, function(v) { g.setNode(v); }); 17 | addSubgraphConstraints(g, cg, vs); 18 | expect(cg.nodeCount()).equals(0); 19 | expect(cg.edgeCount()).equals(0); 20 | }); 21 | 22 | it("doesn't create a constraint for contiguous subgraph nodes", function() { 23 | var vs = ["a", "b", "c"]; 24 | _.forEach(vs, function(v) { 25 | g.setParent(v, "sg"); 26 | }); 27 | addSubgraphConstraints(g, cg, vs); 28 | expect(cg.nodeCount()).equals(0); 29 | expect(cg.edgeCount()).equals(0); 30 | }); 31 | 32 | it("adds a constraint when the parents for adjacent nodes are different", function() { 33 | var vs = ["a", "b"]; 34 | g.setParent("a", "sg1"); 35 | g.setParent("b", "sg2"); 36 | addSubgraphConstraints(g, cg, vs); 37 | expect(cg.edges()).eqls([{ v: "sg1", w: "sg2" }]); 38 | }); 39 | 40 | it("works for multiple levels", function() { 41 | var vs = ["a", "b", "c", "d", "e", "f", "g", "h"]; 42 | _.forEach(vs, function(v) { 43 | g.setNode(v); 44 | }); 45 | g.setParent("b", "sg2"); 46 | g.setParent("sg2", "sg1"); 47 | g.setParent("c", "sg1"); 48 | g.setParent("d", "sg3"); 49 | g.setParent("sg3", "sg1"); 50 | g.setParent("f", "sg4"); 51 | g.setParent("g", "sg5"); 52 | g.setParent("sg5", "sg4"); 53 | addSubgraphConstraints(g, cg, vs); 54 | expect(_.sortBy(cg.edges(), "v")).eqls([ 55 | { v: "sg1", w: "sg4" }, 56 | { v: "sg2", w: "sg3" } 57 | ]); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /test/order/barycenter-test.js: -------------------------------------------------------------------------------- 1 | var expect = require("../chai").expect; 2 | var barycenter = require("../../lib/order/barycenter"); 3 | var Graph = require("../../lib/graphlib").Graph; 4 | 5 | describe("order/barycenter", function() { 6 | var g; 7 | 8 | beforeEach(function() { 9 | g = new Graph() 10 | .setDefaultNodeLabel(function() { return {}; }) 11 | .setDefaultEdgeLabel(function() { return { weight: 1 }; }); 12 | }); 13 | 14 | it("assigns an undefined barycenter for a node with no predecessors", function() { 15 | g.setNode("x", {}); 16 | 17 | var results = barycenter(g, ["x"]); 18 | expect(results).to.have.length(1); 19 | expect(results[0]).to.eql({ v: "x" }); 20 | }); 21 | 22 | it("assigns the position of the sole predecessors", function() { 23 | g.setNode("a", { order: 2 }); 24 | g.setEdge("a", "x"); 25 | 26 | var results = barycenter(g, ["x"]); 27 | expect(results).to.have.length(1); 28 | expect(results[0]).eqls({ v: "x", barycenter: 2, weight: 1 }); 29 | }); 30 | 31 | it("assigns the average of multiple predecessors", function() { 32 | g.setNode("a", { order: 2 }); 33 | g.setNode("b", { order: 4 }); 34 | g.setEdge("a", "x"); 35 | g.setEdge("b", "x"); 36 | 37 | var results = barycenter(g, ["x"]); 38 | expect(results).to.have.length(1); 39 | expect(results[0]).eqls({ v: "x", barycenter: 3, weight: 2 }); 40 | }); 41 | 42 | it("takes into account the weight of edges", function() { 43 | g.setNode("a", { order: 2 }); 44 | g.setNode("b", { order: 4 }); 45 | g.setEdge("a", "x", { weight: 3 }); 46 | g.setEdge("b", "x"); 47 | 48 | var results = barycenter(g, ["x"]); 49 | expect(results).to.have.length(1); 50 | expect(results[0]).eqls({ v: "x", barycenter: 2.5, weight: 4 }); 51 | }); 52 | 53 | it("calculates barycenters for all nodes in the movable layer", function() { 54 | g.setNode("a", { order: 1 }); 55 | g.setNode("b", { order: 2 }); 56 | g.setNode("c", { order: 4 }); 57 | g.setEdge("a", "x"); 58 | g.setEdge("b", "x"); 59 | g.setNode("y"); 60 | g.setEdge("a", "z", { weight: 2 }); 61 | g.setEdge("c", "z"); 62 | 63 | var results = barycenter(g, ["x", "y", "z"]); 64 | expect(results).to.have.length(3); 65 | expect(results[0]).eqls({ v: "x", barycenter: 1.5, weight: 2 }); 66 | expect(results[1]).eqls({ v: "y" }); 67 | expect(results[2]).eqls({ v: "z", barycenter: 2, weight: 3 }); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /test/order/build-layer-graph-test.js: -------------------------------------------------------------------------------- 1 | var _ = require("lodash"); 2 | var expect = require("../chai").expect; 3 | var Graph = require("../../lib/graphlib").Graph; 4 | var buildLayerGraph = require("../../lib/order/build-layer-graph"); 5 | 6 | describe("order/buildLayerGraph", function() { 7 | var g; 8 | 9 | beforeEach(function() { 10 | g = new Graph({ compound: true, multigraph: true }); 11 | }); 12 | 13 | it("places movable nodes with no parents under the root node", function() { 14 | g.setNode("a", { rank: 1 }); 15 | g.setNode("b", { rank: 1 }); 16 | g.setNode("c", { rank: 2 }); 17 | g.setNode("d", { rank: 3 }); 18 | 19 | var lg; 20 | lg = buildLayerGraph(g, 1, "inEdges"); 21 | expect(lg.hasNode(lg.graph().root)); 22 | expect(lg.children()).eqls([lg.graph().root]); 23 | expect(lg.children(lg.graph().root)).eqls(["a", "b"]); 24 | }); 25 | 26 | it("copies flat nodes from the layer to the graph", function() { 27 | g.setNode("a", { rank: 1 }); 28 | g.setNode("b", { rank: 1 }); 29 | g.setNode("c", { rank: 2 }); 30 | g.setNode("d", { rank: 3 }); 31 | 32 | expect(buildLayerGraph(g, 1, "inEdges").nodes()).to.include("a"); 33 | expect(buildLayerGraph(g, 1, "inEdges").nodes()).to.include("b"); 34 | expect(buildLayerGraph(g, 2, "inEdges").nodes()).to.include("c"); 35 | expect(buildLayerGraph(g, 3, "inEdges").nodes()).to.include("d"); 36 | }); 37 | 38 | it("uses the original node label for copied nodes", function() { 39 | // This allows us to make updates to the original graph and have them 40 | // be available automatically in the layer graph. 41 | g.setNode("a", { foo: 1, rank: 1 }); 42 | g.setNode("b", { foo: 2, rank: 2 }); 43 | g.setEdge("a", "b", { weight: 1 }); 44 | 45 | var lg = buildLayerGraph(g, 2, "inEdges"); 46 | 47 | expect(lg.node("a").foo).equals(1); 48 | g.node("a").foo = "updated"; 49 | expect(lg.node("a").foo).equals("updated"); 50 | 51 | expect(lg.node("b").foo).equals(2); 52 | g.node("b").foo = "updated"; 53 | expect(lg.node("b").foo).equals("updated"); 54 | }); 55 | 56 | it("copies edges incident on rank nodes to the graph (inEdges)", function() { 57 | g.setNode("a", { rank: 1 }); 58 | g.setNode("b", { rank: 1 }); 59 | g.setNode("c", { rank: 2 }); 60 | g.setNode("d", { rank: 3 }); 61 | g.setEdge("a", "c", { weight: 2 }); 62 | g.setEdge("b", "c", { weight: 3 }); 63 | g.setEdge("c", "d", { weight: 4 }); 64 | 65 | expect(buildLayerGraph(g, 1, "inEdges").edgeCount()).to.equal(0); 66 | expect(buildLayerGraph(g, 2, "inEdges").edgeCount()).to.equal(2); 67 | expect(buildLayerGraph(g, 2, "inEdges").edge("a", "c")).eqls({ weight: 2 }); 68 | expect(buildLayerGraph(g, 2, "inEdges").edge("b", "c")).eqls({ weight: 3 }); 69 | expect(buildLayerGraph(g, 3, "inEdges").edgeCount()).to.equal(1); 70 | expect(buildLayerGraph(g, 3, "inEdges").edge("c", "d")).eqls({ weight: 4 }); 71 | }); 72 | 73 | it("copies edges incident on rank nodes to the graph (outEdges)", function() { 74 | g.setNode("a", { rank: 1 }); 75 | g.setNode("b", { rank: 1 }); 76 | g.setNode("c", { rank: 2 }); 77 | g.setNode("d", { rank: 3 }); 78 | g.setEdge("a", "c", { weight: 2 }); 79 | g.setEdge("b", "c", { weight: 3 }); 80 | g.setEdge("c", "d", { weight: 4 }); 81 | 82 | expect(buildLayerGraph(g, 1, "outEdges").edgeCount()).to.equal(2); 83 | expect(buildLayerGraph(g, 1, "outEdges").edge("c", "a")).eqls({ weight: 2 }); 84 | expect(buildLayerGraph(g, 1, "outEdges").edge("c", "b")).eqls({ weight: 3 }); 85 | expect(buildLayerGraph(g, 2, "outEdges").edgeCount()).to.equal(1); 86 | expect(buildLayerGraph(g, 2, "outEdges").edge("d", "c")).eqls({ weight: 4 }); 87 | expect(buildLayerGraph(g, 3, "outEdges").edgeCount()).to.equal(0); 88 | }); 89 | 90 | it("collapses multi-edges", function() { 91 | g.setNode("a", { rank: 1 }); 92 | g.setNode("b", { rank: 2 }); 93 | g.setEdge("a", "b", { weight: 2 }); 94 | g.setEdge("a", "b", { weight: 3 }, "multi"); 95 | 96 | expect(buildLayerGraph(g, 2, "inEdges").edge("a", "b")).eqls({ weight: 5 }); 97 | }); 98 | 99 | it("preserves hierarchy for the movable layer", function() { 100 | g.setNode("a", { rank: 0 }); 101 | g.setNode("b", { rank: 0 }); 102 | g.setNode("c", { rank: 0 }); 103 | g.setNode("sg", { 104 | minRank: 0, 105 | maxRank: 0, 106 | borderLeft: ["bl"], 107 | borderRight: ["br"] 108 | }); 109 | _.forEach(["a", "b"], function(v) { g.setParent(v, "sg"); }); 110 | 111 | var lg = buildLayerGraph(g, 0, "inEdges"); 112 | var root = lg.graph().root; 113 | expect(_.sortBy(lg.children(root))).eqls(["c", "sg"]); 114 | expect(lg.parent("a")).equals("sg"); 115 | expect(lg.parent("b")).equals("sg"); 116 | }); 117 | }); 118 | -------------------------------------------------------------------------------- /test/order/cross-count-test.js: -------------------------------------------------------------------------------- 1 | var expect = require("../chai").expect; 2 | var Graph = require("../../lib/graphlib").Graph; 3 | var crossCount = require("../../lib/order/cross-count"); 4 | 5 | describe("crossCount", function() { 6 | var g; 7 | 8 | beforeEach(function() { 9 | g = new Graph() 10 | .setDefaultEdgeLabel(function() { return { weight: 1 }; }); 11 | }); 12 | 13 | it("returns 0 for an empty layering", function() { 14 | expect(crossCount(g, [])).equals(0); 15 | }); 16 | 17 | it("returns 0 for a layering with no crossings", function() { 18 | g.setEdge("a1", "b1"); 19 | g.setEdge("a2", "b2"); 20 | expect(crossCount(g, [["a1", "a2"], ["b1", "b2"]])).equals(0); 21 | }); 22 | 23 | it("returns 1 for a layering with 1 crossing", function() { 24 | g.setEdge("a1", "b1"); 25 | g.setEdge("a2", "b2"); 26 | expect(crossCount(g, [["a1", "a2"], ["b2", "b1"]])).equals(1); 27 | }); 28 | 29 | it("returns a weighted crossing count for a layering with 1 crossing", function() { 30 | g.setEdge("a1", "b1", { weight: 2 }); 31 | g.setEdge("a2", "b2", { weight: 3 }); 32 | expect(crossCount(g, [["a1", "a2"], ["b2", "b1"]])).equals(6); 33 | }); 34 | 35 | it("calculates crossings across layers", function() { 36 | g.setPath(["a1", "b1", "c1"]); 37 | g.setPath(["a2", "b2", "c2"]); 38 | expect(crossCount(g, [["a1", "a2"], ["b2", "b1"], ["c1", "c2"]])).equals(2); 39 | }); 40 | 41 | it("works for graph #1", function() { 42 | g.setPath(["a", "b", "c"]); 43 | g.setPath(["d", "e", "c"]); 44 | g.setPath(["a", "f", "i"]); 45 | g.setEdge("a", "e"); 46 | expect(crossCount(g, [["a", "d"], ["b", "e", "f"], ["c", "i"]])).equals(1); 47 | expect(crossCount(g, [["d", "a"], ["e", "b", "f"], ["c", "i"]])).equals(0); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /test/order/init-order-test.js: -------------------------------------------------------------------------------- 1 | var _ = require("lodash"); 2 | var expect = require("../chai").expect; 3 | var Graph = require("../../lib/graphlib").Graph; 4 | var initOrder = require("../../lib/order/init-order"); 5 | 6 | describe("order/initOrder", function() { 7 | var g; 8 | 9 | beforeEach(function() { 10 | g = new Graph({ compound: true }) 11 | .setDefaultEdgeLabel(function() { return { weight: 1 }; }); 12 | }); 13 | 14 | it("assigns non-overlapping orders for each rank in a tree", function() { 15 | _.forEach({ a: 0, b: 1, c: 2, d: 2, e: 1 }, function(rank, v) { 16 | g.setNode(v, { rank: rank }); 17 | }); 18 | g.setPath(["a", "b", "c"]); 19 | g.setEdge("b", "d"); 20 | g.setEdge("a", "e"); 21 | 22 | var layering = initOrder(g); 23 | expect(layering[0]).to.eql(["a"]); 24 | expect(_.sortBy(layering[1])).to.eql(["b", "e"]); 25 | expect(_.sortBy(layering[2])).to.eql(["c", "d"]); 26 | }); 27 | 28 | it("assigns non-overlapping orders for each rank in a DAG", function() { 29 | _.forEach({ a: 0, b: 1, c: 1, d: 2 }, function(rank, v) { 30 | g.setNode(v, { rank: rank }); 31 | }); 32 | g.setPath(["a", "b", "d"]); 33 | g.setPath(["a", "c", "d"]); 34 | 35 | var layering = initOrder(g); 36 | expect(layering[0]).to.eql(["a"]); 37 | expect(_.sortBy(layering[1])).to.eql(["b", "c"]); 38 | expect(_.sortBy(layering[2])).to.eql(["d"]); 39 | }); 40 | 41 | it("does not assign an order to subgraph nodes", function() { 42 | g.setNode("a", { rank: 0 }); 43 | g.setNode("sg1", {}); 44 | g.setParent("a", "sg1"); 45 | 46 | var layering = initOrder(g); 47 | expect(layering).to.eql([["a"]]); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /test/order/order-test.js: -------------------------------------------------------------------------------- 1 | var _ = require("lodash"); 2 | var expect = require("../chai").expect; 3 | var Graph = require("../../lib/graphlib").Graph; 4 | var order = require("../../lib/order"); 5 | var crossCount = require("../../lib/order/cross-count"); 6 | var util = require("../../lib/util"); 7 | 8 | describe("order", function() { 9 | var g; 10 | 11 | beforeEach(function() { 12 | g = new Graph() 13 | .setDefaultEdgeLabel({ weight: 1 }); 14 | }); 15 | 16 | it("does not add crossings to a tree structure", function() { 17 | g.setNode("a", { rank: 1 }); 18 | _.forEach(["b", "e"], function(v) { g.setNode(v, { rank: 2 }); }); 19 | _.forEach(["c", "d", "f"], function(v) { g.setNode(v, { rank: 3 }); }); 20 | g.setPath(["a", "b", "c"]); 21 | g.setEdge("b", "d"); 22 | g.setPath(["a", "e", "f"]); 23 | order(g); 24 | var layering = util.buildLayerMatrix(g); 25 | expect(crossCount(g, layering)).to.equal(0); 26 | }); 27 | 28 | it("can solve a simple graph", function() { 29 | // This graph resulted in a single crossing for previous versions of dagre. 30 | _.forEach(["a", "d"], function(v) { g.setNode(v, { rank: 1 }); }); 31 | _.forEach(["b", "f", "e"], function(v) { g.setNode(v, { rank: 2 }); }); 32 | _.forEach(["c", "g"], function(v) { g.setNode(v, { rank: 3 }); }); 33 | order(g); 34 | var layering = util.buildLayerMatrix(g); 35 | expect(crossCount(g, layering)).to.equal(0); 36 | }); 37 | 38 | it("can minimize crossings", function() { 39 | g.setNode("a", { rank: 1 }); 40 | _.forEach(["b", "e", "g"], function(v) { g.setNode(v, { rank: 2 }); }); 41 | _.forEach(["c", "f", "h"], function(v) { g.setNode(v, { rank: 3 }); }); 42 | g.setNode("d", { rank: 4 }); 43 | order(g); 44 | var layering = util.buildLayerMatrix(g); 45 | expect(crossCount(g, layering)).to.be.lte(1); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /test/order/resolve-conflicts-test.js: -------------------------------------------------------------------------------- 1 | var _ = require("lodash"); 2 | var expect = require("../chai").expect; 3 | var Graph = require("../../lib/graphlib").Graph; 4 | var resolveConflicts = require("../../lib/order/resolve-conflicts"); 5 | 6 | describe("order/resolveConflicts", function() { 7 | var cg; 8 | 9 | beforeEach(function() { 10 | cg = new Graph(); 11 | }); 12 | 13 | it("returns back nodes unchanged when no constraints exist", function() { 14 | var input = [ 15 | { v: "a", barycenter: 2, weight: 3 }, 16 | { v: "b", barycenter: 1, weight: 2 } 17 | ]; 18 | expect(_.sortBy(resolveConflicts(input, cg), "vs")).eqls([ 19 | { vs: ["a"], i: 0, barycenter: 2, weight: 3 }, 20 | { vs: ["b"], i: 1, barycenter: 1, weight: 2 } 21 | ]); 22 | }); 23 | 24 | it("returns back nodes unchanged when no conflicts exist", function() { 25 | var input = [ 26 | { v: "a", barycenter: 2, weight: 3 }, 27 | { v: "b", barycenter: 1, weight: 2 } 28 | ]; 29 | cg.setEdge("b", "a"); 30 | expect(_.sortBy(resolveConflicts(input, cg), "vs")).eqls([ 31 | { vs: ["a"], i: 0, barycenter: 2, weight: 3 }, 32 | { vs: ["b"], i: 1, barycenter: 1, weight: 2 } 33 | ]); 34 | }); 35 | 36 | it("coalesces nodes when there is a conflict", function() { 37 | var input = [ 38 | { v: "a", barycenter: 2, weight: 3 }, 39 | { v: "b", barycenter: 1, weight: 2 } 40 | ]; 41 | cg.setEdge("a", "b"); 42 | expect(_.sortBy(resolveConflicts(input, cg), "vs")).eqls([ 43 | { vs: ["a", "b"], 44 | i: 0, 45 | barycenter: (3 * 2 + 2 * 1) / (3 + 2), 46 | weight: 3 + 2 47 | } 48 | ]); 49 | }); 50 | 51 | it("coalesces nodes when there is a conflict #2", function() { 52 | var input = [ 53 | { v: "a", barycenter: 4, weight: 1 }, 54 | { v: "b", barycenter: 3, weight: 1 }, 55 | { v: "c", barycenter: 2, weight: 1 }, 56 | { v: "d", barycenter: 1, weight: 1 } 57 | ]; 58 | cg.setPath(["a", "b", "c", "d"]); 59 | expect(_.sortBy(resolveConflicts(input, cg), "vs")).eqls([ 60 | { vs: ["a", "b", "c", "d"], 61 | i: 0, 62 | barycenter: (4 + 3 + 2 + 1) / 4, 63 | weight: 4 64 | } 65 | ]); 66 | }); 67 | 68 | it("works with multiple constraints for the same target #1", function() { 69 | var input = [ 70 | { v: "a", barycenter: 4, weight: 1 }, 71 | { v: "b", barycenter: 3, weight: 1 }, 72 | { v: "c", barycenter: 2, weight: 1 }, 73 | ]; 74 | cg.setEdge("a", "c"); 75 | cg.setEdge("b", "c"); 76 | var results = resolveConflicts(input, cg); 77 | expect(results).to.have.length(1); 78 | expect(_.indexOf(results[0].vs, "c")).to.be.gt(_.indexOf(results[0].vs, "a")); 79 | expect(_.indexOf(results[0].vs, "c")).to.be.gt(_.indexOf(results[0].vs, "b")); 80 | expect(results[0].i).equals(0); 81 | expect(results[0].barycenter).equals((4 + 3 + 2) / 3); 82 | expect(results[0].weight).equals(3); 83 | }); 84 | 85 | it("works with multiple constraints for the same target #2", function() { 86 | var input = [ 87 | { v: "a", barycenter: 4, weight: 1 }, 88 | { v: "b", barycenter: 3, weight: 1 }, 89 | { v: "c", barycenter: 2, weight: 1 }, 90 | { v: "d", barycenter: 1, weight: 1 }, 91 | ]; 92 | cg.setEdge("a", "c"); 93 | cg.setEdge("a", "d"); 94 | cg.setEdge("b", "c"); 95 | cg.setEdge("c", "d"); 96 | var results = resolveConflicts(input, cg); 97 | expect(results).to.have.length(1); 98 | expect(_.indexOf(results[0].vs, "c")).to.be.gt(_.indexOf(results[0].vs, "a")); 99 | expect(_.indexOf(results[0].vs, "c")).to.be.gt(_.indexOf(results[0].vs, "b")); 100 | expect(_.indexOf(results[0].vs, "d")).to.be.gt(_.indexOf(results[0].vs, "c")); 101 | expect(results[0].i).equals(0); 102 | expect(results[0].barycenter).equals((4 + 3 + 2 + 1) / 4); 103 | expect(results[0].weight).equals(4); 104 | }); 105 | 106 | it("does nothing to a node lacking both a barycenter and a constraint", function() { 107 | var input = [ 108 | { v: "a" }, 109 | { v: "b", barycenter: 1, weight: 2 } 110 | ]; 111 | expect(_.sortBy(resolveConflicts(input, cg), "vs")).eqls([ 112 | { vs: ["a"], i: 0 }, 113 | { vs: ["b"], i: 1, barycenter: 1, weight: 2 } 114 | ]); 115 | }); 116 | 117 | it("treats a node w/o a barycenter as always violating constraints #1", function() { 118 | var input = [ 119 | { v: "a" }, 120 | { v: "b", barycenter: 1, weight: 2 } 121 | ]; 122 | cg.setEdge("a", "b"); 123 | expect(_.sortBy(resolveConflicts(input, cg), "vs")).eqls([ 124 | { vs: ["a", "b"], i: 0, barycenter: 1, weight: 2 } 125 | ]); 126 | }); 127 | 128 | it("treats a node w/o a barycenter as always violating constraints #2", function() { 129 | var input = [ 130 | { v: "a" }, 131 | { v: "b", barycenter: 1, weight: 2 } 132 | ]; 133 | cg.setEdge("b", "a"); 134 | expect(_.sortBy(resolveConflicts(input, cg), "vs")).eqls([ 135 | { vs: ["b", "a"], i: 0, barycenter: 1, weight: 2 } 136 | ]); 137 | }); 138 | 139 | it("ignores edges not related to entries", function() { 140 | var input = [ 141 | { v: "a", barycenter: 2, weight: 3 }, 142 | { v: "b", barycenter: 1, weight: 2 } 143 | ]; 144 | cg.setEdge("c", "d"); 145 | expect(_.sortBy(resolveConflicts(input, cg), "vs")).eqls([ 146 | { vs: ["a"], i: 0, barycenter: 2, weight: 3 }, 147 | { vs: ["b"], i: 1, barycenter: 1, weight: 2 } 148 | ]); 149 | }); 150 | }); 151 | -------------------------------------------------------------------------------- /test/order/sort-subgraph-test.js: -------------------------------------------------------------------------------- 1 | var _ = require("lodash"); 2 | var expect = require("../chai").expect; 3 | var sortSubgraph = require("../../lib/order/sort-subgraph"); 4 | var Graph = require("../../lib/graphlib").Graph; 5 | 6 | describe("order/sortSubgraph", function() { 7 | var g, cg; 8 | 9 | beforeEach(function() { 10 | g = new Graph({ compound: true }) 11 | .setDefaultNodeLabel(function() { return {}; }) 12 | .setDefaultEdgeLabel(function() { return { weight: 1 }; }); 13 | _.forEach(_.range(5), function(v) { g.setNode(v, { order: v }); }); 14 | cg = new Graph(); 15 | }); 16 | 17 | it("sorts a flat subgraph based on barycenter", function() { 18 | g.setEdge(3, "x"); 19 | g.setEdge(1, "y", { weight: 2 }); 20 | g.setEdge(4, "y"); 21 | _.forEach(["x", "y"], function(v) { g.setParent(v, "movable"); }); 22 | 23 | expect(sortSubgraph(g, "movable", cg).vs).eqls(["y", "x"]); 24 | }); 25 | 26 | it("preserves the pos of a node (y) w/o neighbors in a flat subgraph", function() { 27 | g.setEdge(3, "x"); 28 | g.setNode("y"); 29 | g.setEdge(1, "z", { weight: 2 }); 30 | g.setEdge(4, "z"); 31 | _.forEach(["x", "y", "z"], function(v) { g.setParent(v, "movable"); }); 32 | 33 | expect(sortSubgraph(g, "movable", cg).vs).eqls(["z", "y", "x"]); 34 | }); 35 | 36 | it("biases to the left without reverse bias", function() { 37 | g.setEdge(1, "x"); 38 | g.setEdge(1, "y"); 39 | _.forEach(["x", "y"], function(v) { g.setParent(v, "movable"); }); 40 | 41 | expect(sortSubgraph(g, "movable", cg).vs).eqls(["x", "y"]); 42 | }); 43 | 44 | it("biases to the right with reverse bias", function() { 45 | g.setEdge(1, "x"); 46 | g.setEdge(1, "y"); 47 | _.forEach(["x", "y"], function(v) { g.setParent(v, "movable"); }); 48 | 49 | expect(sortSubgraph(g, "movable", cg, true).vs).eqls(["y", "x"]); 50 | }); 51 | 52 | it("aggregates stats about the subgraph", function() { 53 | g.setEdge(3, "x"); 54 | g.setEdge(1, "y", { weight: 2 }); 55 | g.setEdge(4, "y"); 56 | _.forEach(["x", "y"], function(v) { g.setParent(v, "movable"); }); 57 | 58 | var results = sortSubgraph(g, "movable", cg); 59 | expect(results.barycenter).to.equal(2.25); 60 | expect(results.weight).to.equal(4); 61 | }); 62 | 63 | it("can sort a nested subgraph with no barycenter", function() { 64 | g.setNodes(["a", "b", "c"]); 65 | g.setParent("a", "y"); 66 | g.setParent("b", "y"); 67 | g.setParent("c", "y"); 68 | g.setEdge(0, "x"); 69 | g.setEdge(1, "z"); 70 | g.setEdge(2, "y"); 71 | _.forEach(["x", "y", "z"], function(v) { g.setParent(v, "movable"); }); 72 | 73 | expect(sortSubgraph(g, "movable", cg).vs).eqls(["x", "z", "a", "b", "c"]); 74 | }); 75 | 76 | it("can sort a nested subgraph with a barycenter", function() { 77 | g.setNodes(["a", "b", "c"]); 78 | g.setParent("a", "y"); 79 | g.setParent("b", "y"); 80 | g.setParent("c", "y"); 81 | g.setEdge(0, "a", { weight: 3 }); 82 | g.setEdge(0, "x"); 83 | g.setEdge(1, "z"); 84 | g.setEdge(2, "y"); 85 | _.forEach(["x", "y", "z"], function(v) { g.setParent(v, "movable"); }); 86 | 87 | expect(sortSubgraph(g, "movable", cg).vs).eqls(["x", "a", "b", "c", "z"]); 88 | }); 89 | 90 | it("can sort a nested subgraph with no in-edges", function() { 91 | g.setNodes(["a", "b", "c"]); 92 | g.setParent("a", "y"); 93 | g.setParent("b", "y"); 94 | g.setParent("c", "y"); 95 | g.setEdge(0, "a"); 96 | g.setEdge(1, "b"); 97 | g.setEdge(0, "x"); 98 | g.setEdge(1, "z"); 99 | _.forEach(["x", "y", "z"], function(v) { g.setParent(v, "movable"); }); 100 | 101 | expect(sortSubgraph(g, "movable", cg).vs).eqls(["x", "a", "b", "c", "z"]); 102 | }); 103 | 104 | it("sorts border nodes to the extremes of the subgraph", function() { 105 | g.setEdge(0, "x"); 106 | g.setEdge(1, "y"); 107 | g.setEdge(2, "z"); 108 | g.setNode("sg1", { borderLeft: "bl", borderRight: "br" }); 109 | _.forEach(["x", "y", "z", "bl", "br"], function(v) { g.setParent(v, "sg1"); }); 110 | expect(sortSubgraph(g, "sg1", cg).vs).eqls(["bl", "x", "y", "z", "br"]); 111 | }); 112 | 113 | it("assigns a barycenter to a subgraph based on previous border nodes", function() { 114 | g.setNode("bl1", { order: 0 }); 115 | g.setNode("br1", { order: 1 }); 116 | g.setEdge("bl1", "bl2"); 117 | g.setEdge("br1", "br2"); 118 | _.forEach(["bl2", "br2"], function(v) { g.setParent(v, "sg"); }); 119 | g.setNode("sg", { borderLeft: "bl2", borderRight: "br2" }); 120 | expect(sortSubgraph(g, "sg", cg)).eqls({ 121 | barycenter: 0.5, 122 | weight: 2, 123 | vs: ["bl2", "br2"] 124 | }); 125 | }); 126 | }); 127 | -------------------------------------------------------------------------------- /test/order/sort-test.js: -------------------------------------------------------------------------------- 1 | var expect = require("../chai").expect; 2 | var sort = require("../../lib/order/sort"); 3 | 4 | describe("sort", function() { 5 | it("sorts nodes by barycenter", function() { 6 | var input = [ 7 | { vs: ["a"], i: 0, barycenter: 2, weight: 3 }, 8 | { vs: ["b"], i: 1, barycenter: 1, weight: 2 } 9 | ]; 10 | expect(sort(input)).eqls({ 11 | vs: ["b", "a"], 12 | barycenter: (2 * 3 + 1 * 2) / (3 + 2), 13 | weight: 3 + 2 }); 14 | }); 15 | 16 | it("can sort super-nodes", function() { 17 | var input = [ 18 | { vs: ["a", "c", "d"], i: 0, barycenter: 2, weight: 3 }, 19 | { vs: ["b"], i: 1, barycenter: 1, weight: 2 } 20 | ]; 21 | expect(sort(input)).eqls({ 22 | vs: ["b", "a", "c", "d"], 23 | barycenter: (2 * 3 + 1 * 2) / (3 + 2), 24 | weight: 3 + 2 }); 25 | }); 26 | 27 | it("biases to the left by default", function() { 28 | var input = [ 29 | { vs: ["a"], i: 0, barycenter: 1, weight: 1 }, 30 | { vs: ["b"], i: 1, barycenter: 1, weight: 1 } 31 | ]; 32 | expect(sort(input)).eqls({ 33 | vs: ["a", "b"], 34 | barycenter: 1, 35 | weight: 2 }); 36 | }); 37 | 38 | it("biases to the right if biasRight = true", function() { 39 | var input = [ 40 | { vs: ["a"], i: 0, barycenter: 1, weight: 1 }, 41 | { vs: ["b"], i: 1, barycenter: 1, weight: 1 } 42 | ]; 43 | expect(sort(input, true)).eqls({ 44 | vs: ["b", "a"], 45 | barycenter: 1, 46 | weight: 2 }); 47 | }); 48 | 49 | it("can sort nodes without a barycenter", function() { 50 | var input = [ 51 | { vs: ["a"], i: 0, barycenter: 2, weight: 1 }, 52 | { vs: ["b"], i: 1, barycenter: 6, weight: 1 }, 53 | { vs: ["c"], i: 2 }, 54 | { vs: ["d"], i: 3, barycenter: 3, weight: 1 } 55 | ]; 56 | expect(sort(input)).eqls({ 57 | vs: ["a", "d", "c", "b"], 58 | barycenter: (2 + 6 + 3) / 3, 59 | weight: 3 60 | }); 61 | }); 62 | 63 | it("can handle no barycenters for any nodes", function() { 64 | var input = [ 65 | { vs: ["a"], i: 0 }, 66 | { vs: ["b"], i: 3 }, 67 | { vs: ["c"], i: 2 }, 68 | { vs: ["d"], i: 1 } 69 | ]; 70 | expect(sort(input)).eqls({ vs: ["a", "d", "c", "b"] }); 71 | }); 72 | 73 | it("can handle a barycenter of 0", function() { 74 | var input = [ 75 | { vs: ["a"], i: 0, barycenter: 0, weight: 1 }, 76 | { vs: ["b"], i: 3 }, 77 | { vs: ["c"], i: 2 }, 78 | { vs: ["d"], i: 1 } 79 | ]; 80 | expect(sort(input)).eqls({ 81 | vs: ["a", "d", "c", "b"], 82 | barycenter: 0, 83 | weight: 1 84 | }); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /test/parent-dummy-chains-test.js: -------------------------------------------------------------------------------- 1 | var expect = require("./chai").expect; 2 | var Graph = require("../lib/graphlib").Graph; 3 | var parentDummyChains = require("../lib/parent-dummy-chains"); 4 | 5 | describe("parentDummyChains", function() { 6 | var g; 7 | 8 | beforeEach(function() { 9 | g = new Graph({ compound: true }).setGraph({}); 10 | }); 11 | 12 | it("does not set a parent if both the tail and head have no parent", function() { 13 | g.setNode("a"); 14 | g.setNode("b"); 15 | g.setNode("d1", { edgeObj: { v: "a", w: "b" } }); 16 | g.graph().dummyChains = ["d1"]; 17 | g.setPath(["a", "d1", "b"]); 18 | 19 | parentDummyChains(g); 20 | expect(g.parent("d1")).to.be.undefined; 21 | }); 22 | 23 | it("uses the tail's parent for the first node if it is not the root", function() { 24 | g.setParent("a", "sg1"); 25 | g.setNode("sg1", { minRank: 0, maxRank: 2 }); 26 | g.setNode("d1", { edgeObj: { v: "a", w: "b" }, rank: 2 }); 27 | g.graph().dummyChains = ["d1"]; 28 | g.setPath(["a", "d1", "b"]); 29 | 30 | parentDummyChains(g); 31 | expect(g.parent("d1")).equals("sg1"); 32 | }); 33 | 34 | it("uses the heads's parent for the first node if tail's is root", function() { 35 | g.setParent("b", "sg1"); 36 | g.setNode("sg1", { minRank: 1, maxRank: 3 }); 37 | g.setNode("d1", { edgeObj: { v: "a", w: "b" }, rank: 1 }); 38 | g.graph().dummyChains = ["d1"]; 39 | g.setPath(["a", "d1", "b"]); 40 | 41 | parentDummyChains(g); 42 | expect(g.parent("d1")).equals("sg1"); 43 | }); 44 | 45 | it("handles a long chain starting in a subgraph", function() { 46 | g.setParent("a", "sg1"); 47 | g.setNode("sg1", { minRank: 0, maxRank: 2 }); 48 | g.setNode("d1", { edgeObj: { v: "a", w: "b" }, rank: 2 }); 49 | g.setNode("d2", { rank: 3 }); 50 | g.setNode("d3", { rank: 4 }); 51 | g.graph().dummyChains = ["d1"]; 52 | g.setPath(["a", "d1", "d2", "d3", "b"]); 53 | 54 | parentDummyChains(g); 55 | expect(g.parent("d1")).equals("sg1"); 56 | expect(g.parent("d2")).to.be.undefined; 57 | expect(g.parent("d3")).to.be.undefined; 58 | }); 59 | 60 | it("handles a long chain ending in a subgraph", function() { 61 | g.setParent("b", "sg1"); 62 | g.setNode("sg1", { minRank: 3, maxRank: 5 }); 63 | g.setNode("d1", { edgeObj: { v: "a", w: "b" }, rank: 1 }); 64 | g.setNode("d2", { rank: 2 }); 65 | g.setNode("d3", { rank: 3 }); 66 | g.graph().dummyChains = ["d1"]; 67 | g.setPath(["a", "d1", "d2", "d3", "b"]); 68 | 69 | parentDummyChains(g); 70 | expect(g.parent("d1")).to.be.undefined; 71 | expect(g.parent("d2")).to.be.undefined; 72 | expect(g.parent("d3")).equals("sg1"); 73 | }); 74 | 75 | it("handles nested subgraphs", function() { 76 | g.setParent("a", "sg2"); 77 | g.setParent("sg2", "sg1"); 78 | g.setNode("sg1", { minRank: 0, maxRank: 4 }); 79 | g.setNode("sg2", { minRank: 1, maxRank: 3 }); 80 | g.setParent("b", "sg4"); 81 | g.setParent("sg4", "sg3"); 82 | g.setNode("sg3", { minRank: 6, maxRank: 10 }); 83 | g.setNode("sg4", { minRank: 7, maxRank: 9 }); 84 | for (var i = 0; i < 5; ++i) { 85 | g.setNode("d" + (i + 1), { rank: i + 3 }); 86 | } 87 | g.node("d1").edgeObj = { v: "a", w: "b" }; 88 | g.graph().dummyChains = ["d1"]; 89 | g.setPath(["a", "d1", "d2", "d3", "d4", "d5", "b"]); 90 | 91 | parentDummyChains(g); 92 | expect(g.parent("d1")).equals("sg2"); 93 | expect(g.parent("d2")).equals("sg1"); 94 | expect(g.parent("d3")).to.be.undefined; 95 | expect(g.parent("d4")).equals("sg3"); 96 | expect(g.parent("d5")).equals("sg4"); 97 | }); 98 | 99 | it("handles overlapping rank ranges", function() { 100 | g.setParent("a", "sg1"); 101 | g.setNode("sg1", { minRank: 0, maxRank: 3 }); 102 | g.setParent("b", "sg2"); 103 | g.setNode("sg2", { minRank: 2, maxRank: 6 }); 104 | g.setNode("d1", { edgeObj: { v: "a", w: "b" }, rank: 2 }); 105 | g.setNode("d2", { rank: 3 }); 106 | g.setNode("d3", { rank: 4 }); 107 | g.graph().dummyChains = ["d1"]; 108 | g.setPath(["a", "d1", "d2", "d3", "b"]); 109 | 110 | parentDummyChains(g); 111 | expect(g.parent("d1")).equals("sg1"); 112 | expect(g.parent("d2")).equals("sg1"); 113 | expect(g.parent("d3")).equals("sg2"); 114 | }); 115 | 116 | it("handles an LCA that is not the root of the graph #1", function() { 117 | g.setParent("a", "sg1"); 118 | g.setParent("sg2", "sg1"); 119 | g.setNode("sg1", { minRank: 0, maxRank: 6 }); 120 | g.setParent("b", "sg2"); 121 | g.setNode("sg2", { minRank: 3, maxRank: 5 }); 122 | g.setNode("d1", { edgeObj: { v: "a", w: "b" }, rank: 2 }); 123 | g.setNode("d2", { rank: 3 }); 124 | g.graph().dummyChains = ["d1"]; 125 | g.setPath(["a", "d1", "d2", "b"]); 126 | 127 | parentDummyChains(g); 128 | expect(g.parent("d1")).equals("sg1"); 129 | expect(g.parent("d2")).equals("sg2"); 130 | }); 131 | 132 | it("handles an LCA that is not the root of the graph #2", function() { 133 | g.setParent("a", "sg2"); 134 | g.setParent("sg2", "sg1"); 135 | g.setNode("sg1", { minRank: 0, maxRank: 6 }); 136 | g.setParent("b", "sg1"); 137 | g.setNode("sg2", { minRank: 1, maxRank: 3 }); 138 | g.setNode("d1", { edgeObj: { v: "a", w: "b" }, rank: 3 }); 139 | g.setNode("d2", { rank: 4 }); 140 | g.graph().dummyChains = ["d1"]; 141 | g.setPath(["a", "d1", "d2", "b"]); 142 | 143 | parentDummyChains(g); 144 | expect(g.parent("d1")).equals("sg2"); 145 | expect(g.parent("d2")).equals("sg1"); 146 | }); 147 | }); 148 | -------------------------------------------------------------------------------- /test/position-test.js: -------------------------------------------------------------------------------- 1 | var expect = require("./chai").expect; 2 | var position = require("../lib/position"); 3 | var Graph = require("../lib/graphlib").Graph; 4 | 5 | describe("position", function() { 6 | var g; 7 | 8 | beforeEach(function() { 9 | g = new Graph({ compound: true }) 10 | .setGraph({ 11 | ranksep: 50, 12 | nodesep: 50, 13 | edgesep: 10 14 | }); 15 | }); 16 | 17 | it("respects ranksep", function() { 18 | g.graph().ranksep = 1000; 19 | g.setNode("a", { width: 50, height: 100, rank: 0, order: 0 }); 20 | g.setNode("b", { width: 50, height: 80, rank: 1, order: 0 }); 21 | g.setEdge("a", "b"); 22 | position(g); 23 | expect(g.node("b").y).to.equal(100 + 1000 + 80 / 2); 24 | }); 25 | 26 | it("use the largest height in each rank with ranksep", function() { 27 | g.graph().ranksep = 1000; 28 | g.setNode("a", { width: 50, height: 100, rank: 0, order: 0 }); 29 | g.setNode("b", { width: 50, height: 80, rank: 0, order: 1 }); 30 | g.setNode("c", { width: 50, height: 90, rank: 1, order: 0 }); 31 | g.setEdge("a", "c"); 32 | position(g); 33 | expect(g.node("a").y).to.equal(100 / 2); 34 | expect(g.node("b").y).to.equal(100 / 2); // Note we used 100 and not 80 here 35 | expect(g.node("c").y).to.equal(100 + 1000 + 90 / 2); 36 | }); 37 | 38 | it("respects nodesep", function() { 39 | g.graph().nodesep = 1000; 40 | g.setNode("a", { width: 50, height: 100, rank: 0, order: 0 }); 41 | g.setNode("b", { width: 70, height: 80, rank: 0, order: 1 }); 42 | position(g); 43 | expect(g.node("b").x).to.equal(g.node("a").x + 50 / 2 + 1000 + 70 / 2); 44 | }); 45 | 46 | it("should not try to position the subgraph node itself", function() { 47 | g.setNode("a", { width: 50, height: 50, rank: 0, order: 0 }); 48 | g.setNode("sg1", {}); 49 | g.setParent("a", "sg1"); 50 | position(g); 51 | expect(g.node("sg1")).to.not.have.property("x"); 52 | expect(g.node("sg1")).to.not.have.property("y"); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /test/rank/feasible-tree-layer-test.js: -------------------------------------------------------------------------------- 1 | var _ = require("lodash"); 2 | var expect = require("../chai").expect; 3 | var Graph = require("../../lib/graphlib").Graph; 4 | var feasibleTree = 5 | require("../../lib/rank/feasible-tree").feasibleTreeWithLayer; 6 | 7 | describe("feasibleTreeWithLayer", function () { 8 | describe("specify layer, rank according to given layer", function () { 9 | it("proper layer", function () { 10 | var g = new Graph() 11 | .setNode("a", { rank: 0 }) 12 | .setNode("b", { rank: 1, layer: 1 }) 13 | .setNode("c", { rank: 2 }) 14 | // .setNode("d", { rank: 2 }) 15 | .setEdge("a", "b", { minlen: 1 }) 16 | .setEdge("a", "c", { minlen: 1 }); 17 | // .setEdge("b", "d", { minlen: 1 }); 18 | 19 | feasibleTree(g); 20 | expect(g.node("b").rank).to.equal(g.node("a").rank + 1); 21 | expect(g.node("c").rank).to.equal(g.node("a").rank + 1); 22 | }); 23 | it("deeper layer", function () { 24 | var g = new Graph() 25 | .setNode("a", { rank: 0 }) 26 | .setNode("b", { rank: 2, layer: 2 }) 27 | .setNode("c", { rank: 2 }) 28 | .setNode("d", { rank: 2 }) 29 | .setEdge("a", "b", { minlen: 1 }) 30 | .setEdge("a", "c", { minlen: 1 }) 31 | .setEdge("b", "d", { minlen: 1 }); 32 | 33 | feasibleTree(g); 34 | expect(g.node("b").rank).to.equal(g.node("a").rank + 2); 35 | expect(g.node("d").rank).to.equal(g.node("b").rank + 1); 36 | expect(g.node("c").rank).to.equal(g.node("a").rank + 1); 37 | }); 38 | it("shalow layer", function () { 39 | var g = new Graph() 40 | .setNode("a", { rank: 0 }) 41 | .setNode("b", { rank: 0, layer: 0 }) 42 | .setNode("c", { rank: 2 }) 43 | .setNode("d", { rank: 2 }) 44 | .setEdge("a", "b", { minlen: 1 }) 45 | .setEdge("a", "c", { minlen: 1 }) 46 | .setEdge("b", "d", { minlen: 1 }); 47 | 48 | feasibleTree(g); 49 | expect(g.node("b").rank).to.equal(g.node("a").rank); 50 | expect(g.node("c").rank).to.equal(g.node("a").rank + 1); 51 | expect(g.node("d").rank).to.equal(g.node("b").rank + 1); 52 | }); 53 | }); 54 | describe("without specify layer, no effect", function () { 55 | it("creates a tree for a trivial input graph", function () { 56 | var g = new Graph() 57 | .setNode("a", { rank: 0 }) 58 | .setNode("b", { rank: 1 }) 59 | .setEdge("a", "b", { minlen: 1 }); 60 | 61 | var tree = feasibleTree(g); 62 | expect(g.node("b").rank).to.equal(g.node("a").rank + 1); 63 | expect(tree.neighbors("a")).to.eql(["b"]); 64 | }); 65 | 66 | it("correctly shortens slack by pulling a node up", function () { 67 | var g = new Graph() 68 | .setNode("a", { rank: 0 }) 69 | .setNode("b", { rank: 1 }) 70 | .setNode("c", { rank: 2 }) 71 | .setNode("d", { rank: 2 }) 72 | .setPath(["a", "b", "c"], { minlen: 1 }) 73 | .setEdge("a", "d", { minlen: 1 }); 74 | 75 | var tree = feasibleTree(g); 76 | expect(g.node("b").rank).to.eql(g.node("a").rank + 1); 77 | expect(g.node("c").rank).to.eql(g.node("b").rank + 1); 78 | expect(g.node("d").rank).to.eql(g.node("a").rank + 1); 79 | expect(_.sortBy(tree.neighbors("a"))).to.eql(["b", "d"]); 80 | expect(_.sortBy(tree.neighbors("b"))).to.eql(["a", "c"]); 81 | expect(tree.neighbors("c")).to.eql(["b"]); 82 | expect(tree.neighbors("d")).to.eql(["a"]); 83 | }); 84 | 85 | it("correctly shortens slack by pulling a node down", function () { 86 | var g = new Graph() 87 | .setNode("a", { rank: 2 }) 88 | .setNode("b", { rank: 0 }) 89 | .setNode("c", { rank: 2 }) 90 | .setEdge("b", "a", { minlen: 1 }) 91 | .setEdge("b", "c", { minlen: 1 }); 92 | 93 | var tree = feasibleTree(g); 94 | expect(g.node("a").rank).to.eql(g.node("b").rank + 1); 95 | expect(g.node("c").rank).to.eql(g.node("b").rank + 1); 96 | expect(_.sortBy(tree.neighbors("a"))).to.eql(["b"]); 97 | expect(_.sortBy(tree.neighbors("b"))).to.eql(["a", "c"]); 98 | expect(_.sortBy(tree.neighbors("c"))).to.eql(["b"]); 99 | }); 100 | }); 101 | }); 102 | -------------------------------------------------------------------------------- /test/rank/feasible-tree-test.js: -------------------------------------------------------------------------------- 1 | var _ = require("lodash"); 2 | var expect = require("../chai").expect; 3 | var Graph = require("../../lib/graphlib").Graph; 4 | var feasibleTree = require("../../lib/rank/feasible-tree").feasibleTree; 5 | 6 | describe("feasibleTree", function() { 7 | it("creates a tree for a trivial input graph", function() { 8 | var g = new Graph() 9 | .setNode("a", { rank: 0 }) 10 | .setNode("b", { rank: 1 }) 11 | .setEdge("a", "b", { minlen: 1 }); 12 | 13 | var tree = feasibleTree(g); 14 | expect(g.node("b").rank).to.equal(g.node("a").rank + 1); 15 | expect(tree.neighbors("a")).to.eql(["b"]); 16 | }); 17 | 18 | it("correctly shortens slack by pulling a node up", function() { 19 | var g = new Graph() 20 | .setNode("a", { rank: 0 }) 21 | .setNode("b", { rank: 1 }) 22 | .setNode("c", { rank: 2 }) 23 | .setNode("d", { rank: 2 }) 24 | .setPath(["a", "b", "c"], { minlen: 1 }) 25 | .setEdge("a", "d", { minlen: 1 }); 26 | 27 | var tree = feasibleTree(g); 28 | expect(g.node("b").rank).to.eql(g.node("a").rank + 1); 29 | expect(g.node("c").rank).to.eql(g.node("b").rank + 1); 30 | expect(g.node("d").rank).to.eql(g.node("a").rank + 1); 31 | expect(_.sortBy(tree.neighbors("a"))).to.eql(["b", "d"]); 32 | expect(_.sortBy(tree.neighbors("b"))).to.eql(["a", "c"]); 33 | expect(tree.neighbors("c")).to.eql(["b"]); 34 | expect(tree.neighbors("d")).to.eql(["a"]); 35 | }); 36 | 37 | it("correctly shortens slack by pulling a node down", function() { 38 | var g = new Graph() 39 | .setNode("a", { rank: 2 }) 40 | .setNode("b", { rank: 0 }) 41 | .setNode("c", { rank: 2 }) 42 | .setEdge("b", "a", { minlen: 1 }) 43 | .setEdge("b", "c", { minlen: 1 }); 44 | 45 | var tree = feasibleTree(g); 46 | expect(g.node("a").rank).to.eql(g.node("b").rank + 1); 47 | expect(g.node("c").rank).to.eql(g.node("b").rank + 1); 48 | expect(_.sortBy(tree.neighbors("a"))).to.eql(["b"]); 49 | expect(_.sortBy(tree.neighbors("b"))).to.eql(["a", "c"]); 50 | expect(_.sortBy(tree.neighbors("c"))).to.eql(["b"]); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /test/rank/rank-test.js: -------------------------------------------------------------------------------- 1 | var _ = require("lodash"); 2 | var expect = require("../chai").expect; 3 | var rank = require("../../lib/rank"); 4 | var Graph = require("../../lib/graphlib").Graph; 5 | 6 | describe("rank", function() { 7 | var RANKERS = [ 8 | "longest-path", "tight-tree", 9 | "network-simplex", "unknown-should-still-work" 10 | ]; 11 | var g; 12 | 13 | beforeEach(function() { 14 | g = new Graph() 15 | .setGraph({}) 16 | .setDefaultNodeLabel(function() { return {}; }) 17 | .setDefaultEdgeLabel(function() { return { minlen: 1, weight: 1 }; }) 18 | .setPath(["a", "b", "c", "d", "h"]) 19 | .setPath(["a", "e", "g", "h"]) 20 | .setPath(["a", "f", "g"]); 21 | }); 22 | 23 | _.forEach(RANKERS, function(ranker) { 24 | describe(ranker, function() { 25 | it("respects the minlen attribute", function() { 26 | g.graph().ranker = ranker; 27 | rank(g); 28 | _.forEach(g.edges(), function(e) { 29 | var vRank = g.node(e.v).rank; 30 | var wRank = g.node(e.w).rank; 31 | expect(wRank - vRank).to.be.gte(g.edge(e).minlen); 32 | }); 33 | }); 34 | 35 | it("can rank a single node graph", function() { 36 | var g = new Graph().setGraph({}).setNode("a", {}); 37 | rank(g, ranker); 38 | expect(g.node("a").rank).to.equal(0); 39 | }); 40 | }); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /test/rank/util-test.js: -------------------------------------------------------------------------------- 1 | var expect = require("../chai").expect; 2 | var Graph = require("../../lib/graphlib").Graph; 3 | var normalizeRanks = require("../../lib/util").normalizeRanks; 4 | var rankUtil = require("../../lib/rank/util"); 5 | var longestPath = rankUtil.longestPath; 6 | 7 | describe("rank/util", function() { 8 | describe("longestPath", function() { 9 | var g; 10 | 11 | beforeEach(function() { 12 | g = new Graph() 13 | .setDefaultNodeLabel(function() { return {}; }) 14 | .setDefaultEdgeLabel(function() { return { minlen: 1 }; }); 15 | }); 16 | 17 | it("can assign a rank to a single node graph", function() { 18 | g.setNode("a"); 19 | longestPath(g); 20 | normalizeRanks(g); 21 | expect(g.node("a").rank).to.equal(0); 22 | }); 23 | 24 | it("can assign ranks to unconnected nodes", function() { 25 | g.setNode("a"); 26 | g.setNode("b"); 27 | longestPath(g); 28 | normalizeRanks(g); 29 | expect(g.node("a").rank).to.equal(0); 30 | expect(g.node("b").rank).to.equal(0); 31 | }); 32 | 33 | it("can assign ranks to connected nodes", function() { 34 | g.setEdge("a", "b"); 35 | longestPath(g); 36 | normalizeRanks(g); 37 | expect(g.node("a").rank).to.equal(0); 38 | expect(g.node("b").rank).to.equal(1); 39 | }); 40 | 41 | it("can assign ranks for a diamond", function() { 42 | g.setPath(["a", "b", "d"]); 43 | g.setPath(["a", "c", "d"]); 44 | longestPath(g); 45 | normalizeRanks(g); 46 | expect(g.node("a").rank).to.equal(0); 47 | expect(g.node("b").rank).to.equal(1); 48 | expect(g.node("c").rank).to.equal(1); 49 | expect(g.node("d").rank).to.equal(2); 50 | }); 51 | 52 | it("uses the minlen attribute on the edge", function() { 53 | g.setPath(["a", "b", "d"]); 54 | g.setEdge("a", "c"); 55 | g.setEdge("c", "d", { minlen: 2 }); 56 | longestPath(g); 57 | normalizeRanks(g); 58 | expect(g.node("a").rank).to.equal(0); 59 | // longest path biases towards the lowest rank it can assign 60 | expect(g.node("b").rank).to.equal(2); 61 | expect(g.node("c").rank).to.equal(1); 62 | expect(g.node("d").rank).to.equal(3); 63 | }); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /test/util-test.js: -------------------------------------------------------------------------------- 1 | /* eslint "no-console": off */ 2 | 3 | var _ = require("lodash"); 4 | var expect = require("./chai").expect; 5 | var Graph = require("../lib/graphlib").Graph; 6 | var util = require("../lib/util"); 7 | 8 | describe("util", function() { 9 | describe("simplify", function() { 10 | var g; 11 | 12 | beforeEach(function() { 13 | g = new Graph({ multigraph: true }); 14 | }); 15 | 16 | it("copies without change a graph with no multi-edges", function() { 17 | g.setEdge("a", "b", { weight: 1, minlen: 1 }); 18 | var g2 = util.simplify(g); 19 | expect(g2.edge("a", "b")).eql({ weight: 1, minlen: 1 }); 20 | expect(g2.edgeCount()).equals(1); 21 | }); 22 | 23 | it("collapses multi-edges", function() { 24 | g.setEdge("a", "b", { weight: 1, minlen: 1 }); 25 | g.setEdge("a", "b", { weight: 2, minlen: 2 }, "multi"); 26 | var g2 = util.simplify(g); 27 | expect(g2.isMultigraph()).to.be.false; 28 | expect(g2.edge("a", "b")).eql({ weight: 3, minlen: 2 }); 29 | expect(g2.edgeCount()).equals(1); 30 | }); 31 | 32 | it("copies the graph object", function() { 33 | g.setGraph({ foo: "bar" }); 34 | var g2 = util.simplify(g); 35 | expect(g2.graph()).eqls({ foo: "bar" }); 36 | }); 37 | }); 38 | 39 | describe("asNonCompoundGraph", function() { 40 | var g; 41 | 42 | beforeEach(function() { 43 | g = new Graph({ compound: true, multigraph: true }); 44 | }); 45 | 46 | it("copies all nodes", function() { 47 | g.setNode("a", { foo: "bar" }); 48 | g.setNode("b"); 49 | var g2 = util.asNonCompoundGraph(g); 50 | expect(g2.node("a")).to.eql({ foo: "bar" }); 51 | expect(g2.hasNode("b")).to.be.true; 52 | }); 53 | 54 | it("copies all edges", function() { 55 | g.setEdge("a", "b", { foo: "bar" }); 56 | g.setEdge("a", "b", { foo: "baz" }, "multi"); 57 | var g2 = util.asNonCompoundGraph(g); 58 | expect(g2.edge("a", "b")).eqls({ foo: "bar" }); 59 | expect(g2.edge("a", "b", "multi")).eqls({ foo: "baz" }); 60 | }); 61 | 62 | it("does not copy compound nodes", function() { 63 | g.setParent("a", "sg1"); 64 | var g2 = util.asNonCompoundGraph(g); 65 | expect(g2.parent(g)).to.be.undefined; 66 | expect(g2.isCompound()).to.be.false; 67 | }); 68 | 69 | it ("copies the graph object", function() { 70 | g.setGraph({ foo: "bar" }); 71 | var g2 = util.asNonCompoundGraph(g); 72 | expect(g2.graph()).eqls({ foo: "bar" }); 73 | }); 74 | }); 75 | 76 | describe("successorWeights", function() { 77 | it("maps a node to its successors with associated weights", function() { 78 | var g = new Graph({ multigraph: true }); 79 | g.setEdge("a", "b", { weight: 2 }); 80 | g.setEdge("b", "c", { weight: 1 }); 81 | g.setEdge("b", "c", { weight: 2 }, "multi"); 82 | g.setEdge("b", "d", { weight: 1 }, "multi"); 83 | expect(util.successorWeights(g).a).to.eql({ b: 2 }); 84 | expect(util.successorWeights(g).b).to.eql({ c: 3, d: 1 }); 85 | expect(util.successorWeights(g).c).to.eql({}); 86 | expect(util.successorWeights(g).d).to.eql({}); 87 | }); 88 | }); 89 | 90 | describe("predecessorWeights", function() { 91 | it("maps a node to its predecessors with associated weights", function() { 92 | var g = new Graph({ multigraph: true }); 93 | g.setEdge("a", "b", { weight: 2 }); 94 | g.setEdge("b", "c", { weight: 1 }); 95 | g.setEdge("b", "c", { weight: 2 }, "multi"); 96 | g.setEdge("b", "d", { weight: 1 }, "multi"); 97 | expect(util.predecessorWeights(g).a).to.eql({}); 98 | expect(util.predecessorWeights(g).b).to.eql({ a: 2 }); 99 | expect(util.predecessorWeights(g).c).to.eql({ b: 3 }); 100 | expect(util.predecessorWeights(g).d).to.eql({ b: 1 }); 101 | }); 102 | }); 103 | 104 | describe("intersectRect", function() { 105 | function expectIntersects(rect, point) { 106 | var cross = util.intersectRect(rect, point); 107 | if (cross.x !== point.x) { 108 | var m = (cross.y - point.y) / (cross.x - point.x); 109 | expect(cross.y - rect.y).equals(m * (cross.x - rect.x)); 110 | } 111 | } 112 | 113 | function expectTouchesBorder(rect, point) { 114 | var cross = util.intersectRect(rect, point); 115 | if (Math.abs(rect.x - cross.x) !== rect.width / 2) { 116 | expect(Math.abs(rect.y - cross.y)).equals(rect.height / 2); 117 | } 118 | } 119 | 120 | it("creates a slope that will intersect the rectangle's center", function() { 121 | var rect = { x: 0, y: 0, width: 1, height: 1 }; 122 | expectIntersects(rect, { x: 2, y: 6 }); 123 | expectIntersects(rect, { x: 2, y: -6 }); 124 | expectIntersects(rect, { x: 6, y: 2 }); 125 | expectIntersects(rect, { x: -6, y: 2 }); 126 | expectIntersects(rect, { x: 5, y: 0 }); 127 | expectIntersects(rect, { x: 0, y: 5 }); 128 | }); 129 | 130 | it("touches the border of the rectangle", function() { 131 | var rect = { x: 0, y: 0, width: 1, height: 1 }; 132 | expectTouchesBorder(rect, { x: 2, y: 6 }); 133 | expectTouchesBorder(rect, { x: 2, y: -6 }); 134 | expectTouchesBorder(rect, { x: 6, y: 2 }); 135 | expectTouchesBorder(rect, { x: -6, y: 2 }); 136 | expectTouchesBorder(rect, { x: 5, y: 0 }); 137 | expectTouchesBorder(rect, { x: 0, y: 5 }); 138 | }); 139 | 140 | it("throws an error if the point is at the center of the rectangle", function() { 141 | var rect = { x: 0, y: 0, width: 1, height: 1 }; 142 | expect(function() { util.intersectRect(rect, { x: 0, y: 0 }); }).to.throw(); 143 | }); 144 | }); 145 | 146 | describe("buildLayerMatrix", function() { 147 | it("creates a matrix based on rank and order of nodes in the graph", function() { 148 | var g = new Graph(); 149 | g.setNode("a", { rank: 0, order: 0 }); 150 | g.setNode("b", { rank: 0, order: 1 }); 151 | g.setNode("c", { rank: 1, order: 0 }); 152 | g.setNode("d", { rank: 1, order: 1 }); 153 | g.setNode("e", { rank: 2, order: 0 }); 154 | 155 | expect(util.buildLayerMatrix(g)).to.eql([ 156 | ["a", "b"], 157 | ["c", "d"], 158 | ["e"] 159 | ]); 160 | }); 161 | }); 162 | 163 | describe("time", function() { 164 | var consoleLog; 165 | 166 | beforeEach(function() { 167 | consoleLog = console.log; 168 | }); 169 | 170 | afterEach(function() { 171 | console.log = consoleLog; 172 | }); 173 | 174 | it("logs timing information", function() { 175 | var capture = []; 176 | console.log = function() { capture.push(_.toArray(arguments)[0]); }; 177 | util.time("foo", function() {}); 178 | expect(capture.length).to.equal(1); 179 | expect(capture[0]).to.match(/^foo time: .*ms/); 180 | }); 181 | 182 | it("returns the value from the evaluated function", function() { 183 | console.log = function() {}; 184 | expect(util.time("foo", _.constant("bar"))).to.equal("bar"); 185 | }); 186 | }); 187 | 188 | describe("normalizeRanks", function() { 189 | it("adjust ranks such that all are >= 0, and at least one is 0", function() { 190 | var g = new Graph() 191 | .setNode("a", { rank: 3 }) 192 | .setNode("b", { rank: 2 }) 193 | .setNode("c", { rank: 4 }); 194 | 195 | util.normalizeRanks(g); 196 | 197 | expect(g.node("a").rank).to.equal(1); 198 | expect(g.node("b").rank).to.equal(0); 199 | expect(g.node("c").rank).to.equal(2); 200 | }); 201 | 202 | it("works for negative ranks", function() { 203 | var g = new Graph() 204 | .setNode("a", { rank: -3 }) 205 | .setNode("b", { rank: -2 }); 206 | 207 | util.normalizeRanks(g); 208 | 209 | expect(g.node("a").rank).to.equal(0); 210 | expect(g.node("b").rank).to.equal(1); 211 | }); 212 | 213 | it("does not assign a rank to subgraphs", function() { 214 | var g = new Graph({ compound: true }) 215 | .setNode("a", { rank: 0 }) 216 | .setNode("sg", {}) 217 | .setParent("a", "sg"); 218 | 219 | util.normalizeRanks(g); 220 | 221 | expect(g.node("sg")).to.not.have.property("rank"); 222 | expect(g.node("a").rank).to.equal(0); 223 | }); 224 | }); 225 | 226 | describe("removeEmptyRanks", function() { 227 | it("Removes border ranks without any nodes", function() { 228 | var g = new Graph() 229 | .setGraph({ nodeRankFactor: 4 }) 230 | .setNode("a", { rank: 0 }) 231 | .setNode("b", { rank: 4 }); 232 | util.removeEmptyRanks(g); 233 | expect(g.node("a").rank).equals(0); 234 | expect(g.node("b").rank).equals(1); 235 | }); 236 | 237 | it("Does not remove non-border ranks", function() { 238 | var g = new Graph() 239 | .setGraph({ nodeRankFactor: 4 }) 240 | .setNode("a", { rank: 0 }) 241 | .setNode("b", { rank: 8 }); 242 | util.removeEmptyRanks(g); 243 | expect(g.node("a").rank).equals(0); 244 | expect(g.node("b").rank).equals(2); 245 | }); 246 | }); 247 | }); 248 | -------------------------------------------------------------------------------- /test/version-test.js: -------------------------------------------------------------------------------- 1 | var expect = require("./chai").expect; 2 | 3 | describe("version", function() { 4 | it("should match the version from package.json", function() { 5 | var packageVersion = require("../package").version; 6 | expect(require("../").version).to.equal(packageVersion); 7 | }); 8 | }); 9 | --------------------------------------------------------------------------------