├── .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 | 
34 | 
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 |
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 |
--------------------------------------------------------------------------------