├── .github └── stale.yml ├── .gitignore ├── .npmrc ├── LICENSE ├── README.md ├── index.js ├── package.json ├── test ├── hops.js ├── index.js └── pagerank.js └── traverse.js /.github/stale.yml: -------------------------------------------------------------------------------- 1 | _extends: .github 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Dominic Tarr 4 | 5 | Permission is hereby granted, free of charge, 6 | to any person obtaining a copy of this software and 7 | associated documentation files (the "Software"), to 8 | deal in the Software without restriction, including 9 | without limitation the rights to use, copy, modify, 10 | merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom 12 | the Software is furnished to do so, 13 | subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice 16 | shall be included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 20 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 21 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR 22 | ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 23 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 24 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # graphreduce 2 | 3 | ```js 4 | var G = require('graphmitter') 5 | var g = {} 6 | 7 | // create edges 8 | G.edge(g, 1, 2) 9 | G.edge(g, 2, 3) 10 | G.edge(g, 3, 4) 11 | 12 | // delete an edge 13 | G.del(g, 3, 4) 14 | 15 | // iterate each node 16 | G.each(g, console.log) 17 | // => 1 { edges: { '2': true } } 18 | // => 2 { edges: { '3': true } } 19 | // => 3 { edges: { '2': true } } 20 | // => 4 { edges: {} } 21 | 22 | // g is a plain old javascript object 23 | g /* => 24 | { '1': { '2': true }, 25 | '2': { '3': true }, 26 | '3': {}, 27 | '4': {} } 28 | */ 29 | 30 | 31 | // rank the connectedness of nodes using a pagerank derivative 32 | G.rank(g) /* => 33 | { '1': 0.037500000000000006, 34 | '2': 0.25, 35 | '3': 0.25, 36 | '4': 0.037500000000000006 } 37 | */ 38 | G.edge(g, 3, 2) 39 | G.rank(g) /* => 40 | { '1': 0.037500000000000006, 41 | '2': 0.4625, 42 | '3': 0.25, 43 | '4': 0.037500000000000006 } 44 | */ 45 | 46 | // helper to generate a random graph 47 | var _g = G.random(100, 100) // 100 nodes, 100 edges 48 | ``` 49 | 50 | 51 | ## License 52 | 53 | MIT 54 | 55 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | function each(obj, iter) { 3 | for(var k in obj) iter(k, obj[k]) 4 | } 5 | function hasEdge (g, f, t) { 6 | return g[f] && Object.hasOwnProperty(g[f], t) 7 | } 8 | 9 | function addNode(g, n) { 10 | g[n] = g[n] || {} 11 | return g 12 | } 13 | 14 | function get (g, f, t) { 15 | if(t == null) throw new Error('not implemented') 16 | return g[f] && g[f][t] || null 17 | } 18 | 19 | function addEdge (g, from, to, data) { 20 | 21 | (g[from] = g[from] || {})[to] = (data === undefined ? true : data) 22 | return g 23 | } 24 | 25 | function removeEdge (g, from, to) { 26 | if('object' === typeof g[from]) delete g[from][to] 27 | return g 28 | } 29 | 30 | function eachEdge (g, iter) { 31 | each(g, function (from, n) { 32 | each(n, function (to, data) { 33 | iter(from, to, data) 34 | }) 35 | }) 36 | } 37 | 38 | //get a random node 39 | function randomNode (g) { 40 | var keys = Object.keys(g) 41 | return keys[~~(keys.length*Math.random())] 42 | } 43 | 44 | //add another subgraph 45 | function addGraph (g1, g2) { 46 | eachEdge(g2, function (from, to, data) { 47 | addEdge(g1, from, to, data) 48 | }) 49 | return g1 50 | } 51 | 52 | 53 | // 54 | // graph generators 55 | // 56 | 57 | function random (nodes, edges, prefix) { 58 | prefix = prefix || '#' 59 | if(isNaN(+nodes)) throw new Error('nodes must be a number') 60 | if(isNaN(+edges)) throw new Error('edges must be a number') 61 | 62 | var n = 0, g = {} 63 | 64 | function rand(n) { 65 | return prefix+~~(Math.random()*n) 66 | } 67 | 68 | for(var i = 0; i < nodes; i++) 69 | addNode(g, prefix+i) 70 | 71 | for(var i = 0; i < edges; i++) { 72 | var a = rand(nodes), b = rand(nodes) 73 | addEdge(g, a, b) 74 | addEdge(g, b, a) 75 | } 76 | 77 | return g 78 | } 79 | 80 | 81 | exports.random = random 82 | exports.each = each 83 | exports.addEdge = addEdge 84 | exports.hasEdge = hasEdge 85 | exports.removeEdge = removeEdge 86 | exports.eachEdge = eachEdge 87 | exports.addGraph = addGraph 88 | exports.get = get 89 | 90 | function count(obj) { 91 | var c = 0 92 | for(var k in obj) c++ 93 | return c 94 | } 95 | 96 | exports.rank = function (g, opts) { 97 | opts = opts || {} 98 | 99 | var ranks = {}, links = {}, _ranks = {} 100 | var N = count(g) 101 | var iterations = opts.iterations || 1 102 | var damping = opts.damping || 0.85 103 | var init = (1 - damping) / N 104 | 105 | //initialize 106 | each(g, function (k, n) { 107 | ranks[k] = 1/N; _ranks[k] = init 108 | links[k] = count(n) 109 | }) 110 | 111 | while(iterations --> 0) { 112 | 113 | //iteration 114 | each(g, function (j, n) { 115 | var r = damping*(ranks[j]/links[j]) 116 | each(n, function (k) { _ranks[k] += r }) 117 | }) 118 | 119 | //reset 120 | for(var k in ranks) 121 | ranks[k] = init 122 | 123 | var __ranks = ranks 124 | ranks = _ranks 125 | _ranks = __ranks 126 | } 127 | return ranks 128 | } 129 | 130 | exports.hops = function (g, start, initial, max, seen) { 131 | if(!g[start]) return {} 132 | var visited = {} 133 | var queue = [start] 134 | visited[start] = initial 135 | while(queue.length) { 136 | var node = queue.shift() 137 | var h = visited[node] 138 | for(var k in g[node]) { 139 | if( 140 | visited[k] == null 141 | && (!seen || (seen[k] == null || seen[k] > h+1)) 142 | && h < max 143 | ) { 144 | queue.push(k) 145 | visited[k] = h + 1 146 | } 147 | } 148 | } 149 | return visited 150 | } 151 | 152 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphreduce", 3 | "version": "3.0.4", 4 | "description": "construct graphs as reduce function", 5 | "homepage": "https://github.com/ssbc/graphreduce", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/ssbc/graphreduce.git" 9 | }, 10 | "author": { 11 | "name": "Dominic Tarr", 12 | "email": "dominic.tarr@gmail.com", 13 | "url": "http://dominictarr.com" 14 | }, 15 | "dependencies": { 16 | "statistics": "^3.3.0" 17 | }, 18 | "devDependencies": { 19 | "tape": "~3.0.3" 20 | }, 21 | "scripts": { 22 | "prepublish": "npm ls && npm test", 23 | "test": "set -e; for t in test/*.js; do node $t; done" 24 | }, 25 | "license": "MIT" 26 | } 27 | -------------------------------------------------------------------------------- /test/hops.js: -------------------------------------------------------------------------------- 1 | 2 | var tape = require('tape') 3 | var G = require('../') 4 | 5 | function group (h) { 6 | var total = {} 7 | for(var i in h) 8 | total[h[i]] = (total[h[i]] || 0) + 1 9 | return total 10 | } 11 | 12 | function first (set, iter) { 13 | for(var k in set) 14 | if(iter(set[k], k)) return k 15 | } 16 | 17 | function _merge(a, b) { 18 | for(var k in b) a[k] = b[k] 19 | return a 20 | } 21 | function merge(a, b) { 22 | return _merge(_merge({}, a), b) 23 | } 24 | 25 | tape('test adding one edge', function (t) { 26 | 27 | // t.plan(10) 28 | 29 | var N = 100, E = 200 30 | var g = G.random(N, E) 31 | 32 | //ensure there is an edge from 0->1 so the test always passes. 33 | //G.addEdge(g, '#0', '#1') 34 | 35 | var h1 = G.hops(g, '#0', 0, 1) 36 | var n_h1 = Object.keys(h1).length 37 | t.ok(n_h1 < N) 38 | t.ok(n_h1 > 0) 39 | 40 | 41 | var h2 = G.hops(g, '#0', 0, 2) 42 | console.log(h1, h2) 43 | var n_h2 = Object.keys(h2).length 44 | t.ok(n_h2 < N) 45 | t.ok(n_h2 > 0) 46 | console.log(n_h1, n_h2) 47 | t.ok(n_h1 < n_h2, 'h1 less than h2') 48 | 49 | var h3 = G.hops(g, '#0', 0, 3) 50 | var n_h3 = Object.keys(h3).length 51 | t.ok(n_h3 <= N) 52 | t.ok(n_h3 > 0) 53 | t.ok(n_h2 < n_h3) 54 | 55 | console.log(n_h1, n_h2, n_h3) 56 | console.log(h3) 57 | for(var k in h1) 58 | t.ok(!isNaN(h2[k])) 59 | for(var k in h2) 60 | t.ok(!isNaN(h3[k])) 61 | 62 | var k = first(h3, function (v, k) { 63 | return h1[k] == null 64 | }) 65 | 66 | console.log('add', k) 67 | G.addEdge(g, '#0', k) 68 | 69 | var h2b = G.hops(g, '#0', 0, 2) 70 | t.ok(Object.keys(h2b).length > Object.keys(h2).length) 71 | 72 | var h2c = G.hops(g, k, h2['#0'] + 1, 2, h2) 73 | console.log(h2b, h2c) 74 | 75 | t.notDeepEqual(h2b, h2) 76 | t.notDeepEqual(h2b, h2c) 77 | 78 | var keys = [] 79 | 80 | for(var k in h2) 81 | keys.push(k) 82 | 83 | for(var k in h2c) 84 | if(h2[k] == null) { 85 | console.log('k', k, h2[k], h2c[k]) 86 | } else 87 | console.log('repeat', k, h2c[k], h2[k]) 88 | 89 | console.log(keys) 90 | 91 | t.deepEqual(merge(h2, h2c), h2b) 92 | 93 | t.end() 94 | 95 | 96 | // console.log('NEW', h) 97 | // 98 | // var i = 0, __hops 99 | // 100 | // G.addEdge(g, '#1', '#new') 101 | // G.addEdge(g, '#0', '#new') 102 | // 103 | // var h2 = G.traverse(g, {start: '#0', hops: 2}) 104 | // t.equal(h2['#new'], 1) 105 | // console.log(h) 106 | // console.log(h2) 107 | // 108 | // t.equal(Object.keys(h2).length, Object.keys(h).length + 1) 109 | // 110 | // t.end() 111 | }) 112 | 113 | tape('empty graph', function (t) { 114 | var hops = G.hops({}, '#0', 0, 2) 115 | t.deepEqual(hops, {}) 116 | t.end() 117 | }) 118 | 119 | tape('single edge graph', function (t) { 120 | var g = {} 121 | G.addEdge(g, '#0', '#1') 122 | var hops = G.hops(g, '#0', 0, 2) 123 | console.log(hops) 124 | t.deepEqual(hops, {'#0':0, '#1': 1}) 125 | t.end() 126 | }) 127 | 128 | 129 | 130 | return 131 | tape('add a whole graph', function (t) { 132 | 133 | var g1 = G.random(10, 30, '#') 134 | var g2 = G.random(3, 10, '@') 135 | 136 | G.addGraph(g1, g2) 137 | 138 | var hops = G.traverse(g1, {start: '#0', hops: 5}) 139 | 140 | function min (a, b) { 141 | return null == a ? b : Math.min(a, b) 142 | } 143 | 144 | G.traverse(g1, {start: '#0', hops: 5, old: false}, function (from, to, h) { 145 | hops[to] = min(hops[to], h) 146 | }) 147 | 148 | G.addGraph(g1, g2) 149 | 150 | t.equal(Object.keys(g1).length, 13) 151 | 152 | // G.addEdge(g1, '#1', '@2') 153 | 154 | 155 | 156 | console.log(hops) 157 | 158 | // t.deepEqual(g1.traverse({start: '#0', hops: 5}), hops) 159 | // console.log(g1.toJSON()) 160 | t.end() 161 | 162 | }) 163 | 164 | return 165 | 166 | tape('test adding a branch.', function (t) { 167 | 168 | var g = {} 169 | var edges = [], expected = [ 170 | ['#0', '#1', 1, undefined], 171 | ['#1', '#2', 2, undefined], 172 | ['#1', '#3', 2, undefined] 173 | ] 174 | //single node graph 175 | g.node('#0') 176 | 177 | t.deepEqual(g.traverse({start: '#0'}), {'#0': 0}) 178 | g.traverse({start: '#0', each: function (from, to, h, _h) { 179 | edges.push([from, to, h, _h]) 180 | }}) 181 | 182 | g.edge('#1', '#2') 183 | g.edge('#1', '#3') 184 | t.deepEqual([], edges) 185 | 186 | g.edge('#0', '#1') 187 | 188 | t.deepEqual(expected, edges) 189 | 190 | t.end() 191 | }) 192 | 193 | tape('test shortening a chain', function (t) { 194 | 195 | var edges = [], expected = [ 196 | ['#0', '#1', 1, undefined], 197 | ['#1', '#2', 2, undefined], 198 | ['#2', '#3', 3, undefined], 199 | ['#3', '#4', 4, undefined], 200 | 201 | ['#0', '#2', 1, 2], 202 | ['#2', '#3', 2, 3], 203 | ['#3', '#4', 3, 4] 204 | ] 205 | var g = Graphmitter() 206 | .edge('#0', '#1').edge('#1', '#2') 207 | .edge('#2', '#3').edge('#3', '#4') 208 | 209 | g.traverse({start: '#0', each: function (from, to, h, _h) { 210 | edges.push([from, to, h, _h]) 211 | }}) 212 | 213 | t.deepEqual(edges, expected.slice(0, 4)) 214 | 215 | g.edge('#0', '#2') 216 | 217 | t.deepEqual(edges, expected) 218 | 219 | t.end() 220 | }) 221 | 222 | tape('test cancel the edge listener', function (t) { 223 | 224 | var edges = [], expected = [ 225 | ['#0', '#1', 1, undefined], 226 | ['#0', '#2', 1, undefined] 227 | ] 228 | 229 | var g = Graphmitter() 230 | .edge('#0', '#1') 231 | 232 | var cancel = g.traverse({start: '#0', each: function (f,t,h,_h) { 233 | edges.push([f,t,h,_h]) 234 | }}) 235 | 236 | t.deepEqual(expected.slice(0, 1), edges) 237 | 238 | g.edge('#0', '#2') 239 | 240 | t.deepEqual(expected.slice(0, 2), edges) 241 | cancel() 242 | 243 | g.edge('#1', '#3') 244 | g.edge('#2', '#3') 245 | 246 | t.deepEqual(expected.slice(0, 2), edges) 247 | t.end() 248 | }) 249 | 250 | tape('join two paths', function (t) { 251 | 252 | var edges = [], expected = [ 253 | ['#0', '#1', 1, undefined], 254 | ['#0', '#2', 1, undefined], 255 | //3->4 is not connected. 256 | 257 | //3 (& 4) to the graph 258 | ['#1', '#3', 2, undefined], 259 | ['#3', '#4', 3, undefined], 260 | 261 | 262 | // ['#2', '#3', 2, 2] 263 | 264 | ] 265 | 266 | var g = Graphmitter() 267 | .edge('#0', '#1') 268 | 269 | var cancel = g.traverse({start: '#0', each: function (f, t, h, _h) { 270 | edges.push([f,t,h,_h]) 271 | }}) 272 | 273 | t.deepEqual(expected.slice(0, 1), edges) 274 | 275 | g.edge('#0', '#2').edge('#3', '#4') 276 | 277 | t.deepEqual(expected.slice(0, 2), edges) 278 | 279 | g.edge('#1', '#3') 280 | t.deepEqual(expected.slice(0, 4), edges) 281 | 282 | g.edge('#2', '#3') 283 | 284 | t.deepEqual(expected.slice(0, 4), edges) 285 | 286 | t.deepEqual(Graphmitter.fromJSON(g.toJSON()).rank(), g.rank()) 287 | console.log(edges) 288 | t.end() 289 | 290 | }) 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | var tape = require('tape') 4 | var G = require('../') 5 | 6 | tape('get', function (t) { 7 | 8 | var g = G.random(10, 30) 9 | 10 | // G.each(g, function (key, node) { 11 | // t.equal(node, G.get(g, key)) 12 | // }) 13 | // 14 | G.eachEdge(g, function (src, dst, v) { 15 | console.log(src, dst) 16 | t.equal(G.get(g, src, dst), v) 17 | }) 18 | 19 | t.end() 20 | 21 | }) 22 | 23 | 24 | //RANDOMLY generate a graph and then check that it 25 | //has some reasonable properties. 26 | 27 | tape('hops', function (t) { 28 | 29 | //a is subset of b 30 | function subset(a, b) { 31 | for(var k in a) { 32 | t.equal(b[k], a[k]) 33 | } 34 | } 35 | 36 | var g = G.random(100, 300) 37 | 38 | var reachable = G.hops(g, '#0', 0, 3) //{start: '#0'}) 39 | 40 | var reachable2 = G.hops(g, '#0', 0, 2) //G.traverse(g, {start: '#0', hops: 2}) 41 | 42 | // var reachable3 = G G.traverse(g, {start: '#0', max: 20}) 43 | 44 | //var reachable4 = G.traverse(g, {start: '#0', hops: 2, max: 20}) 45 | 46 | t.ok(Object.keys(reachable).length) 47 | 48 | console.log('reachable2 is subset of reachable') 49 | subset(reachable2, reachable) 50 | console.log('reachable3 is subset of reachable') 51 | // subset(reachable3, reachable) 52 | 53 | 54 | //if the random graph happened not to have 20 nodes within 2 hops 55 | //then reachable3 will be larger than reachable2. 56 | 57 | // if(Object.keys(reachable2).length > 20) { 58 | // console.log('reachable3 is subset of reachable2') 59 | // subset(reachable3, reachable2) 60 | // } else { 61 | // console.log('reachable2 is subset of reachable3') 62 | // subset(reachable2, reachable3) 63 | // } 64 | // 65 | // //since reachable4 is either 2 hops or 20 nodes 66 | // //it's always the subset of either 2 or 3. 67 | // 68 | // subset(reachable4, reachable3) 69 | // subset(reachable4, reachable2) 70 | 71 | t.end() 72 | }) 73 | 74 | 75 | //make sure the empty graph does not throw 76 | tape('empty graph', function (t) { 77 | var g = G.random(0,0) 78 | var o = G.hops(g, 'a', 0, 3) //G.traverse(g, {start:'a'}) 79 | t.deepEqual(o, {}) 80 | t.end() 81 | }) 82 | 83 | -------------------------------------------------------------------------------- /test/pagerank.js: -------------------------------------------------------------------------------- 1 | var statistics = require('statistics') 2 | var G = require('../') 3 | var tape = require('tape') 4 | 5 | tape('pagerank', function (t) { 6 | 7 | var g = {} 8 | 9 | 10 | G.addEdge(g, 'A', 'C') 11 | G.addEdge(g, 'B', 'A') 12 | G.addEdge(g, 'A', 'C') 13 | G.addEdge(g, 'C', 'A') 14 | G.addEdge(g, 'D', 'A') 15 | G.addEdge(g, 'D', 'B') 16 | G.addEdge(g, 'D', 'C') 17 | 18 | var r = G.rank(g, {}) 19 | 20 | console.log(r) 21 | 22 | t.deepEqual(r, 23 | { A: 0.5333333333333333, 24 | C: 0.3208333333333333, 25 | B: 0.10833333333333334, 26 | D: 0.037500000000000006 27 | }) 28 | 29 | t.end() 30 | 31 | }) 32 | 33 | tape('random graph', function (t) { 34 | var total, N = 100 35 | for(var i = 0; i < N; i++) { 36 | var g = G.random(20, 30) 37 | var ranks = G.rank(g, {iterations: 15}) 38 | var sum = 0 39 | for(var k in ranks) 40 | sum += ranks[k] 41 | total = statistics(total, sum) 42 | // console.log(sum) 43 | } 44 | console.log(total) 45 | //rank should be around 1. 46 | t.ok(total.mean + total.stdev*2 > 1) 47 | t.ok(total.mean - total.stdev*2 < 1) 48 | 49 | t.end() 50 | }) 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /traverse.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | // 3 | // Algorithms 4 | // 5 | 6 | // probably move these to another file when there get to be lots of them. 7 | 8 | exports.hops = function (g, start, initial, max, seen) { 9 | var visited = {} 10 | var queue = [start] 11 | visited[start] = initial 12 | while(queue.length) { 13 | var node = queue.shift() 14 | var h = visited[node] 15 | // console.log('EXPAND', node, h) 16 | for(var k in g[node]) { 17 | // console.log(node, k, visited[k], h) 18 | if( 19 | visited[k] == null 20 | && (!seen || (seen[k] == null || seen[k] > h+1)) 21 | && h < max 22 | ) { 23 | queue.push(k) 24 | visited[k] = h + 1 25 | } 26 | } 27 | } 28 | return visited 29 | } 30 | 31 | 32 | //mutates `reachable`, btw 33 | function widthTraverse (graph, reachable, start, depth, hops, iter) { 34 | if(!start) 35 | throw new Error('Graphmitter#traverse: start must be provided') 36 | 37 | //var nodes = 1 38 | 39 | reachable[start] = reachable[start] == null ? 0 : reachable[start] 40 | 41 | var queue = [start] //{key: start, hops: depth}] 42 | iter = iter || function () {} 43 | while(queue.length) { 44 | var o = queue.shift() 45 | var h = reachable[o] 46 | var node = graph[o] 47 | if(node && (!hops || (h + 1 <= hops))) 48 | for(var k in node) { 49 | // If we have already been to this node by a shorter path, 50 | // then skip this node (this only happens when processing 51 | // a realtime edge) 52 | if(!(reachable[k] != null && reachable[k] < h + 1)) { 53 | if(false === iter(o, k, h + 1, reachable[k])) 54 | return reachable 55 | 56 | reachable[k] = h + 1 57 | // nodes ++ 58 | queue.push(k) 59 | } 60 | } 61 | } 62 | 63 | return reachable 64 | } 65 | 66 | // traverse(g, start, opts, onEach) => seen 67 | 68 | // batch(g, seen, ary, {hops: h}, onEach) => seen 69 | 70 | exports.traverse = function (g, opts, onEach) { 71 | var maxHops = opts.hops || 3 72 | var reachable = {} 73 | opts.each = onEach = onEach || opts.each 74 | 75 | console.log(maxHops, opts) 76 | 77 | widthTraverse( 78 | g, reachable, 79 | opts.start, 80 | 0, //initial hops 81 | maxHops, //max hops 82 | opts.old !== false && onEach 83 | ) 84 | 85 | if(!onEach || opts.live === false) return reachable 86 | 87 | function onEdge (from, to) { 88 | //if this edge is part of the initial set 89 | if(reachable[from] != null && reachable[from] < maxHops) { 90 | //edges to new nodes. 91 | var h = reachable[from] + 1 92 | var _h = reachable[to] 93 | if(_h == null) 94 | onEach(from, to, reachable[to] = h, _h) 95 | else if(Math.min(h, _h) != _h) 96 | onEach(from, to, reachable[to] = Math.min(h, _h), _h) 97 | 98 | //this is used only for realtime adds. 99 | if(h <= maxHops && h != _h) { 100 | //also add other nodes that are now reachable. 101 | widthTraverse(self, reachable, to, h, maxHops, onEach) 102 | 103 | } 104 | } 105 | } 106 | } 107 | 108 | // page rank. I adapted the algorithm to use 109 | // forward links instead of backward links which means 110 | // we only have to traverse the graph one time. 111 | 112 | //find the shortest path between two nodes. 113 | //if there was no path within max hops, return null. 114 | 115 | //convert a spanning tree to an array. 116 | function toArray (span, root) { 117 | if(!span[root]) return null 118 | var a = [root] 119 | while(span[root]) 120 | a.push(root = span[root]) 121 | return a.reverse() 122 | } 123 | 124 | exports.path = function (opts) { 125 | var reverse = {} 126 | if(opts.source == opts.dest) 127 | return [opts.source] 128 | 129 | opts.start = opts.source 130 | opts.live = false 131 | opts.each = function (f, t, h) { 132 | reverse[t] = f 133 | } 134 | 135 | this.traverse(opts) 136 | return toArray(reverse, opts.dest) 137 | } 138 | 139 | --------------------------------------------------------------------------------