├── .gitignore ├── package.json ├── History.md ├── test.js ├── Readme.md └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tiny-directed-graph", 3 | "version": "0.0.6", 4 | "description": "little directed graph with backlink support", 5 | "license": "MIT" 6 | } 7 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 2 | 0.0.6 / 2015-11-19 3 | ================== 4 | 5 | * fix unlink bug (should be splicing indices not elements) 6 | 7 | 0.0.5 / 2015-11-18 8 | ================== 9 | 10 | * check for duplicate links 11 | 12 | 0.0.4 / 2015-10-19 13 | ================== 14 | 15 | * explicit upsort, downsort. add tests 16 | * add function to get sorted nodes from single root 17 | 18 | 0.0.3 / 2015-07-03 19 | ================== 20 | 21 | * fix zero resolving to NaN 22 | 23 | 0.0.2 / 2015-06-26 24 | ================== 25 | 26 | * Do not overwrite nodes 27 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | var Graph = require('./') 3 | var graph = Graph(); 4 | 5 | graph.put('a', 53) 6 | graph.put('b') 7 | graph.put('c') 8 | graph.put('d', 2) 9 | 10 | graph.link('a', 'b') 11 | graph.link('b', 'c') 12 | graph.link('b', 'd') 13 | graph.link('d', 'e') 14 | graph.link('a', 'e') 15 | 16 | graph.up('e', 1, function(k) { 17 | assert.deepEqual(k, { d: 2, a: 53 }) 18 | }) 19 | 20 | var out = [] 21 | graph.down('a', function(obj, k) { 22 | out.push({ obj: obj, k: k }) 23 | }) 24 | assert.equal(out.length, 3) 25 | 26 | assert.ok(Object.keys(out[0].obj).length, 2) 27 | assert.ok(Number.isNaN(out[0].obj.b)) 28 | assert.ok(Number.isNaN(out[0].obj.e)) 29 | assert.equal(out[0].k, 'a') 30 | 31 | assert.ok(Object.keys(out[1].obj).length, 2) 32 | assert.ok(Number.isNaN(out[1].obj.c)) 33 | assert.equal(out[1].obj.d, 2) 34 | assert.equal(out[1].k, 'b') 35 | 36 | assert.ok(Object.keys(out[2].obj).length, 1) 37 | assert.ok(Number.isNaN(out[2].obj.e)) 38 | assert.equal(out[2].k, 'd') 39 | 40 | // Verify topological sorting 41 | assert.deepEqual(graph.downsorted('a'), ['a', 'b', 'd', 'e', 'c']) 42 | assert.deepEqual(graph.upsorted('e', 'up'), ['e', 'd', 'b', 'a']) 43 | assert.deepEqual(graph.downsorted(['d', 'c']), ['c', 'd', 'e']) 44 | assert.deepEqual(graph.downsorted({ b: 1, d: 2 }), ['b', 'd', 'e', 'c']) 45 | 46 | // Unlink a node 47 | graph.unlink('b'); 48 | assert.deepEqual(graph.downsorted(Object.keys(graph.nodes)), ['d', 'c', 'b', 'a', 'e']) 49 | var reached = [] 50 | graph.down('a', function (obj) { 51 | reached.concat(Object.keys(obj)) 52 | }) 53 | assert.equal(reached.indexOf('c'), -1) 54 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # tiny-directed-graph 2 | 3 | Little directed graph with backlink support. 4 | 5 | ## Installation 6 | 7 | ```js 8 | npm install tiny-directed-graph 9 | ``` 10 | 11 | ## Example 12 | 13 | ```js 14 | graph.put('a', 53) 15 | graph.put('b') 16 | graph.put('c') 17 | graph.put('d', 2) 18 | 19 | graph.link('a', 'b') 20 | graph.link('b', 'c') 21 | graph.link('b', 'd') 22 | graph.link('d', 'e') 23 | graph.link('a', 'e') 24 | 25 | graph.up('e', 1, function(k) { 26 | console.log('up', k); 27 | }); 28 | ``` 29 | 30 | ## API 31 | 32 | ### graph = Graph() 33 | 34 | Initialize a new Graph. 35 | 36 | ### graph.put(key, value) 37 | 38 | Add a node to the graph with a `key` and `value` 39 | 40 | ### graph.link(key1, key2) 41 | 42 | Link a `key1` => `key2` 43 | 44 | ### graph.unlink(key1, key2) 45 | 46 | Unlink `key1` from `key2`. You can also pass an array of keys to unlink, or if you don't specify `key2`, it will unlink all edges. 47 | 48 | ### graph.del(key) 49 | 50 | Delete a `key` and remove linked 51 | 52 | ### graph.get(key) 53 | 54 | get the `value` for a given `key` 55 | 56 | ### graph.exists(key) 57 | 58 | Check if the node exists 59 | 60 | ### graph.up(key, [depth], fn) 61 | 62 | Walk **up** the graph calling `fn(parents)`, starting at `key`. Optionally supply a `depth`. If no depth is specified, it will walk up the entire graph. 63 | 64 | `parents` is an object containing key value pairs of all the parents. 65 | 66 | ### graph.down(key, [depth], fn) 67 | 68 | Walk **down** the graph calling `fn(children)`, starting at `key`. Optionally supply a `depth`. If no depth is specified, it will walk down the entire graph. 69 | 70 | `children` is an object containing key value pairs of all the children. 71 | 72 | ### graph.toString() 73 | 74 | Print out the entire graph as JSON. 75 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module Dependencies 3 | */ 4 | 5 | var keys = Object.keys; 6 | 7 | /** 8 | * Traversal methods 9 | */ 10 | 11 | var methods = { 12 | up: 'backrefs', 13 | down: 'edges' 14 | }; 15 | 16 | /** 17 | * Export `Graph` 18 | */ 19 | 20 | module.exports = Graph; 21 | 22 | /** 23 | * Initialize `Graph` 24 | */ 25 | 26 | function Graph() { 27 | if (!(this instanceof Graph)) return new Graph(); 28 | this.nodes = {}; 29 | } 30 | 31 | /** 32 | * put 33 | */ 34 | 35 | Graph.prototype.put = function(key, value) { 36 | value = undefined === value ? NaN : value; 37 | 38 | // update nodes, don't remove links 39 | if (this.nodes[key]) { 40 | this.nodes[key].value = value; 41 | return this; 42 | } 43 | 44 | this.nodes[key] = { 45 | value: value, 46 | backrefs: [], 47 | edges: [] 48 | }; 49 | 50 | return this; 51 | }; 52 | 53 | /** 54 | * link 55 | */ 56 | 57 | Graph.prototype.link = function(from, to) { 58 | if (!this.nodes[from]) this.put(from); 59 | 60 | if (typeof to === 'string') to = [to] 61 | 62 | for (var i = 0, node; node = to[i]; i++) { 63 | if (!this.nodes[node]) this.put(node); 64 | if (!~this.nodes[from].edges.indexOf(node)) this.nodes[from].edges.push(node); 65 | if (!~this.nodes[node].backrefs.indexOf(from)) this.nodes[node].backrefs.push(from); 66 | } 67 | 68 | return this; 69 | }; 70 | 71 | /** 72 | * unlink 73 | */ 74 | 75 | Graph.prototype.unlink = function(from, edges) { 76 | var nodes = this.nodes; 77 | 78 | var node = nodes[from]; 79 | if (!node) return this; 80 | 81 | if (arguments.length == 1) { 82 | edges = node.edges; 83 | for (var i = 0, edge; edge = edges[i]; i++) { 84 | nodes[edge].backrefs.splice(nodes[edge].backrefs.indexOf(from), 1); 85 | } 86 | node.edges = [] 87 | // eliminate backrefs on `from` node 88 | var backrefs = node.backrefs; 89 | for (var i = 0, backref; backref = backrefs[i]; i++) { 90 | nodes[backref].edges.splice(from, 1); 91 | } 92 | node.backrefs = []; 93 | } else if (typeof edges == 'string') { 94 | nodes[edges].backrefs.splice(nodes[edges].backrefs.indexOf(from), 1) 95 | node.edges.splice(node.edges.indexOf(edges), 1); 96 | } else { 97 | for (var i = 0, edge; edge = edges[i]; i++) { 98 | nodes[edge].backrefs.splice(nodes[edge].backrefs.indexOf(from), 1); 99 | node.edges.splice(node.edges.indexOf(edge), 1); 100 | } 101 | } 102 | 103 | return this; 104 | }; 105 | 106 | /** 107 | * del 108 | */ 109 | 110 | Graph.prototype.del = function(key) { 111 | this.unlink(key) 112 | delete this.nodes[key]; 113 | return this; 114 | }; 115 | 116 | /** 117 | * get 118 | */ 119 | 120 | Graph.prototype.get = function(key) { 121 | return this.nodes[key] ? this.nodes[key].value : NaN; 122 | }; 123 | 124 | /** 125 | * exists 126 | */ 127 | 128 | Graph.prototype.exists = function(key) { 129 | return !!this.nodes[key]; 130 | }; 131 | 132 | /** 133 | * Setup the traversal methods 134 | */ 135 | 136 | keys(methods).forEach(function(action) { 137 | var attrs = methods[action]; 138 | 139 | Graph.prototype[action] = function(key, depth, fn, ctx, visiting, visited) { 140 | if ('function' == typeof depth) { 141 | ctx = fn; 142 | fn = depth; 143 | depth = Infinity; 144 | } 145 | 146 | // initialize 147 | visiting = visiting || {}; 148 | visited = visited || {}; 149 | 150 | if (!depth-- || visited[key]) return this; 151 | else if (visiting[key]) throw new Error(key + ' already visited. graph is cyclical.'); 152 | visiting[key] = true; 153 | 154 | var node = this.nodes[key]; 155 | if (!node) throw new Error(key + ' doesn\'t exist.'); 156 | 157 | var arr = node[attrs]; 158 | var parents = {}; 159 | var node; 160 | 161 | if (!arr.length) { 162 | visited[key] = true; 163 | delete visiting[key]; 164 | return this; 165 | } 166 | 167 | for (var i = 0, item; item = arr[i]; i++) { 168 | if (visited[item] || visiting[item]) continue; 169 | node = this.nodes[item]; 170 | parents[item] = node.value; 171 | } 172 | 173 | fn.call(ctx, parents, key); 174 | 175 | for (var i = 0, item; item = arr[i]; i++) { 176 | this[action](item, depth, fn, ctx, visiting, visited); 177 | } 178 | 179 | visited[key] = true; 180 | delete visiting[key]; 181 | 182 | return this; 183 | } 184 | }) 185 | 186 | var visit = function (key, sorted, visited, visiting, dir, graph) { 187 | if (visiting[key]) throw new Error('Graph is not acyclic') 188 | if (visited[key]) return 189 | visiting[key] = true 190 | graph.nodes[key][methods[dir]].forEach(function (edge) { 191 | visit(edge, sorted, visited, visiting, dir, graph) 192 | }) 193 | visited[key] = true 194 | visiting[key] = false 195 | sorted.unshift(key) 196 | } 197 | 198 | Graph.prototype.upsorted = function (keys) { 199 | if (!keys) keys = Object.keys(this.nodes) 200 | return this._sorted(keys, 'up') 201 | } 202 | 203 | Graph.prototype.downsorted = function (keys) { 204 | if (!keys) keys = Object.keys(this.nodes) 205 | return this._sorted(keys, 'down') 206 | } 207 | 208 | Graph.prototype._sorted = function (keys, dir) { 209 | if (typeof keys === 'string') keys = [keys] 210 | else if (typeof keys === 'object' && !Array.isArray(keys)) keys = Object.keys(keys) 211 | dir = dir || 'down' 212 | var sorted = [] 213 | var visiting = {} 214 | var visited = {} 215 | keys.forEach(function (key) { 216 | visit(key, sorted, visited, visiting, dir, this) 217 | }, this) 218 | return sorted 219 | } 220 | 221 | /** 222 | * toString 223 | */ 224 | 225 | Graph.prototype.toString = function() { 226 | return JSON.stringify(this.nodes, true, 2) 227 | }; 228 | --------------------------------------------------------------------------------