├── .pullapprove.yml ├── .eslintignore ├── .eslintrc ├── .gitignore ├── index.d.ts ├── .bithoundrc ├── tests ├── .eslintrc ├── chai.js ├── index-test.js ├── rank │ ├── rank-test.js │ ├── feasible-tree-test.js │ ├── util-test.js │ └── network-simplex-test.js ├── order │ ├── init-order-test.js │ ├── cross-count-test.js │ ├── order-test.js │ ├── add-subgraph-constraints-test.js │ ├── barycenter-test.js │ ├── sort-test.js │ ├── sort-subgraph-test.js │ ├── build-layer-graph-test.js │ └── resolve-conflicts-test.js ├── data │ └── list-test.js ├── position-test.js ├── coordinate-system-test.js ├── acyclic-test.js ├── greedy-fas-test.js ├── add-border-segments-test.js ├── parent-dummy-chains-test.js ├── normalize-test.js ├── nesting-graph-test.js ├── util-test.js └── layout-test.js ├── .npmignore ├── .github └── PULL_REQUEST_TEMPLATE.md ├── src ├── order │ ├── barycenter.js │ ├── init-order.js │ ├── add-subgraph-constraints.js │ ├── sort.js │ ├── cross-count.js │ ├── index.js │ ├── sort-subgraph.js │ ├── build-layer-graph.js │ └── resolve-conflicts.js ├── position │ ├── index.js │ └── bk.js ├── debug.js ├── add-border-segments.js ├── index.js ├── data │ └── list.js ├── rank │ ├── index.js │ ├── util.js │ ├── feasible-tree.js │ └── network-simplex.js ├── acyclic.js ├── coordinate-system.js ├── parent-dummy-chains.js ├── normalize.js ├── greedy-fas.js ├── nesting-graph.js ├── util.js └── layout.js ├── CHANGELOG.md ├── LICENSE ├── README.md ├── package.json ├── index.js └── .travis.yml /.pullapprove.yml: -------------------------------------------------------------------------------- 1 | extends: ui 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | lib/**/* 2 | node_modules/**/* 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["standard"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | lib/ 3 | node_modules/ 4 | .DS_Store 5 | npm-debug.log 6 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | // TypeScript typings 2 | 3 | import { Graph } from "ciena-graphlib"; 4 | 5 | export function layout(graph: Graph): void; 6 | 7 | -------------------------------------------------------------------------------- /.bithoundrc: -------------------------------------------------------------------------------- 1 | { 2 | "ignore": [ 3 | "**/node_modules/**", 4 | "**/tests/**" 5 | ], 6 | "test": [ 7 | "**/tests/**" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /tests/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "afterEach": false, 4 | "beforeEach": false, 5 | "describe": false, 6 | "it": false 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .bithoundrc 2 | .github 3 | .eslintignore 4 | .eslintrc 5 | .pullapprove.yml 6 | .travis.yml 7 | dependency-snapshot.json 8 | CHANGELOG.md 9 | /node_modules 10 | /tests 11 | -------------------------------------------------------------------------------- /tests/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 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **This project uses [semver](http://semver.org), please check the scope of this pr:** 2 | 3 | - [ ] #patch# - backwards-compatible bug fix 4 | - [ ] #minor# - adding functionality in a backwards-compatible manner 5 | - [ ] #major# - incompatible API change 6 | 7 | # CHANGELOG 8 | 9 | Please add a description of your change here, it will be automatically prepended to the `CHANGELOG.md` file. 10 | -------------------------------------------------------------------------------- /src/order/barycenter.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | 3 | export default function barycenter (g, movable) { 4 | return _.map(movable, function (v) { 5 | var inV = g.inEdges(v) 6 | if (!inV.length) { 7 | return { v: v } 8 | } else { 9 | var result = _.reduce(inV, function (acc, e) { 10 | var edge = g.edge(e) 11 | var nodeU = g.node(e.v) 12 | return { 13 | sum: acc.sum + (edge.weight * nodeU.order), 14 | weight: acc.weight + edge.weight 15 | } 16 | }, { sum: 0, weight: 0 }) 17 | 18 | return { 19 | v: v, 20 | barycenter: result.sum / result.weight, 21 | weight: result.weight 22 | } 23 | } 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /src/position/index.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import {asNonCompoundGraph, buildLayerMatrix} from '../util' 3 | import {positionX} from './bk' 4 | 5 | function positionY (g) { 6 | var layering = buildLayerMatrix(g) 7 | var rankSep = g.graph().ranksep 8 | var prevY = 0 9 | _.forEach(layering, function (layer) { 10 | var maxHeight = _.max(_.map(layer, function (v) { return g.node(v).height })) 11 | _.forEach(layer, function (v) { 12 | g.node(v).y = prevY + maxHeight / 2 13 | }) 14 | prevY += maxHeight + rankSep 15 | }) 16 | } 17 | 18 | export default function position (g) { 19 | g = asNonCompoundGraph(g) 20 | 21 | positionY(g) 22 | _.forEach(positionX(g), function (x, v) { 23 | g.node(v).x = x 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /src/debug.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import {buildLayerMatrix} from './util' 3 | import {Graph} from 'ciena-graphlib' 4 | 5 | export function debugOrdering (g) { 6 | var layerMatrix = buildLayerMatrix(g) 7 | 8 | var h = new Graph({ compound: true, multigraph: true }).setGraph({}) 9 | 10 | _.forEach(g.nodes(), function (v) { 11 | h.setNode(v, { label: v }) 12 | h.setParent(v, 'layer' + g.node(v).rank) 13 | }) 14 | 15 | _.forEach(g.edges(), function (e) { 16 | h.setEdge(e.v, e.w, {}, e.name) 17 | }) 18 | 19 | _.forEach(layerMatrix, function (layer, i) { 20 | var layerV = 'layer' + i 21 | h.setNode(layerV, { rank: 'same' }) 22 | _.reduce(layer, function (u, v) { 23 | h.setEdge(u, v, { style: 'invis' }) 24 | return v 25 | }) 26 | }) 27 | 28 | return h 29 | } 30 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.0.7 (2017-05-11) 2 | * **Removed** /src in /.npmignore 3 | 4 | 5 | # 1.0.6 (2017-05-11) 6 | 7 | * **Updated** the secure auth tokens in `.travis.yml` 8 | 9 | 10 | # 1.0.5 (2017-05-08) 11 | 12 | Please add a description of your change here, it will be automatically prepended to the `CHANGELOG.md` file. 13 | 14 | 15 | # 1.0.4 (2017-04-26) 16 | don't use debug timings unless debugTiming == true 17 | 18 | 19 | # 1.0.3 (2017-04-25) 20 | 21 | Please add a description of your change here, it will be automatically prepended to the `CHANGELOG.md` file. 22 | 23 | 24 | # 1.0.2 (2017-04-25) 25 | Fixes graph layout with cycles, re-enable tests 26 | 27 | 28 | # 1.0.1 29 | 30 | Fix `export` to be `export default` in `src/position/bk.js`. 31 | 32 | 33 | # 1.0.0 34 | 35 | * Initial release. 36 | 37 | -------------------------------------------------------------------------------- /src/add-border-segments.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import {addDummyNode} from './util' 3 | 4 | function addBorderNode (g, prop, prefix, sg, sgNode, rank) { 5 | var label = { width: 0, height: 0, rank: rank, borderType: prop } 6 | var prev = sgNode[prop][rank - 1] 7 | var curr = addDummyNode(g, 'border', label, prefix) 8 | sgNode[prop][rank] = curr 9 | g.setParent(curr, sg) 10 | if (prev) { 11 | g.setEdge(prev, curr, { weight: 1 }) 12 | } 13 | } 14 | 15 | export default function (g) { 16 | function dfs (v) { 17 | var children = g.children(v) 18 | var node = g.node(v) 19 | if (children.length) { 20 | _.forEach(children, dfs) 21 | } 22 | 23 | if (_.has(node, 'minRank')) { 24 | node.borderLeft = [] 25 | node.borderRight = [] 26 | for (var rank = node.minRank, maxRank = node.maxRank + 1; 27 | rank < maxRank; 28 | ++rank) { 29 | addBorderNode(g, 'borderLeft', '_bl', v, node, rank) 30 | addBorderNode(g, 'borderRight', '_br', v, node, rank) 31 | } 32 | } 33 | } 34 | 35 | _.forEach(g.children(), dfs) 36 | } 37 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/order/init-order.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | 3 | /* 4 | * Assigns an initial order value for each node by performing a DFS search 5 | * starting from nodes in the first rank. Nodes are assigned an order in their 6 | * rank as they are first visited. 7 | * 8 | * This approach comes from Gansner, et al., "A Technique for Drawing Directed 9 | * Graphs." 10 | * 11 | * Returns a layering matrix with an array per layer and each layer sorted by 12 | * the order of its nodes. 13 | */ 14 | export default function initOrder (g) { 15 | var visited = {} 16 | var simpleNodes = _.filter(g.nodes(), function (v) { 17 | return !g.children(v).length 18 | }) 19 | var maxRank = _.max(_.map(simpleNodes, function (v) { return g.node(v).rank })) 20 | var layers = _.map(_.range(maxRank + 1), function () { return [] }) 21 | 22 | function dfs (v) { 23 | if (_.has(visited, v)) return 24 | visited[v] = true 25 | var node = g.node(v) 26 | layers[node.rank].push(v) 27 | _.forEach(g.successors(v), dfs) 28 | } 29 | 30 | var orderedVs = _.sortBy(simpleNodes, function (v) { return g.node(v).rank }) 31 | _.forEach(orderedVs, dfs) 32 | 33 | return layers 34 | } 35 | -------------------------------------------------------------------------------- /tests/index-test.js: -------------------------------------------------------------------------------- 1 | // These are smoke tests to make sure the bundles look like they are working 2 | // correctly. 3 | 4 | var expect = require('chai').expect 5 | var dagre = require('../lib') 6 | var graphlib = dagre.graphlib 7 | 8 | describe('index', function () { 9 | it('exports dagre', function () { 10 | expect(dagre).to.be.an('object') 11 | expect(dagre.graphlib).to.be.an('object') 12 | expect(dagre.layout).to.be.a('function') 13 | expect(dagre.util).to.be.an('object') 14 | }) 15 | 16 | it('can do trivial layout', function () { 17 | var g = new graphlib.Graph().setGraph({}) 18 | g.setNode('a', { label: 'a', width: 50, height: 100 }) 19 | g.setNode('b', { label: 'b', width: 50, height: 100 }) 20 | g.setEdge('a', 'b', { label: 'ab', width: 50, height: 100 }) 21 | 22 | dagre.layout(g) 23 | expect(g.node('a')).to.have.property('x') 24 | expect(g.node('a')).to.have.property('y') 25 | expect(g.node('a').x).to.be.gte(0) 26 | expect(g.node('a').y).to.be.gte(0) 27 | expect(g.edge('a', 'b')).to.have.property('x') 28 | expect(g.edge('a', 'b')).to.have.property('y') 29 | expect(g.edge('a', 'b').x).to.be.gte(0) 30 | expect(g.edge('a', 'b').y).to.be.gte(0) 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ciena-dagre 2 | 3 | `ciena-dagre` is a fork of [dagre](https://github.com/cpettitt/dagre), 4 | which is a JavaScript library that makes it easy to lay out directed graphs on 5 | the client-side. The history behind this fork's existence can be read at https://github.com/ciena-blueplanet/dagre/issues/24 6 | 7 | ###### Dependencies 8 | 9 | [![NPM][npm-img]][npm-url] 10 | 11 | ###### Health 12 | 13 | [![Travis][ci-img]][ci-url] 14 | [![Coveralls][cov-img]][cov-url] 15 | 16 | ###### Security 17 | 18 | [![bitHound][bithound-img]][bithound-url] 19 | 20 | ## Installation 21 | 22 | ```bash 23 | npm install ciena-dagre --ignore-scripts 24 | ``` 25 | 26 | [bithound-img]: https://www.bithound.io/github/ciena-blueplanet/dagre/badges/score.svg "bitHound" 27 | [bithound-url]: https://www.bithound.io/github/ciena-blueplanet/dagre 28 | 29 | [ci-img]: https://img.shields.io/travis/ciena-blueplanet/dagre.svg "Travis CI Build Status" 30 | [ci-url]: https://travis-ci.org/ciena-blueplanet/dagre 31 | 32 | [cov-img]: https://img.shields.io/coveralls/ciena-blueplanet/dagre.svg "Coveralls Code Coverage" 33 | [cov-url]: https://coveralls.io/github/ciena-blueplanet/dagre 34 | 35 | [npm-img]: https://img.shields.io/npm/v/ciena-dagre.svg "NPM Version" 36 | [npm-url]: https://www.npmjs.com/package/ciena-dagre 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ciena-dagre", 3 | "version": "1.0.10", 4 | "description": "Graph layout for JavaScript", 5 | "scripts": { 6 | "build": "babel src -d lib --presets es2015 --plugins add-module-exports", 7 | "lint": "npm run lint-js", 8 | "lint-js": "eslint lib test *.js", 9 | "test": "npm run lint && npm run utest", 10 | "utest": "npm run build && istanbul cover _mocha -- --recursive tests" 11 | }, 12 | "author": "Chris Pettitt ", 13 | "contributors": [ 14 | "Matthew Dahl (https://github.com/sandersky)" 15 | ], 16 | "main": "index.js", 17 | "typings": "index.d.ts", 18 | "keywords": [ 19 | "graph", 20 | "layout" 21 | ], 22 | "dependencies": { 23 | "ciena-graphlib": "^1.0.0", 24 | "lodash": "^4.15.0", 25 | "npm-install-security-check": "^1.0.2" 26 | }, 27 | "devDependencies": { 28 | "babel-cli": "^6.9.0", 29 | "babel-plugin-add-module-exports": "^0.2.1", 30 | "babel-preset-es2015": "^6.9.0", 31 | "chai": "^3.5.0", 32 | "eslint": "^3.4.0", 33 | "eslint-config-standard": "^6.0.0", 34 | "eslint-plugin-promise": "^3.4.0", 35 | "eslint-plugin-standard": "^2.0.0", 36 | "istanbul": "^0.4.5", 37 | "mocha": "^3.0.0" 38 | }, 39 | "repository": { 40 | "type": "git", 41 | "url": "https://github.com/ciena-blueplanet/dagre.git" 42 | }, 43 | "license": "MIT" 44 | } -------------------------------------------------------------------------------- /tests/rank/rank-test.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect 2 | const Graph = require('ciena-graphlib').Graph 3 | const _ = require('lodash') 4 | 5 | const rank = require('../../lib/rank') 6 | 7 | describe('rank', function () { 8 | var RANKERS = [ 9 | 'longest-path', 'tight-tree', 10 | 'network-simplex', 'unknown-should-still-work' 11 | ] 12 | var g 13 | 14 | beforeEach(function () { 15 | g = new Graph() 16 | .setGraph({}) 17 | .setDefaultNodeLabel(function () { return {} }) 18 | .setDefaultEdgeLabel(function () { return { minlen: 1, weight: 1 } }) 19 | .setPath(['a', 'b', 'c', 'd', 'h']) 20 | .setPath(['a', 'e', 'g', 'h']) 21 | .setPath(['a', 'f', 'g']) 22 | }) 23 | 24 | _.forEach(RANKERS, function (ranker) { 25 | describe(ranker, function () { 26 | it('respects the minlen attribute', function () { 27 | g.graph().ranker = ranker 28 | rank(g) 29 | _.forEach(g.edges(), function (e) { 30 | var vRank = g.node(e.v).rank 31 | var wRank = g.node(e.w).rank 32 | expect(wRank - vRank).to.be.gte(g.edge(e).minlen) 33 | }) 34 | }) 35 | 36 | it('can rank a single node graph', function () { 37 | var g = new Graph().setGraph({}).setNode('a', {}) 38 | rank(g, ranker) 39 | expect(g.node('a').rank).to.equal(0) 40 | }) 41 | }) 42 | }) 43 | }) 44 | -------------------------------------------------------------------------------- /src/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 | import {debugOrdering} from './debug' 24 | 25 | export const debug = { 26 | debugOrdering 27 | } 28 | 29 | export {default as graphlib} from 'ciena-graphlib' 30 | export {default as layout} from './layout' 31 | export {default as util} from './util' 32 | -------------------------------------------------------------------------------- /src/data/list.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Simple doubly linked list implementation derived from Cormen, et al., 3 | * "Introduction to Algorithms". 4 | */ 5 | 6 | function List () { 7 | var sentinel = {} 8 | sentinel._next = sentinel._prev = sentinel 9 | this._sentinel = sentinel 10 | } 11 | 12 | List.prototype.dequeue = function () { 13 | var sentinel = this._sentinel 14 | var entry = sentinel._prev 15 | if (entry !== sentinel) { 16 | unlink(entry) 17 | return entry 18 | } 19 | } 20 | 21 | List.prototype.enqueue = function (entry) { 22 | var sentinel = this._sentinel 23 | if (entry._prev && entry._next) { 24 | unlink(entry) 25 | } 26 | entry._next = sentinel._next 27 | sentinel._next._prev = entry 28 | sentinel._next = entry 29 | entry._prev = sentinel 30 | } 31 | 32 | List.prototype.toString = function () { 33 | var strs = [] 34 | var sentinel = this._sentinel 35 | var curr = sentinel._prev 36 | while (curr !== sentinel) { 37 | strs.push(JSON.stringify(curr, filterOutLinks)) 38 | curr = curr._prev 39 | } 40 | return '[' + strs.join(', ') + ']' 41 | } 42 | 43 | function unlink (entry) { 44 | entry._prev._next = entry._next 45 | entry._next._prev = entry._prev 46 | delete entry._next 47 | delete entry._prev 48 | } 49 | 50 | function filterOutLinks (k, v) { 51 | if (k !== '_next' && k !== '_prev') { 52 | return v 53 | } 54 | } 55 | 56 | export default List 57 | -------------------------------------------------------------------------------- /src/rank/index.js: -------------------------------------------------------------------------------- 1 | import {longestPath} from './util' 2 | import feasibleTree from './feasible-tree' 3 | import networkSimplex from './network-simplex' 4 | 5 | // A fast and simple ranker, but results are far from optimal. 6 | var longestPathRanker = longestPath 7 | 8 | function tightTreeRanker (g) { 9 | longestPath(g) 10 | feasibleTree(g) 11 | } 12 | 13 | function networkSimplexRanker (g) { 14 | networkSimplex(g) 15 | } 16 | 17 | /* 18 | * Assigns a rank to each node in the input graph that respects the "minlen" 19 | * constraint specified on edges between nodes. 20 | * 21 | * This basic structure is derived from Gansner, et al., "A Technique for 22 | * Drawing Directed Graphs." 23 | * 24 | * Pre-conditions: 25 | * 26 | * 1. Graph must be a connected DAG 27 | * 2. Graph nodes must be objects 28 | * 3. Graph edges must have "weight" and "minlen" attributes 29 | * 30 | * Post-conditions: 31 | * 32 | * 1. Graph nodes will have a "rank" attribute based on the results of the 33 | * algorithm. Ranks can start at any index (including negative), we'll 34 | * fix them up later. 35 | */ 36 | export default function rank (g) { 37 | switch (g.graph().ranker) { 38 | case 'network-simplex': networkSimplexRanker(g); break 39 | case 'tight-tree': tightTreeRanker(g); break 40 | case 'longest-path': longestPathRanker(g); break 41 | default: networkSimplexRanker(g) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/order/add-subgraph-constraints.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | 3 | export default function addSubgraphConstraints (g, cg, vs) { 4 | var prev = {} 5 | var rootPrev 6 | 7 | _.forEach(vs, function (v) { 8 | var child = g.parent(v) 9 | var parent 10 | var prevChild 11 | while (child) { 12 | parent = g.parent(child) 13 | if (parent) { 14 | prevChild = prev[parent] 15 | prev[parent] = child 16 | } else { 17 | prevChild = rootPrev 18 | rootPrev = child 19 | } 20 | if (prevChild && prevChild !== child) { 21 | cg.setEdge(prevChild, child) 22 | return 23 | } 24 | child = parent 25 | } 26 | }) 27 | 28 | /* 29 | function dfs(v) { 30 | var children = v ? g.children(v) : g.children(); 31 | if (children.length) { 32 | var min = Number.POSITIVE_INFINITY, 33 | subgraphs = []; 34 | _.forEach(children, function(child) { 35 | var childMin = dfs(child); 36 | if (g.children(child).length) { 37 | subgraphs.push({ v: child, order: childMin }); 38 | } 39 | min = Math.min(min, childMin); 40 | }); 41 | _.reduce(_.sortBy(subgraphs, "order"), function(prev, curr) { 42 | cg.setEdge(prev.v, curr.v); 43 | return curr; 44 | }); 45 | return min; 46 | } 47 | return g.node(v).order; 48 | } 49 | dfs(undefined); 50 | */ 51 | } 52 | -------------------------------------------------------------------------------- /src/acyclic.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import greedyFAS from './greedy-fas' 3 | 4 | function dfsFAS (g) { 5 | var fas = [] 6 | var stack = {} 7 | var visited = {} 8 | 9 | function dfs (v) { 10 | if (_.has(visited, v)) { 11 | return 12 | } 13 | visited[v] = true 14 | stack[v] = true 15 | _.forEach(g.outEdges(v), function (e) { 16 | if (_.has(stack, e.w)) { 17 | fas.push(e) 18 | } else { 19 | dfs(e.w) 20 | } 21 | }) 22 | delete stack[v] 23 | } 24 | 25 | _.forEach(g.nodes(), dfs) 26 | return fas 27 | } 28 | 29 | export function undo (g) { 30 | _.forEach(g.edges(), function (e) { 31 | var label = g.edge(e) 32 | if (label.reversed) { 33 | g.removeEdge(e) 34 | 35 | var forwardName = label.forwardName 36 | delete label.reversed 37 | delete label.forwardName 38 | g.setEdge(e.w, e.v, label, forwardName) 39 | } 40 | }) 41 | } 42 | 43 | export function run (g) { 44 | var fas = (g.graph().acyclicer === 'greedy' 45 | ? greedyFAS(g, weightFn(g)) 46 | : dfsFAS(g)) 47 | _.forEach(fas, function (e) { 48 | var label = g.edge(e) 49 | g.removeEdge(e) 50 | label.forwardName = e.name 51 | label.reversed = true 52 | g.setEdge(e.w, e.v, label, _.uniqueId('rev')) 53 | }) 54 | 55 | function weightFn (g) { 56 | return function (e) { 57 | return g.edge(e).weight 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/order/sort.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import {partition} from '../util' 3 | 4 | function consumeUnsortable (vs, unsortable, index) { 5 | var last 6 | while (unsortable.length && (last = _.last(unsortable)).i <= index) { 7 | unsortable.pop() 8 | vs.push(last.vs) 9 | index++ 10 | } 11 | return index 12 | } 13 | 14 | function compareWithBias (bias) { 15 | return function (entryV, entryW) { 16 | if (entryV.barycenter < entryW.barycenter) { 17 | return -1 18 | } else if (entryV.barycenter > entryW.barycenter) { 19 | return 1 20 | } 21 | 22 | return !bias ? entryV.i - entryW.i : entryW.i - entryV.i 23 | } 24 | } 25 | 26 | export default function sort (entries, biasRight) { 27 | var parts = partition(entries, function (entry) { 28 | return _.has(entry, 'barycenter') 29 | }) 30 | var sortable = parts.lhs 31 | var unsortable = _.sortBy(parts.rhs, function (entry) { return -entry.i }) 32 | var vs = [] 33 | var sum = 0 34 | var weight = 0 35 | var vsIndex = 0 36 | 37 | sortable.sort(compareWithBias(!!biasRight)) 38 | 39 | vsIndex = consumeUnsortable(vs, unsortable, vsIndex) 40 | 41 | _.forEach(sortable, function (entry) { 42 | vsIndex += entry.vs.length 43 | vs.push(entry.vs) 44 | sum += entry.barycenter * entry.weight 45 | weight += entry.weight 46 | vsIndex = consumeUnsortable(vs, unsortable, vsIndex) 47 | }) 48 | 49 | var result = { vs: _.flatten(vs, true) } 50 | if (weight) { 51 | result.barycenter = sum / weight 52 | result.weight = weight 53 | } 54 | return result 55 | } 56 | -------------------------------------------------------------------------------- /src/coordinate-system.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | 3 | function swapWidthHeight (g) { 4 | _.forEach(g.nodes(), function (v) { swapWidthHeightOne(g.node(v)) }) 5 | _.forEach(g.edges(), function (e) { swapWidthHeightOne(g.edge(e)) }) 6 | } 7 | 8 | function swapWidthHeightOne (attrs) { 9 | var w = attrs.width 10 | attrs.width = attrs.height 11 | attrs.height = w 12 | } 13 | 14 | function reverseY (g) { 15 | _.forEach(g.nodes(), function (v) { reverseYOne(g.node(v)) }) 16 | 17 | _.forEach(g.edges(), function (e) { 18 | var edge = g.edge(e) 19 | _.forEach(edge.points, reverseYOne) 20 | if (_.has(edge, 'y')) { 21 | reverseYOne(edge) 22 | } 23 | }) 24 | } 25 | 26 | function reverseYOne (attrs) { 27 | attrs.y = -attrs.y 28 | } 29 | 30 | function swapXY (g) { 31 | _.forEach(g.nodes(), function (v) { swapXYOne(g.node(v)) }) 32 | 33 | _.forEach(g.edges(), function (e) { 34 | var edge = g.edge(e) 35 | _.forEach(edge.points, swapXYOne) 36 | if (_.has(edge, 'x')) { 37 | swapXYOne(edge) 38 | } 39 | }) 40 | } 41 | 42 | function swapXYOne (attrs) { 43 | var x = attrs.x 44 | attrs.x = attrs.y 45 | attrs.y = x 46 | } 47 | 48 | export function adjust (g) { 49 | var rankDir = g.graph().rankdir.toLowerCase() 50 | if (rankDir === 'lr' || rankDir === 'rl') { 51 | swapWidthHeight(g) 52 | } 53 | } 54 | 55 | export function undo (g) { 56 | var rankDir = g.graph().rankdir.toLowerCase() 57 | if (rankDir === 'bt' || rankDir === 'rl') { 58 | reverseY(g) 59 | } 60 | 61 | if (rankDir === 'lr' || rankDir === 'rl') { 62 | swapXY(g) 63 | swapWidthHeight(g) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /tests/order/init-order-test.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect 2 | const Graph = require('ciena-graphlib').Graph 3 | const _ = require('lodash') 4 | 5 | const initOrder = require('../../lib/order/init-order') 6 | 7 | describe('order/initOrder', function () { 8 | var g 9 | 10 | beforeEach(function () { 11 | g = new Graph({ compound: true }) 12 | .setDefaultEdgeLabel(function () { return { weight: 1 } }) 13 | }) 14 | 15 | it('assigns non-overlapping orders for each rank in a tree', function () { 16 | _.forEach({ a: 0, b: 1, c: 2, d: 2, e: 1 }, function (rank, v) { 17 | g.setNode(v, { rank: rank }) 18 | }) 19 | g.setPath(['a', 'b', 'c']) 20 | g.setEdge('b', 'd') 21 | g.setEdge('a', 'e') 22 | 23 | var layering = initOrder(g) 24 | expect(layering[0]).to.eql(['a']) 25 | expect(_.sortBy(layering[1])).to.eql(['b', 'e']) 26 | expect(_.sortBy(layering[2])).to.eql(['c', 'd']) 27 | }) 28 | 29 | it('assigns non-overlapping orders for each rank in a DAG', function () { 30 | _.forEach({ a: 0, b: 1, c: 1, d: 2 }, function (rank, v) { 31 | g.setNode(v, { rank: rank }) 32 | }) 33 | g.setPath(['a', 'b', 'd']) 34 | g.setPath(['a', 'c', 'd']) 35 | 36 | var layering = initOrder(g) 37 | expect(layering[0]).to.eql(['a']) 38 | expect(_.sortBy(layering[1])).to.eql(['b', 'c']) 39 | expect(_.sortBy(layering[2])).to.eql(['d']) 40 | }) 41 | 42 | it('does not assign an order to subgraph nodes', function () { 43 | g.setNode('a', { rank: 0 }) 44 | g.setNode('sg1', {}) 45 | g.setParent('a', 'sg1') 46 | 47 | var layering = initOrder(g) 48 | expect(layering).to.eql([['a']]) 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /tests/order/cross-count-test.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect 2 | const Graph = require('ciena-graphlib').Graph 3 | 4 | const crossCount = require('../../lib/order/cross-count') 5 | 6 | describe('crossCount', function () { 7 | var g 8 | 9 | beforeEach(function () { 10 | g = new Graph() 11 | .setDefaultEdgeLabel(function () { return { weight: 1 } }) 12 | }) 13 | 14 | it('returns 0 for an empty layering', function () { 15 | expect(crossCount(g, [])).equals(0) 16 | }) 17 | 18 | it('returns 0 for a layering with no crossings', function () { 19 | g.setEdge('a1', 'b1') 20 | g.setEdge('a2', 'b2') 21 | expect(crossCount(g, [['a1', 'a2'], ['b1', 'b2']])).equals(0) 22 | }) 23 | 24 | it('returns 1 for a layering with 1 crossing', function () { 25 | g.setEdge('a1', 'b1') 26 | g.setEdge('a2', 'b2') 27 | expect(crossCount(g, [['a1', 'a2'], ['b2', 'b1']])).equals(1) 28 | }) 29 | 30 | it('returns a weighted crossing count for a layering with 1 crossing', function () { 31 | g.setEdge('a1', 'b1', { weight: 2 }) 32 | g.setEdge('a2', 'b2', { weight: 3 }) 33 | expect(crossCount(g, [['a1', 'a2'], ['b2', 'b1']])).equals(6) 34 | }) 35 | 36 | it('calculates crossings across layers', function () { 37 | g.setPath(['a1', 'b1', 'c1']) 38 | g.setPath(['a2', 'b2', 'c2']) 39 | expect(crossCount(g, [['a1', 'a2'], ['b2', 'b1'], ['c1', 'c2']])).equals(2) 40 | }) 41 | 42 | it('works for graph #1', function () { 43 | g.setPath(['a', 'b', 'c']) 44 | g.setPath(['d', 'e', 'c']) 45 | g.setPath(['a', 'f', 'i']) 46 | g.setEdge('a', 'e') 47 | expect(crossCount(g, [['a', 'd'], ['b', 'e', 'f'], ['c', 'i']])).equals(1) 48 | expect(crossCount(g, [['d', 'a'], ['e', 'b', 'f'], ['c', 'i']])).equals(0) 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /tests/data/list-test.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect 2 | 3 | const List = require('../../lib/data/list') 4 | 5 | describe('data.List', function () { 6 | var list 7 | 8 | beforeEach(function () { 9 | list = new List() 10 | }) 11 | 12 | describe('dequeue', function () { 13 | it('returns undefined with an empty list', function () { 14 | expect(list.dequeue()).to.be.undefined 15 | }) 16 | 17 | it('unlinks and returns the first entry', function () { 18 | var obj = {} 19 | list.enqueue(obj) 20 | expect(list.dequeue()).to.equal(obj) 21 | }) 22 | 23 | it('unlinks and returns multiple entries in FIFO order', function () { 24 | var obj1 = {} 25 | var obj2 = {} 26 | list.enqueue(obj1) 27 | list.enqueue(obj2) 28 | 29 | expect(list.dequeue()).to.equal(obj1) 30 | expect(list.dequeue()).to.equal(obj2) 31 | }) 32 | 33 | it('unlinks and relinks an entry if it is re-enqueued', function () { 34 | var obj1 = {} 35 | var obj2 = {} 36 | list.enqueue(obj1) 37 | list.enqueue(obj2) 38 | list.enqueue(obj1) 39 | 40 | expect(list.dequeue()).to.equal(obj2) 41 | expect(list.dequeue()).to.equal(obj1) 42 | }) 43 | 44 | it('unlinks and relinks an entry if it is enqueued on another list', function () { 45 | var obj = {} 46 | var list2 = new List() 47 | list.enqueue(obj) 48 | list2.enqueue(obj) 49 | 50 | expect(list.dequeue()).to.be.undefined 51 | expect(list2.dequeue()).to.equal(obj) 52 | }) 53 | 54 | it('can return a string representation', function () { 55 | list.enqueue({ entry: 1 }) 56 | list.enqueue({ entry: 2 }) 57 | 58 | expect(list.toString()).to.equal('[{"entry":1}, {"entry":2}]') 59 | }) 60 | }) 61 | }) 62 | -------------------------------------------------------------------------------- /tests/order/order-test.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect 2 | const Graph = require('ciena-graphlib').Graph 3 | const _ = require('lodash') 4 | 5 | const order = require('../../lib/order') 6 | const crossCount = require('../../lib/order/cross-count') 7 | const buildLayerMatrix = require('../../lib/util').buildLayerMatrix 8 | 9 | describe('order', function () { 10 | var g 11 | 12 | beforeEach(function () { 13 | g = new Graph() 14 | .setDefaultEdgeLabel({ weight: 1 }) 15 | }) 16 | 17 | it('does not add crossings to a tree structure', function () { 18 | g.setNode('a', { rank: 1 }) 19 | _.forEach(['b', 'e'], function (v) { g.setNode(v, { rank: 2 }) }) 20 | _.forEach(['c', 'd', 'f'], function (v) { g.setNode(v, { rank: 3 }) }) 21 | g.setPath(['a', 'b', 'c']) 22 | g.setEdge('b', 'd') 23 | g.setPath(['a', 'e', 'f']) 24 | order(g) 25 | var layering = buildLayerMatrix(g) 26 | expect(crossCount(g, layering)).to.equal(0) 27 | }) 28 | 29 | it('can solve a simple graph', function () { 30 | // This graph resulted in a single crossing for previous versions of dagre. 31 | _.forEach(['a', 'd'], function (v) { g.setNode(v, { rank: 1 }) }) 32 | _.forEach(['b', 'f', 'e'], function (v) { g.setNode(v, { rank: 2 }) }) 33 | _.forEach(['c', 'g'], function (v) { g.setNode(v, { rank: 3 }) }) 34 | order(g) 35 | var layering = buildLayerMatrix(g) 36 | expect(crossCount(g, layering)).to.equal(0) 37 | }) 38 | 39 | it('can minimize crossings', function () { 40 | g.setNode('a', { rank: 1 }) 41 | _.forEach(['b', 'e', 'g'], function (v) { g.setNode(v, { rank: 2 }) }) 42 | _.forEach(['c', 'f', 'h'], function (v) { g.setNode(v, { rank: 3 }) }) 43 | g.setNode('d', { rank: 4 }) 44 | order(g) 45 | var layering = buildLayerMatrix(g) 46 | expect(crossCount(g, layering)).to.be.lte(1) 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /src/rank/util.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | 3 | /* 4 | * Initializes ranks for the input graph using the longest path algorithm. This 5 | * algorithm scales well and is fast in practice, it yields rather poor 6 | * solutions. Nodes are pushed to the lowest layer possible, leaving the bottom 7 | * ranks wide and leaving edges longer than necessary. However, due to its 8 | * speed, this algorithm is good for getting an initial ranking that can be fed 9 | * into other algorithms. 10 | * 11 | * This algorithm does not normalize layers because it will be used by other 12 | * algorithms in most cases. If using this algorithm directly, be sure to 13 | * run normalize at the end. 14 | * 15 | * Pre-conditions: 16 | * 17 | * 1. Input graph is a DAG. 18 | * 2. Input graph node labels can be assigned properties. 19 | * 20 | * Post-conditions: 21 | * 22 | * 1. Each node will be assign an (unnormalized) "rank" property. 23 | */ 24 | export function longestPath (g) { 25 | var visited = {} 26 | 27 | function dfs (v) { 28 | var label = g.node(v) 29 | if (_.has(visited, v)) { 30 | return label.rank 31 | } 32 | visited[v] = true 33 | 34 | var rank = _.min(_.map(g.outEdges(v), function (e) { 35 | return dfs(e.w) - g.edge(e).minlen 36 | })) 37 | 38 | if ( 39 | rank === Number.POSITIVE_INFINITY || // return value of _.map([]) for Lodash 3 40 | rank === undefined || // return value of _.map([]) for Lodash 4 41 | rank === null // return value of _.map([null]) 42 | ) { 43 | rank = 0 44 | } 45 | 46 | return (label.rank = rank) 47 | } 48 | 49 | _.forEach(g.sources(), dfs) 50 | } 51 | 52 | /* 53 | * Returns the amount of slack for the given edge. The slack is defined as the 54 | * difference between the length of the edge and its minimum length. 55 | */ 56 | export function slack (g, e) { 57 | return g.node(e.w).rank - g.node(e.v).rank - g.edge(e).minlen 58 | } 59 | -------------------------------------------------------------------------------- /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 | import {debugOrdering} from './src/debug' 24 | 25 | export var debug = { 26 | debugOrdering 27 | } 28 | 29 | export {default as layout} from './src/layout' 30 | 31 | import { 32 | addBorderNode, 33 | addDummyNode, 34 | asNonCompoundGraph, 35 | buildLayerMatrix, 36 | intersectRect, 37 | maxRank, 38 | normalizeRanks, 39 | notime, 40 | partition, 41 | predecessorWeights, 42 | removeEmptyRanks, 43 | simplify, 44 | successorWeights, 45 | time 46 | } from './src/util' 47 | 48 | export var util = { 49 | addBorderNode, 50 | addDummyNode, 51 | asNonCompoundGraph, 52 | buildLayerMatrix, 53 | intersectRect, 54 | maxRank, 55 | normalizeRanks, 56 | notime, 57 | partition, 58 | predecessorWeights, 59 | removeEmptyRanks, 60 | simplify, 61 | successorWeights, 62 | time 63 | } 64 | -------------------------------------------------------------------------------- /tests/position-test.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect 2 | const Graph = require('ciena-graphlib').Graph 3 | 4 | const position = require('../lib/position') 5 | 6 | describe('position', function () { 7 | var g 8 | 9 | beforeEach(function () { 10 | g = new Graph({ compound: true }) 11 | .setGraph({ 12 | ranksep: 50, 13 | nodesep: 50, 14 | edgesep: 10 15 | }) 16 | }) 17 | 18 | it('respects ranksep', function () { 19 | g.graph().ranksep = 1000 20 | g.setNode('a', { width: 50, height: 100, rank: 0, order: 0 }) 21 | g.setNode('b', { width: 50, height: 80, rank: 1, order: 0 }) 22 | g.setEdge('a', 'b') 23 | position(g) 24 | expect(g.node('b').y).to.equal(100 + 1000 + 80 / 2) 25 | }) 26 | 27 | it('use the largest height in each rank with ranksep', function () { 28 | g.graph().ranksep = 1000 29 | g.setNode('a', { width: 50, height: 100, rank: 0, order: 0 }) 30 | g.setNode('b', { width: 50, height: 80, rank: 0, order: 1 }) 31 | g.setNode('c', { width: 50, height: 90, rank: 1, order: 0 }) 32 | g.setEdge('a', 'c') 33 | position(g) 34 | expect(g.node('a').y).to.equal(100 / 2) 35 | expect(g.node('b').y).to.equal(100 / 2) // Note we used 100 and not 80 here 36 | expect(g.node('c').y).to.equal(100 + 1000 + 90 / 2) 37 | }) 38 | 39 | it('respects nodesep', function () { 40 | g.graph().nodesep = 1000 41 | g.setNode('a', { width: 50, height: 100, rank: 0, order: 0 }) 42 | g.setNode('b', { width: 70, height: 80, rank: 0, order: 1 }) 43 | position(g) 44 | expect(g.node('b').x).to.equal(g.node('a').x + 50 / 2 + 1000 + 70 / 2) 45 | }) 46 | 47 | it('should not try to position the subgraph node itself', function () { 48 | g.setNode('a', { width: 50, height: 50, rank: 0, order: 0 }) 49 | g.setNode('sg1', {}) 50 | g.setParent('a', 'sg1') 51 | position(g) 52 | expect(g.node('sg1')).to.not.have.property('x') 53 | expect(g.node('sg1')).to.not.have.property('y') 54 | }) 55 | }) 56 | -------------------------------------------------------------------------------- /tests/order/add-subgraph-constraints-test.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect 2 | const Graph = require('ciena-graphlib').Graph 3 | const _ = require('lodash') 4 | 5 | const addSubgraphConstraints = require('../../lib/order/add-subgraph-constraints') 6 | 7 | describe('order/addSubgraphConstraints', function () { 8 | var g, cg 9 | 10 | beforeEach(function () { 11 | g = new Graph({ compound: true }) 12 | cg = new Graph() 13 | }) 14 | 15 | it('does not change CG for a flat set of nodes', function () { 16 | var vs = ['a', 'b', 'c', 'd'] 17 | _.forEach(vs, function (v) { g.setNode(v) }) 18 | addSubgraphConstraints(g, cg, vs) 19 | expect(cg.nodeCount()).equals(0) 20 | expect(cg.edgeCount()).equals(0) 21 | }) 22 | 23 | it("doesn't create a constraint for contiguous subgraph nodes", function () { 24 | var vs = ['a', 'b', 'c'] 25 | _.forEach(vs, function (v) { 26 | g.setParent(v, 'sg') 27 | }) 28 | addSubgraphConstraints(g, cg, vs) 29 | expect(cg.nodeCount()).equals(0) 30 | expect(cg.edgeCount()).equals(0) 31 | }) 32 | 33 | it('adds a constraint when the parents for adjacent nodes are different', function () { 34 | var vs = ['a', 'b'] 35 | g.setParent('a', 'sg1') 36 | g.setParent('b', 'sg2') 37 | addSubgraphConstraints(g, cg, vs) 38 | expect(cg.edges()).eqls([{ v: 'sg1', w: 'sg2' }]) 39 | }) 40 | 41 | it('works for multiple levels', function () { 42 | var vs = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'] 43 | _.forEach(vs, function (v) { 44 | g.setNode(v) 45 | }) 46 | g.setParent('b', 'sg2') 47 | g.setParent('sg2', 'sg1') 48 | g.setParent('c', 'sg1') 49 | g.setParent('d', 'sg3') 50 | g.setParent('sg3', 'sg1') 51 | g.setParent('f', 'sg4') 52 | g.setParent('g', 'sg5') 53 | g.setParent('sg5', 'sg4') 54 | addSubgraphConstraints(g, cg, vs) 55 | expect(_.sortBy(cg.edges(), 'v')).eqls([ 56 | { v: 'sg1', w: 'sg4' }, 57 | { v: 'sg2', w: 'sg3' } 58 | ]) 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /tests/rank/feasible-tree-test.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect 2 | const Graph = require('ciena-graphlib').Graph 3 | const _ = require('lodash') 4 | 5 | const feasibleTree = require('../../lib/rank/feasible-tree') 6 | 7 | describe('feasibleTree', function () { 8 | it('creates a tree for a trivial input graph', function () { 9 | var g = new Graph() 10 | .setNode('a', { rank: 0 }) 11 | .setNode('b', { rank: 1 }) 12 | .setEdge('a', 'b', { minlen: 1 }) 13 | 14 | var tree = feasibleTree(g) 15 | expect(g.node('b').rank).to.equal(g.node('a').rank + 1) 16 | expect(tree.neighbors('a')).to.eql(['b']) 17 | }) 18 | 19 | it('correctly shortens slack by pulling a node up', function () { 20 | var g = new Graph() 21 | .setNode('a', { rank: 0 }) 22 | .setNode('b', { rank: 1 }) 23 | .setNode('c', { rank: 2 }) 24 | .setNode('d', { rank: 2 }) 25 | .setPath(['a', 'b', 'c'], { minlen: 1 }) 26 | .setEdge('a', 'd', { minlen: 1 }) 27 | 28 | var tree = feasibleTree(g) 29 | expect(g.node('b').rank).to.eql(g.node('a').rank + 1) 30 | expect(g.node('c').rank).to.eql(g.node('b').rank + 1) 31 | expect(g.node('d').rank).to.eql(g.node('a').rank + 1) 32 | expect(_.sortBy(tree.neighbors('a'))).to.eql(['b', 'd']) 33 | expect(_.sortBy(tree.neighbors('b'))).to.eql(['a', 'c']) 34 | expect(tree.neighbors('c')).to.eql(['b']) 35 | expect(tree.neighbors('d')).to.eql(['a']) 36 | }) 37 | 38 | it('correctly shortens slack by pulling a node down', function () { 39 | var g = new Graph() 40 | .setNode('a', { rank: 2 }) 41 | .setNode('b', { rank: 0 }) 42 | .setNode('c', { rank: 2 }) 43 | .setEdge('b', 'a', { minlen: 1 }) 44 | .setEdge('b', 'c', { minlen: 1 }) 45 | 46 | var tree = feasibleTree(g) 47 | expect(g.node('a').rank).to.eql(g.node('b').rank + 1) 48 | expect(g.node('c').rank).to.eql(g.node('b').rank + 1) 49 | expect(_.sortBy(tree.neighbors('a'))).to.eql(['b']) 50 | expect(_.sortBy(tree.neighbors('b'))).to.eql(['a', 'c']) 51 | expect(_.sortBy(tree.neighbors('c'))).to.eql(['b']) 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /tests/rank/util-test.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect 2 | const Graph = require('ciena-graphlib').Graph 3 | 4 | const normalizeRanks = require('../../lib/util').normalizeRanks 5 | 6 | const rankUtil = require('../../lib/rank/util') 7 | const longestPath = rankUtil.longestPath 8 | 9 | describe('rank/util', function () { 10 | describe('longestPath', function () { 11 | var g 12 | 13 | beforeEach(function () { 14 | g = new Graph() 15 | .setDefaultNodeLabel(function () { return {} }) 16 | .setDefaultEdgeLabel(function () { return { minlen: 1 } }) 17 | }) 18 | 19 | it('can assign a rank to a single node graph', function () { 20 | g.setNode('a') 21 | longestPath(g) 22 | normalizeRanks(g) 23 | expect(g.node('a').rank).to.equal(0) 24 | }) 25 | 26 | it('can assign ranks to unconnected nodes', function () { 27 | g.setNode('a') 28 | g.setNode('b') 29 | longestPath(g) 30 | normalizeRanks(g) 31 | expect(g.node('a').rank).to.equal(0) 32 | expect(g.node('b').rank).to.equal(0) 33 | }) 34 | 35 | it('can assign ranks to connected nodes', function () { 36 | g.setEdge('a', 'b') 37 | longestPath(g) 38 | normalizeRanks(g) 39 | expect(g.node('a').rank).to.equal(0) 40 | expect(g.node('b').rank).to.equal(1) 41 | }) 42 | 43 | it('can assign ranks for a diamond', function () { 44 | g.setPath(['a', 'b', 'd']) 45 | g.setPath(['a', 'c', 'd']) 46 | longestPath(g) 47 | normalizeRanks(g) 48 | expect(g.node('a').rank).to.equal(0) 49 | expect(g.node('b').rank).to.equal(1) 50 | expect(g.node('c').rank).to.equal(1) 51 | expect(g.node('d').rank).to.equal(2) 52 | }) 53 | 54 | it('uses the minlen attribute on the edge', function () { 55 | g.setPath(['a', 'b', 'd']) 56 | g.setEdge('a', 'c') 57 | g.setEdge('c', 'd', { minlen: 2 }) 58 | longestPath(g) 59 | normalizeRanks(g) 60 | expect(g.node('a').rank).to.equal(0) 61 | // longest path biases towards the lowest rank it can assign 62 | expect(g.node('b').rank).to.equal(2) 63 | expect(g.node('c').rank).to.equal(1) 64 | expect(g.node('d').rank).to.equal(3) 65 | }) 66 | }) 67 | }) 68 | -------------------------------------------------------------------------------- /src/order/cross-count.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | 3 | function twoLayerCrossCount (g, northLayer, southLayer) { 4 | // Sort all of the edges between the north and south layers by their position 5 | // in the north layer and then the south. Map these edges to the position of 6 | // their head in the south layer. 7 | var southPos = _.zipObject(southLayer, 8 | _.map(southLayer, function (v, i) { return i })) 9 | var southEntries = _.flatten(_.map(northLayer, function (v) { 10 | return _.chain(g.outEdges(v)) 11 | .map(function (e) { 12 | return { pos: southPos[e.w], weight: g.edge(e).weight } 13 | }) 14 | .sortBy('pos') 15 | .value() 16 | }), true) 17 | 18 | // Build the accumulator tree 19 | var firstIndex = 1 20 | while (firstIndex < southLayer.length) firstIndex <<= 1 21 | var treeSize = 2 * firstIndex - 1 22 | firstIndex -= 1 23 | var tree = _.map(new Array(treeSize), function () { return 0 }) 24 | 25 | // Calculate the weighted crossings 26 | var cc = 0 27 | _.forEach(southEntries.forEach(function (entry) { 28 | var index = entry.pos + firstIndex 29 | tree[index] += entry.weight 30 | var weightSum = 0 31 | while (index > 0) { 32 | if (index % 2) { 33 | weightSum += tree[index + 1] 34 | } 35 | index = (index - 1) >> 1 36 | tree[index] += entry.weight 37 | } 38 | cc += entry.weight * weightSum 39 | })) 40 | 41 | return cc 42 | } 43 | 44 | /* 45 | * A function that takes a layering (an array of layers, each with an array of 46 | * ordererd nodes) and a graph and returns a weighted crossing count. 47 | * 48 | * Pre-conditions: 49 | * 50 | * 1. Input graph must be simple (not a multigraph), directed, and include 51 | * only simple edges. 52 | * 2. Edges in the input graph must have assigned weights. 53 | * 54 | * Post-conditions: 55 | * 56 | * 1. The graph and layering matrix are left unchanged. 57 | * 58 | * This algorithm is derived from Barth, et al., "Bilayer Cross Counting." 59 | */ 60 | export default function crossCount (g, layering) { 61 | var cc = 0 62 | for (var i = 1; i < layering.length; ++i) { 63 | cc += twoLayerCrossCount(g, layering[i - 1], layering[i]) 64 | } 65 | return cc 66 | } 67 | -------------------------------------------------------------------------------- /src/parent-dummy-chains.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | 3 | // Find a path from v to w through the lowest common ancestor (LCA). Return the 4 | // full path and the LCA. 5 | function findPath (g, postorderNums, v, w) { 6 | var vPath = [] 7 | var wPath = [] 8 | var low = Math.min(postorderNums[v].low, postorderNums[w].low) 9 | var lim = Math.max(postorderNums[v].lim, postorderNums[w].lim) 10 | var parent 11 | var lca 12 | 13 | // Traverse up from v to find the LCA 14 | parent = v 15 | do { 16 | parent = g.parent(parent) 17 | vPath.push(parent) 18 | } while (parent && 19 | (postorderNums[parent].low > low || lim > postorderNums[parent].lim)) 20 | lca = parent 21 | 22 | // Traverse from w to LCA 23 | parent = w 24 | while ((parent = g.parent(parent)) !== lca) { 25 | wPath.push(parent) 26 | } 27 | 28 | return { path: vPath.concat(wPath.reverse()), lca: lca } 29 | } 30 | 31 | function postorder (g) { 32 | var result = {} 33 | var lim = 0 34 | 35 | function dfs (v) { 36 | var low = lim 37 | _.forEach(g.children(v), dfs) 38 | result[v] = { low: low, lim: lim++ } 39 | } 40 | _.forEach(g.children(), dfs) 41 | 42 | return result 43 | } 44 | 45 | export default function parentDummyChains (g) { 46 | var postorderNums = postorder(g) 47 | 48 | _.forEach(g.graph().dummyChains, function (v) { 49 | var node = g.node(v) 50 | var edgeObj = node.edgeObj 51 | var pathData = findPath(g, postorderNums, edgeObj.v, edgeObj.w) 52 | var path = pathData.path 53 | var lca = pathData.lca 54 | var pathIdx = 0 55 | var pathV = path[pathIdx] 56 | var ascending = true 57 | 58 | while (v !== edgeObj.w) { 59 | node = g.node(v) 60 | 61 | if (ascending) { 62 | while ((pathV = path[pathIdx]) !== lca && 63 | g.node(pathV).maxRank < node.rank) { 64 | pathIdx++ 65 | } 66 | 67 | if (pathV === lca) { 68 | ascending = false 69 | } 70 | } 71 | 72 | if (!ascending) { 73 | while (pathIdx < path.length - 1 && 74 | g.node(pathV = path[pathIdx + 1]).minRank <= node.rank) { 75 | pathIdx++ 76 | } 77 | pathV = path[pathIdx] 78 | } 79 | 80 | g.setParent(v, pathV) 81 | v = g.successors(v)[0] 82 | } 83 | }) 84 | } 85 | -------------------------------------------------------------------------------- /src/order/index.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import initOrder from './init-order' 3 | import crossCount from './cross-count' 4 | import sortSubgraph from './sort-subgraph' 5 | import buildLayerGraph from './build-layer-graph' 6 | import addSubgraphConstraints from './add-subgraph-constraints' 7 | import {Graph} from 'ciena-graphlib' 8 | import {buildLayerMatrix, maxRank} from '../util' 9 | 10 | function buildLayerGraphs (g, ranks, relationship) { 11 | return _.map(ranks, function (rank) { 12 | return buildLayerGraph(g, rank, relationship) 13 | }) 14 | } 15 | 16 | function sweepLayerGraphs (layerGraphs, biasRight) { 17 | var cg = new Graph() 18 | _.forEach(layerGraphs, function (lg) { 19 | var root = lg.graph().root 20 | var sorted = sortSubgraph(lg, root, cg, biasRight) 21 | _.forEach(sorted.vs, function (v, i) { 22 | lg.node(v).order = i 23 | }) 24 | addSubgraphConstraints(lg, cg, sorted.vs) 25 | }) 26 | } 27 | 28 | function assignOrder (g, layering) { 29 | _.forEach(layering, function (layer) { 30 | _.forEach(layer, function (v, i) { 31 | g.node(v).order = i 32 | }) 33 | }) 34 | } 35 | 36 | /* 37 | * Applies heuristics to minimize edge crossings in the graph and sets the best 38 | * order solution as an order attribute on each node. 39 | * 40 | * Pre-conditions: 41 | * 42 | * 1. Graph must be DAG 43 | * 2. Graph nodes must be objects with a "rank" attribute 44 | * 3. Graph edges must have the "weight" attribute 45 | * 46 | * Post-conditions: 47 | * 48 | * 1. Graph nodes will have an "order" attribute based on the results of the 49 | * algorithm. 50 | */ 51 | export default function order (g) { 52 | var mr = maxRank(g) 53 | var downLayerGraphs = buildLayerGraphs(g, _.range(1, mr + 1), 'inEdges') 54 | var upLayerGraphs = buildLayerGraphs(g, _.range(mr - 1, -1, -1), 'outEdges') 55 | 56 | var layering = initOrder(g) 57 | assignOrder(g, layering) 58 | 59 | var bestCC = Number.POSITIVE_INFINITY 60 | var best 61 | 62 | for (var i = 0, lastBest = 0; lastBest < 4; ++i, ++lastBest) { 63 | sweepLayerGraphs(i % 2 ? downLayerGraphs : upLayerGraphs, i % 4 >= 2) 64 | 65 | layering = buildLayerMatrix(g) 66 | var cc = crossCount(g, layering) 67 | if (cc < bestCC) { 68 | lastBest = 0 69 | best = _.cloneDeep(layering) 70 | bestCC = cc 71 | } 72 | } 73 | 74 | assignOrder(g, best) 75 | } 76 | -------------------------------------------------------------------------------- /src/order/sort-subgraph.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import barycenter from './barycenter' 3 | import resolveConflicts from './resolve-conflicts' 4 | import sort from './sort' 5 | 6 | function expandSubgraphs (entries, subgraphs) { 7 | _.forEach(entries, function (entry) { 8 | entry.vs = _.flatten(entry.vs.map(function (v) { 9 | if (subgraphs[v]) { 10 | return subgraphs[v].vs 11 | } 12 | return v 13 | }), true) 14 | }) 15 | } 16 | 17 | function mergeBarycenters (target, other) { 18 | if (!_.isUndefined(target.barycenter)) { 19 | target.barycenter = (target.barycenter * target.weight + 20 | other.barycenter * other.weight) / 21 | (target.weight + other.weight) 22 | target.weight += other.weight 23 | } else { 24 | target.barycenter = other.barycenter 25 | target.weight = other.weight 26 | } 27 | } 28 | 29 | export default function sortSubgraph (g, v, cg, biasRight) { 30 | var movable = g.children(v) 31 | var node = g.node(v) 32 | var bl = node ? node.borderLeft : undefined 33 | var br = node ? node.borderRight : undefined 34 | var subgraphs = {} 35 | 36 | if (bl) { 37 | movable = _.filter(movable, function (w) { 38 | return w !== bl && w !== br 39 | }) 40 | } 41 | 42 | var barycenters = barycenter(g, movable) 43 | _.forEach(barycenters, function (entry) { 44 | if (g.children(entry.v).length) { 45 | var subgraphResult = sortSubgraph(g, entry.v, cg, biasRight) 46 | subgraphs[entry.v] = subgraphResult 47 | if (_.has(subgraphResult, 'barycenter')) { 48 | mergeBarycenters(entry, subgraphResult) 49 | } 50 | } 51 | }) 52 | 53 | var entries = resolveConflicts(barycenters, cg) 54 | expandSubgraphs(entries, subgraphs) 55 | 56 | var result = sort(entries, biasRight) 57 | 58 | if (bl) { 59 | result.vs = _.flatten([bl, result.vs, br], true) 60 | if (g.predecessors(bl).length) { 61 | var blPred = g.node(g.predecessors(bl)[0]) 62 | var brPred = g.node(g.predecessors(br)[0]) 63 | if (!_.has(result, 'barycenter')) { 64 | result.barycenter = 0 65 | result.weight = 0 66 | } 67 | result.barycenter = (result.barycenter * result.weight + 68 | blPred.order + brPred.order) / (result.weight + 2) 69 | result.weight += 2 70 | } 71 | } 72 | 73 | return result 74 | } 75 | -------------------------------------------------------------------------------- /tests/order/barycenter-test.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect 2 | const Graph = require('ciena-graphlib').Graph 3 | 4 | const barycenter = require('../../lib/order/barycenter') 5 | 6 | describe('order/barycenter', function () { 7 | var g 8 | 9 | beforeEach(function () { 10 | g = new Graph() 11 | .setDefaultNodeLabel(function () { return {} }) 12 | .setDefaultEdgeLabel(function () { return { weight: 1 } }) 13 | }) 14 | 15 | it('assigns an undefined barycenter for a node with no predecessors', function () { 16 | g.setNode('x', {}) 17 | 18 | var results = barycenter(g, ['x']) 19 | expect(results).to.have.length(1) 20 | expect(results[0]).to.eql({ v: 'x' }) 21 | }) 22 | 23 | it('assigns the position of the sole predecessors', function () { 24 | g.setNode('a', { order: 2 }) 25 | g.setEdge('a', 'x') 26 | 27 | var results = barycenter(g, ['x']) 28 | expect(results).to.have.length(1) 29 | expect(results[0]).eqls({ v: 'x', barycenter: 2, weight: 1 }) 30 | }) 31 | 32 | it('assigns the average of multiple predecessors', function () { 33 | g.setNode('a', { order: 2 }) 34 | g.setNode('b', { order: 4 }) 35 | g.setEdge('a', 'x') 36 | g.setEdge('b', 'x') 37 | 38 | var results = barycenter(g, ['x']) 39 | expect(results).to.have.length(1) 40 | expect(results[0]).eqls({ v: 'x', barycenter: 3, weight: 2 }) 41 | }) 42 | 43 | it('takes into account the weight of edges', function () { 44 | g.setNode('a', { order: 2 }) 45 | g.setNode('b', { order: 4 }) 46 | g.setEdge('a', 'x', { weight: 3 }) 47 | g.setEdge('b', 'x') 48 | 49 | var results = barycenter(g, ['x']) 50 | expect(results).to.have.length(1) 51 | expect(results[0]).eqls({ v: 'x', barycenter: 2.5, weight: 4 }) 52 | }) 53 | 54 | it('calculates barycenters for all nodes in the movable layer', function () { 55 | g.setNode('a', { order: 1 }) 56 | g.setNode('b', { order: 2 }) 57 | g.setNode('c', { order: 4 }) 58 | g.setEdge('a', 'x') 59 | g.setEdge('b', 'x') 60 | g.setNode('y') 61 | g.setEdge('a', 'z', { weight: 2 }) 62 | g.setEdge('c', 'z') 63 | 64 | var results = barycenter(g, ['x', 'y', 'z']) 65 | expect(results).to.have.length(3) 66 | expect(results[0]).eqls({ v: 'x', barycenter: 1.5, weight: 2 }) 67 | expect(results[1]).eqls({ v: 'y' }) 68 | expect(results[2]).eqls({ v: 'z', barycenter: 2, weight: 3 }) 69 | }) 70 | }) 71 | -------------------------------------------------------------------------------- /src/rank/feasible-tree.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import {Graph} from 'ciena-graphlib' 3 | 4 | import {slack} from './util' 5 | 6 | /* 7 | * Finds a maximal tree of tight edges and returns the number of nodes in the 8 | * tree. 9 | */ 10 | function tightTree (t, g) { 11 | function dfs (v) { 12 | _.forEach(g.nodeEdges(v), function (e) { 13 | var edgeV = e.v 14 | var w = (v === edgeV) ? e.w : edgeV 15 | if (!t.hasNode(w) && !slack(g, e)) { 16 | t.setNode(w, {}) 17 | t.setEdge(v, w, {}) 18 | dfs(w) 19 | } 20 | }) 21 | } 22 | 23 | _.forEach(t.nodes(), dfs) 24 | return t.nodeCount() 25 | } 26 | 27 | /* 28 | * Finds the edge with the smallest slack that is incident on tree and returns 29 | * it. 30 | */ 31 | function findMinSlackEdge (t, g) { 32 | return _.minBy(g.edges(), function (e) { 33 | if (t.hasNode(e.v) !== t.hasNode(e.w)) { 34 | return slack(g, e) 35 | } 36 | }) 37 | } 38 | 39 | function shiftRanks (t, g, delta) { 40 | _.forEach(t.nodes(), function (v) { 41 | g.node(v).rank += delta 42 | }) 43 | } 44 | 45 | /* 46 | * Constructs a spanning tree with tight edges and adjusted the input node's 47 | * ranks to achieve this. A tight edge is one that is has a length that matches 48 | * its "minlen" attribute. 49 | * 50 | * The basic structure for this function is derived from Gansner, et al., "A 51 | * Technique for Drawing Directed Graphs." 52 | * 53 | * Pre-conditions: 54 | * 55 | * 1. Graph must be a DAG. 56 | * 2. Graph must be connected. 57 | * 3. Graph must have at least one node. 58 | * 5. Graph nodes must have been previously assigned a "rank" property that 59 | * respects the "minlen" property of incident edges. 60 | * 6. Graph edges must have a "minlen" property. 61 | * 62 | * Post-conditions: 63 | * 64 | * - Graph nodes will have their rank adjusted to ensure that all edges are 65 | * tight. 66 | * 67 | * Returns a tree (undirected graph) that is constructed using only "tight" 68 | * edges. 69 | */ 70 | export default function feasibleTree (g) { 71 | var t = new Graph({ directed: false }) 72 | 73 | // Choose arbitrary node from which to start our tree 74 | var start = g.nodes()[0] 75 | var size = g.nodeCount() 76 | t.setNode(start, {}) 77 | 78 | var edge, delta 79 | while (tightTree(t, g) < size) { 80 | edge = findMinSlackEdge(t, g) 81 | delta = t.hasNode(edge.v) ? slack(g, edge) : -slack(g, edge) 82 | shiftRanks(t, g, delta) 83 | } 84 | 85 | return t 86 | } 87 | -------------------------------------------------------------------------------- /src/normalize.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import {addDummyNode} from './util' 3 | 4 | function normalizeEdge (g, e) { 5 | var v = e.v 6 | var vRank = g.node(v).rank 7 | var w = e.w 8 | var wRank = g.node(w).rank 9 | var name = e.name 10 | var edgeLabel = g.edge(e) 11 | var labelRank = edgeLabel.labelRank 12 | 13 | if (wRank === vRank + 1) return 14 | 15 | g.removeEdge(e) 16 | 17 | var dummy, attrs, i 18 | for (i = 0, ++vRank; vRank < wRank; ++i, ++vRank) { 19 | edgeLabel.points = [] 20 | attrs = { 21 | width: 0, 22 | height: 0, 23 | edgeLabel: edgeLabel, 24 | edgeObj: e, 25 | rank: vRank 26 | } 27 | dummy = addDummyNode(g, 'edge', attrs, '_d') 28 | if (vRank === labelRank) { 29 | attrs.width = edgeLabel.width 30 | attrs.height = edgeLabel.height 31 | attrs.dummy = 'edge-label' 32 | attrs.labelpos = edgeLabel.labelpos 33 | } 34 | g.setEdge(v, dummy, { weight: edgeLabel.weight }, name) 35 | if (i === 0) { 36 | g.graph().dummyChains.push(dummy) 37 | } 38 | v = dummy 39 | } 40 | 41 | g.setEdge(v, w, { weight: edgeLabel.weight }, name) 42 | } 43 | 44 | /* 45 | * Breaks any long edges in the graph into short segments that span 1 layer 46 | * each. This operation is undoable with the denormalize function. 47 | * 48 | * Pre-conditions: 49 | * 50 | * 1. The input graph is a DAG. 51 | * 2. Each node in the graph has a "rank" property. 52 | * 53 | * Post-condition: 54 | * 55 | * 1. All edges in the graph have a length of 1. 56 | * 2. Dummy nodes are added where edges have been split into segments. 57 | * 3. The graph is augmented with a "dummyChains" attribute which contains 58 | * the first dummy in each chain of dummy nodes produced. 59 | */ 60 | export function run (g) { 61 | g.graph().dummyChains = [] 62 | _.forEach(g.edges(), function (edge) { normalizeEdge(g, edge) }) 63 | } 64 | 65 | export function undo (g) { 66 | _.forEach(g.graph().dummyChains, function (v) { 67 | var node = g.node(v) 68 | var origLabel = node.edgeLabel 69 | var w 70 | g.setEdge(node.edgeObj, origLabel) 71 | while (node.dummy) { 72 | w = g.successors(v)[0] 73 | g.removeNode(v) 74 | origLabel.points.push({ x: node.x, y: node.y }) 75 | if (node.dummy === 'edge-label') { 76 | origLabel.x = node.x 77 | origLabel.y = node.y 78 | origLabel.width = node.width 79 | origLabel.height = node.height 80 | } 81 | v = w 82 | node = g.node(v) 83 | } 84 | }) 85 | } 86 | -------------------------------------------------------------------------------- /tests/coordinate-system-test.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect 2 | const Graph = require('ciena-graphlib').Graph 3 | 4 | const coordinateSystem = require('../lib/coordinate-system') 5 | 6 | describe('coordinateSystem', function () { 7 | var g 8 | 9 | beforeEach(function () { 10 | g = new Graph() 11 | }) 12 | 13 | describe('coordinateSystem.adjust', function () { 14 | beforeEach(function () { 15 | g.setNode('a', { width: 100, height: 200 }) 16 | }) 17 | 18 | it('does nothing to node dimensions with rankdir = TB', function () { 19 | g.setGraph({ rankdir: 'TB' }) 20 | coordinateSystem.adjust(g) 21 | expect(g.node('a')).eqls({ width: 100, height: 200 }) 22 | }) 23 | 24 | it('does nothing to node dimensions with rankdir = BT', function () { 25 | g.setGraph({ rankdir: 'BT' }) 26 | coordinateSystem.adjust(g) 27 | expect(g.node('a')).eqls({ width: 100, height: 200 }) 28 | }) 29 | 30 | it('swaps width and height for nodes with rankdir = LR', function () { 31 | g.setGraph({ rankdir: 'LR' }) 32 | coordinateSystem.adjust(g) 33 | expect(g.node('a')).eqls({ width: 200, height: 100 }) 34 | }) 35 | 36 | it('swaps width and height for nodes with rankdir = RL', function () { 37 | g.setGraph({ rankdir: 'RL' }) 38 | coordinateSystem.adjust(g) 39 | expect(g.node('a')).eqls({ width: 200, height: 100 }) 40 | }) 41 | }) 42 | 43 | describe('coordinateSystem.undo', function () { 44 | beforeEach(function () { 45 | g.setNode('a', { width: 100, height: 200, x: 20, y: 40 }) 46 | }) 47 | 48 | it('does nothing to points with rankdir = TB', function () { 49 | g.setGraph({ rankdir: 'TB' }) 50 | coordinateSystem.undo(g) 51 | expect(g.node('a')).eqls({ x: 20, y: 40, width: 100, height: 200 }) 52 | }) 53 | 54 | it('flips the y coordinate for points with rankdir = BT', function () { 55 | g.setGraph({ rankdir: 'BT' }) 56 | coordinateSystem.undo(g) 57 | expect(g.node('a')).eqls({ x: 20, y: -40, width: 100, height: 200 }) 58 | }) 59 | 60 | it('swaps dimensions and coordinates for points with rankdir = LR', function () { 61 | g.setGraph({ rankdir: 'LR' }) 62 | coordinateSystem.undo(g) 63 | expect(g.node('a')).eqls({ x: 40, y: 20, width: 200, height: 100 }) 64 | }) 65 | 66 | it('swaps dims and coords and flips x for points with rankdir = RL', function () { 67 | g.setGraph({ rankdir: 'RL' }) 68 | coordinateSystem.undo(g) 69 | expect(g.node('a')).eqls({ x: -40, y: 20, width: 200, height: 100 }) 70 | }) 71 | }) 72 | }) 73 | -------------------------------------------------------------------------------- /tests/order/sort-test.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect 2 | 3 | const sort = require('../../lib/order/sort') 4 | 5 | describe('sort', function () { 6 | it('sorts nodes by barycenter', function () { 7 | var input = [ 8 | { vs: ['a'], i: 0, barycenter: 2, weight: 3 }, 9 | { vs: ['b'], i: 1, barycenter: 1, weight: 2 } 10 | ] 11 | expect(sort(input)).eqls({ 12 | vs: ['b', 'a'], 13 | barycenter: (2 * 3 + 1 * 2) / (3 + 2), 14 | weight: 3 + 2 }) 15 | }) 16 | 17 | it('can sort super-nodes', function () { 18 | var input = [ 19 | { vs: ['a', 'c', 'd'], i: 0, barycenter: 2, weight: 3 }, 20 | { vs: ['b'], i: 1, barycenter: 1, weight: 2 } 21 | ] 22 | expect(sort(input)).eqls({ 23 | vs: ['b', 'a', 'c', 'd'], 24 | barycenter: (2 * 3 + 1 * 2) / (3 + 2), 25 | weight: 3 + 2 }) 26 | }) 27 | 28 | it('biases to the left by default', function () { 29 | var input = [ 30 | { vs: ['a'], i: 0, barycenter: 1, weight: 1 }, 31 | { vs: ['b'], i: 1, barycenter: 1, weight: 1 } 32 | ] 33 | expect(sort(input)).eqls({ 34 | vs: ['a', 'b'], 35 | barycenter: 1, 36 | weight: 2 }) 37 | }) 38 | 39 | it('biases to the right if biasRight = true', function () { 40 | var input = [ 41 | { vs: ['a'], i: 0, barycenter: 1, weight: 1 }, 42 | { vs: ['b'], i: 1, barycenter: 1, weight: 1 } 43 | ] 44 | expect(sort(input, true)).eqls({ 45 | vs: ['b', 'a'], 46 | barycenter: 1, 47 | weight: 2 }) 48 | }) 49 | 50 | it('can sort nodes without a barycenter', function () { 51 | var input = [ 52 | { vs: ['a'], i: 0, barycenter: 2, weight: 1 }, 53 | { vs: ['b'], i: 1, barycenter: 6, weight: 1 }, 54 | { vs: ['c'], i: 2 }, 55 | { vs: ['d'], i: 3, barycenter: 3, weight: 1 } 56 | ] 57 | expect(sort(input)).eqls({ 58 | vs: ['a', 'd', 'c', 'b'], 59 | barycenter: (2 + 6 + 3) / 3, 60 | weight: 3 61 | }) 62 | }) 63 | 64 | it('can handle no barycenters for any nodes', function () { 65 | var input = [ 66 | { vs: ['a'], i: 0 }, 67 | { vs: ['b'], i: 3 }, 68 | { vs: ['c'], i: 2 }, 69 | { vs: ['d'], i: 1 } 70 | ] 71 | expect(sort(input)).eqls({ vs: ['a', 'd', 'c', 'b'] }) 72 | }) 73 | 74 | it('can handle a barycenter of 0', function () { 75 | var input = [ 76 | { vs: ['a'], i: 0, barycenter: 0, weight: 1 }, 77 | { vs: ['b'], i: 3 }, 78 | { vs: ['c'], i: 2 }, 79 | { vs: ['d'], i: 1 } 80 | ] 81 | expect(sort(input)).eqls({ 82 | vs: ['a', 'd', 'c', 'b'], 83 | barycenter: 0, 84 | weight: 1 85 | }) 86 | }) 87 | }) 88 | -------------------------------------------------------------------------------- /src/order/build-layer-graph.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import {Graph} from 'ciena-graphlib' 3 | 4 | function createRootNode (g) { 5 | var v 6 | while (g.hasNode((v = _.uniqueId('_root')))); 7 | return v 8 | } 9 | 10 | /* 11 | * Constructs a graph that can be used to sort a layer of nodes. The graph will 12 | * contain all base and subgraph nodes from the request layer in their original 13 | * hierarchy and any edges that are incident on these nodes and are of the type 14 | * requested by the "relationship" parameter. 15 | * 16 | * Nodes from the requested rank that do not have parents are assigned a root 17 | * node in the output graph, which is set in the root graph attribute. This 18 | * makes it easy to walk the hierarchy of movable nodes during ordering. 19 | * 20 | * Pre-conditions: 21 | * 22 | * 1. Input graph is a DAG 23 | * 2. Base nodes in the input graph have a rank attribute 24 | * 3. Subgraph nodes in the input graph has minRank and maxRank attributes 25 | * 4. Edges have an assigned weight 26 | * 27 | * Post-conditions: 28 | * 29 | * 1. Output graph has all nodes in the movable rank with preserved 30 | * hierarchy. 31 | * 2. Root nodes in the movable layer are made children of the node 32 | * indicated by the root attribute of the graph. 33 | * 3. Non-movable nodes incident on movable nodes, selected by the 34 | * relationship parameter, are included in the graph (without hierarchy). 35 | * 4. Edges incident on movable nodes, selected by the relationship 36 | * parameter, are added to the output graph. 37 | * 5. The weights for copied edges are aggregated as need, since the output 38 | * graph is not a multi-graph. 39 | */ 40 | export default function buildLayerGraph (g, rank, relationship) { 41 | var root = createRootNode(g) 42 | var result = new Graph({ compound: true }).setGraph({ root: root }) 43 | .setDefaultNodeLabel(function (v) { return g.node(v) }) 44 | 45 | _.forEach(g.nodes(), function (v) { 46 | var node = g.node(v) 47 | var parent = g.parent(v) 48 | 49 | if (node.rank === rank || node.minRank <= rank && rank <= node.maxRank) { 50 | result.setNode(v) 51 | result.setParent(v, parent || root) 52 | 53 | // This assumes we have only short edges! 54 | _.forEach(g[relationship](v), function (e) { 55 | var u = e.v === v ? e.w : e.v 56 | var edge = result.edge(u, v) 57 | var weight = !_.isUndefined(edge) ? edge.weight : 0 58 | result.setEdge(u, v, { weight: g.edge(e).weight + weight }) 59 | }) 60 | 61 | if (_.has(node, 'minRank')) { 62 | result.setNode(v, { 63 | borderLeft: node.borderLeft[rank], 64 | borderRight: node.borderRight[rank] 65 | }) 66 | } 67 | } 68 | }) 69 | 70 | return result 71 | } 72 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - '6.9.1' 5 | - 'stable' 6 | branches: 7 | except: 8 | - /^v[0-9\.]+/ 9 | addons: 10 | apt: 11 | sources: 12 | - ubuntu-toolchain-r-test 13 | packages: 14 | - g++-4.8 15 | firefox: 'latest' 16 | cache: 17 | directories: 18 | - node_modules 19 | env: 20 | matrix: 21 | - CXX=g++-4.8 22 | global: 23 | secure: LfSnOVkNuPG01PH1aYzwswiV3sOV6vvlWmM81TXZvrSmobjCDoRaong+BAuFEk2U1QjuKkmmad8WdEM/oPNEsxm6Uv6wVyMlO0GJrDiV/zkh8kiiWBlxbUjtO+qssFT9Ri+tMG86b62pVaRz7sY5kVAjIBjlbELzmYn367s3rlx3S6CCgwxZEUkmv9hV0m5F5t2sOEr/XxFq/N34Kf2gGJ68AQPHZVM7Qunmn75AXZ5wmXnREw9sZ2qCzJHI2JRlw234Sfj62Hxrh9rnUzlBr/tzBAPsvPBXj3x7OlMGZIiDV8gPdRkjCNWlBI3oWy1a7hO4JZezEmDQSinbm1phJejBARVh3u6rJWSRyDg2/LOF2RQh8Fq5L150rmuNVThBVuHg7k6BQfHN9FhzA/P/9Z/xmpGSIo1X5nOOOH1Xjh2PspaZrPsYLqO6fV1mR9DKBIaRgnFW3b0aPqT5YLNQTENP5EWggOvqvkPtpsSazUg/dhjEznk63RYV6AUWSFfuU/jyRJKSHgJZEfjKw02Qtko0XRfOEBSQd41GxCaFRyTv88pjpiQ7lzxxlOnXhuGhzLGAiKSExWaROepXTnskx0qLzkfcngtObklWAn+hXIbQooHd2C1luJ9VdKxIzwuc/gJHBP+gaiUTi8zTiT0xtZ1lLkUQpqUj6Es5x5gTtE8= 24 | before_install: 25 | - npm config set spin false 26 | - npm install -g coveralls pr-bumper --ignore-scripts 27 | - pr-bumper check 28 | install: 29 | - npm install --ignore-scripts 30 | before_script: 31 | - "export DISPLAY=:99.0" 32 | - "sh -e /etc/init.d/xvfb start" 33 | - sleep 3 # give xvfb some time to start 34 | script: 35 | - npm test 36 | after_success: 37 | - cat coverage/lcov.info | coveralls 38 | before_deploy: 39 | - pr-bumper bump 40 | deploy: 41 | provider: npm 42 | email: npm.ciena@gmail.com 43 | skip_cleanup: true 44 | api_key: 45 | secure: a7PbXfhfnC2IUq9DJytLsfvt2FiTjsOWNAbyA0MJVXSTnRJanTfHeL+pzsYxrl6MJsfVgFarIyfEsWCDRuoW8kKO0DmmZiFsDG1n3127pzZ5sA18tJPIW77bbBjtQ0sekzm4yMSl8+rPtoz9oJlK8V5Syjm9yK3zbp8DHWaIknsg+EyLdCfSlDS/4o7Xs2w9qY/6LLDNgz1vFa8zg3JJlrDy9BtGJgG5RAevHAegZRF4rLQWt6rHQJLXNP9z18L9SwBfrDqYAMm8j/mBxFZEk/7Flt8uXMJN+DSxAD5/9WK6k5NhHK3HqkxOuAGTfvWTQjYeiJlaqUUvDmJmxl11I6mxufZg9ktCWSGYoodk9ps7AvSUaCkOcsn+WJ+TKhDNm+WHiHNJVzHsYv4vGeHz/apvZu/RGiccl0dtgY0WMVU0ZDUsM5dWbRPcpEQA9v5LKlGzK8iu4aqiJZVhiT1Fmp/L5MAmdZrqgs5tjv2m4qlGjJcRNB4/AW+AAyXb7qJzeG+7pVc4SFgX//GSs5/f5Evwo02lMCVFEU0NVsHSXXjFBAdGZKaeJ2jqqIQ12GX+iYqtXBBfnY3jZKYpXvaEC2OR0Ruz0bCDsc9sxxTddLZNodQcUKdfXc9M12WTzeqEHrA48X36naW1rJ5ZY4Wu6+H9LpviorewrMr84xPFqow= 46 | on: 47 | branch: master 48 | node: 'stable' 49 | tags: false 50 | notifications: 51 | slack: 52 | secure: STMKMeeLTFRN+cDNexBy62UOCHmXsuq2Lhy3Iz5QHCkP6X5+6upwHdaZ2KDTW50ldwNPH/8z3K07/zqU1FZei1GOqzmNWrClJ1ZTCJLK1/atEslVXpIDKVIziJKY0uvCe3lhHC+NAYqlrahXC8S/boLYV8Ij+TLh0MrKJ11BCu6zBHq+h2bDVKMhf71Dk9RWXPTTm2rha542v/DmdABQqCBZxTc12qU40Y4LzbSuk3EpsDYP4C0aWDJE3hpZid0mRTmLTEJw1wUMLmZnyas5xFNPiiwAKYsiYBkbwi4WPckylBie44AXVPPgw5DXD1Kg5szanYjB/MYS70TL2ampkfUY4fy1sW6PDwfdKyM/zSqTmB09X2NNqs4ZPvxxcg2u5zfCvr+wUmUwCwRMgwgfmUaS0d4ghBJtaoiZ8nimESZ/sJGhnvmL5J2nmLf0sdgS0IpVl7nFxDVoSWcBgiA+SDPwM7jz4PbfMFIEotqiiPwIdbS85KqLzm0b5cooIR+ZtNP41cY6ifTNZp2G+b5L2+wXpM5ld7Exndr/42xo0juoDPwMG5EP7/VUEvY1zROuFhezZ9oONAmwyw8QHfC794r5qIdS8Ge8uLW9mmHv+/9f1abvBin2bW4hBm/aeKo1/UsTpmiRoYhL81ZRPjqUkuW5nFKENz0H41oWSQSquzg= 53 | -------------------------------------------------------------------------------- /tests/acyclic-test.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect 2 | const graphlib = require('ciena-graphlib') 3 | const alg = graphlib.alg 4 | const Graph = graphlib.Graph 5 | const findCycles = alg.findCycles 6 | const _ = require('lodash') 7 | 8 | const acyclic = require('../lib/acyclic') 9 | 10 | describe('acyclic', function () { 11 | var ACYCLICERS = [ 12 | 'greedy', 13 | 'dfs', 14 | 'unknown-should-still-work' 15 | ] 16 | var g 17 | 18 | beforeEach(function () { 19 | g = new Graph({ multigraph: true }) 20 | .setDefaultEdgeLabel(function () { return { minlen: 1, weight: 1 } }) 21 | }) 22 | 23 | _.forEach(ACYCLICERS, function (acyclicer) { 24 | describe(acyclicer, function () { 25 | beforeEach(function () { 26 | g.setGraph({ acyclicer: acyclicer }) 27 | }) 28 | 29 | describe('run', function () { 30 | it('does not change an already acyclic graph', function () { 31 | g.setPath(['a', 'b', 'd']) 32 | g.setPath(['a', 'c', 'd']) 33 | acyclic.run(g) 34 | var results = _.map(g.edges(), stripLabel) 35 | expect(_.sortBy(results, ['v', 'w'])).to.eql([ 36 | { v: 'a', w: 'b' }, 37 | { v: 'a', w: 'c' }, 38 | { v: 'b', w: 'd' }, 39 | { v: 'c', w: 'd' } 40 | ]) 41 | }) 42 | 43 | it('breaks cycles in the input graph', function () { 44 | g.setPath(['a', 'b', 'c', 'd', 'a']) 45 | acyclic.run(g) 46 | expect(findCycles(g)).to.eql([]) 47 | }) 48 | 49 | it('creates a multi-edge where necessary', function () { 50 | g.setPath(['a', 'b', 'a']) 51 | acyclic.run(g) 52 | expect(findCycles(g)).to.eql([]) 53 | if (g.hasEdge('a', 'b')) { 54 | expect(g.outEdges('a', 'b')).to.have.length(2) 55 | } else { 56 | expect(g.outEdges('b', 'a')).to.have.length(2) 57 | } 58 | expect(g.edgeCount()).to.equal(2) 59 | }) 60 | }) 61 | 62 | describe('undo', function () { 63 | it('does not change edges where the original graph was acyclic', function () { 64 | g.setEdge('a', 'b', { minlen: 2, weight: 3 }) 65 | acyclic.run(g) 66 | acyclic.undo(g) 67 | expect(g.edge('a', 'b')).to.eql({ minlen: 2, weight: 3 }) 68 | expect(g.edges()).to.have.length(1) 69 | }) 70 | 71 | it('can restore previosuly reversed edges', function () { 72 | g.setEdge('a', 'b', { minlen: 2, weight: 3 }) 73 | g.setEdge('b', 'a', { minlen: 3, weight: 4 }) 74 | acyclic.run(g) 75 | acyclic.undo(g) 76 | expect(g.edge('a', 'b')).to.eql({ minlen: 2, weight: 3 }) 77 | expect(g.edge('b', 'a')).to.eql({ minlen: 3, weight: 4 }) 78 | expect(g.edges()).to.have.length(2) 79 | }) 80 | }) 81 | }) 82 | }) 83 | 84 | describe('greedy-specific functionality', function () { 85 | it('prefers to break cycles at low-weight edges', function () { 86 | g.setGraph({ acyclicer: 'greedy' }) 87 | g.setDefaultEdgeLabel(function () { return { minlen: 1, weight: 2 } }) 88 | g.setPath(['a', 'b', 'c', 'd', 'a']) 89 | g.setEdge('c', 'd', { weight: 1 }) 90 | acyclic.run(g) 91 | expect(findCycles(g)).to.eql([]) 92 | expect(g.hasEdge('c', 'd')).to.be.false 93 | }) 94 | }) 95 | }) 96 | 97 | function stripLabel (edge) { 98 | var c = _.clone(edge) 99 | delete c.label 100 | return c 101 | } 102 | -------------------------------------------------------------------------------- /tests/greedy-fas-test.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect 2 | const graphlib = require('ciena-graphlib') 3 | const alg = graphlib.alg 4 | const Graph = graphlib.Graph 5 | const findCycles = alg.findCycles 6 | const _ = require('lodash') 7 | 8 | const greedyFAS = require('../lib/greedy-fas') 9 | 10 | describe('greedyFAS', function () { 11 | var g 12 | 13 | beforeEach(function () { 14 | g = new Graph() 15 | }) 16 | 17 | it('returns the empty set for empty graphs', function () { 18 | expect(greedyFAS(g)).to.eql([]) 19 | }) 20 | 21 | it('returns the empty set for single-node graphs', function () { 22 | g.setNode('a') 23 | expect(greedyFAS(g)).to.eql([]) 24 | }) 25 | 26 | it('returns an empty set if the input graph is acyclic', function () { 27 | var g = new Graph() 28 | g.setEdge('a', 'b') 29 | g.setEdge('b', 'c') 30 | g.setEdge('b', 'd') 31 | g.setEdge('a', 'e') 32 | expect(greedyFAS(g)).to.eql([]) 33 | }) 34 | 35 | it('returns a single edge with a simple cycle', function () { 36 | var g = new Graph() 37 | g.setEdge('a', 'b') 38 | g.setEdge('b', 'a') 39 | checkFAS(g, greedyFAS(g)) 40 | }) 41 | 42 | it('returns a single edge in a 4-node cycle', function () { 43 | var g = new Graph() 44 | g.setEdge('n1', 'n2') 45 | g.setPath(['n2', 'n3', 'n4', 'n5', 'n2']) 46 | g.setEdge('n3', 'n5') 47 | g.setEdge('n4', 'n2') 48 | g.setEdge('n4', 'n6') 49 | checkFAS(g, greedyFAS(g)) 50 | }) 51 | 52 | it('returns two edges for two 4-node cycles', function () { 53 | var g = new Graph() 54 | g.setEdge('n1', 'n2') 55 | g.setPath(['n2', 'n3', 'n4', 'n5', 'n2']) 56 | g.setEdge('n3', 'n5') 57 | g.setEdge('n4', 'n2') 58 | g.setEdge('n4', 'n6') 59 | g.setPath(['n6', 'n7', 'n8', 'n9', 'n6']) 60 | g.setEdge('n7', 'n9') 61 | g.setEdge('n8', 'n6') 62 | g.setEdge('n8', 'n10') 63 | checkFAS(g, greedyFAS(g)) 64 | }) 65 | 66 | it('works with arbitrarily weighted edges', function () { 67 | // Our algorithm should also work for graphs with multi-edges, a graph 68 | // where more than one edge can be pointing in the same direction between 69 | // the same pair of incident nodes. We try this by assigning weights to 70 | // our edges representing the number of edges from one node to the other. 71 | 72 | var g1 = new Graph() 73 | g1.setEdge('n1', 'n2', 2) 74 | g1.setEdge('n2', 'n1', 1) 75 | expect(greedyFAS(g1, weightFn(g1))).to.eql([{v: 'n2', w: 'n1'}]) 76 | 77 | var g2 = new Graph() 78 | g2.setEdge('n1', 'n2', 1) 79 | g2.setEdge('n2', 'n1', 2) 80 | expect(greedyFAS(g2, weightFn(g2))).to.eql([{v: 'n1', w: 'n2'}]) 81 | }) 82 | 83 | it('works for multigraphs', function () { 84 | var g = new Graph({ multigraph: true }) 85 | g.setEdge('a', 'b', 5, 'foo') 86 | g.setEdge('b', 'a', 2, 'bar') 87 | g.setEdge('b', 'a', 2, 'baz') 88 | expect(_.sortBy(greedyFAS(g, weightFn(g)), 'name')).to.eql([ 89 | { v: 'b', w: 'a', name: 'bar' }, 90 | { v: 'b', w: 'a', name: 'baz' } 91 | ]) 92 | }) 93 | }) 94 | 95 | function checkFAS (g, fas) { 96 | var n = g.nodeCount() 97 | var m = g.edgeCount() 98 | _.forEach(fas, function (edge) { 99 | g.removeEdge(edge.v, edge.w) 100 | }) 101 | expect(findCycles(g)).to.eql([]) 102 | // The more direct m/2 - n/6 fails for the simple cycle A <-> B, where one 103 | // edge must be reversed, but the performance bound implies that only 2/3rds 104 | // of an edge can be reversed. I'm using floors to acount for this. 105 | expect(fas.length).to.be.lte(Math.floor(m / 2) - Math.floor(n / 6)) 106 | } 107 | 108 | function weightFn (g) { 109 | return function (e) { 110 | return g.edge(e) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/greedy-fas.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import {Graph} from 'ciena-graphlib' 3 | import List from './data/list' 4 | 5 | var DEFAULT_WEIGHT_FN = _.constant(1) 6 | 7 | function doGreedyFAS (g, buckets, zeroIdx) { 8 | var results = [] 9 | var sources = buckets[buckets.length - 1] 10 | var sinks = buckets[0] 11 | 12 | var entry 13 | while (g.nodeCount()) { 14 | while ((entry = sinks.dequeue())) { removeNode(g, buckets, zeroIdx, entry) } 15 | while ((entry = sources.dequeue())) { removeNode(g, buckets, zeroIdx, entry) } 16 | if (g.nodeCount()) { 17 | for (var i = buckets.length - 2; i > 0; --i) { 18 | entry = buckets[i].dequeue() 19 | if (entry) { 20 | results = results.concat(removeNode(g, buckets, zeroIdx, entry, true)) 21 | break 22 | } 23 | } 24 | } 25 | } 26 | 27 | return results 28 | } 29 | 30 | function removeNode (g, buckets, zeroIdx, entry, collectPredecessors) { 31 | var results = collectPredecessors ? [] : undefined 32 | 33 | _.forEach(g.inEdges(entry.v), function (edge) { 34 | var weight = g.edge(edge) 35 | var uEntry = g.node(edge.v) 36 | 37 | if (collectPredecessors) { 38 | results.push({ v: edge.v, w: edge.w }) 39 | } 40 | 41 | uEntry.out -= weight 42 | assignBucket(buckets, zeroIdx, uEntry) 43 | }) 44 | 45 | _.forEach(g.outEdges(entry.v), function (edge) { 46 | var weight = g.edge(edge) 47 | var w = edge.w 48 | var wEntry = g.node(w) 49 | wEntry['in'] -= weight 50 | assignBucket(buckets, zeroIdx, wEntry) 51 | }) 52 | 53 | g.removeNode(entry.v) 54 | 55 | return results 56 | } 57 | 58 | function buildState (g, weightFn) { 59 | var fasGraph = new Graph() 60 | var maxIn = 0 61 | var maxOut = 0 62 | 63 | _.forEach(g.nodes(), function (v) { 64 | fasGraph.setNode(v, { v: v, 'in': 0, out: 0 }) 65 | }) 66 | 67 | // Aggregate weights on nodes, but also sum the weights across multi-edges 68 | // into a single edge for the fasGraph. 69 | _.forEach(g.edges(), function (e) { 70 | var prevWeight = fasGraph.edge(e.v, e.w) || 0 71 | var weight = weightFn(e) 72 | var edgeWeight = prevWeight + weight 73 | fasGraph.setEdge(e.v, e.w, edgeWeight) 74 | maxOut = Math.max(maxOut, fasGraph.node(e.v).out += weight) 75 | maxIn = Math.max(maxIn, fasGraph.node(e.w)['in'] += weight) 76 | }) 77 | 78 | var buckets = _.range(maxOut + maxIn + 3).map(function () { return new List() }) 79 | var zeroIdx = maxIn + 1 80 | 81 | _.forEach(fasGraph.nodes(), function (v) { 82 | assignBucket(buckets, zeroIdx, fasGraph.node(v)) 83 | }) 84 | 85 | return { graph: fasGraph, buckets: buckets, zeroIdx: zeroIdx } 86 | } 87 | 88 | function assignBucket (buckets, zeroIdx, entry) { 89 | if (!entry.out) { 90 | buckets[0].enqueue(entry) 91 | } else if (!entry['in']) { 92 | buckets[buckets.length - 1].enqueue(entry) 93 | } else { 94 | buckets[entry.out - entry['in'] + zeroIdx].enqueue(entry) 95 | } 96 | } 97 | 98 | /* 99 | * A greedy heuristic for finding a feedback arc set for a graph. A feedback 100 | * arc set is a set of edges that can be removed to make a graph acyclic. 101 | * The algorithm comes from: P. Eades, X. Lin, and W. F. Smyth, "A fast and 102 | * effective heuristic for the feedback arc set problem." This implementation 103 | * adjusts that from the paper to allow for weighted edges. 104 | */ 105 | export default function (g, weightFn) { 106 | if (g.nodeCount() <= 1) { 107 | return [] 108 | } 109 | var state = buildState(g, weightFn || DEFAULT_WEIGHT_FN) 110 | var results = doGreedyFAS(state.graph, state.buckets, state.zeroIdx) 111 | 112 | // Expand multi-edges 113 | return _.flatten(_.map(results, function (e) { 114 | return g.outEdges(e.v, e.w) 115 | }), true) 116 | } 117 | -------------------------------------------------------------------------------- /src/order/resolve-conflicts.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | 3 | function doResolveConflicts (sourceSet) { 4 | var entries = [] 5 | 6 | function handleIn (vEntry) { 7 | return function (uEntry) { 8 | if (uEntry.merged) { 9 | return 10 | } 11 | if (_.isUndefined(uEntry.barycenter) || 12 | _.isUndefined(vEntry.barycenter) || 13 | uEntry.barycenter >= vEntry.barycenter) { 14 | mergeEntries(vEntry, uEntry) 15 | } 16 | } 17 | } 18 | 19 | function handleOut (vEntry) { 20 | return function (wEntry) { 21 | wEntry['in'].push(vEntry) 22 | if (--wEntry.indegree === 0) { 23 | sourceSet.push(wEntry) 24 | } 25 | } 26 | } 27 | 28 | while (sourceSet.length) { 29 | var entry = sourceSet.pop() 30 | entries.push(entry) 31 | _.forEach(entry['in'].reverse(), handleIn(entry)) 32 | _.forEach(entry.out, handleOut(entry)) 33 | } 34 | 35 | return _.chain(entries) 36 | .filter(function (entry) { return !entry.merged }) 37 | .map(function (entry) { 38 | return _.pick(entry, ['vs', 'i', 'barycenter', 'weight']) 39 | }) 40 | .value() 41 | } 42 | 43 | function mergeEntries (target, source) { 44 | var sum = 0 45 | var weight = 0 46 | 47 | if (target.weight) { 48 | sum += target.barycenter * target.weight 49 | weight += target.weight 50 | } 51 | 52 | if (source.weight) { 53 | sum += source.barycenter * source.weight 54 | weight += source.weight 55 | } 56 | 57 | target.vs = source.vs.concat(target.vs) 58 | target.barycenter = sum / weight 59 | target.weight = weight 60 | target.i = Math.min(source.i, target.i) 61 | source.merged = true 62 | } 63 | 64 | /* 65 | * Given a list of entries of the form {v, barycenter, weight} and a 66 | * constraint graph this function will resolve any conflicts between the 67 | * constraint graph and the barycenters for the entries. If the barycenters for 68 | * an entry would violate a constraint in the constraint graph then we coalesce 69 | * the nodes in the conflict into a new node that respects the contraint and 70 | * aggregates barycenter and weight information. 71 | * 72 | * This implementation is based on the description in Forster, "A Fast and 73 | * Simple Hueristic for Constrained Two-Level Crossing Reduction," thought it 74 | * differs in some specific details. 75 | * 76 | * Pre-conditions: 77 | * 78 | * 1. Each entry has the form {v, barycenter, weight}, or if the node has 79 | * no barycenter, then {v}. 80 | * 81 | * Returns: 82 | * 83 | * A new list of entries of the form {vs, i, barycenter, weight}. The list 84 | * `vs` may either be a singleton or it may be an aggregation of nodes 85 | * ordered such that they do not violate constraints from the constraint 86 | * graph. The property `i` is the lowest original index of any of the 87 | * elements in `vs`. 88 | */ 89 | export default function resolveConflicts (entries, cg) { 90 | var mappedEntries = {} 91 | _.forEach(entries, function (entry, i) { 92 | var tmp = mappedEntries[entry.v] = { 93 | indegree: 0, 94 | 'in': [], 95 | out: [], 96 | vs: [entry.v], 97 | i: i 98 | } 99 | if (!_.isUndefined(entry.barycenter)) { 100 | tmp.barycenter = entry.barycenter 101 | tmp.weight = entry.weight 102 | } 103 | }) 104 | 105 | _.forEach(cg.edges(), function (e) { 106 | var entryV = mappedEntries[e.v] 107 | var entryW = mappedEntries[e.w] 108 | if (!_.isUndefined(entryV) && !_.isUndefined(entryW)) { 109 | entryW.indegree++ 110 | entryV.out.push(mappedEntries[e.w]) 111 | } 112 | }) 113 | 114 | var sourceSet = _.filter(mappedEntries, function (entry) { 115 | return !entry.indegree 116 | }) 117 | 118 | return doResolveConflicts(sourceSet) 119 | } 120 | -------------------------------------------------------------------------------- /src/nesting-graph.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import {addBorderNode, addDummyNode} from './util' 3 | 4 | function dfs (g, root, nodeSep, weight, height, depths, v) { 5 | var children = g.children(v) 6 | if (!children.length) { 7 | if (v !== root) { 8 | g.setEdge(root, v, { weight: 0, minlen: nodeSep }) 9 | } 10 | return 11 | } 12 | 13 | var top = addBorderNode(g, '_bt') 14 | var bottom = addBorderNode(g, '_bb') 15 | var label = g.node(v) 16 | 17 | g.setParent(top, v) 18 | label.borderTop = top 19 | g.setParent(bottom, v) 20 | label.borderBottom = bottom 21 | 22 | _.forEach(children, function (child) { 23 | dfs(g, root, nodeSep, weight, height, depths, child) 24 | 25 | var childNode = g.node(child) 26 | var childTop = childNode.borderTop ? childNode.borderTop : child 27 | var childBottom = childNode.borderBottom ? childNode.borderBottom : child 28 | var thisWeight = childNode.borderTop ? weight : 2 * weight 29 | var minlen = childTop !== childBottom ? 1 : height - depths[v] + 1 30 | 31 | g.setEdge(top, childTop, { 32 | weight: thisWeight, 33 | minlen: minlen, 34 | nestingEdge: true 35 | }) 36 | 37 | g.setEdge(childBottom, bottom, { 38 | weight: thisWeight, 39 | minlen: minlen, 40 | nestingEdge: true 41 | }) 42 | }) 43 | 44 | if (!g.parent(v)) { 45 | g.setEdge(root, top, { weight: 0, minlen: height + depths[v] }) 46 | } 47 | } 48 | 49 | function sumWeights (g) { 50 | return _.reduce(g.edges(), function (acc, e) { 51 | return acc + g.edge(e).weight 52 | }, 0) 53 | } 54 | 55 | function treeDepths (g) { 56 | var depths = {} 57 | function dfs (v, depth) { 58 | var children = g.children(v) 59 | if (children && children.length) { 60 | _.forEach(children, function (child) { 61 | dfs(child, depth + 1) 62 | }) 63 | } 64 | depths[v] = depth 65 | } 66 | _.forEach(g.children(), function (v) { dfs(v, 1) }) 67 | return depths 68 | } 69 | 70 | export function cleanup (g) { 71 | var graphLabel = g.graph() 72 | g.removeNode(graphLabel.nestingRoot) 73 | delete graphLabel.nestingRoot 74 | _.forEach(g.edges(), function (e) { 75 | var edge = g.edge(e) 76 | if (edge.nestingEdge) { 77 | g.removeEdge(e) 78 | } 79 | }) 80 | } 81 | 82 | /* 83 | * A nesting graph creates dummy nodes for the tops and bottoms of subgraphs, 84 | * adds appropriate edges to ensure that all cluster nodes are placed between 85 | * these boundries, and ensures that the graph is connected. 86 | * 87 | * In addition we ensure, through the use of the minlen property, that nodes 88 | * and subgraph border nodes to not end up on the same rank. 89 | * 90 | * Preconditions: 91 | * 92 | * 1. Input graph is a DAG 93 | * 2. Nodes in the input graph has a minlen attribute 94 | * 95 | * Postconditions: 96 | * 97 | * 1. Input graph is connected. 98 | * 2. Dummy nodes are added for the tops and bottoms of subgraphs. 99 | * 3. The minlen attribute for nodes is adjusted to ensure nodes do not 100 | * get placed on the same rank as subgraph border nodes. 101 | * 102 | * The nesting graph idea comes from Sander, "Layout of Compound Directed 103 | * Graphs." 104 | */ 105 | export function run (g) { 106 | var root = addDummyNode(g, 'root', {}, '_root') 107 | var depths = treeDepths(g) 108 | var height = _.max(_.values(depths)) - 1 // Note: depths is an Object not an array 109 | var nodeSep = 2 * height + 1 110 | 111 | g.graph().nestingRoot = root 112 | 113 | // Multiply minlen by nodeSep to align nodes on non-border ranks. 114 | _.forEach(g.edges(), function (e) { g.edge(e).minlen *= nodeSep }) 115 | 116 | // Calculate a weight that is sufficient to keep subgraphs vertically compact 117 | var weight = sumWeights(g) + 1 118 | 119 | // Create border nodes and link them up 120 | _.forEach(g.children(), function (child) { 121 | dfs(g, root, nodeSep, weight, height, depths, child) 122 | }) 123 | 124 | // Save the multiplier for node layers for later removal of empty border 125 | // layers. 126 | g.graph().nodeRankFactor = nodeSep 127 | } 128 | -------------------------------------------------------------------------------- /tests/add-border-segments-test.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect 2 | const Graph = require('ciena-graphlib').Graph 3 | 4 | const addBorderSegments = require('../lib/add-border-segments') 5 | 6 | describe('addBorderSegments', function () { 7 | var g 8 | 9 | beforeEach(function () { 10 | g = new Graph({ compound: true }) 11 | }) 12 | 13 | it('does not add border nodes for a non-compound graph', function () { 14 | var g = new Graph() 15 | g.setNode('a', { rank: 0 }) 16 | addBorderSegments(g) 17 | expect(g.nodeCount()).to.equal(1) 18 | expect(g.node('a')).to.eql({ rank: 0 }) 19 | }) 20 | 21 | it('does not add border nodes for a graph with no clusters', function () { 22 | g.setNode('a', { rank: 0 }) 23 | addBorderSegments(g) 24 | expect(g.nodeCount()).to.equal(1) 25 | expect(g.node('a')).to.eql({ rank: 0 }) 26 | }) 27 | 28 | it('adds a border for a single-rank subgraph', function () { 29 | g.setNode('sg', { minRank: 1, maxRank: 1 }) 30 | addBorderSegments(g) 31 | 32 | var bl = g.node('sg').borderLeft[1] 33 | var br = g.node('sg').borderRight[1] 34 | expect(g.node(bl)).eqls({ 35 | dummy: 'border', 36 | borderType: 'borderLeft', 37 | rank: 1, 38 | width: 0, 39 | height: 0 40 | }) 41 | expect(g.parent(bl)).equals('sg') 42 | expect(g.node(br)).eqls({ 43 | dummy: 'border', 44 | borderType: 'borderRight', 45 | rank: 1, 46 | width: 0, 47 | height: 0 48 | }) 49 | expect(g.parent(br)).equals('sg') 50 | }) 51 | 52 | it('adds a border for a multi-rank subgraph', function () { 53 | g.setNode('sg', { minRank: 1, maxRank: 2 }) 54 | addBorderSegments(g) 55 | 56 | var sgNode = g.node('sg') 57 | var bl2 = sgNode.borderLeft[1] 58 | var br2 = sgNode.borderRight[1] 59 | expect(g.node(bl2)).eqls({ 60 | dummy: 'border', 61 | borderType: 'borderLeft', 62 | rank: 1, 63 | width: 0, 64 | height: 0 65 | }) 66 | expect(g.parent(bl2)).equals('sg') 67 | expect(g.node(br2)).eqls({ 68 | dummy: 'border', 69 | borderType: 'borderRight', 70 | rank: 1, 71 | width: 0, 72 | height: 0 73 | }) 74 | expect(g.parent(br2)).equals('sg') 75 | 76 | var bl1 = sgNode.borderLeft[2] 77 | var br1 = sgNode.borderRight[2] 78 | expect(g.node(bl1)).eqls({ 79 | dummy: 'border', 80 | borderType: 'borderLeft', 81 | rank: 2, 82 | width: 0, 83 | height: 0 84 | }) 85 | expect(g.parent(bl1)).equals('sg') 86 | expect(g.node(br1)).eqls({ 87 | dummy: 'border', 88 | borderType: 'borderRight', 89 | rank: 2, 90 | width: 0, 91 | height: 0 92 | }) 93 | expect(g.parent(br1)).equals('sg') 94 | 95 | expect(g.hasEdge(sgNode.borderLeft[1], sgNode.borderLeft[2])).to.be.true 96 | expect(g.hasEdge(sgNode.borderRight[1], sgNode.borderRight[2])).to.be.true 97 | }) 98 | 99 | it('adds borders for nested subgraphs', function () { 100 | g.setNode('sg1', { minRank: 1, maxRank: 1 }) 101 | g.setNode('sg2', { minRank: 1, maxRank: 1 }) 102 | g.setParent('sg2', 'sg1') 103 | addBorderSegments(g) 104 | 105 | var bl1 = g.node('sg1').borderLeft[1] 106 | var br1 = g.node('sg1').borderRight[1] 107 | expect(g.node(bl1)).eqls({ 108 | dummy: 'border', 109 | borderType: 'borderLeft', 110 | rank: 1, 111 | width: 0, 112 | height: 0 113 | }) 114 | expect(g.parent(bl1)).equals('sg1') 115 | expect(g.node(br1)).eqls({ 116 | dummy: 'border', 117 | borderType: 'borderRight', 118 | rank: 1, 119 | width: 0, 120 | height: 0 121 | }) 122 | expect(g.parent(br1)).equals('sg1') 123 | 124 | var bl2 = g.node('sg2').borderLeft[1] 125 | var br2 = g.node('sg2').borderRight[1] 126 | expect(g.node(bl2)).eqls({ 127 | dummy: 'border', 128 | borderType: 'borderLeft', 129 | rank: 1, 130 | width: 0, 131 | height: 0 132 | }) 133 | expect(g.parent(bl2)).equals('sg2') 134 | expect(g.node(br2)).eqls({ 135 | dummy: 'border', 136 | borderType: 'borderRight', 137 | rank: 1, 138 | width: 0, 139 | height: 0 140 | }) 141 | expect(g.parent(br2)).equals('sg2') 142 | }) 143 | }) 144 | -------------------------------------------------------------------------------- /tests/order/sort-subgraph-test.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect 2 | const Graph = require('ciena-graphlib').Graph 3 | const _ = require('lodash') 4 | 5 | const sortSubgraph = require('../../lib/order/sort-subgraph') 6 | 7 | describe('order/sortSubgraph', function () { 8 | var g, cg 9 | 10 | beforeEach(function () { 11 | g = new Graph({ compound: true }) 12 | .setDefaultNodeLabel(function () { return {} }) 13 | .setDefaultEdgeLabel(function () { return { weight: 1 } }) 14 | _.forEach(_.range(5), function (v) { g.setNode(v, { order: v }) }) 15 | cg = new Graph() 16 | }) 17 | 18 | it('sorts a flat subgraph based on barycenter', function () { 19 | g.setEdge(3, 'x') 20 | g.setEdge(1, 'y', { weight: 2 }) 21 | g.setEdge(4, 'y') 22 | _.forEach(['x', 'y'], function (v) { g.setParent(v, 'movable') }) 23 | 24 | expect(sortSubgraph(g, 'movable', cg).vs).eqls(['y', 'x']) 25 | }) 26 | 27 | it('preserves the pos of a node (y) w/o neighbors in a flat subgraph', function () { 28 | g.setEdge(3, 'x') 29 | g.setNode('y') 30 | g.setEdge(1, 'z', { weight: 2 }) 31 | g.setEdge(4, 'z') 32 | _.forEach(['x', 'y', 'z'], function (v) { g.setParent(v, 'movable') }) 33 | 34 | expect(sortSubgraph(g, 'movable', cg).vs).eqls(['z', 'y', 'x']) 35 | }) 36 | 37 | it('biases to the left without reverse bias', function () { 38 | g.setEdge(1, 'x') 39 | g.setEdge(1, 'y') 40 | _.forEach(['x', 'y'], function (v) { g.setParent(v, 'movable') }) 41 | 42 | expect(sortSubgraph(g, 'movable', cg).vs).eqls(['x', 'y']) 43 | }) 44 | 45 | it('biases to the right with reverse bias', function () { 46 | g.setEdge(1, 'x') 47 | g.setEdge(1, 'y') 48 | _.forEach(['x', 'y'], function (v) { g.setParent(v, 'movable') }) 49 | 50 | expect(sortSubgraph(g, 'movable', cg, true).vs).eqls(['y', 'x']) 51 | }) 52 | 53 | it('aggregates stats about the subgraph', function () { 54 | g.setEdge(3, 'x') 55 | g.setEdge(1, 'y', { weight: 2 }) 56 | g.setEdge(4, 'y') 57 | _.forEach(['x', 'y'], function (v) { g.setParent(v, 'movable') }) 58 | 59 | var results = sortSubgraph(g, 'movable', cg) 60 | expect(results.barycenter).to.equal(2.25) 61 | expect(results.weight).to.equal(4) 62 | }) 63 | 64 | it('can sort a nested subgraph with no barycenter', function () { 65 | g.setNodes(['a', 'b', 'c']) 66 | g.setParent('a', 'y') 67 | g.setParent('b', 'y') 68 | g.setParent('c', 'y') 69 | g.setEdge(0, 'x') 70 | g.setEdge(1, 'z') 71 | g.setEdge(2, 'y') 72 | _.forEach(['x', 'y', 'z'], function (v) { g.setParent(v, 'movable') }) 73 | 74 | expect(sortSubgraph(g, 'movable', cg).vs).eqls(['x', 'z', 'a', 'b', 'c']) 75 | }) 76 | 77 | it('can sort a nested subgraph with a barycenter', function () { 78 | g.setNodes(['a', 'b', 'c']) 79 | g.setParent('a', 'y') 80 | g.setParent('b', 'y') 81 | g.setParent('c', 'y') 82 | g.setEdge(0, 'a', { weight: 3 }) 83 | g.setEdge(0, 'x') 84 | g.setEdge(1, 'z') 85 | g.setEdge(2, 'y') 86 | _.forEach(['x', 'y', 'z'], function (v) { g.setParent(v, 'movable') }) 87 | 88 | expect(sortSubgraph(g, 'movable', cg).vs).eqls(['x', 'a', 'b', 'c', 'z']) 89 | }) 90 | 91 | it('can sort a nested subgraph with no in-edges', function () { 92 | g.setNodes(['a', 'b', 'c']) 93 | g.setParent('a', 'y') 94 | g.setParent('b', 'y') 95 | g.setParent('c', 'y') 96 | g.setEdge(0, 'a') 97 | g.setEdge(1, 'b') 98 | g.setEdge(0, 'x') 99 | g.setEdge(1, 'z') 100 | _.forEach(['x', 'y', 'z'], function (v) { g.setParent(v, 'movable') }) 101 | 102 | expect(sortSubgraph(g, 'movable', cg).vs).eqls(['x', 'a', 'b', 'c', 'z']) 103 | }) 104 | 105 | it('sorts border nodes to the extremes of the subgraph', function () { 106 | g.setEdge(0, 'x') 107 | g.setEdge(1, 'y') 108 | g.setEdge(2, 'z') 109 | g.setNode('sg1', { borderLeft: 'bl', borderRight: 'br' }) 110 | _.forEach(['x', 'y', 'z', 'bl', 'br'], function (v) { g.setParent(v, 'sg1') }) 111 | expect(sortSubgraph(g, 'sg1', cg).vs).eqls(['bl', 'x', 'y', 'z', 'br']) 112 | }) 113 | 114 | it('assigns a barycenter to a subgraph based on previous border nodes', function () { 115 | g.setNode('bl1', { order: 0 }) 116 | g.setNode('br1', { order: 1 }) 117 | g.setEdge('bl1', 'bl2') 118 | g.setEdge('br1', 'br2') 119 | _.forEach(['bl2', 'br2'], function (v) { g.setParent(v, 'sg') }) 120 | g.setNode('sg', { borderLeft: 'bl2', borderRight: 'br2' }) 121 | expect(sortSubgraph(g, 'sg', cg)).eqls({ 122 | barycenter: 0.5, 123 | weight: 2, 124 | vs: ['bl2', 'br2'] 125 | }) 126 | }) 127 | }) 128 | -------------------------------------------------------------------------------- /tests/order/build-layer-graph-test.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect 2 | const Graph = require('ciena-graphlib').Graph 3 | const _ = require('lodash') 4 | 5 | const buildLayerGraph = require('../../lib/order/build-layer-graph') 6 | 7 | describe('order/buildLayerGraph', function () { 8 | var g 9 | 10 | beforeEach(function () { 11 | g = new Graph({ compound: true, multigraph: true }) 12 | }) 13 | 14 | it('places movable nodes with no parents under the root node', function () { 15 | g.setNode('a', { rank: 1 }) 16 | g.setNode('b', { rank: 1 }) 17 | g.setNode('c', { rank: 2 }) 18 | g.setNode('d', { rank: 3 }) 19 | 20 | var lg 21 | lg = buildLayerGraph(g, 1, 'inEdges') 22 | expect(lg.hasNode(lg.graph().root)) 23 | expect(lg.children()).eqls([lg.graph().root]) 24 | expect(lg.children(lg.graph().root)).eqls(['a', 'b']) 25 | }) 26 | 27 | it('copies flat nodes from the layer to the graph', function () { 28 | g.setNode('a', { rank: 1 }) 29 | g.setNode('b', { rank: 1 }) 30 | g.setNode('c', { rank: 2 }) 31 | g.setNode('d', { rank: 3 }) 32 | 33 | expect(buildLayerGraph(g, 1, 'inEdges').nodes()).to.include('a') 34 | expect(buildLayerGraph(g, 1, 'inEdges').nodes()).to.include('b') 35 | expect(buildLayerGraph(g, 2, 'inEdges').nodes()).to.include('c') 36 | expect(buildLayerGraph(g, 3, 'inEdges').nodes()).to.include('d') 37 | }) 38 | 39 | it('uses the original node label for copied nodes', function () { 40 | // This allows us to make updates to the original graph and have them 41 | // be available automatically in the layer graph. 42 | g.setNode('a', { foo: 1, rank: 1 }) 43 | g.setNode('b', { foo: 2, rank: 2 }) 44 | g.setEdge('a', 'b', { weight: 1 }) 45 | 46 | var lg = buildLayerGraph(g, 2, 'inEdges') 47 | 48 | expect(lg.node('a').foo).equals(1) 49 | g.node('a').foo = 'updated' 50 | expect(lg.node('a').foo).equals('updated') 51 | 52 | expect(lg.node('b').foo).equals(2) 53 | g.node('b').foo = 'updated' 54 | expect(lg.node('b').foo).equals('updated') 55 | }) 56 | 57 | it('copies edges incident on rank nodes to the graph (inEdges)', function () { 58 | g.setNode('a', { rank: 1 }) 59 | g.setNode('b', { rank: 1 }) 60 | g.setNode('c', { rank: 2 }) 61 | g.setNode('d', { rank: 3 }) 62 | g.setEdge('a', 'c', { weight: 2 }) 63 | g.setEdge('b', 'c', { weight: 3 }) 64 | g.setEdge('c', 'd', { weight: 4 }) 65 | 66 | expect(buildLayerGraph(g, 1, 'inEdges').edgeCount()).to.equal(0) 67 | expect(buildLayerGraph(g, 2, 'inEdges').edgeCount()).to.equal(2) 68 | expect(buildLayerGraph(g, 2, 'inEdges').edge('a', 'c')).eqls({ weight: 2 }) 69 | expect(buildLayerGraph(g, 2, 'inEdges').edge('b', 'c')).eqls({ weight: 3 }) 70 | expect(buildLayerGraph(g, 3, 'inEdges').edgeCount()).to.equal(1) 71 | expect(buildLayerGraph(g, 3, 'inEdges').edge('c', 'd')).eqls({ weight: 4 }) 72 | }) 73 | 74 | it('copies edges incident on rank nodes to the graph (outEdges)', function () { 75 | g.setNode('a', { rank: 1 }) 76 | g.setNode('b', { rank: 1 }) 77 | g.setNode('c', { rank: 2 }) 78 | g.setNode('d', { rank: 3 }) 79 | g.setEdge('a', 'c', { weight: 2 }) 80 | g.setEdge('b', 'c', { weight: 3 }) 81 | g.setEdge('c', 'd', { weight: 4 }) 82 | 83 | expect(buildLayerGraph(g, 1, 'outEdges').edgeCount()).to.equal(2) 84 | expect(buildLayerGraph(g, 1, 'outEdges').edge('c', 'a')).eqls({ weight: 2 }) 85 | expect(buildLayerGraph(g, 1, 'outEdges').edge('c', 'b')).eqls({ weight: 3 }) 86 | expect(buildLayerGraph(g, 2, 'outEdges').edgeCount()).to.equal(1) 87 | expect(buildLayerGraph(g, 2, 'outEdges').edge('d', 'c')).eqls({ weight: 4 }) 88 | expect(buildLayerGraph(g, 3, 'outEdges').edgeCount()).to.equal(0) 89 | }) 90 | 91 | it('collapses multi-edges', function () { 92 | g.setNode('a', { rank: 1 }) 93 | g.setNode('b', { rank: 2 }) 94 | g.setEdge('a', 'b', { weight: 2 }) 95 | g.setEdge('a', 'b', { weight: 3 }, 'multi') 96 | 97 | expect(buildLayerGraph(g, 2, 'inEdges').edge('a', 'b')).eqls({ weight: 5 }) 98 | }) 99 | 100 | it('preserves hierarchy for the movable layer', function () { 101 | g.setNode('a', { rank: 0 }) 102 | g.setNode('b', { rank: 0 }) 103 | g.setNode('c', { rank: 0 }) 104 | g.setNode('sg', { 105 | minRank: 0, 106 | maxRank: 0, 107 | borderLeft: ['bl'], 108 | borderRight: ['br'] 109 | }) 110 | _.forEach(['a', 'b'], function (v) { g.setParent(v, 'sg') }) 111 | 112 | var lg = buildLayerGraph(g, 0, 'inEdges') 113 | var root = lg.graph().root 114 | expect(_.sortBy(lg.children(root))).eqls(['c', 'sg']) 115 | expect(lg.parent('a')).equals('sg') 116 | expect(lg.parent('b')).equals('sg') 117 | }) 118 | }) 119 | -------------------------------------------------------------------------------- /tests/parent-dummy-chains-test.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect 2 | const Graph = require('ciena-graphlib').Graph 3 | 4 | const parentDummyChains = require('../lib/parent-dummy-chains') 5 | 6 | describe('parentDummyChains', function () { 7 | var g 8 | 9 | beforeEach(function () { 10 | g = new Graph({ compound: true }).setGraph({}) 11 | }) 12 | 13 | it('does not set a parent if both the tail and head have no parent', function () { 14 | g.setNode('a') 15 | g.setNode('b') 16 | g.setNode('d1', { edgeObj: { v: 'a', w: 'b' } }) 17 | g.graph().dummyChains = ['d1'] 18 | g.setPath(['a', 'd1', 'b']) 19 | 20 | parentDummyChains(g) 21 | expect(g.parent('d1')).to.be.undefined 22 | }) 23 | 24 | it("uses the tail's parent for the first node if it is not the root", function () { 25 | g.setParent('a', 'sg1') 26 | g.setNode('sg1', { minRank: 0, maxRank: 2 }) 27 | g.setNode('d1', { edgeObj: { v: 'a', w: 'b' }, rank: 2 }) 28 | g.graph().dummyChains = ['d1'] 29 | g.setPath(['a', 'd1', 'b']) 30 | 31 | parentDummyChains(g) 32 | expect(g.parent('d1')).equals('sg1') 33 | }) 34 | 35 | it("uses the heads's parent for the first node if tail's is root", function () { 36 | g.setParent('b', 'sg1') 37 | g.setNode('sg1', { minRank: 1, maxRank: 3 }) 38 | g.setNode('d1', { edgeObj: { v: 'a', w: 'b' }, rank: 1 }) 39 | g.graph().dummyChains = ['d1'] 40 | g.setPath(['a', 'd1', 'b']) 41 | 42 | parentDummyChains(g) 43 | expect(g.parent('d1')).equals('sg1') 44 | }) 45 | 46 | it('handles a long chain starting in a subgraph', function () { 47 | g.setParent('a', 'sg1') 48 | g.setNode('sg1', { minRank: 0, maxRank: 2 }) 49 | g.setNode('d1', { edgeObj: { v: 'a', w: 'b' }, rank: 2 }) 50 | g.setNode('d2', { rank: 3 }) 51 | g.setNode('d3', { rank: 4 }) 52 | g.graph().dummyChains = ['d1'] 53 | g.setPath(['a', 'd1', 'd2', 'd3', 'b']) 54 | 55 | parentDummyChains(g) 56 | expect(g.parent('d1')).equals('sg1') 57 | expect(g.parent('d2')).to.be.undefined 58 | expect(g.parent('d3')).to.be.undefined 59 | }) 60 | 61 | it('handles a long chain ending in a subgraph', function () { 62 | g.setParent('b', 'sg1') 63 | g.setNode('sg1', { minRank: 3, maxRank: 5 }) 64 | g.setNode('d1', { edgeObj: { v: 'a', w: 'b' }, rank: 1 }) 65 | g.setNode('d2', { rank: 2 }) 66 | g.setNode('d3', { rank: 3 }) 67 | g.graph().dummyChains = ['d1'] 68 | g.setPath(['a', 'd1', 'd2', 'd3', 'b']) 69 | 70 | parentDummyChains(g) 71 | expect(g.parent('d1')).to.be.undefined 72 | expect(g.parent('d2')).to.be.undefined 73 | expect(g.parent('d3')).equals('sg1') 74 | }) 75 | 76 | it('handles nested subgraphs', function () { 77 | g.setParent('a', 'sg2') 78 | g.setParent('sg2', 'sg1') 79 | g.setNode('sg1', { minRank: 0, maxRank: 4 }) 80 | g.setNode('sg2', { minRank: 1, maxRank: 3 }) 81 | g.setParent('b', 'sg4') 82 | g.setParent('sg4', 'sg3') 83 | g.setNode('sg3', { minRank: 6, maxRank: 10 }) 84 | g.setNode('sg4', { minRank: 7, maxRank: 9 }) 85 | for (var i = 0; i < 5; ++i) { 86 | g.setNode('d' + (i + 1), { rank: i + 3 }) 87 | } 88 | g.node('d1').edgeObj = { v: 'a', w: 'b' } 89 | g.graph().dummyChains = ['d1'] 90 | g.setPath(['a', 'd1', 'd2', 'd3', 'd4', 'd5', 'b']) 91 | 92 | parentDummyChains(g) 93 | expect(g.parent('d1')).equals('sg2') 94 | expect(g.parent('d2')).equals('sg1') 95 | expect(g.parent('d3')).to.be.undefined 96 | expect(g.parent('d4')).equals('sg3') 97 | expect(g.parent('d5')).equals('sg4') 98 | }) 99 | 100 | it('handles overlapping rank ranges', function () { 101 | g.setParent('a', 'sg1') 102 | g.setNode('sg1', { minRank: 0, maxRank: 3 }) 103 | g.setParent('b', 'sg2') 104 | g.setNode('sg2', { minRank: 2, maxRank: 6 }) 105 | g.setNode('d1', { edgeObj: { v: 'a', w: 'b' }, rank: 2 }) 106 | g.setNode('d2', { rank: 3 }) 107 | g.setNode('d3', { rank: 4 }) 108 | g.graph().dummyChains = ['d1'] 109 | g.setPath(['a', 'd1', 'd2', 'd3', 'b']) 110 | 111 | parentDummyChains(g) 112 | expect(g.parent('d1')).equals('sg1') 113 | expect(g.parent('d2')).equals('sg1') 114 | expect(g.parent('d3')).equals('sg2') 115 | }) 116 | 117 | it('handles an LCA that is not the root of the graph #1', function () { 118 | g.setParent('a', 'sg1') 119 | g.setParent('sg2', 'sg1') 120 | g.setNode('sg1', { minRank: 0, maxRank: 6 }) 121 | g.setParent('b', 'sg2') 122 | g.setNode('sg2', { minRank: 3, maxRank: 5 }) 123 | g.setNode('d1', { edgeObj: { v: 'a', w: 'b' }, rank: 2 }) 124 | g.setNode('d2', { rank: 3 }) 125 | g.graph().dummyChains = ['d1'] 126 | g.setPath(['a', 'd1', 'd2', 'b']) 127 | 128 | parentDummyChains(g) 129 | expect(g.parent('d1')).equals('sg1') 130 | expect(g.parent('d2')).equals('sg2') 131 | }) 132 | 133 | it('handles an LCA that is not the root of the graph #2', function () { 134 | g.setParent('a', 'sg2') 135 | g.setParent('sg2', 'sg1') 136 | g.setNode('sg1', { minRank: 0, maxRank: 6 }) 137 | g.setParent('b', 'sg1') 138 | g.setNode('sg2', { minRank: 1, maxRank: 3 }) 139 | g.setNode('d1', { edgeObj: { v: 'a', w: 'b' }, rank: 3 }) 140 | g.setNode('d2', { rank: 4 }) 141 | g.graph().dummyChains = ['d1'] 142 | g.setPath(['a', 'd1', 'd2', 'b']) 143 | 144 | parentDummyChains(g) 145 | expect(g.parent('d1')).equals('sg2') 146 | expect(g.parent('d2')).equals('sg1') 147 | }) 148 | }) 149 | -------------------------------------------------------------------------------- /tests/order/resolve-conflicts-test.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect 2 | const Graph = require('ciena-graphlib').Graph 3 | const _ = require('lodash') 4 | 5 | const resolveConflicts = require('../../lib/order/resolve-conflicts') 6 | 7 | describe('order/resolveConflicts', function () { 8 | var cg 9 | 10 | beforeEach(function () { 11 | cg = new Graph() 12 | }) 13 | 14 | it('returns back nodes unchanged when no constraints exist', function () { 15 | var input = [ 16 | { v: 'a', barycenter: 2, weight: 3 }, 17 | { v: 'b', barycenter: 1, weight: 2 } 18 | ] 19 | expect(_.sortBy(resolveConflicts(input, cg), 'vs')).eqls([ 20 | { vs: ['a'], i: 0, barycenter: 2, weight: 3 }, 21 | { vs: ['b'], i: 1, barycenter: 1, weight: 2 } 22 | ]) 23 | }) 24 | 25 | it('returns back nodes unchanged when no conflicts exist', function () { 26 | var input = [ 27 | { v: 'a', barycenter: 2, weight: 3 }, 28 | { v: 'b', barycenter: 1, weight: 2 } 29 | ] 30 | cg.setEdge('b', 'a') 31 | expect(_.sortBy(resolveConflicts(input, cg), 'vs')).eqls([ 32 | { vs: ['a'], i: 0, barycenter: 2, weight: 3 }, 33 | { vs: ['b'], i: 1, barycenter: 1, weight: 2 } 34 | ]) 35 | }) 36 | 37 | it('coalesces nodes when there is a conflict', function () { 38 | var input = [ 39 | { v: 'a', barycenter: 2, weight: 3 }, 40 | { v: 'b', barycenter: 1, weight: 2 } 41 | ] 42 | cg.setEdge('a', 'b') 43 | expect(_.sortBy(resolveConflicts(input, cg), 'vs')).eqls([ 44 | { vs: ['a', 'b'], 45 | i: 0, 46 | barycenter: (3 * 2 + 2 * 1) / (3 + 2), 47 | weight: 3 + 2 48 | } 49 | ]) 50 | }) 51 | 52 | it('coalesces nodes when there is a conflict #2', function () { 53 | var input = [ 54 | { v: 'a', barycenter: 4, weight: 1 }, 55 | { v: 'b', barycenter: 3, weight: 1 }, 56 | { v: 'c', barycenter: 2, weight: 1 }, 57 | { v: 'd', barycenter: 1, weight: 1 } 58 | ] 59 | cg.setPath(['a', 'b', 'c', 'd']) 60 | expect(_.sortBy(resolveConflicts(input, cg), 'vs')).eqls([ 61 | { vs: ['a', 'b', 'c', 'd'], 62 | i: 0, 63 | barycenter: (4 + 3 + 2 + 1) / 4, 64 | weight: 4 65 | } 66 | ]) 67 | }) 68 | 69 | it('works with multiple constraints for the same target #1', function () { 70 | var input = [ 71 | { v: 'a', barycenter: 4, weight: 1 }, 72 | { v: 'b', barycenter: 3, weight: 1 }, 73 | { v: 'c', barycenter: 2, weight: 1 } 74 | ] 75 | cg.setEdge('a', 'c') 76 | cg.setEdge('b', 'c') 77 | var results = resolveConflicts(input, cg) 78 | expect(results).to.have.length(1) 79 | expect(_.indexOf(results[0].vs, 'c')).to.be.gt(_.indexOf(results[0].vs, 'a')) 80 | expect(_.indexOf(results[0].vs, 'c')).to.be.gt(_.indexOf(results[0].vs, 'b')) 81 | expect(results[0].i).equals(0) 82 | expect(results[0].barycenter).equals((4 + 3 + 2) / 3) 83 | expect(results[0].weight).equals(3) 84 | }) 85 | 86 | it('works with multiple constraints for the same target #2', function () { 87 | var input = [ 88 | { v: 'a', barycenter: 4, weight: 1 }, 89 | { v: 'b', barycenter: 3, weight: 1 }, 90 | { v: 'c', barycenter: 2, weight: 1 }, 91 | { v: 'd', barycenter: 1, weight: 1 } 92 | ] 93 | cg.setEdge('a', 'c') 94 | cg.setEdge('a', 'd') 95 | cg.setEdge('b', 'c') 96 | cg.setEdge('c', 'd') 97 | var results = resolveConflicts(input, cg) 98 | expect(results).to.have.length(1) 99 | expect(_.indexOf(results[0].vs, 'c')).to.be.gt(_.indexOf(results[0].vs, 'a')) 100 | expect(_.indexOf(results[0].vs, 'c')).to.be.gt(_.indexOf(results[0].vs, 'b')) 101 | expect(_.indexOf(results[0].vs, 'd')).to.be.gt(_.indexOf(results[0].vs, 'c')) 102 | expect(results[0].i).equals(0) 103 | expect(results[0].barycenter).equals((4 + 3 + 2 + 1) / 4) 104 | expect(results[0].weight).equals(4) 105 | }) 106 | 107 | it('does nothing to a node lacking both a barycenter and a constraint', function () { 108 | var input = [ 109 | { v: 'a' }, 110 | { v: 'b', barycenter: 1, weight: 2 } 111 | ] 112 | expect(_.sortBy(resolveConflicts(input, cg), 'vs')).eqls([ 113 | { vs: ['a'], i: 0 }, 114 | { vs: ['b'], i: 1, barycenter: 1, weight: 2 } 115 | ]) 116 | }) 117 | 118 | it('treats a node w/o a barycenter as always violating constraints #1', function () { 119 | var input = [ 120 | { v: 'a' }, 121 | { v: 'b', barycenter: 1, weight: 2 } 122 | ] 123 | cg.setEdge('a', 'b') 124 | expect(_.sortBy(resolveConflicts(input, cg), 'vs')).eqls([ 125 | { vs: ['a', 'b'], i: 0, barycenter: 1, weight: 2 } 126 | ]) 127 | }) 128 | 129 | it('treats a node w/o a barycenter as always violating constraints #2', function () { 130 | var input = [ 131 | { v: 'a' }, 132 | { v: 'b', barycenter: 1, weight: 2 } 133 | ] 134 | cg.setEdge('b', 'a') 135 | expect(_.sortBy(resolveConflicts(input, cg), 'vs')).eqls([ 136 | { vs: ['b', 'a'], i: 0, barycenter: 1, weight: 2 } 137 | ]) 138 | }) 139 | 140 | it('ignores edges not related to entries', function () { 141 | var input = [ 142 | { v: 'a', barycenter: 2, weight: 3 }, 143 | { v: 'b', barycenter: 1, weight: 2 } 144 | ] 145 | cg.setEdge('c', 'd') 146 | expect(_.sortBy(resolveConflicts(input, cg), 'vs')).eqls([ 147 | { vs: ['a'], i: 0, barycenter: 2, weight: 3 }, 148 | { vs: ['b'], i: 1, barycenter: 1, weight: 2 } 149 | ]) 150 | }) 151 | }) 152 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import {Graph} from 'ciena-graphlib' 3 | 4 | /* 5 | * Adds a dummy node to the graph and return v. 6 | */ 7 | export function addDummyNode (g, type, attrs, name) { 8 | var v 9 | do { 10 | v = _.uniqueId(name) 11 | } while (g.hasNode(v)) 12 | 13 | attrs.dummy = type 14 | g.setNode(v, attrs) 15 | return v 16 | } 17 | 18 | /* 19 | * Returns a new graph with only simple edges. Handles aggregation of data 20 | * associated with multi-edges. 21 | */ 22 | export function simplify (g) { 23 | var simplified = new Graph().setGraph(g.graph()) 24 | _.forEach(g.nodes(), function (v) { simplified.setNode(v, g.node(v)) }) 25 | _.forEach(g.edges(), function (e) { 26 | var simpleLabel = simplified.edge(e.v, e.w) || { weight: 0, minlen: 1 } 27 | var label = g.edge(e) 28 | simplified.setEdge(e.v, e.w, { 29 | weight: simpleLabel.weight + label.weight, 30 | minlen: Math.max(simpleLabel.minlen, label.minlen) 31 | }) 32 | }) 33 | return simplified 34 | } 35 | 36 | export function asNonCompoundGraph (g) { 37 | var simplified = new Graph({ multigraph: g.isMultigraph() }).setGraph(g.graph()) 38 | _.forEach(g.nodes(), function (v) { 39 | if (!g.children(v).length) { 40 | simplified.setNode(v, g.node(v)) 41 | } 42 | }) 43 | _.forEach(g.edges(), function (e) { 44 | simplified.setEdge(e, g.edge(e)) 45 | }) 46 | return simplified 47 | } 48 | 49 | export function successorWeights (g) { 50 | var weightMap = _.map(g.nodes(), function (v) { 51 | var sucs = {} 52 | _.forEach(g.outEdges(v), function (e) { 53 | sucs[e.w] = (sucs[e.w] || 0) + g.edge(e).weight 54 | }) 55 | return sucs 56 | }) 57 | return _.zipObject(g.nodes(), weightMap) 58 | } 59 | 60 | export function predecessorWeights (g) { 61 | var weightMap = _.map(g.nodes(), function (v) { 62 | var preds = {} 63 | _.forEach(g.inEdges(v), function (e) { 64 | preds[e.v] = (preds[e.v] || 0) + g.edge(e).weight 65 | }) 66 | return preds 67 | }) 68 | return _.zipObject(g.nodes(), weightMap) 69 | } 70 | 71 | /* 72 | * Finds where a line starting at point ({x, y}) would intersect a rectangle 73 | * ({x, y, width, height}) if it were pointing at the rectangle's center. 74 | */ 75 | export function intersectRect (rect, point) { 76 | var x = rect.x 77 | var y = rect.y 78 | 79 | // Rectangle intersection algorithm from: 80 | // http://math.stackexchange.com/questions/108113/find-edge-between-two-boxes 81 | var dx = point.x - x 82 | var dy = point.y - y 83 | var w = rect.width / 2 84 | var h = rect.height / 2 85 | 86 | if (!dx && !dy) { 87 | throw new Error('Not possible to find intersection inside of the rectangle') 88 | } 89 | 90 | var sx, sy 91 | if (Math.abs(dy) * w > Math.abs(dx) * h) { 92 | // Intersection is top or bottom of rect. 93 | if (dy < 0) { 94 | h = -h 95 | } 96 | sx = h * dx / dy 97 | sy = h 98 | } else { 99 | // Intersection is left or right of rect. 100 | if (dx < 0) { 101 | w = -w 102 | } 103 | sx = w 104 | sy = w * dy / dx 105 | } 106 | 107 | return { x: x + sx, y: y + sy } 108 | } 109 | 110 | /* 111 | * Given a DAG with each node assigned "rank" and "order" properties, this 112 | * function will produce a matrix with the ids of each node. 113 | */ 114 | export function buildLayerMatrix (g) { 115 | var layering = _.map(_.range(maxRank(g) + 1), function () { return [] }) 116 | _.forEach(g.nodes(), function (v) { 117 | var node = g.node(v) 118 | var rank = node.rank 119 | if (!_.isUndefined(rank)) { 120 | layering[rank][node.order] = v 121 | } 122 | }) 123 | return layering 124 | } 125 | 126 | /* 127 | * Adjusts the ranks for all nodes in the graph such that all nodes v have 128 | * rank(v) >= 0 and at least one node w has rank(w) = 0. 129 | */ 130 | export function normalizeRanks (g) { 131 | var min = _.min(_.map(g.nodes(), function (v) { return g.node(v).rank })) 132 | _.forEach(g.nodes(), function (v) { 133 | var node = g.node(v) 134 | if (_.has(node, 'rank')) { 135 | node.rank -= min 136 | } 137 | }) 138 | } 139 | 140 | export function removeEmptyRanks (g) { 141 | // Ranks may not start at 0, so we need to offset them 142 | var offset = _.min(_.map(g.nodes(), function (v) { return g.node(v).rank })) 143 | 144 | var layers = [] 145 | _.forEach(g.nodes(), function (v) { 146 | var rank = g.node(v).rank - offset 147 | if (!layers[rank]) { 148 | layers[rank] = [] 149 | } 150 | layers[rank].push(v) 151 | }) 152 | 153 | var delta = 0 154 | var nodeRankFactor = g.graph().nodeRankFactor 155 | _.forEach(layers, function (vs, i) { 156 | if (_.isUndefined(vs) && i % nodeRankFactor !== 0) { 157 | --delta 158 | } else if (delta) { 159 | _.forEach(vs, function (v) { g.node(v).rank += delta }) 160 | } 161 | }) 162 | } 163 | 164 | export function addBorderNode (g, prefix, rank, order) { 165 | var node = { 166 | width: 0, 167 | height: 0 168 | } 169 | if (arguments.length >= 4) { 170 | node.rank = rank 171 | node.order = order 172 | } 173 | return addDummyNode(g, 'border', node, prefix) 174 | } 175 | 176 | export function maxRank (g) { 177 | return _.max(_.map(g.nodes(), function (v) { 178 | var rank = g.node(v).rank 179 | if (!_.isUndefined(rank)) { 180 | return rank 181 | } 182 | })) 183 | } 184 | 185 | /* 186 | * Partition a collection into two groups: `lhs` and `rhs`. If the supplied 187 | * function returns true for an entry it goes into `lhs`. Otherwise it goes 188 | * into `rhs. 189 | */ 190 | export function partition (collection, fn) { 191 | var result = { lhs: [], rhs: [] } 192 | _.forEach(collection, function (value) { 193 | if (fn(value)) { 194 | result.lhs.push(value) 195 | } else { 196 | result.rhs.push(value) 197 | } 198 | }) 199 | return result 200 | } 201 | 202 | /* 203 | * Returns a new function that wraps `fn` with a timer. The wrapper logs the 204 | * time it takes to execute the function. 205 | */ 206 | export function time (name, fn) { 207 | var start = _.now() 208 | try { 209 | return fn() 210 | } finally { 211 | console.log(name + ' time: ' + (_.now() - start) + 'ms') 212 | } 213 | } 214 | 215 | export function notime (name, fn) { 216 | return fn() 217 | } 218 | 219 | export default { 220 | addBorderNode, 221 | addDummyNode, 222 | asNonCompoundGraph, 223 | buildLayerMatrix, 224 | intersectRect, 225 | maxRank, 226 | partition, 227 | predecessorWeights, 228 | normalizeRanks, 229 | notime, 230 | removeEmptyRanks, 231 | simplify, 232 | successorWeights, 233 | time 234 | } 235 | -------------------------------------------------------------------------------- /tests/normalize-test.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect 2 | const Graph = require('ciena-graphlib').Graph 3 | const _ = require('lodash') 4 | 5 | const normalize = require('../lib/normalize') 6 | 7 | describe('normalize', function () { 8 | var g 9 | 10 | beforeEach(function () { 11 | g = new Graph({ multigraph: true, compound: true }).setGraph({}) 12 | }) 13 | 14 | describe('run', function () { 15 | it('does not change a short edge', function () { 16 | g.setNode('a', { rank: 0 }) 17 | g.setNode('b', { rank: 1 }) 18 | g.setEdge('a', 'b', {}) 19 | 20 | normalize.run(g) 21 | 22 | expect(_.map(g.edges(), incidentNodes)).to.eql([{ v: 'a', w: 'b' }]) 23 | expect(g.node('a').rank).to.equal(0) 24 | expect(g.node('b').rank).to.equal(1) 25 | }) 26 | 27 | it('splits a two layer edge into two segments', function () { 28 | g.setNode('a', { rank: 0 }) 29 | g.setNode('b', { rank: 2 }) 30 | g.setEdge('a', 'b', {}) 31 | 32 | normalize.run(g) 33 | 34 | expect(g.successors('a')).to.have.length(1) 35 | var successor = g.successors('a')[0] 36 | expect(g.node(successor).dummy).to.equal('edge') 37 | expect(g.node(successor).rank).to.equal(1) 38 | expect(g.successors(successor)).to.eql(['b']) 39 | expect(g.node('a').rank).to.equal(0) 40 | expect(g.node('b').rank).to.equal(2) 41 | 42 | expect(g.graph().dummyChains).to.have.length(1) 43 | expect(g.graph().dummyChains[0]).to.equal(successor) 44 | }) 45 | 46 | it('assigns width = 0, height = 0 to dummy nodes by default', function () { 47 | g.setNode('a', { rank: 0 }) 48 | g.setNode('b', { rank: 2 }) 49 | g.setEdge('a', 'b', { width: 10, height: 10 }) 50 | 51 | normalize.run(g) 52 | 53 | expect(g.successors('a')).to.have.length(1) 54 | var successor = g.successors('a')[0] 55 | expect(g.node(successor).width).to.equal(0) 56 | expect(g.node(successor).height).to.equal(0) 57 | }) 58 | 59 | it('assigns width and height from the edge for the node on labelRank', function () { 60 | g.setNode('a', { rank: 0 }) 61 | g.setNode('b', { rank: 4 }) 62 | g.setEdge('a', 'b', { width: 20, height: 10, labelRank: 2 }) 63 | 64 | normalize.run(g) 65 | 66 | var labelV = g.successors(g.successors('a')[0])[0] 67 | var labelNode = g.node(labelV) 68 | expect(labelNode.width).to.equal(20) 69 | expect(labelNode.height).to.equal(10) 70 | }) 71 | 72 | it('preserves the weight for the edge', function () { 73 | g.setNode('a', { rank: 0 }) 74 | g.setNode('b', { rank: 2 }) 75 | g.setEdge('a', 'b', { weight: 2 }) 76 | 77 | normalize.run(g) 78 | 79 | expect(g.successors('a')).to.have.length(1) 80 | expect(g.edge('a', g.successors('a')[0]).weight).to.equal(2) 81 | }) 82 | }) 83 | 84 | describe('undo', function () { 85 | it('reverses the run operation', function () { 86 | g.setNode('a', { rank: 0 }) 87 | g.setNode('b', { rank: 2 }) 88 | g.setEdge('a', 'b', {}) 89 | 90 | normalize.run(g) 91 | normalize.undo(g) 92 | 93 | expect(_.map(g.edges(), incidentNodes)).to.eql([{ v: 'a', w: 'b' }]) 94 | expect(g.node('a').rank).to.equal(0) 95 | expect(g.node('b').rank).to.equal(2) 96 | }) 97 | 98 | it('restores previous edge labels', function () { 99 | g.setNode('a', { rank: 0 }) 100 | g.setNode('b', { rank: 2 }) 101 | g.setEdge('a', 'b', { foo: 'bar' }) 102 | 103 | normalize.run(g) 104 | normalize.undo(g) 105 | 106 | expect(g.edge('a', 'b').foo).equals('bar') 107 | }) 108 | 109 | it("collects assigned coordinates into the 'points' attribute", function () { 110 | g.setNode('a', { rank: 0 }) 111 | g.setNode('b', { rank: 2 }) 112 | g.setEdge('a', 'b', {}) 113 | 114 | normalize.run(g) 115 | 116 | var dummyLabel = g.node(g.neighbors('a')[0]) 117 | dummyLabel.x = 5 118 | dummyLabel.y = 10 119 | 120 | normalize.undo(g) 121 | 122 | expect(g.edge('a', 'b').points).eqls([{ x: 5, y: 10 }]) 123 | }) 124 | 125 | it("merges assigned coordinates into the 'points' attribute", function () { 126 | g.setNode('a', { rank: 0 }) 127 | g.setNode('b', { rank: 4 }) 128 | g.setEdge('a', 'b', {}) 129 | 130 | normalize.run(g) 131 | 132 | var aSucLabel = g.node(g.neighbors('a')[0]) 133 | aSucLabel.x = 5 134 | aSucLabel.y = 10 135 | 136 | var midLabel = g.node(g.successors(g.successors('a')[0])[0]) 137 | midLabel.x = 20 138 | midLabel.y = 25 139 | 140 | var bPredLabel = g.node(g.neighbors('b')[0]) 141 | bPredLabel.x = 100 142 | bPredLabel.y = 200 143 | 144 | normalize.undo(g) 145 | 146 | expect(g.edge('a', 'b').points) 147 | .eqls([{ x: 5, y: 10 }, { x: 20, y: 25 }, { x: 100, y: 200 }]) 148 | }) 149 | 150 | it('sets coords and dims for the label, if the edge has one', function () { 151 | g.setNode('a', { rank: 0 }) 152 | g.setNode('b', { rank: 2 }) 153 | g.setEdge('a', 'b', { width: 10, height: 20, labelRank: 1 }) 154 | 155 | normalize.run(g) 156 | 157 | var labelNode = g.node(g.successors('a')[0]) 158 | labelNode.x = 50 159 | labelNode.y = 60 160 | labelNode.width = 20 161 | labelNode.height = 10 162 | 163 | normalize.undo(g) 164 | 165 | expect(_.pick(g.edge('a', 'b'), ['x', 'y', 'width', 'height'])).eqls({ 166 | x: 50, y: 60, width: 20, height: 10 167 | }) 168 | }) 169 | 170 | it('sets coords and dims for the label, if the long edge has one', function () { 171 | g.setNode('a', { rank: 0 }) 172 | g.setNode('b', { rank: 4 }) 173 | g.setEdge('a', 'b', { width: 10, height: 20, labelRank: 2 }) 174 | 175 | normalize.run(g) 176 | 177 | var labelNode = g.node(g.successors(g.successors('a')[0])[0]) 178 | labelNode.x = 50 179 | labelNode.y = 60 180 | labelNode.width = 20 181 | labelNode.height = 10 182 | 183 | normalize.undo(g) 184 | 185 | expect(_.pick(g.edge('a', 'b'), ['x', 'y', 'width', 'height'])).eqls({ 186 | x: 50, y: 60, width: 20, height: 10 187 | }) 188 | }) 189 | 190 | it('restores multi-edges', function () { 191 | g.setNode('a', { rank: 0 }) 192 | g.setNode('b', { rank: 2 }) 193 | g.setEdge('a', 'b', {}, 'bar') 194 | g.setEdge('a', 'b', {}, 'foo') 195 | 196 | normalize.run(g) 197 | 198 | var outEdges = _.sortBy(g.outEdges('a'), 'name') 199 | expect(outEdges).to.have.length(2) 200 | 201 | var barDummy = g.node(outEdges[0].w) 202 | barDummy.x = 5 203 | barDummy.y = 10 204 | 205 | var fooDummy = g.node(outEdges[1].w) 206 | fooDummy.x = 15 207 | fooDummy.y = 20 208 | 209 | normalize.undo(g) 210 | 211 | expect(g.hasEdge('a', 'b')).to.be.false 212 | expect(g.edge('a', 'b', 'bar').points).eqls([{ x: 5, y: 10 }]) 213 | expect(g.edge('a', 'b', 'foo').points).eqls([{ x: 15, y: 20 }]) 214 | }) 215 | }) 216 | }) 217 | 218 | function incidentNodes (edge) { 219 | return { v: edge.v, w: edge.w } 220 | } 221 | -------------------------------------------------------------------------------- /src/rank/network-simplex.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import {alg} from 'ciena-graphlib' 3 | const {preorder, postorder} = alg 4 | 5 | import feasibleTree from './feasible-tree' 6 | import {longestPath, slack} from './util' 7 | import {simplify} from '../util' 8 | 9 | // Expose some internals for testing purposes 10 | networkSimplex.initLowLimValues = initLowLimValues 11 | networkSimplex.initCutValues = initCutValues 12 | networkSimplex.calcCutValue = calcCutValue 13 | networkSimplex.leaveEdge = leaveEdge 14 | networkSimplex.enterEdge = enterEdge 15 | networkSimplex.exchangeEdges = exchangeEdges 16 | 17 | /* 18 | * Initializes cut values for all edges in the tree. 19 | */ 20 | function initCutValues (t, g) { 21 | var vs = postorder(t, t.nodes()) 22 | vs = vs.slice(0, vs.length - 1) 23 | _.forEach(vs, function (v) { 24 | assignCutValue(t, g, v) 25 | }) 26 | } 27 | 28 | function assignCutValue (t, g, child) { 29 | var childLab = t.node(child) 30 | var parent = childLab.parent 31 | t.edge(child, parent).cutvalue = calcCutValue(t, g, child) 32 | } 33 | 34 | /* 35 | * Given the tight tree, its graph, and a child in the graph calculate and 36 | * return the cut value for the edge between the child and its parent. 37 | */ 38 | function calcCutValue (t, g, child) { 39 | var childLab = t.node(child) 40 | var parent = childLab.parent 41 | // True if the child is on the tail end of the edge in the directed graph 42 | var childIsTail = true 43 | // The graph's view of the tree edge we're inspecting 44 | var graphEdge = g.edge(child, parent) 45 | // The accumulated cut value for the edge between this node and its parent 46 | var cutValue = 0 47 | 48 | if (!graphEdge) { 49 | childIsTail = false 50 | graphEdge = g.edge(parent, child) 51 | } 52 | 53 | cutValue = graphEdge.weight 54 | 55 | _.forEach(g.nodeEdges(child), function (e) { 56 | var isOutEdge = e.v === child 57 | var other = isOutEdge ? e.w : e.v 58 | 59 | if (other !== parent) { 60 | var pointsToHead = isOutEdge === childIsTail 61 | var otherWeight = g.edge(e).weight 62 | 63 | cutValue += pointsToHead ? otherWeight : -otherWeight 64 | if (isTreeEdge(t, child, other)) { 65 | var otherCutValue = t.edge(child, other).cutvalue 66 | cutValue += pointsToHead ? -otherCutValue : otherCutValue 67 | } 68 | } 69 | }) 70 | 71 | return cutValue 72 | } 73 | 74 | function initLowLimValues (tree, root) { 75 | if (arguments.length < 2) { 76 | root = tree.nodes()[0] 77 | } 78 | dfsAssignLowLim(tree, {}, 1, root) 79 | } 80 | 81 | function dfsAssignLowLim (tree, visited, nextLim, v, parent) { 82 | var low = nextLim 83 | var label = tree.node(v) 84 | 85 | visited[v] = true 86 | _.forEach(tree.neighbors(v), function (w) { 87 | if (!_.has(visited, w)) { 88 | nextLim = dfsAssignLowLim(tree, visited, nextLim, w, v) 89 | } 90 | }) 91 | 92 | label.low = low 93 | label.lim = nextLim++ 94 | if (parent) { 95 | label.parent = parent 96 | } else { 97 | // TODO should be able to remove this when we incrementally update low lim 98 | delete label.parent 99 | } 100 | 101 | return nextLim 102 | } 103 | 104 | function leaveEdge (tree) { 105 | return _.find(tree.edges(), function (e) { 106 | return tree.edge(e).cutvalue < 0 107 | }) 108 | } 109 | 110 | function enterEdge (t, g, edge) { 111 | var v = edge.v 112 | var w = edge.w 113 | 114 | // For the rest of this function we assume that v is the tail and w is the 115 | // head, so if we don't have this edge in the graph we should flip it to 116 | // match the correct orientation. 117 | if (!g.hasEdge(v, w)) { 118 | v = edge.w 119 | w = edge.v 120 | } 121 | 122 | var vLabel = t.node(v) 123 | var wLabel = t.node(w) 124 | var tailLabel = vLabel 125 | var flip = false 126 | 127 | // If the root is in the tail of the edge then we need to flip the logic that 128 | // checks for the head and tail nodes in the candidates function below. 129 | if (vLabel.lim > wLabel.lim) { 130 | tailLabel = wLabel 131 | flip = true 132 | } 133 | 134 | var candidates = _.filter(g.edges(), function (edge) { 135 | return flip === isDescendant(t, t.node(edge.v), tailLabel) && 136 | flip !== isDescendant(t, t.node(edge.w), tailLabel) 137 | }) 138 | 139 | return _.minBy(candidates, function (edge) { return slack(g, edge) }) 140 | } 141 | 142 | function exchangeEdges (t, g, e, f) { 143 | var v = e.v 144 | var w = e.w 145 | t.removeEdge(v, w) 146 | t.setEdge(f.v, f.w, {}) 147 | initLowLimValues(t) 148 | initCutValues(t, g) 149 | updateRanks(t, g) 150 | } 151 | 152 | function updateRanks (t, g) { 153 | var root = _.find(t.nodes(), function (v) { return !g.node(v).parent }) 154 | var vs = preorder(t, root) 155 | vs = vs.slice(1) 156 | _.forEach(vs, function (v) { 157 | var parent = t.node(v).parent 158 | var edge = g.edge(v, parent) 159 | var flipped = false 160 | 161 | if (!edge) { 162 | edge = g.edge(parent, v) 163 | flipped = true 164 | } 165 | 166 | g.node(v).rank = g.node(parent).rank + (flipped ? edge.minlen : -edge.minlen) 167 | }) 168 | } 169 | 170 | /* 171 | * Returns true if the edge is in the tree. 172 | */ 173 | function isTreeEdge (tree, u, v) { 174 | return tree.hasEdge(u, v) 175 | } 176 | 177 | /* 178 | * Returns true if the specified node is descendant of the root node per the 179 | * assigned low and lim attributes in the tree. 180 | */ 181 | function isDescendant (tree, vLabel, rootLabel) { 182 | return rootLabel.low <= vLabel.lim && vLabel.lim <= rootLabel.lim 183 | } 184 | 185 | /* 186 | * The network simplex algorithm assigns ranks to each node in the input graph 187 | * and iteratively improves the ranking to reduce the length of edges. 188 | * 189 | * Preconditions: 190 | * 191 | * 1. The input graph must be a DAG. 192 | * 2. All nodes in the graph must have an object value. 193 | * 3. All edges in the graph must have "minlen" and "weight" attributes. 194 | * 195 | * Postconditions: 196 | * 197 | * 1. All nodes in the graph will have an assigned "rank" attribute that has 198 | * been optimized by the network simplex algorithm. Ranks start at 0. 199 | * 200 | * 201 | * A rough sketch of the algorithm is as follows: 202 | * 203 | * 1. Assign initial ranks to each node. We use the longest path algorithm, 204 | * which assigns ranks to the lowest position possible. In general this 205 | * leads to very wide bottom ranks and unnecessarily long edges. 206 | * 2. Construct a feasible tight tree. A tight tree is one such that all 207 | * edges in the tree have no slack (difference between length of edge 208 | * and minlen for the edge). This by itself greatly improves the assigned 209 | * rankings by shorting edges. 210 | * 3. Iteratively find edges that have negative cut values. Generally a 211 | * negative cut value indicates that the edge could be removed and a new 212 | * tree edge could be added to produce a more compact graph. 213 | * 214 | * Much of the algorithms here are derived from Gansner, et al., "A Technique 215 | * for Drawing Directed Graphs." The structure of the file roughly follows the 216 | * structure of the overall algorithm. 217 | */ 218 | export default function networkSimplex (g) { 219 | g = simplify(g) 220 | longestPath(g) 221 | var t = feasibleTree(g) 222 | initLowLimValues(t) 223 | initCutValues(t, g) 224 | 225 | var e, f 226 | while ((e = leaveEdge(t))) { 227 | f = enterEdge(t, g, e) 228 | exchangeEdges(t, g, e, f) 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /tests/nesting-graph-test.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect 2 | const graphlib = require('ciena-graphlib') 3 | const alg = graphlib.alg 4 | const Graph = graphlib.Graph 5 | const components = alg.components 6 | 7 | const nestingGraph = require('../lib/nesting-graph') 8 | 9 | describe('rank/nestingGraph', function () { 10 | var g 11 | 12 | beforeEach(function () { 13 | g = new Graph({ compound: true }) 14 | .setGraph({}) 15 | .setDefaultNodeLabel(function () { return {} }) 16 | }) 17 | 18 | describe('run', function () { 19 | it('connects a disconnected graph', function () { 20 | g.setNode('a') 21 | g.setNode('b') 22 | expect(components(g)).to.have.length(2) 23 | nestingGraph.run(g) 24 | expect(components(g)).to.have.length(1) 25 | expect(g.hasNode('a')) 26 | expect(g.hasNode('b')) 27 | }) 28 | 29 | it('adds border nodes to the top and bottom of a subgraph', function () { 30 | g.setParent('a', 'sg1') 31 | nestingGraph.run(g) 32 | 33 | var borderTop = g.node('sg1').borderTop 34 | var borderBottom = g.node('sg1').borderBottom 35 | expect(borderTop).to.exist 36 | expect(borderBottom).to.exist 37 | expect(g.parent(borderTop)).to.equal('sg1') 38 | expect(g.parent(borderBottom)).to.equal('sg1') 39 | expect(g.outEdges(borderTop, 'a')).to.have.length(1) 40 | expect(g.edge(g.outEdges(borderTop, 'a')[0]).minlen).equals(1) 41 | expect(g.outEdges('a', borderBottom)).to.have.length(1) 42 | expect(g.edge(g.outEdges('a', borderBottom)[0]).minlen).equals(1) 43 | expect(g.node(borderTop)).eqls({ width: 0, height: 0, dummy: 'border' }) 44 | expect(g.node(borderBottom)).eqls({ width: 0, height: 0, dummy: 'border' }) 45 | }) 46 | 47 | it('adds edges between borders of nested subgraphs', function () { 48 | g.setParent('sg2', 'sg1') 49 | g.setParent('a', 'sg2') 50 | nestingGraph.run(g) 51 | 52 | var sg1Top = g.node('sg1').borderTop 53 | var sg1Bottom = g.node('sg1').borderBottom 54 | var sg2Top = g.node('sg2').borderTop 55 | var sg2Bottom = g.node('sg2').borderBottom 56 | expect(sg1Top).to.exist 57 | expect(sg1Bottom).to.exist 58 | expect(sg2Top).to.exist 59 | expect(sg2Bottom).to.exist 60 | expect(g.outEdges(sg1Top, sg2Top)).to.have.length(1) 61 | expect(g.edge(g.outEdges(sg1Top, sg2Top)[0]).minlen).equals(1) 62 | expect(g.outEdges(sg2Bottom, sg1Bottom)).to.have.length(1) 63 | expect(g.edge(g.outEdges(sg2Bottom, sg1Bottom)[0]).minlen).equals(1) 64 | }) 65 | 66 | it('adds sufficient weight to border to node edges', function () { 67 | // We want to keep subgraphs tight, so we should ensure that the weight for 68 | // the edge between the top (and bottom) border nodes and nodes in the 69 | // subgraph have weights exceeding anything in the graph. 70 | g.setParent('x', 'sg') 71 | g.setEdge('a', 'x', { weight: 100 }) 72 | g.setEdge('x', 'b', { weight: 200 }) 73 | nestingGraph.run(g) 74 | 75 | var top = g.node('sg').borderTop 76 | var bot = g.node('sg').borderBottom 77 | expect(g.edge(top, 'x').weight).to.be.gt(300) 78 | expect(g.edge('x', bot).weight).to.be.gt(300) 79 | }) 80 | 81 | it('adds an edge from the root to the tops of top-level subgraphs', function () { 82 | g.setParent('a', 'sg1') 83 | nestingGraph.run(g) 84 | 85 | var root = g.graph().nestingRoot 86 | var borderTop = g.node('sg1').borderTop 87 | expect(root).to.exist 88 | expect(borderTop).to.exist 89 | expect(g.outEdges(root, borderTop)).to.have.length(1) 90 | expect(g.hasEdge(g.outEdges(root, borderTop)[0])).to.be.true 91 | }) 92 | 93 | it('adds an edge from root to each node with the correct minlen #1', function () { 94 | g.setNode('a') 95 | nestingGraph.run(g) 96 | 97 | var root = g.graph().nestingRoot 98 | expect(root).to.exist 99 | expect(g.outEdges(root, 'a')).to.have.length(1) 100 | expect(g.edge(g.outEdges(root, 'a')[0])).eqls({ weight: 0, minlen: 1 }) 101 | }) 102 | 103 | it('adds an edge from root to each node with the correct minlen #2', function () { 104 | g.setParent('a', 'sg1') 105 | nestingGraph.run(g) 106 | 107 | var root = g.graph().nestingRoot 108 | expect(root).to.exist 109 | expect(g.outEdges(root, 'a')).to.have.length(1) 110 | expect(g.edge(g.outEdges(root, 'a')[0])).eqls({ weight: 0, minlen: 3 }) 111 | }) 112 | 113 | it('adds an edge from root to each node with the correct minlen #3', function () { 114 | g.setParent('sg2', 'sg1') 115 | g.setParent('a', 'sg2') 116 | nestingGraph.run(g) 117 | 118 | var root = g.graph().nestingRoot 119 | expect(root).to.exist 120 | expect(g.outEdges(root, 'a')).to.have.length(1) 121 | expect(g.edge(g.outEdges(root, 'a')[0])).eqls({ weight: 0, minlen: 5 }) 122 | }) 123 | 124 | it('does not add an edge from the root to itself', function () { 125 | g.setNode('a') 126 | nestingGraph.run(g) 127 | 128 | var root = g.graph().nestingRoot 129 | expect(g.outEdges(root, root)).eqls([]) 130 | }) 131 | 132 | it('expands inter-node edges to separate SG border and nodes #1', function () { 133 | g.setEdge('a', 'b', { minlen: 1 }) 134 | nestingGraph.run(g) 135 | expect(g.edge('a', 'b').minlen).equals(1) 136 | }) 137 | 138 | it('expands inter-node edges to separate SG border and nodes #2', function () { 139 | g.setParent('a', 'sg1') 140 | g.setEdge('a', 'b', { minlen: 1 }) 141 | nestingGraph.run(g) 142 | expect(g.edge('a', 'b').minlen).equals(3) 143 | }) 144 | 145 | it('expands inter-node edges to separate SG border and nodes #3', function () { 146 | g.setParent('sg2', 'sg1') 147 | g.setParent('a', 'sg2') 148 | g.setEdge('a', 'b', { minlen: 1 }) 149 | nestingGraph.run(g) 150 | expect(g.edge('a', 'b').minlen).equals(5) 151 | }) 152 | 153 | it('sets minlen correctly for nested SG boder to children', function () { 154 | g.setParent('a', 'sg1') 155 | g.setParent('sg2', 'sg1') 156 | g.setParent('b', 'sg2') 157 | nestingGraph.run(g) 158 | 159 | // We expect the following layering: 160 | // 161 | // 0: root 162 | // 1: empty (close sg2) 163 | // 2: empty (close sg1) 164 | // 3: open sg1 165 | // 4: open sg2 166 | // 5: a, b 167 | // 6: close sg2 168 | // 7: close sg1 169 | 170 | var root = g.graph().nestingRoot 171 | var sg1Top = g.node('sg1').borderTop 172 | var sg1Bot = g.node('sg1').borderBottom 173 | var sg2Top = g.node('sg2').borderTop 174 | var sg2Bot = g.node('sg2').borderBottom 175 | 176 | expect(g.edge(root, sg1Top).minlen).equals(3) 177 | expect(g.edge(sg1Top, sg2Top).minlen).equals(1) 178 | expect(g.edge(sg1Top, 'a').minlen).equals(2) 179 | expect(g.edge('a', sg1Bot).minlen).equals(2) 180 | expect(g.edge(sg2Top, 'b').minlen).equals(1) 181 | expect(g.edge('b', sg2Bot).minlen).equals(1) 182 | expect(g.edge(sg2Bot, sg1Bot).minlen).equals(1) 183 | }) 184 | }) 185 | 186 | describe('cleanup', function () { 187 | it('removes nesting graph edges', function () { 188 | g.setParent('a', 'sg1') 189 | g.setEdge('a', 'b', { minlen: 1 }) 190 | nestingGraph.run(g) 191 | nestingGraph.cleanup(g) 192 | expect(g.successors('a')).eqls(['b']) 193 | }) 194 | 195 | it('removes the root node', function () { 196 | g.setParent('a', 'sg1') 197 | nestingGraph.run(g) 198 | nestingGraph.cleanup(g) 199 | expect(g.nodeCount()).to.equal(4) // sg1 + sg1Top + sg1Bottom + "a" 200 | }) 201 | }) 202 | }) 203 | -------------------------------------------------------------------------------- /tests/util-test.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect 2 | const Graph = require('ciena-graphlib').Graph 3 | const _ = require('lodash') 4 | 5 | const utils = require('../lib/util') 6 | 7 | const asNonCompoundGraph = utils.asNonCompoundGraph 8 | const buildLayerMatrix = utils.buildLayerMatrix 9 | const intersectRect = utils.intersectRect 10 | const normalizeRanks = utils.normalizeRanks 11 | const predecessorWeights = utils.predecessorWeights 12 | const removeEmptyRanks = utils.removeEmptyRanks 13 | const simplify = utils.simplify 14 | const successorWeights = utils.successorWeights 15 | const time = utils.time 16 | 17 | describe('util', function () { 18 | describe('simplify', function () { 19 | var g 20 | 21 | beforeEach(function () { 22 | g = new Graph({ multigraph: true }) 23 | }) 24 | 25 | it('copies without change a graph with no multi-edges', function () { 26 | g.setEdge('a', 'b', { weight: 1, minlen: 1 }) 27 | var g2 = simplify(g) 28 | expect(g2.edge('a', 'b')).eql({ weight: 1, minlen: 1 }) 29 | expect(g2.edgeCount()).equals(1) 30 | }) 31 | 32 | it('collapses multi-edges', function () { 33 | g.setEdge('a', 'b', { weight: 1, minlen: 1 }) 34 | g.setEdge('a', 'b', { weight: 2, minlen: 2 }, 'multi') 35 | var g2 = simplify(g) 36 | expect(g2.isMultigraph()).to.be.false 37 | expect(g2.edge('a', 'b')).eql({ weight: 3, minlen: 2 }) 38 | expect(g2.edgeCount()).equals(1) 39 | }) 40 | 41 | it('copies the graph object', function () { 42 | g.setGraph({ foo: 'bar' }) 43 | var g2 = simplify(g) 44 | expect(g2.graph()).eqls({ foo: 'bar' }) 45 | }) 46 | }) 47 | 48 | describe('asNonCompoundGraph', function () { 49 | var g 50 | 51 | beforeEach(function () { 52 | g = new Graph({ compound: true, multigraph: true }) 53 | }) 54 | 55 | it('copies all nodes', function () { 56 | g.setNode('a', { foo: 'bar' }) 57 | g.setNode('b') 58 | var g2 = asNonCompoundGraph(g) 59 | expect(g2.node('a')).to.eql({ foo: 'bar' }) 60 | expect(g2.hasNode('b')).to.be.true 61 | }) 62 | 63 | it('copies all edges', function () { 64 | g.setEdge('a', 'b', { foo: 'bar' }) 65 | g.setEdge('a', 'b', { foo: 'baz' }, 'multi') 66 | var g2 = asNonCompoundGraph(g) 67 | expect(g2.edge('a', 'b')).eqls({ foo: 'bar' }) 68 | expect(g2.edge('a', 'b', 'multi')).eqls({ foo: 'baz' }) 69 | }) 70 | 71 | it('does not copy compound nodes', function () { 72 | g.setParent('a', 'sg1') 73 | var g2 = asNonCompoundGraph(g) 74 | expect(g2.parent(g)).to.be.undefined 75 | expect(g2.isCompound()).to.be.false 76 | }) 77 | 78 | it('copies the graph object', function () { 79 | g.setGraph({ foo: 'bar' }) 80 | var g2 = asNonCompoundGraph(g) 81 | expect(g2.graph()).eqls({ foo: 'bar' }) 82 | }) 83 | }) 84 | 85 | describe('successorWeights', function () { 86 | it('maps a node to its successors with associated weights', function () { 87 | var g = new Graph({ multigraph: true }) 88 | g.setEdge('a', 'b', { weight: 2 }) 89 | g.setEdge('b', 'c', { weight: 1 }) 90 | g.setEdge('b', 'c', { weight: 2 }, 'multi') 91 | g.setEdge('b', 'd', { weight: 1 }, 'multi') 92 | expect(successorWeights(g).a).to.eql({ b: 2 }) 93 | expect(successorWeights(g).b).to.eql({ c: 3, d: 1 }) 94 | expect(successorWeights(g).c).to.eql({}) 95 | expect(successorWeights(g).d).to.eql({}) 96 | }) 97 | }) 98 | 99 | describe('predecessorWeights', function () { 100 | it('maps a node to its predecessors with associated weights', function () { 101 | var g = new Graph({ multigraph: true }) 102 | g.setEdge('a', 'b', { weight: 2 }) 103 | g.setEdge('b', 'c', { weight: 1 }) 104 | g.setEdge('b', 'c', { weight: 2 }, 'multi') 105 | g.setEdge('b', 'd', { weight: 1 }, 'multi') 106 | expect(predecessorWeights(g).a).to.eql({}) 107 | expect(predecessorWeights(g).b).to.eql({ a: 2 }) 108 | expect(predecessorWeights(g).c).to.eql({ b: 3 }) 109 | expect(predecessorWeights(g).d).to.eql({ b: 1 }) 110 | }) 111 | }) 112 | 113 | describe('intersectRect', function () { 114 | function expectIntersects (rect, point) { 115 | var cross = intersectRect(rect, point) 116 | if (cross.x !== point.x) { 117 | var m = (cross.y - point.y) / (cross.x - point.x) 118 | expect(cross.y - rect.y).equals(m * (cross.x - rect.x)) 119 | } 120 | } 121 | 122 | function expectTouchesBorder (rect, point) { 123 | var cross = intersectRect(rect, point) 124 | if (Math.abs(rect.x - cross.x) !== rect.width / 2) { 125 | expect(Math.abs(rect.y - cross.y)).equals(rect.height / 2) 126 | } 127 | } 128 | 129 | it("creates a slope that will intersect the rectangle's center", function () { 130 | var rect = { x: 0, y: 0, width: 1, height: 1 } 131 | expectIntersects(rect, { x: 2, y: 6 }) 132 | expectIntersects(rect, { x: 2, y: -6 }) 133 | expectIntersects(rect, { x: 6, y: 2 }) 134 | expectIntersects(rect, { x: -6, y: 2 }) 135 | expectIntersects(rect, { x: 5, y: 0 }) 136 | expectIntersects(rect, { x: 0, y: 5 }) 137 | }) 138 | 139 | it('touches the border of the rectangle', function () { 140 | var rect = { x: 0, y: 0, width: 1, height: 1 } 141 | expectTouchesBorder(rect, { x: 2, y: 6 }) 142 | expectTouchesBorder(rect, { x: 2, y: -6 }) 143 | expectTouchesBorder(rect, { x: 6, y: 2 }) 144 | expectTouchesBorder(rect, { x: -6, y: 2 }) 145 | expectTouchesBorder(rect, { x: 5, y: 0 }) 146 | expectTouchesBorder(rect, { x: 0, y: 5 }) 147 | }) 148 | 149 | it('throws an error if the point is at the center of the rectangle', function () { 150 | var rect = { x: 0, y: 0, width: 1, height: 1 } 151 | expect(function () { intersectRect(rect, { x: 0, y: 0 }) }).to.throw() 152 | }) 153 | }) 154 | 155 | describe('buildLayerMatrix', function () { 156 | it('creates a matrix based on rank and order of nodes in the graph', function () { 157 | var g = new Graph() 158 | g.setNode('a', { rank: 0, order: 0 }) 159 | g.setNode('b', { rank: 0, order: 1 }) 160 | g.setNode('c', { rank: 1, order: 0 }) 161 | g.setNode('d', { rank: 1, order: 1 }) 162 | g.setNode('e', { rank: 2, order: 0 }) 163 | 164 | expect(buildLayerMatrix(g)).to.eql([ 165 | ['a', 'b'], 166 | ['c', 'd'], 167 | ['e'] 168 | ]) 169 | }) 170 | }) 171 | 172 | describe('time', function () { 173 | var consoleLog 174 | 175 | beforeEach(function () { 176 | consoleLog = console.log 177 | }) 178 | 179 | afterEach(function () { 180 | console.log = consoleLog 181 | }) 182 | 183 | it('logs timing information', function () { 184 | var capture = [] 185 | console.log = function () { capture.push(_.toArray(arguments)[0]) } 186 | time('foo', function () {}) 187 | expect(capture.length).to.equal(1) 188 | expect(capture[0]).to.match(/^foo time: .*ms/) 189 | }) 190 | 191 | it('returns the value from the evaluated function', function () { 192 | console.log = function () {} 193 | expect(time('foo', _.constant('bar'))).to.equal('bar') 194 | }) 195 | }) 196 | 197 | describe('normalizeRanks', function () { 198 | it('adjust ranks such that all are >= 0, and at least one is 0', function () { 199 | var g = new Graph() 200 | .setNode('a', { rank: 3 }) 201 | .setNode('b', { rank: 2 }) 202 | .setNode('c', { rank: 4 }) 203 | 204 | normalizeRanks(g) 205 | 206 | expect(g.node('a').rank).to.equal(1) 207 | expect(g.node('b').rank).to.equal(0) 208 | expect(g.node('c').rank).to.equal(2) 209 | }) 210 | 211 | it('works for negative ranks', function () { 212 | var g = new Graph() 213 | .setNode('a', { rank: -3 }) 214 | .setNode('b', { rank: -2 }) 215 | 216 | normalizeRanks(g) 217 | 218 | expect(g.node('a').rank).to.equal(0) 219 | expect(g.node('b').rank).to.equal(1) 220 | }) 221 | 222 | it('does not assign a rank to subgraphs', function () { 223 | var g = new Graph({ compound: true }) 224 | .setNode('a', { rank: 0 }) 225 | .setNode('sg', {}) 226 | .setParent('a', 'sg') 227 | 228 | normalizeRanks(g) 229 | 230 | expect(g.node('sg')).to.not.have.property('rank') 231 | expect(g.node('a').rank).to.equal(0) 232 | }) 233 | }) 234 | 235 | describe('removeEmptyRanks', function () { 236 | it('Removes border ranks without any nodes', function () { 237 | var g = new Graph() 238 | .setGraph({ nodeRankFactor: 4 }) 239 | .setNode('a', { rank: 0 }) 240 | .setNode('b', { rank: 4 }) 241 | removeEmptyRanks(g) 242 | expect(g.node('a').rank).equals(0) 243 | expect(g.node('b').rank).equals(1) 244 | }) 245 | 246 | it('Does not remove non-border ranks', function () { 247 | var g = new Graph() 248 | .setGraph({ nodeRankFactor: 4 }) 249 | .setNode('a', { rank: 0 }) 250 | .setNode('b', { rank: 8 }) 251 | removeEmptyRanks(g) 252 | expect(g.node('a').rank).equals(0) 253 | expect(g.node('b').rank).equals(2) 254 | }) 255 | }) 256 | }) 257 | -------------------------------------------------------------------------------- /tests/layout-test.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect 2 | const Graph = require('ciena-graphlib').Graph 3 | const _ = require('lodash') 4 | 5 | const layout = require('../lib/layout') 6 | 7 | describe('layout', function () { 8 | var g 9 | 10 | beforeEach(function () { 11 | g = new Graph({ multigraph: true, compound: true }) 12 | .setGraph({}) 13 | .setDefaultEdgeLabel(function () { return {} }) 14 | }) 15 | 16 | it('can layout a single node', function () { 17 | g.setNode('a', { width: 50, height: 100 }) 18 | layout(g) 19 | expect(extractCoordinates(g)).to.eql({ 20 | a: { x: 50 / 2, y: 100 / 2 } 21 | }) 22 | expect(g.node('a').x).to.equal(50 / 2) 23 | expect(g.node('a').y).to.equal(100 / 2) 24 | }) 25 | 26 | it('can layout two nodes on the same rank', function () { 27 | g.graph().nodesep = 200 28 | g.setNode('a', { width: 50, height: 100 }) 29 | g.setNode('b', { width: 75, height: 200 }) 30 | layout(g) 31 | expect(extractCoordinates(g)).to.eql({ 32 | a: { x: 50 / 2, y: 200 / 2 }, 33 | b: { x: 50 + 200 + 75 / 2, y: 200 / 2 } 34 | }) 35 | }) 36 | 37 | it('can layout two nodes connected by an edge', function () { 38 | g.graph().ranksep = 300 39 | g.setNode('a', { width: 50, height: 100 }) 40 | g.setNode('b', { width: 75, height: 200 }) 41 | g.setEdge('a', 'b') 42 | layout(g) 43 | expect(extractCoordinates(g)).to.eql({ 44 | a: { x: 75 / 2, y: 100 / 2 }, 45 | b: { x: 75 / 2, y: 100 + 300 + 200 / 2 } 46 | }) 47 | 48 | // We should not get x, y coordinates if the edge has no label 49 | expect(g.edge('a', 'b')).to.not.have.property('x') 50 | expect(g.edge('a', 'b')).to.not.have.property('y') 51 | }) 52 | 53 | it('can layout an edge with a label', function () { 54 | g.graph().ranksep = 300 55 | g.setNode('a', { width: 50, height: 100 }) 56 | g.setNode('b', { width: 75, height: 200 }) 57 | g.setEdge('a', 'b', { width: 60, height: 70, labelpos: 'c' }) 58 | layout(g) 59 | expect(extractCoordinates(g)).to.eql({ 60 | a: { x: 75 / 2, y: 100 / 2 }, 61 | b: { x: 75 / 2, y: 100 + 150 + 70 + 150 + 200 / 2 } 62 | }) 63 | expect(_.pick(g.edge('a', 'b'), ['x', 'y'])) 64 | .eqls({ x: 75 / 2, y: 100 + 150 + 70 / 2 }) 65 | }) 66 | 67 | describe('can layout an edge with a long label, with rankdir =', function () { 68 | _.forEach(['TB', 'BT', 'LR', 'RL'], function (rankdir) { 69 | it(rankdir, function () { 70 | g.graph().nodesep = g.graph().edgesep = 10 71 | g.graph().rankdir = rankdir 72 | _.forEach(['a', 'b', 'c', 'd'], function (v) { 73 | g.setNode(v, { width: 10, height: 10 }) 74 | }) 75 | g.setEdge('a', 'c', { width: 2000, height: 10, labelpos: 'c' }) 76 | g.setEdge('b', 'd', { width: 1, height: 1 }) 77 | layout(g) 78 | 79 | var p1, p2 80 | if (rankdir === 'TB' || rankdir === 'BT') { 81 | p1 = g.edge('a', 'c') 82 | p2 = g.edge('b', 'd') 83 | } else { 84 | p1 = g.node('a') 85 | p2 = g.node('c') 86 | } 87 | 88 | expect(Math.abs(p1.x - p2.x)).gt(1000) 89 | }) 90 | }) 91 | }) 92 | 93 | describe('can apply an offset, with rankdir =', function () { 94 | _.forEach(['TB', 'BT', 'LR', 'RL'], function (rankdir) { 95 | it(rankdir, function () { 96 | g.graph().nodesep = g.graph().edgesep = 10 97 | g.graph().rankdir = rankdir 98 | _.forEach(['a', 'b', 'c', 'd'], function (v) { 99 | g.setNode(v, { width: 10, height: 10 }) 100 | }) 101 | g.setEdge('a', 'b', { width: 10, height: 10, labelpos: 'l', labeloffset: 1000 }) 102 | g.setEdge('c', 'd', { width: 10, height: 10, labelpos: 'r', labeloffset: 1000 }) 103 | layout(g) 104 | 105 | if (rankdir === 'TB' || rankdir === 'BT') { 106 | expect(g.edge('a', 'b').x - g.edge('a', 'b').points[0].x).equals(-1000 - 10 / 2) 107 | expect(g.edge('c', 'd').x - g.edge('c', 'd').points[0].x).equals(1000 + 10 / 2) 108 | } else { 109 | expect(g.edge('a', 'b').y - g.edge('a', 'b').points[0].y).equals(-1000 - 10 / 2) 110 | expect(g.edge('c', 'd').y - g.edge('c', 'd').points[0].y).equals(1000 + 10 / 2) 111 | } 112 | }) 113 | }) 114 | }) 115 | 116 | it('can layout a long edge with a label', function () { 117 | g.graph().ranksep = 300 118 | g.setNode('a', { width: 50, height: 100 }) 119 | g.setNode('b', { width: 75, height: 200 }) 120 | g.setEdge('a', 'b', { width: 60, height: 70, minlen: 2, labelpos: 'c' }) 121 | layout(g) 122 | expect(g.edge('a', 'b').x).to.equal(75 / 2) 123 | expect(g.edge('a', 'b').y) 124 | .to.be.gt(g.node('a').y) 125 | .to.be.lt(g.node('b').y) 126 | }) 127 | 128 | it('can layout out a short cycle', function () { 129 | g.graph().ranksep = 200 130 | g.setNode('a', { width: 100, height: 100 }) 131 | g.setNode('b', { width: 100, height: 100 }) 132 | g.setEdge('a', 'b', { weight: 2 }) 133 | g.setEdge('b', 'a') 134 | layout(g) 135 | expect(extractCoordinates(g)).to.eql({ 136 | a: { x: 100 / 2, y: 100 / 2 }, 137 | b: { x: 100 / 2, y: 100 + 200 + 100 / 2 } 138 | }) 139 | // One arrow should point down, one up 140 | expect(g.edge('a', 'b').points[1].y).gt(g.edge('a', 'b').points[0].y) 141 | expect(g.edge('b', 'a').points[0].y).gt(g.edge('b', 'a').points[1].y) 142 | }) 143 | 144 | it('adds rectangle intersects for edges', function () { 145 | g.graph().ranksep = 200 146 | g.setNode('a', { width: 100, height: 100 }) 147 | g.setNode('b', { width: 100, height: 100 }) 148 | g.setEdge('a', 'b') 149 | layout(g) 150 | var points = g.edge('a', 'b').points 151 | expect(points).to.have.length(3) 152 | expect(points).eqls([ 153 | { x: 100 / 2, y: 100 }, // intersect with bottom of a 154 | { x: 100 / 2, y: 100 + 200 / 2 }, // point for edge label 155 | { x: 100 / 2, y: 100 + 200 } // intersect with top of b 156 | ]) 157 | }) 158 | 159 | it('adds rectangle intersects for edges spanning multiple ranks', function () { 160 | g.graph().ranksep = 200 161 | g.setNode('a', { width: 100, height: 100 }) 162 | g.setNode('b', { width: 100, height: 100 }) 163 | g.setEdge('a', 'b', { minlen: 2 }) 164 | layout(g) 165 | var points = g.edge('a', 'b').points 166 | expect(points).to.have.length(5) 167 | expect(points).eqls([ 168 | { x: 100 / 2, y: 100 }, // intersect with bottom of a 169 | { x: 100 / 2, y: 100 + 200 / 2 }, // bend #1 170 | { x: 100 / 2, y: 100 + 400 / 2 }, // point for edge label 171 | { x: 100 / 2, y: 100 + 600 / 2 }, // bend #2 172 | { x: 100 / 2, y: 100 + 800 / 2 } // intersect with top of b 173 | ]) 174 | }) 175 | 176 | describe('can layout a self loop', function () { 177 | _.forEach(['TB', 'BT', 'LR', 'RL'], function (rankdir) { 178 | it('in rankdir = ' + rankdir, function () { 179 | g.graph().edgesep = 75 180 | g.graph().rankdir = rankdir 181 | g.setNode('a', { width: 100, height: 100 }) 182 | g.setEdge('a', 'a', { width: 50, height: 50 }) 183 | layout(g) 184 | var nodeA = g.node('a') 185 | var points = g.edge('a', 'a').points 186 | expect(points).to.have.length(7) 187 | _.forEach(points, function (point) { 188 | if (rankdir !== 'LR' && rankdir !== 'RL') { 189 | expect(point.x).gt(nodeA.x) 190 | expect(Math.abs(point.y - nodeA.y)).lte(nodeA.height / 2) 191 | } else { 192 | expect(point.y).gt(nodeA.y) 193 | expect(Math.abs(point.x - nodeA.x)).lte(nodeA.width / 2) 194 | } 195 | }) 196 | }) 197 | }) 198 | }) 199 | 200 | it('can layout a graph with subgraphs', function () { 201 | // To be expanded, this primarily ensures nothing blows up for the moment. 202 | g.setNode('a', { width: 50, height: 50 }) 203 | g.setParent('a', 'sg1') 204 | layout(g) 205 | }) 206 | 207 | it('minimizes the height of subgraphs', function () { 208 | _.forEach(['a', 'b', 'c', 'd', 'x', 'y'], function (v) { 209 | g.setNode(v, { width: 50, height: 50 }) 210 | }) 211 | g.setPath(['a', 'b', 'c', 'd']) 212 | g.setEdge('a', 'x', { weight: 100 }) 213 | g.setEdge('y', 'd', { weight: 100 }) 214 | g.setParent('x', 'sg') 215 | g.setParent('y', 'sg') 216 | 217 | // We did not set up an edge (x, y), and we set up high-weight edges from 218 | // outside of the subgraph to nodes in the subgraph. This is to try to 219 | // force nodes x and y to be on different ranks, which we want our ranker 220 | // to avoid. 221 | layout(g) 222 | expect(g.node('x').y).to.equal(g.node('y').y) 223 | }) 224 | 225 | it('can layout subgraphs with different rankdirs', function () { 226 | g.setNode('a', { width: 50, height: 50 }) 227 | g.setNode('sg', {}) 228 | g.setParent('a', 'sg') 229 | 230 | function check (rankdir) { 231 | expect(g.node('sg').width, 'width ' + rankdir).gt(50) 232 | expect(g.node('sg').height, 'height ' + rankdir).gt(50) 233 | expect(g.node('sg').x, 'x ' + rankdir).gt(50 / 2) 234 | expect(g.node('sg').y, 'y ' + rankdir).gt(50 / 2) 235 | } 236 | 237 | _.forEach(['tb', 'bt', 'lr', 'rl'], function (rankdir) { 238 | g.graph().rankdir = rankdir 239 | layout(g) 240 | check(rankdir) 241 | }) 242 | }) 243 | 244 | it('adds dimensions to the graph', function () { 245 | g.setNode('a', { width: 100, height: 50 }) 246 | layout(g) 247 | expect(g.graph().width).equals(100) 248 | expect(g.graph().height).equals(50) 249 | }) 250 | 251 | describe('ensures all coordinates are in the bounding box for the graph', function () { 252 | _.forEach(['TB', 'BT', 'LR', 'RL'], function (rankdir) { 253 | describe(rankdir, function () { 254 | beforeEach(function () { 255 | g.graph().rankdir = rankdir 256 | }) 257 | 258 | it('node', function () { 259 | g.setNode('a', { width: 100, height: 200 }) 260 | layout(g) 261 | expect(g.node('a').x).equals(100 / 2) 262 | expect(g.node('a').y).equals(200 / 2) 263 | }) 264 | 265 | it('edge, labelpos = l', function () { 266 | g.setNode('a', { width: 100, height: 100 }) 267 | g.setNode('b', { width: 100, height: 100 }) 268 | g.setEdge('a', 'b', { 269 | width: 1000, height: 2000, labelpos: 'l', labeloffset: 0 270 | }) 271 | layout(g) 272 | if (rankdir === 'TB' || rankdir === 'BT') { 273 | expect(g.edge('a', 'b').x).equals(1000 / 2) 274 | } else { 275 | expect(g.edge('a', 'b').y).equals(2000 / 2) 276 | } 277 | }) 278 | }) 279 | }) 280 | }) 281 | 282 | it('treats attributes with case-insensitivity', function () { 283 | g.graph().nodeSep = 200 // note the capital S 284 | g.setNode('a', { width: 50, height: 100 }) 285 | g.setNode('b', { width: 75, height: 200 }) 286 | layout(g) 287 | expect(extractCoordinates(g)).to.eql({ 288 | a: { x: 50 / 2, y: 200 / 2 }, 289 | b: { x: 50 + 200 + 75 / 2, y: 200 / 2 } 290 | }) 291 | }) 292 | }) 293 | 294 | function extractCoordinates (g) { 295 | var nodes = g.nodes() 296 | return _.zipObject(nodes, _.map(nodes, function (v) { 297 | return _.pick(g.node(v), ['x', 'y']) 298 | })) 299 | } 300 | -------------------------------------------------------------------------------- /src/position/bk.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import {Graph} from 'ciena-graphlib' 3 | import {buildLayerMatrix} from '../util' 4 | 5 | /* 6 | * This module provides coordinate assignment based on Brandes and Köpf, "Fast 7 | * and Simple Horizontal Coordinate Assignment." 8 | */ 9 | 10 | /* 11 | * Marks all edges in the graph with a type-1 conflict with the "type1Conflict" 12 | * property. A type-1 conflict is one where a non-inner segment crosses an 13 | * inner segment. An inner segment is an edge with both incident nodes marked 14 | * with the "dummy" property. 15 | * 16 | * This algorithm scans layer by layer, starting with the second, for type-1 17 | * conflicts between the current layer and the previous layer. For each layer 18 | * it scans the nodes from left to right until it reaches one that is incident 19 | * on an inner segment. It then scans predecessors to determine if they have 20 | * edges that cross that inner segment. At the end a final scan is done for all 21 | * nodes on the current rank to see if they cross the last visited inner 22 | * segment. 23 | * 24 | * This algorithm (safely) assumes that a dummy node will only be incident on a 25 | * single node in the layers being scanned. 26 | */ 27 | export function findType1Conflicts (g, layering) { 28 | var conflicts = {} 29 | 30 | function visitLayer (prevLayer, layer) { 31 | // last visited node in the previous layer that is incident on an inner 32 | // segment. 33 | var k0 = 0 34 | // Tracks the last node in this layer scanned for crossings with a type-1 35 | // segment. 36 | var scanPos = 0 37 | var prevLayerLength = prevLayer.length 38 | var lastNode = _.last(layer) 39 | 40 | _.forEach(layer, function (v, i) { 41 | var w = findOtherInnerSegmentNode(g, v) 42 | var k1 = w ? g.node(w).order : prevLayerLength 43 | 44 | if (w || v === lastNode) { 45 | _.forEach(layer.slice(scanPos, i + 1), function (scanNode) { 46 | _.forEach(g.predecessors(scanNode), function (u) { 47 | var uLabel = g.node(u) 48 | var uPos = uLabel.order 49 | if ((uPos < k0 || k1 < uPos) && 50 | !(uLabel.dummy && g.node(scanNode).dummy)) { 51 | addConflict(conflicts, u, scanNode) 52 | } 53 | }) 54 | }) 55 | scanPos = i + 1 56 | k0 = k1 57 | } 58 | }) 59 | 60 | return layer 61 | } 62 | 63 | _.reduce(layering, visitLayer) 64 | return conflicts 65 | } 66 | 67 | export function findType2Conflicts (g, layering) { 68 | var conflicts = {} 69 | 70 | function scan (south, southPos, southEnd, prevNorthBorder, nextNorthBorder) { 71 | var v 72 | _.forEach(_.range(southPos, southEnd), function (i) { 73 | v = south[i] 74 | if (g.node(v).dummy) { 75 | _.forEach(g.predecessors(v), function (u) { 76 | var uNode = g.node(u) 77 | if (uNode.dummy && 78 | (uNode.order < prevNorthBorder || uNode.order > nextNorthBorder)) { 79 | addConflict(conflicts, u, v) 80 | } 81 | }) 82 | } 83 | }) 84 | } 85 | 86 | function visitLayer (north, south) { 87 | var prevNorthPos = -1 88 | var nextNorthPos 89 | var southPos = 0 90 | 91 | _.forEach(south, function (v, southLookahead) { 92 | if (g.node(v).dummy === 'border') { 93 | var predecessors = g.predecessors(v) 94 | if (predecessors.length) { 95 | nextNorthPos = g.node(predecessors[0]).order 96 | scan(south, southPos, southLookahead, prevNorthPos, nextNorthPos) 97 | southPos = southLookahead 98 | prevNorthPos = nextNorthPos 99 | } 100 | } 101 | scan(south, southPos, south.length, nextNorthPos, north.length) 102 | }) 103 | 104 | return south 105 | } 106 | 107 | _.reduce(layering, visitLayer) 108 | return conflicts 109 | } 110 | 111 | export function findOtherInnerSegmentNode (g, v) { 112 | if (g.node(v).dummy) { 113 | return _.find(g.predecessors(v), function (u) { 114 | return g.node(u).dummy 115 | }) 116 | } 117 | } 118 | 119 | export function addConflict (conflicts, v, w) { 120 | if (v > w) { 121 | var tmp = v 122 | v = w 123 | w = tmp 124 | } 125 | 126 | var conflictsV = conflicts[v] 127 | if (!conflictsV) { 128 | conflicts[v] = conflictsV = {} 129 | } 130 | conflictsV[w] = true 131 | } 132 | 133 | export function hasConflict (conflicts, v, w) { 134 | if (v > w) { 135 | var tmp = v 136 | v = w 137 | w = tmp 138 | } 139 | return _.has(conflicts[v], w) 140 | } 141 | 142 | /* 143 | * Try to align nodes into vertical "blocks" where possible. This algorithm 144 | * attempts to align a node with one of its median neighbors. If the edge 145 | * connecting a neighbor is a type-1 conflict then we ignore that possibility. 146 | * If a previous node has already formed a block with a node after the node 147 | * we're trying to form a block with, we also ignore that possibility - our 148 | * blocks would be split in that scenario. 149 | */ 150 | export function verticalAlignment (g, layering, conflicts, neighborFn) { 151 | var root = {} 152 | var align = {} 153 | var pos = {} 154 | 155 | // We cache the position here based on the layering because the graph and 156 | // layering may be out of sync. The layering matrix is manipulated to 157 | // generate different extreme alignments. 158 | _.forEach(layering, function (layer) { 159 | _.forEach(layer, function (v, order) { 160 | root[v] = v 161 | align[v] = v 162 | pos[v] = order 163 | }) 164 | }) 165 | 166 | _.forEach(layering, function (layer) { 167 | var prevIdx = -1 168 | _.forEach(layer, function (v) { 169 | var ws = neighborFn(v) 170 | if (ws.length) { 171 | ws = _.sortBy(ws, function (w) { return pos[w] }) 172 | var mp = (ws.length - 1) / 2 173 | for (var i = Math.floor(mp), il = Math.ceil(mp); i <= il; ++i) { 174 | var w = ws[i] 175 | if (align[v] === v && 176 | prevIdx < pos[w] && 177 | !hasConflict(conflicts, v, w)) { 178 | align[w] = v 179 | align[v] = root[v] = root[w] 180 | prevIdx = pos[w] 181 | } 182 | } 183 | } 184 | }) 185 | }) 186 | 187 | return { root: root, align: align } 188 | } 189 | 190 | export function horizontalCompaction (g, layering, root, align, reverseSep) { 191 | // This portion of the algorithm differs from BK due to a number of problems. 192 | // Instead of their algorithm we construct a new block graph and do two 193 | // sweeps. The first sweep places blocks with the smallest possible 194 | // coordinates. The second sweep removes unused space by moving blocks to the 195 | // greatest coordinates without violating separation. 196 | var xs = {} 197 | var blockG = buildBlockGraph(g, layering, root, reverseSep) 198 | 199 | // First pass, assign smallest coordinates via DFS 200 | var visited = {} 201 | function pass1 (v) { 202 | if (!_.has(visited, v)) { 203 | visited[v] = true 204 | xs[v] = _.reduce(blockG.inEdges(v), function (max, e) { 205 | pass1(e.v) 206 | return Math.max(max, xs[e.v] + blockG.edge(e)) 207 | }, 0) 208 | } 209 | } 210 | _.forEach(blockG.nodes(), pass1) 211 | 212 | var borderType = reverseSep ? 'borderLeft' : 'borderRight' 213 | function pass2 (v) { 214 | if (visited[v] !== 2) { 215 | visited[v]++ 216 | var node = g.node(v) 217 | var min = _.reduce(blockG.outEdges(v), function (min, e) { 218 | pass2(e.w) 219 | return Math.min(min, xs[e.w] - blockG.edge(e)) 220 | }, Number.POSITIVE_INFINITY) 221 | if (min !== Number.POSITIVE_INFINITY && node.borderType !== borderType) { 222 | xs[v] = Math.max(xs[v], min) 223 | } 224 | } 225 | } 226 | _.forEach(blockG.nodes(), pass2) 227 | 228 | // Assign x coordinates to all nodes 229 | _.forEach(align, function (v) { 230 | xs[v] = xs[root[v]] 231 | }) 232 | 233 | return xs 234 | } 235 | 236 | export function buildBlockGraph (g, layering, root, reverseSep) { 237 | var blockGraph = new Graph() 238 | var graphLabel = g.graph() 239 | var sepFn = sep(graphLabel.nodesep, graphLabel.edgesep, reverseSep) 240 | 241 | _.forEach(layering, function (layer) { 242 | var u 243 | _.forEach(layer, function (v) { 244 | var vRoot = root[v] 245 | blockGraph.setNode(vRoot) 246 | if (u) { 247 | var uRoot = root[u] 248 | var prevMax = blockGraph.edge(uRoot, vRoot) 249 | blockGraph.setEdge(uRoot, vRoot, Math.max(sepFn(g, v, u), prevMax || 0)) 250 | } 251 | u = v 252 | }) 253 | }) 254 | 255 | return blockGraph 256 | } 257 | 258 | /* 259 | * Returns the alignment that has the smallest width of the given alignments. 260 | */ 261 | export function findSmallestWidthAlignment (g, xss) { 262 | var vals = _.values(xss) 263 | 264 | return _.minBy(vals, function (xs) { 265 | var maxVals = [] 266 | var minVals = [] 267 | 268 | _.forIn(xs, function (x, v) { 269 | var halfWidth = width(g, v) / 2 270 | 271 | maxVals.push(x + halfWidth) 272 | minVals.push(x - halfWidth) 273 | }) 274 | 275 | return _.max(maxVals) - _.min(minVals) 276 | }) 277 | } 278 | 279 | /* 280 | * Align the coordinates of each of the layout alignments such that 281 | * left-biased alignments have their minimum coordinate at the same point as 282 | * the minimum coordinate of the smallest width alignment and right-biased 283 | * alignments have their maximum coordinate at the same point as the maximum 284 | * coordinate of the smallest width alignment. 285 | */ 286 | export function alignCoordinates (xss, alignTo) { 287 | var vals = _.values(alignTo) 288 | var alignToMin = _.min(vals) 289 | var alignToMax = _.max(vals) 290 | 291 | _.forEach(['u', 'd'], function (vert) { 292 | _.forEach(['l', 'r'], function (horiz) { 293 | var alignment = vert + horiz 294 | var xs = xss[alignment] 295 | var delta 296 | if (xs === alignTo) return 297 | 298 | var xsVals = _.values(xs) 299 | delta = horiz === 'l' ? alignToMin - _.min(xsVals) : alignToMax - _.max(xsVals) 300 | 301 | if (delta) { 302 | xss[alignment] = _.mapValues(xs, function (x) { return x + delta }) 303 | } 304 | }) 305 | }) 306 | } 307 | 308 | export function balance (xss, align) { 309 | return _.mapValues(xss.ul, function (ignore, v) { 310 | if (align) { 311 | return xss[align.toLowerCase()][v] 312 | } else { 313 | var xs = _.sortBy(_.map(xss, v)) 314 | return (xs[1] + xs[2]) / 2 315 | } 316 | }) 317 | } 318 | 319 | export function positionX (g) { 320 | var layering = buildLayerMatrix(g) 321 | var conflicts = _.merge(findType1Conflicts(g, layering), 322 | findType2Conflicts(g, layering)) 323 | 324 | var xss = {} 325 | var adjustedLayering 326 | _.forEach(['u', 'd'], function (vert) { 327 | adjustedLayering = vert === 'u' ? layering : _.values(layering).reverse() 328 | _.forEach(['l', 'r'], function (horiz) { 329 | if (horiz === 'r') { 330 | adjustedLayering = _.map(adjustedLayering, function (inner) { 331 | return _.values(inner).reverse() 332 | }) 333 | } 334 | 335 | var neighborFn = _.bind(vert === 'u' ? g.predecessors : g.successors, g) 336 | var align = verticalAlignment(g, adjustedLayering, conflicts, neighborFn) 337 | var xs = horizontalCompaction(g, adjustedLayering, 338 | align.root, align.align, 339 | horiz === 'r') 340 | if (horiz === 'r') { 341 | xs = _.mapValues(xs, function (x) { return -x }) 342 | } 343 | xss[vert + horiz] = xs 344 | }) 345 | }) 346 | 347 | var smallestWidth = findSmallestWidthAlignment(g, xss) 348 | alignCoordinates(xss, smallestWidth) 349 | return balance(xss, g.graph().align) 350 | } 351 | 352 | export function sep (nodeSep, edgeSep, reverseSep) { 353 | return function (g, v, w) { 354 | var vLabel = g.node(v) 355 | var wLabel = g.node(w) 356 | var sum = 0 357 | var delta 358 | 359 | sum += vLabel.width / 2 360 | if (_.has(vLabel, 'labelpos')) { 361 | switch (vLabel.labelpos.toLowerCase()) { 362 | case 'l': delta = -vLabel.width / 2; break 363 | case 'r': delta = vLabel.width / 2; break 364 | } 365 | } 366 | if (delta) { 367 | sum += reverseSep ? delta : -delta 368 | } 369 | delta = 0 370 | 371 | sum += (vLabel.dummy ? edgeSep : nodeSep) / 2 372 | sum += (wLabel.dummy ? edgeSep : nodeSep) / 2 373 | 374 | sum += wLabel.width / 2 375 | if (_.has(wLabel, 'labelpos')) { 376 | switch (wLabel.labelpos.toLowerCase()) { 377 | case 'l': delta = wLabel.width / 2; break 378 | case 'r': delta = -wLabel.width / 2; break 379 | } 380 | } 381 | if (delta) { 382 | sum += reverseSep ? delta : -delta 383 | } 384 | delta = 0 385 | 386 | return sum 387 | } 388 | } 389 | 390 | export function width (g, v) { 391 | return g.node(v).width 392 | } 393 | 394 | export default { 395 | alignCoordinates, 396 | balance, 397 | buildBlockGraph, 398 | findOtherInnerSegmentNode, 399 | findSmallestWidthAlignment, 400 | findType1Conflicts, 401 | findType2Conflicts, 402 | hasConflict, 403 | horizontalCompaction, 404 | positionX, 405 | sep, 406 | verticalAlignment, 407 | width 408 | } 409 | -------------------------------------------------------------------------------- /src/layout.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | 3 | import { 4 | run as acyclicRun, 5 | undo as acyclicUndo 6 | } from './acyclic' 7 | 8 | import { 9 | run as normalizeRun, 10 | undo as normalizeUndo 11 | } from './normalize' 12 | 13 | import rank from './rank' 14 | 15 | import { 16 | addDummyNode, 17 | asNonCompoundGraph, 18 | buildLayerMatrix, 19 | intersectRect, 20 | normalizeRanks, 21 | notime, 22 | removeEmptyRanks, 23 | time 24 | } from './util' 25 | 26 | import parentDummyChains from './parent-dummy-chains' 27 | 28 | import { 29 | cleanup as nestingGraphCleanup, 30 | run as nestingGraphRun 31 | } from './nesting-graph' 32 | 33 | import addBorderSegments from './add-border-segments' 34 | 35 | import { 36 | adjust as coordinateSystemAdjust, 37 | undo as coordinateSystemUndo 38 | } from './coordinate-system' 39 | 40 | import order from './order' 41 | import position from './position' 42 | import {Graph} from 'ciena-graphlib' 43 | 44 | function runLayout (g, time) { 45 | time(' makeSpaceForEdgeLabels', function () { makeSpaceForEdgeLabels(g) }) 46 | time(' removeSelfEdges', function () { removeSelfEdges(g) }) 47 | time(' acyclic', function () { acyclicRun(g) }) 48 | time(' nestingGraph.run', function () { nestingGraphRun(g) }) 49 | time(' rank', function () { rank(asNonCompoundGraph(g)) }) 50 | time(' injectEdgeLabelProxies', function () { injectEdgeLabelProxies(g) }) 51 | time(' removeEmptyRanks', function () { removeEmptyRanks(g) }) 52 | time(' nestingGraph.cleanup', function () { nestingGraphCleanup(g) }) 53 | time(' normalizeRanks', function () { normalizeRanks(g) }) 54 | time(' assignRankMinMax', function () { assignRankMinMax(g) }) 55 | time(' removeEdgeLabelProxies', function () { removeEdgeLabelProxies(g) }) 56 | time(' normalize.run', function () { normalizeRun(g) }) 57 | time(' parentDummyChains', function () { parentDummyChains(g) }) 58 | time(' addBorderSegments', function () { addBorderSegments(g) }) 59 | time(' order', function () { order(g) }) 60 | time(' insertSelfEdges', function () { insertSelfEdges(g) }) 61 | time(' adjustCoordinateSystem', function () { coordinateSystemAdjust(g) }) 62 | time(' position', function () { position(g) }) 63 | time(' positionSelfEdges', function () { positionSelfEdges(g) }) 64 | time(' removeBorderNodes', function () { removeBorderNodes(g) }) 65 | time(' normalize.undo', function () { normalizeUndo(g) }) 66 | time(' fixupEdgeLabelCoords', function () { fixupEdgeLabelCoords(g) }) 67 | time(' undoCoordinateSystem', function () { coordinateSystemUndo(g) }) 68 | time(' translateGraph', function () { translateGraph(g) }) 69 | time(' assignNodeIntersects', function () { assignNodeIntersects(g) }) 70 | time(' reversePoints', function () { reversePointsForReversedEdges(g) }) 71 | time(' acyclic.undo', function () { acyclicUndo(g) }) 72 | } 73 | 74 | /* 75 | * Copies final layout information from the layout graph back to the input 76 | * graph. This process only copies whitelisted attributes from the layout graph 77 | * to the input graph, so it serves as a good place to determine what 78 | * attributes can influence layout. 79 | */ 80 | function updateInputGraph (inputGraph, layoutGraph) { 81 | _.forEach(inputGraph.nodes(), function (v) { 82 | var inputLabel = inputGraph.node(v) 83 | var layoutLabel = layoutGraph.node(v) 84 | 85 | if (inputLabel) { 86 | inputLabel.x = layoutLabel.x 87 | inputLabel.y = layoutLabel.y 88 | 89 | if (layoutGraph.children(v).length) { 90 | inputLabel.width = layoutLabel.width 91 | inputLabel.height = layoutLabel.height 92 | } 93 | } 94 | }) 95 | 96 | _.forEach(inputGraph.edges(), function (e) { 97 | var inputLabel = inputGraph.edge(e) 98 | var layoutLabel = layoutGraph.edge(e) 99 | 100 | inputLabel.points = layoutLabel.points 101 | if (_.has(layoutLabel, 'x')) { 102 | inputLabel.x = layoutLabel.x 103 | inputLabel.y = layoutLabel.y 104 | } 105 | }) 106 | 107 | inputGraph.graph().width = layoutGraph.graph().width 108 | inputGraph.graph().height = layoutGraph.graph().height 109 | } 110 | 111 | var graphNumAttrs = ['nodesep', 'edgesep', 'ranksep', 'marginx', 'marginy'] 112 | var graphDefaults = { ranksep: 50, edgesep: 20, nodesep: 50, rankdir: 'tb' } 113 | var graphAttrs = ['acyclicer', 'ranker', 'rankdir', 'align'] 114 | var nodeNumAttrs = ['width', 'height'] 115 | var nodeDefaults = { width: 0, height: 0 } 116 | var edgeNumAttrs = ['minlen', 'weight', 'width', 'height', 'labeloffset'] 117 | var edgeDefaults = { 118 | minlen: 1, 119 | weight: 1, 120 | width: 0, 121 | height: 0, 122 | labeloffset: 10, 123 | labelpos: 'r' 124 | } 125 | var edgeAttrs = ['labelpos'] 126 | 127 | /* 128 | * Constructs a new graph from the input graph, which can be used for layout. 129 | * This process copies only whitelisted attributes from the input graph to the 130 | * layout graph. Thus this function serves as a good place to determine what 131 | * attributes can influence layout. 132 | */ 133 | function buildLayoutGraph (inputGraph) { 134 | var g = new Graph({ multigraph: true, compound: true }) 135 | var graph = canonicalize(inputGraph.graph()) 136 | 137 | g.setGraph(_.merge({}, 138 | graphDefaults, 139 | selectNumberAttrs(graph, graphNumAttrs), 140 | _.pick(graph, graphAttrs))) 141 | 142 | _.forEach(inputGraph.nodes(), function (v) { 143 | var node = canonicalize(inputGraph.node(v)) 144 | g.setNode(v, _.defaults(selectNumberAttrs(node, nodeNumAttrs), nodeDefaults)) 145 | g.setParent(v, inputGraph.parent(v)) 146 | }) 147 | 148 | _.forEach(inputGraph.edges(), function (e) { 149 | var edge = canonicalize(inputGraph.edge(e)) 150 | g.setEdge(e, _.merge({}, 151 | edgeDefaults, 152 | selectNumberAttrs(edge, edgeNumAttrs), 153 | _.pick(edge, edgeAttrs))) 154 | }) 155 | 156 | return g 157 | } 158 | 159 | /* 160 | * This idea comes from the Gansner paper: to account for edge labels in our 161 | * layout we split each rank in half by doubling minlen and halving ranksep. 162 | * Then we can place labels at these mid-points between nodes. 163 | * 164 | * We also add some minimal padding to the width to push the label for the edge 165 | * away from the edge itself a bit. 166 | */ 167 | function makeSpaceForEdgeLabels (g) { 168 | var graph = g.graph() 169 | graph.ranksep /= 2 170 | _.forEach(g.edges(), function (e) { 171 | var edge = g.edge(e) 172 | edge.minlen *= 2 173 | if (edge.labelpos.toLowerCase() !== 'c') { 174 | if (graph.rankdir === 'TB' || graph.rankdir === 'BT') { 175 | edge.width += edge.labeloffset 176 | } else { 177 | edge.height += edge.labeloffset 178 | } 179 | } 180 | }) 181 | } 182 | 183 | /* 184 | * Creates temporary dummy nodes that capture the rank in which each edge's 185 | * label is going to, if it has one of non-zero width and height. We do this 186 | * so that we can safely remove empty ranks while preserving balance for the 187 | * label's position. 188 | */ 189 | function injectEdgeLabelProxies (g) { 190 | _.forEach(g.edges(), function (e) { 191 | var edge = g.edge(e) 192 | if (edge.width && edge.height) { 193 | var v = g.node(e.v) 194 | var w = g.node(e.w) 195 | var label = { rank: (w.rank - v.rank) / 2 + v.rank, e: e } 196 | addDummyNode(g, 'edge-proxy', label, '_ep') 197 | } 198 | }) 199 | } 200 | 201 | function assignRankMinMax (g) { 202 | var maxRank = 0 203 | _.forEach(g.nodes(), function (v) { 204 | var node = g.node(v) 205 | if (node.borderTop) { 206 | node.minRank = g.node(node.borderTop).rank 207 | node.maxRank = g.node(node.borderBottom).rank 208 | maxRank = _.max(maxRank, node.maxRank) 209 | } 210 | }) 211 | g.graph().maxRank = maxRank 212 | } 213 | 214 | function removeEdgeLabelProxies (g) { 215 | _.forEach(g.nodes(), function (v) { 216 | var node = g.node(v) 217 | if (node.dummy === 'edge-proxy') { 218 | g.edge(node.e).labelRank = node.rank 219 | g.removeNode(v) 220 | } 221 | }) 222 | } 223 | 224 | function translateGraph (g) { 225 | var minX = Number.POSITIVE_INFINITY 226 | var maxX = 0 227 | var minY = Number.POSITIVE_INFINITY 228 | var maxY = 0 229 | var graphLabel = g.graph() 230 | var marginX = graphLabel.marginx || 0 231 | var marginY = graphLabel.marginy || 0 232 | 233 | function getExtremes (attrs) { 234 | var x = attrs.x 235 | var y = attrs.y 236 | var w = attrs.width 237 | var h = attrs.height 238 | minX = Math.min(minX, x - w / 2) 239 | maxX = Math.max(maxX, x + w / 2) 240 | minY = Math.min(minY, y - h / 2) 241 | maxY = Math.max(maxY, y + h / 2) 242 | } 243 | 244 | _.forEach(g.nodes(), function (v) { getExtremes(g.node(v)) }) 245 | _.forEach(g.edges(), function (e) { 246 | var edge = g.edge(e) 247 | if (_.has(edge, 'x')) { 248 | getExtremes(edge) 249 | } 250 | }) 251 | 252 | minX -= marginX 253 | minY -= marginY 254 | 255 | _.forEach(g.nodes(), function (v) { 256 | var node = g.node(v) 257 | node.x -= minX 258 | node.y -= minY 259 | }) 260 | 261 | _.forEach(g.edges(), function (e) { 262 | var edge = g.edge(e) 263 | _.forEach(edge.points, function (p) { 264 | p.x -= minX 265 | p.y -= minY 266 | }) 267 | if (_.has(edge, 'x')) { edge.x -= minX } 268 | if (_.has(edge, 'y')) { edge.y -= minY } 269 | }) 270 | 271 | graphLabel.width = maxX - minX + marginX 272 | graphLabel.height = maxY - minY + marginY 273 | } 274 | 275 | function assignNodeIntersects (g) { 276 | _.forEach(g.edges(), function (e) { 277 | var edge = g.edge(e) 278 | var nodeV = g.node(e.v) 279 | var nodeW = g.node(e.w) 280 | var p1 281 | var p2 282 | if (!edge.points) { 283 | edge.points = [] 284 | p1 = nodeW 285 | p2 = nodeV 286 | } else { 287 | p1 = edge.points[0] 288 | p2 = edge.points[edge.points.length - 1] 289 | } 290 | edge.points.unshift(intersectRect(nodeV, p1)) 291 | edge.points.push(intersectRect(nodeW, p2)) 292 | }) 293 | } 294 | 295 | function fixupEdgeLabelCoords (g) { 296 | _.forEach(g.edges(), function (e) { 297 | var edge = g.edge(e) 298 | if (_.has(edge, 'x')) { 299 | if (edge.labelpos === 'l' || edge.labelpos === 'r') { 300 | edge.width -= edge.labeloffset 301 | } 302 | switch (edge.labelpos) { 303 | case 'l': edge.x -= edge.width / 2 + edge.labeloffset; break 304 | case 'r': edge.x += edge.width / 2 + edge.labeloffset; break 305 | } 306 | } 307 | }) 308 | } 309 | 310 | function reversePointsForReversedEdges (g) { 311 | _.forEach(g.edges(), function (e) { 312 | var edge = g.edge(e) 313 | if (edge.reversed) { 314 | edge.points.reverse() 315 | } 316 | }) 317 | } 318 | 319 | function removeBorderNodes (g) { 320 | _.forEach(g.nodes(), function (v) { 321 | if (g.children(v).length) { 322 | var node = g.node(v) 323 | var t = g.node(node.borderTop) 324 | var b = g.node(node.borderBottom) 325 | var l = g.node(_.last(node.borderLeft)) 326 | var r = g.node(_.last(node.borderRight)) 327 | 328 | node.width = Math.abs(r.x - l.x) 329 | node.height = Math.abs(b.y - t.y) 330 | node.x = l.x + node.width / 2 331 | node.y = t.y + node.height / 2 332 | } 333 | }) 334 | 335 | _.forEach(g.nodes(), function (v) { 336 | if (g.node(v).dummy === 'border') { 337 | g.removeNode(v) 338 | } 339 | }) 340 | } 341 | 342 | function removeSelfEdges (g) { 343 | _.forEach(g.edges(), function (e) { 344 | if (e.v === e.w) { 345 | var node = g.node(e.v) 346 | if (!node.selfEdges) { 347 | node.selfEdges = [] 348 | } 349 | node.selfEdges.push({ e: e, label: g.edge(e) }) 350 | g.removeEdge(e) 351 | } 352 | }) 353 | } 354 | 355 | function insertSelfEdges (g) { 356 | var layers = buildLayerMatrix(g) 357 | _.forEach(layers, function (layer) { 358 | var orderShift = 0 359 | _.forEach(layer, function (v, i) { 360 | var node = g.node(v) 361 | node.order = i + orderShift 362 | _.forEach(node.selfEdges, function (selfEdge) { 363 | addDummyNode(g, 'selfedge', { 364 | width: selfEdge.label.width, 365 | height: selfEdge.label.height, 366 | rank: node.rank, 367 | order: i + (++orderShift), 368 | e: selfEdge.e, 369 | label: selfEdge.label 370 | }, '_se') 371 | }) 372 | delete node.selfEdges 373 | }) 374 | }) 375 | } 376 | 377 | function positionSelfEdges (g) { 378 | _.forEach(g.nodes(), function (v) { 379 | var node = g.node(v) 380 | if (node.dummy === 'selfedge') { 381 | var selfNode = g.node(node.e.v) 382 | var x = selfNode.x + selfNode.width / 2 383 | var y = selfNode.y 384 | var dx = node.x - x 385 | var dy = selfNode.height / 2 386 | g.setEdge(node.e, node.label) 387 | g.removeNode(v) 388 | node.label.points = [ 389 | { x: x + 2 * dx / 3, y: y - dy }, 390 | { x: x + 5 * dx / 6, y: y - dy }, 391 | { x: x + dx, y: y }, 392 | { x: x + 5 * dx / 6, y: y + dy }, 393 | { x: x + 2 * dx / 3, y: y + dy } 394 | ] 395 | node.label.x = node.x 396 | node.label.y = node.y 397 | } 398 | }) 399 | } 400 | 401 | function selectNumberAttrs (obj, attrs) { 402 | return _.mapValues(_.pick(obj, attrs), Number) 403 | } 404 | 405 | function canonicalize (attrs) { 406 | var newAttrs = {} 407 | _.forEach(attrs, function (v, k) { 408 | newAttrs[k.toLowerCase()] = v 409 | }) 410 | return newAttrs 411 | } 412 | 413 | export default function layout (g, opts) { 414 | var timeFn = opts && opts.debugTiming ? time : notime 415 | timeFn('layout', function () { 416 | var layoutGraph = timeFn(' buildLayoutGraph', 417 | function () { return buildLayoutGraph(g) }) 418 | timeFn(' runLayout', function () { runLayout(layoutGraph, timeFn) }) 419 | timeFn(' updateInputGraph', function () { updateInputGraph(g, layoutGraph) }) 420 | }) 421 | } 422 | -------------------------------------------------------------------------------- /tests/rank/network-simplex-test.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect 2 | const Graph = require('ciena-graphlib').Graph 3 | const _ = require('lodash') 4 | 5 | const networkSimplex = require('../../lib/rank/network-simplex') 6 | 7 | const calcCutValue = networkSimplex.calcCutValue 8 | const enterEdge = networkSimplex.enterEdge 9 | const exchangeEdges = networkSimplex.exchangeEdges 10 | const initCutValues = networkSimplex.initCutValues 11 | const initLowLimValues = networkSimplex.initLowLimValues 12 | const leaveEdge = networkSimplex.leaveEdge 13 | 14 | const util = require('../../lib/util') 15 | const normalizeRanks = util.normalizeRanks 16 | 17 | const rankUtil = require('../../lib/rank/util') 18 | const longestPath = rankUtil.longestPath 19 | 20 | describe('network simplex', function () { 21 | var g, t, gansnerGraph, gansnerTree 22 | 23 | beforeEach(function () { 24 | g = new Graph({ multigraph: true }) 25 | .setDefaultNodeLabel(function () { return {} }) 26 | .setDefaultEdgeLabel(function () { return { minlen: 1, weight: 1 } }) 27 | 28 | t = new Graph({ directed: false }) 29 | .setDefaultNodeLabel(function () { return {} }) 30 | .setDefaultEdgeLabel(function () { return {} }) 31 | 32 | gansnerGraph = new Graph() 33 | .setDefaultNodeLabel(function () { return {} }) 34 | .setDefaultEdgeLabel(function () { return { minlen: 1, weight: 1 } }) 35 | .setPath(['a', 'b', 'c', 'd', 'h']) 36 | .setPath(['a', 'e', 'g', 'h']) 37 | .setPath(['a', 'f', 'g']) 38 | 39 | gansnerTree = new Graph({ directed: false }) 40 | .setDefaultNodeLabel(function () { return {} }) 41 | .setDefaultEdgeLabel(function () { return {} }) 42 | .setPath(['a', 'b', 'c', 'd', 'h', 'g', 'e']) 43 | .setEdge('g', 'f') 44 | }) 45 | 46 | it('can assign a rank to a single node', function () { 47 | g.setNode('a') 48 | ns(g) 49 | expect(g.node('a').rank).to.equal(0) 50 | }) 51 | 52 | it('can assign a rank to a 2-node connected graph', function () { 53 | g.setEdge('a', 'b') 54 | ns(g) 55 | expect(g.node('a').rank).to.equal(0) 56 | expect(g.node('b').rank).to.equal(1) 57 | }) 58 | 59 | it('can assign ranks for a diamond', function () { 60 | g.setPath(['a', 'b', 'd']) 61 | g.setPath(['a', 'c', 'd']) 62 | ns(g) 63 | expect(g.node('a').rank).to.equal(0) 64 | expect(g.node('b').rank).to.equal(1) 65 | expect(g.node('c').rank).to.equal(1) 66 | expect(g.node('d').rank).to.equal(2) 67 | }) 68 | 69 | it('uses the minlen attribute on the edge', function () { 70 | g.setPath(['a', 'b', 'd']) 71 | g.setEdge('a', 'c') 72 | g.setEdge('c', 'd', { minlen: 2 }) 73 | ns(g) 74 | expect(g.node('a').rank).to.equal(0) 75 | // longest path biases towards the lowest rank it can assign. Since the 76 | // graph has no optimization opportunities we can assume that the longest 77 | // path ranking is used. 78 | expect(g.node('b').rank).to.equal(2) 79 | expect(g.node('c').rank).to.equal(1) 80 | expect(g.node('d').rank).to.equal(3) 81 | }) 82 | 83 | it('can rank the gansner graph', function () { 84 | g = gansnerGraph 85 | ns(g) 86 | expect(g.node('a').rank).to.equal(0) 87 | expect(g.node('b').rank).to.equal(1) 88 | expect(g.node('c').rank).to.equal(2) 89 | expect(g.node('d').rank).to.equal(3) 90 | expect(g.node('h').rank).to.equal(4) 91 | expect(g.node('e').rank).to.equal(1) 92 | expect(g.node('f').rank).to.equal(1) 93 | expect(g.node('g').rank).to.equal(2) 94 | }) 95 | 96 | it('can handle multi-edges', function () { 97 | g.setPath(['a', 'b', 'c', 'd']) 98 | g.setEdge('a', 'e', { weight: 2, minlen: 1 }) 99 | g.setEdge('e', 'd') 100 | g.setEdge('b', 'c', { weight: 1, minlen: 2 }, 'multi') 101 | ns(g) 102 | expect(g.node('a').rank).to.equal(0) 103 | expect(g.node('b').rank).to.equal(1) 104 | // b -> c has minlen = 1 and minlen = 2, so it should be 2 ranks apart. 105 | expect(g.node('c').rank).to.equal(3) 106 | expect(g.node('d').rank).to.equal(4) 107 | expect(g.node('e').rank).to.equal(1) 108 | }) 109 | 110 | describe('leaveEdge', function () { 111 | it('returns undefined if there is no edge with a negative cutvalue', function () { 112 | var tree = new Graph({ directed: false }) 113 | tree.setEdge('a', 'b', { cutvalue: 1 }) 114 | tree.setEdge('b', 'c', { cutvalue: 1 }) 115 | expect(leaveEdge(tree)).to.be.undefined 116 | }) 117 | 118 | it('returns an edge if one is found with a negative cutvalue', function () { 119 | var tree = new Graph({ directed: false }) 120 | tree.setEdge('a', 'b', { cutvalue: 1 }) 121 | tree.setEdge('b', 'c', { cutvalue: -1 }) 122 | expect(leaveEdge(tree)).to.eql({ v: 'b', w: 'c' }) 123 | }) 124 | }) 125 | 126 | describe('enterEdge', function () { 127 | it('finds an edge from the head to tail component', function () { 128 | g 129 | .setNode('a', { rank: 0 }) 130 | .setNode('b', { rank: 2 }) 131 | .setNode('c', { rank: 3 }) 132 | .setPath(['a', 'b', 'c']) 133 | .setEdge('a', 'c') 134 | t.setPath(['b', 'c', 'a']) 135 | initLowLimValues(t, 'c') 136 | 137 | var f = enterEdge(t, g, { v: 'b', w: 'c' }) 138 | expect(undirectedEdge(f)).to.eql(undirectedEdge({ v: 'a', w: 'b' })) 139 | }) 140 | 141 | it('works when the root of the tree is in the tail component', function () { 142 | g 143 | .setNode('a', { rank: 0 }) 144 | .setNode('b', { rank: 2 }) 145 | .setNode('c', { rank: 3 }) 146 | .setPath(['a', 'b', 'c']) 147 | .setEdge('a', 'c') 148 | t.setPath(['b', 'c', 'a']) 149 | initLowLimValues(t, 'b') 150 | 151 | var f = enterEdge(t, g, { v: 'b', w: 'c' }) 152 | expect(undirectedEdge(f)).to.eql(undirectedEdge({ v: 'a', w: 'b' })) 153 | }) 154 | 155 | it('finds the edge with the least slack', function () { 156 | g 157 | .setNode('a', { rank: 0 }) 158 | .setNode('b', { rank: 1 }) 159 | .setNode('c', { rank: 3 }) 160 | .setNode('d', { rank: 4 }) 161 | .setEdge('a', 'd') 162 | .setPath(['a', 'c', 'd']) 163 | .setEdge('b', 'c') 164 | t.setPath(['c', 'd', 'a', 'b']) 165 | initLowLimValues(t, 'a') 166 | 167 | var f = enterEdge(t, g, { v: 'c', w: 'd' }) 168 | expect(undirectedEdge(f)).to.eql(undirectedEdge({ v: 'b', w: 'c' })) 169 | }) 170 | 171 | it('finds an appropriate edge for gansner graph #1', function () { 172 | g = gansnerGraph 173 | t = gansnerTree 174 | longestPath(g) 175 | initLowLimValues(t, 'a') 176 | 177 | var f = enterEdge(t, g, { v: 'g', w: 'h' }) 178 | expect(undirectedEdge(f).v).to.equal('a') 179 | expect(['e', 'f']).to.include(undirectedEdge(f).w) 180 | }) 181 | 182 | it('finds an appropriate edge for gansner graph #2', function () { 183 | g = gansnerGraph 184 | t = gansnerTree 185 | longestPath(g) 186 | initLowLimValues(t, 'e') 187 | 188 | var f = enterEdge(t, g, { v: 'g', w: 'h' }) 189 | expect(undirectedEdge(f).v).to.equal('a') 190 | expect(['e', 'f']).to.include(undirectedEdge(f).w) 191 | }) 192 | 193 | it('finds an appropriate edge for gansner graph #3', function () { 194 | g = gansnerGraph 195 | t = gansnerTree 196 | longestPath(g) 197 | initLowLimValues(t, 'a') 198 | 199 | var f = enterEdge(t, g, { v: 'h', w: 'g' }) 200 | expect(undirectedEdge(f).v).to.equal('a') 201 | expect(['e', 'f']).to.include(undirectedEdge(f).w) 202 | }) 203 | 204 | it('finds an appropriate edge for gansner graph #4', function () { 205 | g = gansnerGraph 206 | t = gansnerTree 207 | longestPath(g) 208 | initLowLimValues(t, 'e') 209 | 210 | var f = enterEdge(t, g, { v: 'h', w: 'g' }) 211 | expect(undirectedEdge(f).v).to.equal('a') 212 | expect(['e', 'f']).to.include(undirectedEdge(f).w) 213 | }) 214 | }) 215 | 216 | describe('initLowLimValues', function () { 217 | it('assigns low, lim, and parent for each node in a tree', function () { 218 | var g = new Graph() 219 | .setDefaultNodeLabel(function () { return {} }) 220 | .setNodes(['a', 'b', 'c', 'd', 'e']) 221 | .setPath(['a', 'b', 'a', 'c', 'd', 'c', 'e']) 222 | 223 | initLowLimValues(g, 'a') 224 | 225 | var a = g.node('a') 226 | var b = g.node('b') 227 | var c = g.node('c') 228 | var d = g.node('d') 229 | var e = g.node('e') 230 | 231 | expect(_.sortBy(_.map(g.nodes(), function (v) { return g.node(v).lim }))) 232 | .to.eql(_.range(1, 6)) 233 | 234 | expect(a).to.eql({ low: 1, lim: 5 }) 235 | 236 | expect(b.parent).to.equal('a') 237 | expect(b.lim).to.be.lt(a.lim) 238 | 239 | expect(c.parent).to.equal('a') 240 | expect(c.lim).to.be.lt(a.lim) 241 | expect(c.lim).to.not.equal(b.lim) 242 | 243 | expect(d.parent).to.equal('c') 244 | expect(d.lim).to.be.lt(c.lim) 245 | 246 | expect(e.parent).to.equal('c') 247 | expect(e.lim).to.be.lt(c.lim) 248 | expect(e.lim).to.not.equal(d.lim) 249 | }) 250 | }) 251 | 252 | describe('exchangeEdges', function () { 253 | it('exchanges edges and updates cut values and low/lim numbers', function () { 254 | g = gansnerGraph 255 | t = gansnerTree 256 | longestPath(g) 257 | initLowLimValues(t) 258 | 259 | exchangeEdges(t, g, { v: 'g', w: 'h' }, { v: 'a', w: 'e' }) 260 | 261 | // check new cut values 262 | expect(t.edge('a', 'b').cutvalue).to.equal(2) 263 | expect(t.edge('b', 'c').cutvalue).to.equal(2) 264 | expect(t.edge('c', 'd').cutvalue).to.equal(2) 265 | expect(t.edge('d', 'h').cutvalue).to.equal(2) 266 | expect(t.edge('a', 'e').cutvalue).to.equal(1) 267 | expect(t.edge('e', 'g').cutvalue).to.equal(1) 268 | expect(t.edge('g', 'f').cutvalue).to.equal(0) 269 | 270 | // ensure lim numbers look right 271 | var lims = _.sortBy(_.map(t.nodes(), function (v) { return t.node(v).lim })) 272 | expect(lims).to.eql(_.range(1, 9)) 273 | }) 274 | 275 | it('updates ranks', function () { 276 | g = gansnerGraph 277 | t = gansnerTree 278 | longestPath(g) 279 | initLowLimValues(t) 280 | 281 | exchangeEdges(t, g, { v: 'g', w: 'h' }, { v: 'a', w: 'e' }) 282 | normalizeRanks(g) 283 | 284 | // check new ranks 285 | expect(g.node('a').rank).to.equal(0) 286 | expect(g.node('b').rank).to.equal(1) 287 | expect(g.node('c').rank).to.equal(2) 288 | expect(g.node('d').rank).to.equal(3) 289 | expect(g.node('e').rank).to.equal(1) 290 | expect(g.node('f').rank).to.equal(1) 291 | expect(g.node('g').rank).to.equal(2) 292 | expect(g.node('h').rank).to.equal(4) 293 | }) 294 | }) 295 | 296 | // Note: we use p for parent, c for child, gc_x for grandchild nodes, and o for 297 | // other nodes in the tree for these tests. 298 | describe('calcCutValue', function () { 299 | it('works for a 2-node tree with c -> p', function () { 300 | g.setPath(['c', 'p']) 301 | t.setPath(['p', 'c']) 302 | initLowLimValues(t, 'p') 303 | 304 | expect(calcCutValue(t, g, 'c')).to.equal(1) 305 | }) 306 | 307 | it('works for a 2-node tree with c <- p', function () { 308 | g.setPath(['p', 'c']) 309 | t.setPath(['p', 'c']) 310 | initLowLimValues(t, 'p') 311 | 312 | expect(calcCutValue(t, g, 'c')).to.equal(1) 313 | }) 314 | 315 | it('works for 3-node tree with gc -> c -> p', function () { 316 | g.setPath(['gc', 'c', 'p']) 317 | t 318 | .setEdge('gc', 'c', { cutvalue: 3 }) 319 | .setEdge('p', 'c') 320 | initLowLimValues(t, 'p') 321 | 322 | expect(calcCutValue(t, g, 'c')).to.equal(3) 323 | }) 324 | 325 | it('works for 3-node tree with gc -> c <- p', function () { 326 | g 327 | .setEdge('p', 'c') 328 | .setEdge('gc', 'c') 329 | t 330 | .setEdge('gc', 'c', { cutvalue: 3 }) 331 | .setEdge('p', 'c') 332 | initLowLimValues(t, 'p') 333 | 334 | expect(calcCutValue(t, g, 'c')).to.equal(-1) 335 | }) 336 | 337 | it('works for 3-node tree with gc <- c -> p', function () { 338 | g 339 | .setEdge('c', 'p') 340 | .setEdge('c', 'gc') 341 | t 342 | .setEdge('gc', 'c', { cutvalue: 3 }) 343 | .setEdge('p', 'c') 344 | initLowLimValues(t, 'p') 345 | 346 | expect(calcCutValue(t, g, 'c')).to.equal(-1) 347 | }) 348 | 349 | it('works for 3-node tree with gc <- c <- p', function () { 350 | g.setPath(['p', 'c', 'gc']) 351 | t 352 | .setEdge('gc', 'c', { cutvalue: 3 }) 353 | .setEdge('p', 'c') 354 | initLowLimValues(t, 'p') 355 | 356 | expect(calcCutValue(t, g, 'c')).to.equal(3) 357 | }) 358 | 359 | it('works for 4-node tree with gc -> c -> p -> o, with o -> c', function () { 360 | g 361 | .setEdge('o', 'c', { weight: 7 }) 362 | .setPath(['gc', 'c', 'p', 'o']) 363 | t 364 | .setEdge('gc', 'c', { cutvalue: 3 }) 365 | .setPath(['c', 'p', 'o']) 366 | initLowLimValues(t, 'p') 367 | 368 | expect(calcCutValue(t, g, 'c')).to.equal(-4) 369 | }) 370 | 371 | it('works for 4-node tree with gc -> c -> p -> o, with o <- c', function () { 372 | g 373 | .setEdge('c', 'o', { weight: 7 }) 374 | .setPath(['gc', 'c', 'p', 'o']) 375 | t 376 | .setEdge('gc', 'c', { cutvalue: 3 }) 377 | .setPath(['c', 'p', 'o']) 378 | initLowLimValues(t, 'p') 379 | 380 | expect(calcCutValue(t, g, 'c')).to.equal(10) 381 | }) 382 | 383 | it('works for 4-node tree with o -> gc -> c -> p, with o -> c', function () { 384 | g 385 | .setEdge('o', 'c', { weight: 7 }) 386 | .setPath(['o', 'gc', 'c', 'p']) 387 | t 388 | .setEdge('o', 'gc') 389 | .setEdge('gc', 'c', { cutvalue: 3 }) 390 | .setEdge('c', 'p') 391 | initLowLimValues(t, 'p') 392 | 393 | expect(calcCutValue(t, g, 'c')).to.equal(-4) 394 | }) 395 | 396 | it('works for 4-node tree with o -> gc -> c -> p, with o <- c', function () { 397 | g 398 | .setEdge('c', 'o', { weight: 7 }) 399 | .setPath(['o', 'gc', 'c', 'p']) 400 | t 401 | .setEdge('o', 'gc') 402 | .setEdge('gc', 'c', { cutvalue: 3 }) 403 | .setEdge('c', 'p') 404 | initLowLimValues(t, 'p') 405 | 406 | expect(calcCutValue(t, g, 'c')).to.equal(10) 407 | }) 408 | 409 | it('works for 4-node tree with gc -> c <- p -> o, with o -> c', function () { 410 | g 411 | .setEdge('gc', 'c') 412 | .setEdge('p', 'c') 413 | .setEdge('p', 'o') 414 | .setEdge('o', 'c', { weight: 7 }) 415 | t 416 | .setEdge('o', 'gc') 417 | .setEdge('gc', 'c', { cutvalue: 3 }) 418 | .setEdge('c', 'p') 419 | initLowLimValues(t, 'p') 420 | 421 | expect(calcCutValue(t, g, 'c')).to.equal(6) 422 | }) 423 | 424 | it('works for 4-node tree with gc -> c <- p -> o, with o <- c', function () { 425 | g 426 | .setEdge('gc', 'c') 427 | .setEdge('p', 'c') 428 | .setEdge('p', 'o') 429 | .setEdge('c', 'o', { weight: 7 }) 430 | t 431 | .setEdge('o', 'gc') 432 | .setEdge('gc', 'c', { cutvalue: 3 }) 433 | .setEdge('c', 'p') 434 | initLowLimValues(t, 'p') 435 | 436 | expect(calcCutValue(t, g, 'c')).to.equal(-8) 437 | }) 438 | 439 | it('works for 4-node tree with o -> gc -> c <- p, with o -> c', function () { 440 | g 441 | .setEdge('o', 'c', { weight: 7 }) 442 | .setPath(['o', 'gc', 'c']) 443 | .setEdge('p', 'c') 444 | t 445 | .setEdge('o', 'gc') 446 | .setEdge('gc', 'c', { cutvalue: 3 }) 447 | .setEdge('c', 'p') 448 | initLowLimValues(t, 'p') 449 | 450 | expect(calcCutValue(t, g, 'c')).to.equal(6) 451 | }) 452 | 453 | it('works for 4-node tree with o -> gc -> c <- p, with o <- c', function () { 454 | g 455 | .setEdge('c', 'o', { weight: 7 }) 456 | .setPath(['o', 'gc', 'c']) 457 | .setEdge('p', 'c') 458 | t 459 | .setEdge('o', 'gc') 460 | .setEdge('gc', 'c', { cutvalue: 3 }) 461 | .setEdge('c', 'p') 462 | initLowLimValues(t, 'p') 463 | 464 | expect(calcCutValue(t, g, 'c')).to.equal(-8) 465 | }) 466 | }) 467 | 468 | describe('initCutValues', function () { 469 | it('works for gansnerGraph', function () { 470 | initLowLimValues(gansnerTree) 471 | initCutValues(gansnerTree, gansnerGraph) 472 | expect(gansnerTree.edge('a', 'b').cutvalue).to.equal(3) 473 | expect(gansnerTree.edge('b', 'c').cutvalue).to.equal(3) 474 | expect(gansnerTree.edge('c', 'd').cutvalue).to.equal(3) 475 | expect(gansnerTree.edge('d', 'h').cutvalue).to.equal(3) 476 | expect(gansnerTree.edge('g', 'h').cutvalue).to.equal(-1) 477 | expect(gansnerTree.edge('e', 'g').cutvalue).to.equal(0) 478 | expect(gansnerTree.edge('f', 'g').cutvalue).to.equal(0) 479 | }) 480 | 481 | it('works for updated gansnerGraph', function () { 482 | gansnerTree.removeEdge('g', 'h') 483 | gansnerTree.setEdge('a', 'e') 484 | initLowLimValues(gansnerTree) 485 | initCutValues(gansnerTree, gansnerGraph) 486 | expect(gansnerTree.edge('a', 'b').cutvalue).to.equal(2) 487 | expect(gansnerTree.edge('b', 'c').cutvalue).to.equal(2) 488 | expect(gansnerTree.edge('c', 'd').cutvalue).to.equal(2) 489 | expect(gansnerTree.edge('d', 'h').cutvalue).to.equal(2) 490 | expect(gansnerTree.edge('a', 'e').cutvalue).to.equal(1) 491 | expect(gansnerTree.edge('e', 'g').cutvalue).to.equal(1) 492 | expect(gansnerTree.edge('f', 'g').cutvalue).to.equal(0) 493 | }) 494 | }) 495 | }) 496 | 497 | function ns (g) { 498 | networkSimplex(g) 499 | normalizeRanks(g) 500 | } 501 | 502 | function undirectedEdge (e) { 503 | return e.v < e.w ? { v: e.v, w: e.w } : { v: e.w, w: e.v } 504 | } 505 | --------------------------------------------------------------------------------