Skip to content

Add topic & node dependency graph#2

Merged
nshro merged 19 commits intomainfrom
feature/add-graph-panel
Aug 11, 2025
Merged

Add topic & node dependency graph#2
nshro merged 19 commits intomainfrom
feature/add-graph-panel

Conversation

@nshro
Copy link
Copy Markdown

@nshro nshro commented Aug 4, 2025

Overview

This introduces a new feature to visualize ROS2 entity dependencies as an interactive tree structure. It adds a dependency graph model, a dedicated UI panel, and integrates this into the entity inspection workflow.
output

Highlights

Dependency Graph Model

Implemented RosDependencyGraph and RosDependencyNode classes to recursively model hierarchical relationships between ROS entities.

Supports:

  • Node → Subscribed Topics
  • Topic → Publishing Nodes

Skips system topics (e.g., /parameter_events) to maintain clarity.

Graph UI Panel

Added a new widget: RosEntityGraphPanel that renders the dependency tree.

Features:

  • Lazy subtree expansion
  • Error handling (invalid/missing entity info)
  • Styling for nodes/topics (color & underline)
  • "Space" key to collapse/expand all items at the same level

Notes

Tree nodes are color-coded:

  • 🟩 Green for Nodes
  • ⚪ White for Topics
  • 🔶 Yellow for “No Publisher”
  • 🔴 Red for errors

@nshro nshro requested a review from Tiryoh August 4, 2025 05:58
Copy link
Copy Markdown

@Tiryoh Tiryoh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good patch! The dependency graph feature looks solid. I have a few suggestions for improvements.

Also, could you replace the demo movie in top-level README.md with the following URL?

https://github.com/user-attachments/assets/576b9ecd-81cd-4b26-948c-65cea6ce49af

Comment thread rtui2/ros/dependency_graph.py Outdated
self.root = self._build_graph(root_entity, depth=0, visited=set())

def _build_graph(
self, entity: RosEntity, depth: int, visited: set[str]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The visited parameter is unused here. Since max_depth already prevents infinite recursion, we can remove this parameter to clean up the interface.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! I forgot to remove them. bcc0547

Comment thread rtui2/widgets/graph_panel.py Outdated
Comment on lines +63 to +75
def update_graph(self) -> None:
if self._tree:
self._tree.remove()

if self._entity is None:
return

self._tree = Tree(TreeLabel.label(self._entity), data=self._entity)
self.mount(self._tree)

graph = RosDependencyGraph(self._entity, self._ros, max_depth=EXPANSION_DEPTH)
self._populate_tree(self._tree.root, graph.root)
self._tree.root.expand()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The node expansion logic is duplicated between on_tree_node_selected() and update_graph(). Both methods follow the same pattern:

  1. Create RosDependencyGraph
  2. Call _populate_tree()
  3. Expand the node

How about creating a helper method like _rebuild_node() to reduce duplication?

Suggested change
def update_graph(self) -> None:
if self._tree:
self._tree.remove()
if self._entity is None:
return
self._tree = Tree(TreeLabel.label(self._entity), data=self._entity)
self.mount(self._tree)
graph = RosDependencyGraph(self._entity, self._ros, max_depth=EXPANSION_DEPTH)
self._populate_tree(self._tree.root, graph.root)
self._tree.root.expand()
def update_graph(self) -> None:
if self._tree:
self._tree.remove()
if self._entity is None:
return
self._tree = Tree(TreeLabel.label(self._entity), data=self._entity)
self.mount(self._tree)
self._rebuild_node(self._tree.root, self._entity)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have consolidate the node expansion logic in _populate_tree() 340d4ab

Comment thread rtui2/widgets/graph_panel.py Outdated
Comment on lines +101 to +125
def on_tree_node_selected(self, event: Tree.NodeSelected) -> None:
node = event.node
entity = node.data # type: RosEntity | None

if not isinstance(entity, RosEntity):
return

if entity.type != RosEntityType.Node:
return

if node.children and all(child.is_expanded for child in node.children):
return

if self._subtree_depth(node) > EXPANSION_DEPTH:
return

try:
node._children.clear()
node._expanded = False

graph = RosDependencyGraph(entity, self._ros, max_depth=EXPANSION_DEPTH)
self._populate_tree(node, graph.root)
node.expand()
except Exception as e:
node.add_leaf(TreeLabel.ERROR(str(e)))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The conditional logic in on_tree_node_selected() could be extracted into _should_expand_node() for better readability.

Suggested change
def on_tree_node_selected(self, event: Tree.NodeSelected) -> None:
node = event.node
entity = node.data # type: RosEntity | None
if not isinstance(entity, RosEntity):
return
if entity.type != RosEntityType.Node:
return
if node.children and all(child.is_expanded for child in node.children):
return
if self._subtree_depth(node) > EXPANSION_DEPTH:
return
try:
node._children.clear()
node._expanded = False
graph = RosDependencyGraph(entity, self._ros, max_depth=EXPANSION_DEPTH)
self._populate_tree(node, graph.root)
node.expand()
except Exception as e:
node.add_leaf(TreeLabel.ERROR(str(e)))
def on_tree_node_selected(self, event: Tree.NodeSelected) -> None:
node = event.node
entity = node.data
if not self._should_expand_node(node, entity):
return
self._rebuild_node(node, entity)
def _should_expand_node(self, node: TreeNode, entity: RosEntity | None) -> bool:
if not isinstance(entity, RosEntity):
return False
if entity.type != RosEntityType.Node:
return False
if node.children and all(child.is_expanded for child in node.children):
return False
if self._subtree_depth(node) > EXPANSION_DEPTH:
return False
return True
def _rebuild_node(self, node: TreeNode, entity: RosEntity) -> None:
try:
node._children.clear()
node._expanded = False
graph = RosDependencyGraph(entity, self._ros, max_depth=EXPANSION_DEPTH)
self._populate_tree(node, graph.root)
node.expand()
except Exception as e:
node.add_leaf(TreeLabel.error(str(e)))

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I refactored on_tree_node_selected as you pointed out. Thanks! 226414d

@nshro
Copy link
Copy Markdown
Author

nshro commented Aug 6, 2025

@Tiryoh I have attached the screencast to README.md. bf42255
image

@nshro nshro requested a review from Tiryoh August 6, 2025 07:25
Copy link
Copy Markdown

@Tiryoh Tiryoh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@Tiryoh Tiryoh force-pushed the feature/add-graph-panel branch from 4114785 to 653f46c Compare August 11, 2025 03:25
@Tiryoh
Copy link
Copy Markdown

Tiryoh commented Aug 11, 2025

I accidentally pushed the documentation update to this branch. Please ignore it.

@nshro nshro merged commit ca23878 into main Aug 11, 2025
8 checks passed
@nshro nshro deleted the feature/add-graph-panel branch August 11, 2025 06:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants