import base64 import os import re from abc import ABC, abstractmethod from pyvis.network import Network class FlowVisualizer(ABC): def __init__(self, flow): self.flow = flow self.colors = { "bg": "#FFFFFF", "start": "#FF5A50", "method": "#333333", "router": "#333333", # Dark gray for router background "router_border": "#FF8C00", # Orange for router border "edge": "#666666", "router_edge": "#FF8C00", # Orange for router edges "text": "#FFFFFF", } self.node_styles = { "start": { "color": self.colors["start"], "shape": "box", "font": {"color": self.colors["text"]}, }, "method": { "color": self.colors["method"], "shape": "box", "font": {"color": self.colors["text"]}, }, "router": { "color": self.colors["router"], "shape": "box", "font": {"color": self.colors["text"]}, "borderWidth": 2, "borderWidthSelected": 4, "borderDashes": [5, 5], # Dashed border "borderColor": self.colors["router_border"], }, } @abstractmethod def visualize(self, filename): pass class PyvisFlowVisualizer(FlowVisualizer): def visualize(self, filename): net = Network( directed=True, height="750px", width="100%", bgcolor=self.colors["bg"], layout=None, ) # Calculate levels for nodes node_levels = self._calculate_node_levels() print("node_levels", node_levels) # Assign positions to nodes based on levels y_spacing = 150 # Adjust spacing between levels (positive for top-down) x_spacing = 150 # Adjust spacing between nodes level_nodes = {} # Store node positions for edge calculations node_positions = {} for method_name, level in node_levels.items(): level_nodes.setdefault(level, []).append(method_name) # Compute positions for level, nodes in level_nodes.items(): x_offset = -(len(nodes) - 1) * x_spacing / 2 # Center nodes horizontally for i, method_name in enumerate(nodes): x = x_offset + i * x_spacing y = level * y_spacing # Use level directly for y position node_positions[method_name] = (x, y) # Store positions method = self.flow._methods.get(method_name) if hasattr(method, "__is_start_method__"): node_style = self.node_styles["start"] elif method_name in self.flow._routers.values(): node_style = self.node_styles["router"] else: node_style = self.node_styles["method"] net.add_node( method_name, label=method_name, x=x, y=y, fixed=True, physics=False, # Disable physics for fixed positioning **node_style, ) # Prepare data structures for edge calculations ancestors = self._build_ancestor_dict() parent_children = self._build_parent_children_dict() # Add edges for method_name in self.flow._listeners: condition_type, trigger_methods = self.flow._listeners[method_name] is_and_condition = condition_type == "AND" for trigger in trigger_methods: if trigger in self.flow._methods: is_router_edge = any( trigger in paths for paths in self.flow._router_paths.values() ) edge_color = ( self.colors["router_edge"] if is_router_edge else self.colors["edge"] ) # Determine if this edge forms a cycle is_cycle_edge = self._is_ancestor(method_name, trigger, ancestors) # Determine if parent has multiple children parent_has_multiple_children = ( len(parent_children.get(trigger, [])) > 1 ) # Determine if edge should be curved needs_curvature = is_cycle_edge or parent_has_multiple_children if needs_curvature: if is_cycle_edge: # For cycles, curve left and up edge_smooth = {"type": "curvedCCW", "roundness": 0.3} else: # For multiple children, adjust curvature to prevent overlap index = self._get_child_index( trigger, method_name, parent_children ) edge_smooth = { "type": "curvedCW" if index % 2 == 0 else "curvedCCW", "roundness": 0.2 + (0.1 * index), } else: edge_smooth = False # Draw straight line edge_style = { "color": edge_color, "width": 2, "arrows": "to", "dashes": True if is_router_edge or is_and_condition else False, "smooth": edge_smooth, } net.add_edge(trigger, method_name, **edge_style) # Add edges from router methods to their possible paths for router_method_name, paths in self.flow._router_paths.items(): for path in paths: for listener_name, ( condition_type, trigger_methods, ) in self.flow._listeners.items(): if path in trigger_methods: is_cycle_edge = self._is_ancestor( listener_name, router_method_name, ancestors ) # Determine if parent has multiple children parent_has_multiple_children = ( len(parent_children.get(router_method_name, [])) > 1 ) # Determine if edge should be curved needs_curvature = is_cycle_edge or parent_has_multiple_children if needs_curvature: if is_cycle_edge: # Curve left and up for cycles edge_smooth = {"type": "curvedCCW", "roundness": 0.3} else: # For multiple children, adjust curvature index = self._get_child_index( router_method_name, listener_name, parent_children ) edge_smooth = { "type": ( "curvedCW" if index % 2 == 0 else "curvedCCW" ), "roundness": 0.2 + (0.1 * index), } else: edge_smooth = False # Straight line edge_style = { "color": self.colors["router_edge"], "width": 2, "arrows": "to", "dashes": True, "smooth": edge_smooth, } net.add_edge(router_method_name, listener_name, **edge_style) # Set options to disable physics net.set_options( """ var options = { "physics": { "enabled": false } } """ ) network_html = net.generate_html() # Extract just the body content from the generated HTML match = re.search("