BFS seems not to work
flowt-au opened this issue · comments
I am learning Cytoscape.js (migrating from Graphology). I have an issue with Breadth First Search which is probably me not understanding it properly.
The issue is no matter what I do, I can only get it to visit the root node - it doesn't traverse down the tree. Looking at the docs it seems the bfs()
might be designed to only return the target node (which is what it is doing). If that is the case, what function will return the output shown at the end of my question (below)?
Here is a screenshot of the graph.

Here is my Vue file:
<template>
<q-page class="flex full-width full-height">
<div class="col q-pa-md">
<div class="column full-width full-height" style="">
<!-- border: 1px solid red; -->
<div class="test-h6">Animals Example Graph</div>
<h4 v-if="loading">Loading</h4>
<h4 v-if="error" class="text-danger">{{ error }}</h4>
<div id="cy" class="cy col full-height"></div>
<!-- style="border: 1px solid blue;" -->
</div>
</div>
</q-page>
</template>
<script setup>
// From: https://yajanarao.medium.com/create-a-data-flow-map-using-cytoscape-and-vue-js-5be3b3ef11d2
import { onMounted, ref } from 'vue'
import cydagre from 'cytoscape-dagre'
import cytoscape from 'cytoscape'
const loading = ref(false)
const error = ref(null)
const nodes = [
{ data: { id: 0, name: 'Animal: 0', description: '', active: true, width: 140 } },
{ data: { id: 1, name: 'Mammal: 1', description: '', active: false, width: 140 } },
{ data: { id: 2, name: 'Reptile: 2', description: '', active: false, width: 140 } },
{ data: { id: 3, name: 'Horse: 3', description: '', active: false, width: 140 } },
{ data: { id: 4, name: 'Dog: 4', description: 'Join', active: false, width: 140 } },
{ data: { id: 5, name: 'Goat: 5', description: 'Branch Out', active: false, width: 140 } },
{ data: { id: 6, name: 'Hound: 6', description: '', active: false, width: 140 } },
{ data: { id: 7, name: 'German Shephard: 7', description: '', active: false, width: 140 } },
]
const edges = [
{ data: { source: 0, target: 1, label: 'Sub 1' } },
{ data: { source: 0, target: 2, label: 'Sub 2' } },
{ data: { source: 1, target: 3, label: 'Sub 3' } },
{ data: { source: 1, target: 4, label: 'Sub 4' } },
{ data: { source: 1, target: 5, label: 'Sub 5' } },
{ data: { source: 4, target: 6, label: 'Sub 6' } },
{ data: { source: 4, target: 7, label: 'Sub 7' } },
]
const drawGraph = () => {
console.clear()
console.log('Start drawGraph=', nodes, edges)
//See: https://github.com/cytoscape/cytoscape.js-dagre
cytoscape.use(cydagre)
cydagre(cytoscape)
// See: https://github.com/cytoscape/cytoscape.js
const cy = cytoscape({
container: document.getElementById('cy'),
boxSelectionEnabled: false,
autounselectify: true,
style: cytoscape
.stylesheet()
.selector('node')
.css({ shape: 'roundrectangle', height: 40, width: 'data(width)', 'background-color': (node) => (node.data('active') ? 'green' : 'white'), color: (node) => (node.data('active') ? 'white' : 'black'), 'border-color': 'gray', 'border-width': 3, 'border-radius': 4, content: 'data(name)', 'text-wrap': 'wrap', 'text-valign': 'center', 'text-halign': 'center', })
.selector('edge')
.css({ label: 'data(label)', 'text-outline-color': 'white', 'text-outline-width': 3, 'text-valign': 'top', 'text-halign': 'left', 'curve-style': 'bezier', width: 3, 'target-arrow-shape': 'triangle', 'line-color': 'gray', 'target-arrow-color': 'gray', }),
elements: {
nodes: nodes,
edges: edges,
},
layout: {
name: 'dagre',
spacingFactor: 1.5,
rankDir: 'TB', // or RL, LR, BT. 'TB' for top to bottom flow, 'LR' for left to right,
fit: true,
},
})
// You can dynamically add nodes and edges like this:
cy.add({ group: 'nodes', data: { id: 10, name: 'Human 10', description: '', active: false, width: 140 }, position: { x: 1, y: 1 }, })
cy.add({ group: 'edges', data: { source: 1, target: 10, label: 'Sub 10' } })
setTimeout(() => {
console.log( 'All nodes:', cy.nodes().map((n) => n.id()),
)
console.log( 'All edges:', cy.edges().map((e) => `${e.source().id()} -> ${e.target().id()}`), )
console.log('Node #0 exists:', cy.$('#0').length > 0)
console.log( 'Edges from node #0:', cy .$('#5') .connectedEdges() .map((e) => `${e.source().id()} -> ${e.target().id()}`), )
// Check edge directions
cy.edges().forEach((edge) => {
console.log( `Edge: ${edge.id()}, Source: ${edge.source().id()}, Target: ${edge.target().id()}`, )
})
// Check what edges are reachable from node 0
console.log( 'Outgoing edges from #0:', cy .$('#0') .outgoers('edge') .map((e) => e.id()), )
console.log( 'All connected edges from #0:', cy .$('#0') .connectedEdges() .map((e) => e.id()), )
// Function to get all nodes below a starting node and their depths
const getNodesBelowWithDepth = (startNodeId) => {
const result = new Map() // Map to store node -> depth
// BFS to find all nodes below the starting node
cy.elements().bfs({
roots: `#${startNodeId}`,
visit: (v, e, u, i, depth) => {
console.log( 'BFS visit =', v.id(), e ? e.data().attributes.type : 'no edge', u ? u.id() : 'no prev', i, depth, )
// Only add nodes that are not the starting node
if (v.id() !== startNodeId) {
result.set(v.id(), depth)
}
return true
},
directed: true, // Only follow edges in their direction
})
return result
}
// Example: Get all nodes below node 1 (Mammal)
const nodesBelow = getNodesBelowWithDepth(1)
console.log('Nodes below node 1 (Mammal):')
nodesBelow.forEach((depth, nodeId) => {
const node = cy.getElementById(nodeId)
console.log(`Node ${nodeId} (${node.data('name')}) is at depth ${depth}`)
})
}, 100)
}
onMounted(() => {
loading.value = true
try {
drawGraph()
} catch (e) {
error.value = e
} finally {
loading.value = false
}
})
</script>
<style lang="scss" scoped>
#cy {
border: 1px solid black;
}
</style>
And here is the console log:
All nodes: (9) ['0', '1', '2', '3', '4', '5', '6', '7', '10']
All edges: (8) ['0 -> 1', '0 -> 2', '1 -> 3', '1 -> 4', '1 -> 5', '4 -> 6', '4 -> 7', '1 -> 10']
Node #0 exists: true
Edges from node #0: ['1 -> 5']
Edge: d0cff7bc-02b2-4fd4-8435-718cb3854819, Source: 0, Target: 1
Edge: 5f9dd58e-9179-4ca8-bb19-ed216125f6df, Source: 0, Target: 2
Edge: 900afc00-bdd4-42f8-9ab2-c1b6b8714751, Source: 1, Target: 3
Edge: ae6786bc-0e05-4bb3-9b0b-a5935b0766aa, Source: 1, Target: 4
Edge: 8526bd0b-d5ba-4b32-8594-ec74c4c67815, Source: 1, Target: 5
Edge: a9de7feb-8888-4100-955b-b7147002d05a, Source: 4, Target: 6
Edge: a546126e-c381-440f-ac5d-e3358b92fcad, Source: 4, Target: 7
Edge: f254394f-f530-4b4d-92b9-a3671d4c5a1c, Source: 1, Target: 10
Outgoing edges from #0: (2) ['d0cff7bc-02b2-4fd4-8435-718cb3854819', '5f9dd58e-9179-4ca8-bb19-ed216125f6df']
All connected edges from #0: (2) ['d0cff7bc-02b2-4fd4-8435-718cb3854819', '5f9dd58e-9179-4ca8-bb19-ed216125f6df']
BFS visit = 1 no edge no prev 0 0
Nodes below node 1 (Mammal):
Node 1 (Mammal: 1) is at depth 0
But what I am not getting is:
Node 3 (Horse) is at depth 1
Node 4 (Dog) is at depth 1
Node 5 (Goat) is at depth 1
Node 6 (Hound) is at depth 2
Node 7 (German Shephard) is at depth 2
Node 10 (Human) is at depth
Thanks,
Murray
This workaround does what I was expecting. I am just wondering if I completely misunderstood the BFS().
// Function to get all nodes below a starting node and their depths
const getNodesBelowWithDepth = (startNodeId) => {
const result = new Map() // Map to store node -> depth
const queue = [{ node: cy.getElementById(startNodeId), depth: 0 }]
const visited = new Set([startNodeId])
while (queue.length > 0) {
const { node, depth } = queue.shift()
// Get all outgoing nodes (children)
const children = node.outgoers('node')
children.forEach((child) => {
if (!visited.has(child.id())) {
visited.add(child.id())
result.set(child.id(), depth + 1)
queue.push({ node: child, depth: depth + 1 })
}
})
}
return result
}
// Example: Get all nodes below node 1 (Mammal)
const nodesBelow = getNodesBelowWithDepth(1)
console.log('Nodes below node 1 (Mammal):')
nodesBelow.forEach((depth, nodeId) => {
const node = cy.getElementById(nodeId)
console.log(`Node ${nodeId} (${node.data('name')}) is at depth ${depth}`)
})
Outputs:
Nodes below node 1 (Mammal):
Node 3 (Horse: 3) is at depth 1
Node 4 (Dog: 4) is at depth 1
Node 5 (Goat: 5) is at depth 1
Node 10 (Human 10) is at depth 1
Node 6 (Hound: 6) is at depth 2
Node 7 (German Shephard: 7) is at depth 2
You should return true
only if/when you're done searching
Thanks. I tried return false
and also removed the return
altogether, but it is still only visiting the starting node.
Interesting. Yes, it uses the breadth first layout, but that is not the same as using the bfs()
method when there is no layout. My example does use a layout just so I can verify the graph looks like I imagine it, but what I was trying to test was the bfs() method separately from there being a layout.
Most of my app uses the graph db as the in-memory database and I need to manipulate the data in various ways. One way is to use a bfs() to find certain nodes and edges below a given "root" / starting node. Hence my attempts to use the bfs() method, not in a layout.
Thanks again.
Closing for now. Feel free to open up a discussion in future