@@ -54,6 +54,7 @@ def __init__(
5454 self .graph = None
5555 self .root : str = None
5656 self .nodes : dict [str , Node ] = {}
57+ self .prev_hover : Node = None
5758 self .hovered_node : Node = None
5859 self .selected_node : Node = None
5960 self .default_axis_range = default_axis_range
@@ -66,6 +67,8 @@ def __init__(
6667 self ._y_offset = 0.0
6768 # Plot-space positions from the last layout pass; used to transform to pixels
6869 self ._plot_positions : dict [str , tuple [float , float ]] = {}
70+ self ._graph_layers : list [str ] = None
71+ self ._rendered_nodes : set [str ] = set ()
6972
7073 # Set when visibility changes; cleared after layout is recomputed
7174 self ._layout_dirty : bool = False
@@ -167,6 +170,8 @@ def set_graph(self, graph: nx.DiGraph) -> None:
167170 self .graph = graph
168171
169172 if graph :
173+ # Topological order ensures parents are drawn (and positioned) before children
174+ self ._graph_layers = list (nx .topological_sort (graph ))
170175 self .root = next (n for n , in_deg in graph .in_degree () if in_deg == 0 )
171176
172177 for n , data in graph .nodes .items ():
@@ -189,14 +194,14 @@ def clear_highlights(self) -> None:
189194 def highlight_node (self , node : Node | str , color : style .RGBA = style .green ) -> None :
190195 if isinstance (node , Node ):
191196 node = node .id
192-
197+
193198 self ._manual_highlights [node ] = color
194199 self ._render_required = True
195200
196201 def unhighlight_node (self , node : Node | str ) -> None :
197202 if isinstance (node , Node ):
198203 node = node .id
199-
204+
200205 del self ._manual_highlights [node ]
201206 self ._render_required = True
202207
@@ -521,7 +526,7 @@ def fold_node(self, node: Node) -> None:
521526 def reveal (self , node : Node | str ) -> None :
522527 if not node :
523528 return
524-
529+
525530 if isinstance (node , str ):
526531 node = self .nodes [node ]
527532
@@ -602,6 +607,7 @@ def _render_graph(self, sender: str, app_data: list, user_data: Any) -> None:
602607 helper_data = app_data [0 ]
603608 mouse_x = helper_data ["MouseX_PixelSpace" ]
604609 mouse_y = helper_data ["MouseY_PixelSpace" ]
610+ self .prev_hover = self .hovered_node
605611 self .hovered_node = None
606612
607613 # One series anchor gives us offset once we know scale.
@@ -622,6 +628,9 @@ def _render_graph(self, sender: str, app_data: list, user_data: Any) -> None:
622628 self ._render_required = False
623629
624630 if self ._layout_dirty :
631+ dpg .delete_item (sender , children_only = True , slot = 2 )
632+ self ._rendered_nodes .clear ()
633+
625634 for node in self .nodes .values ():
626635 if node .visible and node .size is None :
627636 # Estimate size in plot space; layout uses plot-space units
@@ -631,49 +640,103 @@ def _render_graph(self, sender: str, app_data: list, user_data: Any) -> None:
631640 self ._render_required = True
632641 self ._layout_dirty = False
633642
634- dpg .delete_item (sender , children_only = True , slot = 2 )
635643 dpg .push_container_stack (sender )
636644 dpg .configure_item (sender , tooltip = False )
637645
638- # Topological order ensures parents are drawn (and positioned) before children
639- for n in nx .topological_sort (self .graph ):
646+ for n in self ._graph_layers :
640647 node = self .nodes [n ]
641- plot_pos = self ._plot_positions .get (n )
642- if not node .visible or plot_pos is None :
643- continue
644-
645- px , py = self ._to_pixel (* plot_pos )
646- pw_node , ph_node = (
647- self ._size_to_pixel (* node .size ) if node .size else (0.0 , 0.0 )
648- )
648+ if n not in self ._rendered_nodes :
649+ self ._draw_node (node , mouse_x , mouse_y )
650+ self ._rendered_nodes .add (n )
651+ else :
652+ self ._update_node (node , mouse_x , mouse_y )
649653
650- # Hit-test with pixel-space mouse coords and pixel-space box
651- if not self .hovered_node :
652- x1 , y1 = px , py
653- x2 , y2 = px + pw_node , py + ph_node
654- if x1 <= mouse_x < x2 and y1 <= mouse_y < y2 :
655- self .hovered_node = node
656-
657- if self .draw_edges :
658- for child_id in self .graph .successors (node .id ):
659- child_node = self .nodes [child_id ]
660- child_pos = self ._plot_positions .get (child_id )
661- if child_node .visible and child_pos is not None :
662- cx , cy = self ._to_pixel (* child_pos )
663- self ._draw_edge (
664- node , px , py , pw_node , ph_node , child_node , cx , cy
665- )
666-
667- self ._draw_node_box (node , px , py , pw_node , ph_node )
654+ if self .prev_hover != self .hovered_node :
655+ self ._set_node_hover_style (self .prev_hover )
656+ self ._set_node_hover_style (node )
668657
669658 dpg .pop_container_stack ()
670659
660+ def _node_tag (self , node : Node , suffix : str = None ) -> str :
661+ t = f"{ self .tag } _node_{ node .id } "
662+ if suffix :
663+ t += "_" + suffix
664+ return t
665+
666+ def _set_node_hover_style (self , node : Node ) -> None :
667+ if not node or not dpg .does_item_exist (self ._node_tag (node , "box" )):
668+ return
669+
670+ color = style .white
671+ thickness = 1
672+
673+ if self .select_enabled and node == self .selected_node :
674+ color = style .blue
675+ thickness = 2
676+ else :
677+ if node .id in self ._manual_highlights :
678+ color = self ._manual_highlights [node .id ]
679+
680+ if self .hover_enabled and node == self .hovered_node :
681+ thickness = 2
682+
683+ dpg .configure_item (self ._node_tag (node , "box" ), thickness = thickness , color = color )
684+
685+ def _update_node (self , node : Node , mouse_x : float , mouse_y : float ) -> None :
686+ plot_pos = self ._plot_positions .get (node .id )
687+ if not node .visible or plot_pos is None :
688+ return
689+
690+ px , py = self ._to_pixel (* plot_pos )
691+ pw , ph = self ._size_to_pixel (* node .size ) if node .size else (0.0 , 0.0 )
692+ margin = self .layout .text_margin
693+
694+ # Hit-test with pixel-space mouse coords and pixel-space box
695+ if not self .hovered_node :
696+ x1 , y1 = px , py
697+ x2 , y2 = px + pw , py + ph
698+ if x1 <= mouse_x < x2 and y1 <= mouse_y < y2 :
699+ self .hovered_node = node
700+
701+ dpg .configure_item (self ._node_tag (node , "box" ), pmin = (px , py ), pmax = (px + pw , py + ph ))
702+
703+ for i in range (9 ):
704+ line_tag = self ._node_tag (node , f"text_{ i } " )
705+ if dpg .does_item_exist (line_tag ):
706+ # TODO adjust py
707+ dpg .configure_item (line_tag , pos = (px + margin , py + margin ))
708+
709+ # TODO edges
710+
711+ def _draw_node (self , node : Node , mouse_x : float , mouse_y : float ) -> str :
712+ plot_pos = self ._plot_positions .get (node .id )
713+ if not node .visible or plot_pos is None :
714+ return
715+
716+ px , py = self ._to_pixel (* plot_pos )
717+ pw , ph = self ._size_to_pixel (* node .size ) if node .size else (0.0 , 0.0 )
718+
719+ # Hit-test with pixel-space mouse coords and pixel-space box
720+ if not self .hovered_node :
721+ x1 , y1 = px , py
722+ x2 , y2 = px + pw , py + ph
723+ if x1 <= mouse_x < x2 and y1 <= mouse_y < y2 :
724+ self .hovered_node = node
725+
726+ if self .draw_edges :
727+ for child_id in self .graph .successors (node .id ):
728+ child_node = self .nodes [child_id ]
729+ child_pos = self ._plot_positions .get (child_id )
730+ if child_node .visible and child_pos is not None :
731+ cx , cy = self ._to_pixel (* child_pos )
732+ self ._draw_edge (node , px , py , pw , ph , child_node , cx , cy )
733+
734+ return self ._draw_node_box (node , px , py , pw , ph )
735+
671736 def _draw_node_box (
672- self , node : Node , px : float , py : float , pixel_w : float , pixel_h : float
737+ self , node : Node , px : float , py : float , pw : float , ph : float
673738 ) -> None :
674- tag = f"{ self .tag } _node_{ node .id } "
675-
676- if dpg .does_item_exist (tag ):
739+ if dpg .does_item_exist (self ._node_tag (node , "box" )):
677740 return
678741
679742 scale = self .zoom_factor
@@ -694,26 +757,13 @@ def _draw_node_box(
694757 max_len = max (len (s ) for s in lines )
695758 lines = [s .center (max_len ) for s in lines ]
696759
697- edge_color = style .white
698- thickness = 1
699-
700- if self .select_enabled and node == self .selected_node :
701- edge_color = style .blue
702- thickness = 2
703- else :
704- if node .id in self ._manual_highlights :
705- edge_color = self ._manual_highlights [node .id ]
706-
707- if self .hover_enabled and node == self .hovered_node :
708- thickness = 2
709-
710760 dpg .draw_rectangle (
711761 (px , py ),
712- (px + pixel_w , py + pixel_h ),
762+ (px + pw , py + ph ),
713763 fill = style .dark_grey ,
714- color = edge_color ,
715- thickness = thickness ,
716- tag = f" { tag } _box" ,
764+ color = style . white ,
765+ thickness = 1 ,
766+ tag = self . _node_tag ( node , "box" ) ,
717767 )
718768
719769 for i , text in enumerate (lines ):
@@ -722,6 +772,7 @@ def _draw_node_box(
722772 text ,
723773 size = 12 * scale ,
724774 color = colors [i ],
775+ tag = self ._node_tag (node , f"text_{ i } " ),
725776 )
726777
727778 def _draw_edge (
0 commit comments