Source code for orkes.visualizer.generator

import json
import itertools
from typing import Union, Dict, List, Optional
from pathlib import Path
import os
import argparse
from orkes.shared.utils import format_elapsed_time, format_start_time

# Default color palette for function nodes in the visualization.
DEFAULT_FUNCTION_NODE_COLORS = [
    "#E3FAFC", "#D0EBFF", "#C5F6FA", "#E5DBFF", "#D0BFFF",
    "#EDF2FF", "#E9ECEF", "#F1F3F5", "#DEE2E6", "#F3F0FF",
]

[docs] class TraceInspector: """A class to generate Vis.js compatible HTML visualizations from trace data. This class takes a trace of a graph's execution and generates an interactive HTML file that visualizes the graph's structure and execution flow. Attributes: template_path (Path): The path to the HTML template file. node_colors (itertools.cycle): An iterator for cycling through the default node colors. html_template (str): The content of the HTML template file. """
[docs] def __init__(self, template_path: Optional[str] = None): """Initializes the TraceInspector. Args: template_path (Optional[str], optional): The path to the HTML template. If not provided, it defaults to 'inspector_template.html' in the same directory as this file. Raises: FileNotFoundError: If the template file cannot be found. """ if template_path: self.template_path = Path(template_path) else: self.template_path = Path(__file__).parent / "inspector_template.html" if not self.template_path.exists(): raise FileNotFoundError(f"Template not found at: {self.template_path}") self.node_colors = itertools.cycle(DEFAULT_FUNCTION_NODE_COLORS) self.html_template = self.template_path.read_text(encoding='utf-8')
def _build_title_card(self, title_data: dict = {}) -> str: """Generates the HTML for the title card in the visualization. Args: title_data (dict, optional): A dictionary of data to display in the title card. Defaults to {}. Returns: str: The HTML for the title card. """ data = title_data main_title = data.pop('page_title', 'Orkes Trace Visualizer') status_color_wheel = { "FINISHED": "#007bff", "FAILED": "#dc3545", "INTERRUPTED": "#ffc107", } body_rows = [] status = data.get("Status", "").upper() status_color = status_color_wheel.get(status, "#6c757d") header_html = f''' <div class="title-header"> <div class="title-main"> <span style="color:{status_color};">&#9679;</span> {main_title} </div> <button class="min-btn-title" onclick="toggleTitleCard()" title="Minimize/Maximize">&minus;</button> </div> ''' for key, value in data.items(): if isinstance(value, (dict, list)): continue label = key.replace("_", " ").title().replace("Id", "ID") val_str = str(value) row = f'<div class="prop-row"><span class="key">{label}:</span><div class="title-sub">{val_str}</div></div>' body_rows.append(row) body_html = f'<div id="title-card-body">{"".join(body_rows)}</div>' return header_html + body_html def _get_next_color(self) -> str: """Returns the next color from the node color iterator.""" return next(self.node_colors) def _process_nodes(self, nodes_trace: List[Dict]) -> List[Dict]: """Transforms raw node traces into the format expected by Vis.js. Args: nodes_trace (List[Dict]): A list of node traces. Returns: List[Dict]: A list of nodes in Vis.js format. """ nodes = [] for node_trace in nodes_trace: nt = node_trace.copy() node_id = nt['node_name'] node_label = nt['node_name'] node_meta = nt.pop('meta', {}) node_type = node_meta.get('type', 'function_node') shape = 'box' color = '#97c2fc' if node_type == 'start_node': shape = 'ellipse' color = "#af71f7" elif node_type == 'end_node': shape = 'ellipse' color = "#3deeb9" elif node_type == 'function_node': shape = 'box' color = self._get_next_color() node_data = { "id": node_id, "label": node_label, "shape": shape, "color": color, "borderRadius": 4, } node_data.update(nt) nodes.append(node_data) return nodes def _process_edges(self, edges_trace: List[Dict]) -> List[Dict]: """Transforms raw edge traces into the format expected by Vis.js. Args: edges_trace (List[Dict]): A list of edge traces. Returns: List[Dict]: A list of edges in Vis.js format. """ edges = [] for edge_trace in edges_trace: et = edge_trace.copy() edge_meta = et.pop('meta', {}) edge_type = edge_meta.get('type', 'forward_edge') dashes: Union[bool, List[int]] = False width = 0.5 # Revert to default width if edge_type == 'conditional_edge': dashes = [5, 3] # More prominent dashes et["elapsed"] = format_elapsed_time(et["elapsed"]) from_node = et.pop('from_node') to_nodes = et.pop('to_node') # Handle parallel edges where to_nodes is a list if isinstance(to_nodes, list): for to_node in to_nodes: edge_data = { "from": from_node, "to": to_node, "label": f"{et.get('edge_run_number', '')}", "dashes": [10, 5, 2, 5], # Distinct dot-dash pattern "width": width # Default width } edge_data.update(et) edges.append(edge_data) else: # Standard forward or conditional edge edge_data = { "from": from_node, "to": to_nodes, "label": f"{et.get('edge_run_number', '')}", "dashes": dashes, "width": width } edge_data.update(et) edges.append(edge_data) return edges def generate_html(self, trace_data: Union[str, Dict]) -> str: """Generates the HTML content for the visualization. Args: trace_data (Union[str, Dict]): Either a dictionary containing the trace or a path to a JSON file. Returns: str: The complete HTML content. """ if isinstance(trace_data, (str, Path)): with open(trace_data, 'r', encoding='utf-8') as f: data = json.load(f) else: data = trace_data self.node_colors = itertools.cycle(DEFAULT_FUNCTION_NODE_COLORS) run_id = data.get('run_id') elapsed = format_elapsed_time(data.get('elapsed_time')) start_time = format_start_time(data.get('start_time')) graph_name = data.get('graph_name', 'Unknown Graph') status = data.get('status', 'FAILED') nodes = self._process_nodes(data.get('nodes_trace', [])) graph_description = data.get('graph_description') if data.get('graph_description') else "No description provided." edges = self._process_edges(data.get('edges_trace', [])) title_card_content = self._build_title_card({ "page_title": f"Graph: {graph_name}", "Run ID": run_id, "Status": status.upper(), "Start Time": start_time, "Elapsed": elapsed, "Description": graph_description }) final_html = self.html_template.replace( "JSON_NODES", json.dumps(nodes, indent=2) ).replace( "JSON_EDGES", json.dumps(edges, indent=2) ).replace( "TITLE_CARD_CONTENT", title_card_content) return final_html def generate_viz(self, trace_data: Union[str, Dict], output_path: str = ""): """Generates the HTML visualization and saves it to a file. Args: trace_data (Union[str, Dict]): Either a dictionary containing the trace or a path to a JSON file. output_path (str, optional): The path to save the HTML file. Defaults to "". """ html_content = self.generate_html(trace_data) with open(output_path, "w", encoding="utf-8") as f: f.write(html_content) print(f"Created visualization at: {output_path}")