Enhance PocketFlow Visualization with D3.js Integration

- Added interactive D3.js visualization for PocketFlow workflows, allowing users to drag nodes and view group boundaries.
- Implemented functions to convert flow graphs to JSON format suitable for D3.js.
- Created convenience functions for visualizing flows with both Mermaid diagrams and D3.js.
- Updated documentation to reflect new visualization features and usage examples.
This commit is contained in:
remy 2025-05-07 00:31:46 +10:00
parent f8d39a8de9
commit 2362abff27
7 changed files with 2121 additions and 2 deletions

View File

@ -1,4 +1,9 @@
---
description:
globs:
alwaysApply: false
---
---
description: Guidelines for using PocketFlow, Utility Function, Viz and Debug
globs:
alwaysApply: false
@ -87,7 +92,146 @@ graph LR
end
```
## 2. Call Stack Debugging
## 2. Interactive D3.js Visualization
For more complex flows, a static diagram may not be sufficient. We provide a D3.js-based interactive visualization that allows for dragging nodes, showing group boundaries for flows, and connecting flows at their boundaries.
### Converting Flow to JSON
First, we convert the PocketFlow graph to JSON format suitable for D3.js:
```python
def flow_to_json(start):
"""Convert a flow to JSON format suitable for D3.js visualization.
This function walks through the flow graph and builds a structure with:
- nodes: All non-Flow nodes with their group memberships
- links: Connections between nodes within the same group
- group_links: Connections between different groups (for inter-flow connections)
- flows: Flow information for group labeling
"""
nodes = []
links = []
group_links = [] # For connections between groups (Flow to Flow)
ids = {}
node_types = {}
flow_nodes = {} # Keep track of flow nodes
ctr = 1
# Implementation details...
# Post-processing: Generate group links based on node connections between different groups
node_groups = {n["id"]: n["group"] for n in nodes}
filtered_links = []
# Filter out direct node-to-node connections between different groups
for link in links:
source_id = link["source"]
target_id = link["target"]
source_group = node_groups.get(source_id, 0)
target_group = node_groups.get(target_id, 0)
if source_group != target_group and source_group > 0 and target_group > 0:
# Create group-to-group links instead of node-to-node links across groups
if not any(gl["source"] == source_group and gl["target"] == target_group
for gl in group_links):
group_links.append({
"source": source_group,
"target": target_group,
"action": link["action"]
})
# Skip adding this link to filtered_links - we don't want direct node connections across groups
else:
# Keep links within the same group
filtered_links.append(link)
return {
"nodes": nodes,
"links": filtered_links,
"group_links": group_links,
"flows": {str(k): v.__class__.__name__ for k, v in flow_nodes.items()},
}
```
### Creating the Visualization
Then, we generate an HTML file with D3.js visualization:
```python
def create_d3_visualization(json_data, output_dir="./viz", filename="flow_viz"):
"""Create a D3.js visualization from JSON data."""
# Create output directory
os.makedirs(output_dir, exist_ok=True)
# Save JSON data to file
json_path = os.path.join(output_dir, f"{filename}.json")
with open(json_path, "w") as f:
json.dump(json_data, f, indent=2)
# Generate HTML with D3.js visualization
# ...HTML template with D3.js code...
# Key features implemented in the visualization:
# 1. Nodes can be dragged to reorganize the layout
# 2. Flows are shown as dashed rectangles (groups)
# 3. Inter-group connections shown as dashed lines connecting at group boundaries
# 4. Edge labels show transition actions
# Write HTML to file
html_path = os.path.join(output_dir, f"{filename}.html")
with open(html_path, "w") as f:
f.write(html_content)
print(f"Visualization created at {html_path}")
return html_path
```
### Convenience Function
A convenience function to visualize flows:
```python
def visualize_flow(flow, flow_name):
"""Helper function to visualize a flow with both mermaid and D3.js"""
print(f"\n--- {flow_name} Mermaid Diagram ---")
print(build_mermaid(start=flow))
print(f"\n--- {flow_name} D3.js Visualization ---")
json_data = flow_to_json(flow)
create_d3_visualization(
json_data, filename=f"{flow_name.lower().replace(' ', '_')}"
)
```
### Usage Example
```python
from visualize import visualize_flow
# Create a complex flow with nested subflows
# ...flow definition...
# Generate visualization
visualize_flow(data_science_flow, "Data Science Flow")
```
### Customizing the Visualization
You can customize the visualization by adjusting the force simulation parameters:
```javascript
const simulation = d3.forceSimulation(data.nodes)
// Controls the distance between connected nodes
.force("link", d3.forceLink(data.links).id(d => d.id).distance(100))
// Controls how nodes repel each other - lower values bring nodes closer
.force("charge", d3.forceManyBody().strength(-30))
// Centers the entire graph in the SVG
.force("center", d3.forceCenter(width / 2, height / 2))
// Prevents nodes from overlapping - acts like a minimum distance
.force("collide", d3.forceCollide().radius(50));
```
## 3. Call Stack Debugging
It would be useful to print the Node call stacks for debugging. This can be achieved by inspecting the runtime call stack:

View File

@ -0,0 +1,131 @@
# PocketFlow Visualization
This directory contains tools for visualizing PocketFlow workflow graphs using interactive D3.js visualizations.
## Overview
The visualization tools allow you to:
1. View PocketFlow nodes and flows as an interactive graph
2. See how different flows connect to each other
3. Understand the relationships between nodes within flows
## Features
- **Interactive Graph**: Nodes can be dragged to reorganize the layout
- **Group Visualization**: Flows are displayed as groups with dashed borders
- **Inter-Group Links**: Connections between flows are shown as dashed lines connecting group boundaries
- **Action Labels**: Edge labels show the actions that trigger transitions between nodes
## Requirements
- Python 3.6 or higher
- Modern web browser (Chrome, Firefox, Edge) for viewing the visualizations
## Usage
### 1. Basic Visualization
To visualize a PocketFlow graph, you can use the `visualize_flow` function in `visualize.py`:
```python
from visualize import visualize_flow
from your_flow_module import your_flow
# Generate visualization
visualize_flow(your_flow, "Your Flow Name")
```
This will:
1. Print a Mermaid diagram to the console
2. Generate a D3.js visualization in the `./viz` directory
### 2. Running the Example
The included example shows an order processing pipeline with payment, inventory, and shipping flows:
```bash
# Navigate to the directory
cd cookbook/pocketflow-minimal-flow2flow
# Run the visualization script
python visualize.py
```
This will generate visualization files in the `./viz` directory.
### 3. Viewing the Visualization
After running the script:
1. Host with
```
cd ./viz/
```
2. Interact with the visualization:
- **Drag nodes** to reorganize
- **Hover over nodes** to see node names
- **Observe connections** between nodes and flows
## Customizing the Visualization
### Adjusting Layout Parameters
You can adjust the force simulation parameters in `visualize.py` to change how nodes and groups are positioned:
```javascript
// Create a force simulation
const simulation = d3.forceSimulation(data.nodes)
// Controls the distance between connected nodes
.force("link", d3.forceLink(data.links).id(d => d.id).distance(100))
// Controls how nodes repel each other - lower values bring nodes closer
.force("charge", d3.forceManyBody().strength(-30))
// Centers the entire graph in the SVG
.force("center", d3.forceCenter(width / 2, height / 2))
// Prevents nodes from overlapping - acts like a minimum distance
.force("collide", d3.forceCollide().radius(50));
```
### Styling
Adjust the CSS styles in the HTML template inside `create_d3_visualization` function to change colors, shapes, and other visual properties.
## How It Works
The visualization process consists of three main steps:
1. **Flow to JSON Conversion**: The `flow_to_json` function traverses the PocketFlow graph and converts it to a structure with nodes, links, and group information.
2. **D3.js Visualization**: The JSON data is used to create an interactive D3.js visualization with:
- Nodes represented as circles
- Flows represented as dashed rectangles containing nodes
- Links showing connections within and between flows
3. **Group Boundary Connections**: The visualization calculates intersection points with group boundaries to ensure inter-group links connect at the borders rather than centers.
## Extending the Visualization
You can extend the visualization tools by:
1. Adding new node shapes
2. Implementing additional layout algorithms
3. Adding tooltips with more detailed information
4. Creating animation for flow execution
## Troubleshooting
If you encounter any issues:
- Make sure your flow objects are properly constructed with nodes connected correctly
- Check the browser console for any JavaScript errors
- Verify that the generated JSON data structure matches what you expect
## Example Output
The visualization displays:
- Payment processing flow nodes
- Inventory management flow nodes
- Shipping flow nodes
- Group boundaries around each flow
- Connections between flows (Payment → Inventory → Shipping)

View File

@ -0,0 +1,165 @@
from pocketflow import AsyncNode, AsyncFlow
import asyncio
# Define Payment Nodes
class ValidatePayment(AsyncNode):
async def exec_async(self, prep_res):
print("1.1.Validating payment...")
return "Payment validated successfully"
async def post_async(self, shared, prep_res, exec_res):
shared["payment_status"] = exec_res
return "default"
class ProcessPayment(AsyncNode):
async def exec_async(self, prep_res):
print("1.2.Processing payment...")
return "Payment processed successfully"
async def post_async(self, shared, prep_res, exec_res):
shared["payment_result"] = exec_res
return "default"
class PaymentConfirmation(AsyncNode):
async def exec_async(self, prep_res):
print("1.3.Confirming payment...")
return "Payment confirmed"
async def post_async(self, shared, prep_res, exec_res):
shared["payment_confirmation"] = exec_res
return "default"
# Define Inventory Nodes
class CheckStock(AsyncNode):
async def exec_async(self, prep_res):
print("2.1.Checking inventory stock...")
return "Stock available"
async def post_async(self, shared, prep_res, exec_res):
shared["stock_status"] = exec_res
return "default"
class ReserveItems(AsyncNode):
async def exec_async(self, prep_res):
print("2.2.Reserving items...")
return "Items reserved"
async def post_async(self, shared, prep_res, exec_res):
shared["reservation_status"] = exec_res
return "default"
class UpdateInventory(AsyncNode):
async def exec_async(self, prep_res):
print("2.3. Updating inventory...")
return "Inventory updated"
async def post_async(self, shared, prep_res, exec_res):
shared["inventory_update"] = exec_res
return "default"
# Define Shipping Nodes
class CreateLabel(AsyncNode):
async def exec_async(self, prep_res):
print("3.1 Creating shipping label...")
return "Shipping label created"
async def post_async(self, shared, prep_res, exec_res):
shared["shipping_label"] = exec_res
return "default"
class AssignCarrier(AsyncNode):
async def exec_async(self, prep_res):
print("3.2 Assigning carrier...")
return "Carrier assigned"
async def post_async(self, shared, prep_res, exec_res):
shared["carrier"] = exec_res
return "default"
class SchedulePickup(AsyncNode):
async def exec_async(self, prep_res):
print("3.3 Scheduling pickup...")
return "Pickup scheduled"
async def post_async(self, shared, prep_res, exec_res):
shared["pickup_status"] = exec_res
return "default"
# Create node instances
validate_payment = ValidatePayment()
process_payment = ProcessPayment()
payment_confirmation = PaymentConfirmation()
check_stock = CheckStock()
reserve_items = ReserveItems()
update_inventory = UpdateInventory()
create_label = CreateLabel()
assign_carrier = AssignCarrier()
schedule_pickup = SchedulePickup()
# Payment processing sub-flow
validate_payment >> process_payment >> payment_confirmation
payment_flow = AsyncFlow(start=validate_payment)
# Inventory sub-flow
check_stock >> reserve_items >> update_inventory
inventory_flow = AsyncFlow(start=check_stock)
# Shipping sub-flow
create_label >> assign_carrier >> schedule_pickup
shipping_flow = AsyncFlow(start=create_label)
# Connect the flows into a main order pipeline
payment_flow >> inventory_flow >> shipping_flow
# payment_flow >> inventory_flow >> create_label
# payment_flow >> inventory_flow >> assign_carrier
# Create the master flow
class OrderFlow(AsyncFlow):
pass
order_pipeline = OrderFlow(start=payment_flow)
# Create shared data structure
shared_data = {
"order_id": "ORD-12345",
"customer": "John Doe",
"items": [
{"id": "ITEM-001", "name": "Smartphone", "price": 999.99, "quantity": 1},
{"id": "ITEM-002", "name": "Phone case", "price": 29.99, "quantity": 1},
],
"shipping_address": {
"street": "123 Main St",
"city": "Anytown",
"state": "CA",
"zip": "12345",
},
}
# Run the entire pipeline asynchronously
async def main():
await order_pipeline.run_async(shared_data)
# Print final status
print("\nOrder processing completed!")
print(f"Payment: {shared_data.get('payment_confirmation')}")
print(f"Inventory: {shared_data.get('inventory_update')}")
print(f"Shipping: {shared_data.get('pickup_status')}")
if __name__ == "__main__":
asyncio.run(main())

View File

@ -0,0 +1,932 @@
# %%
import json
import os
import http.server
import socketserver
import threading
import webbrowser
import time
import socket
import importlib
import sys
from pathlib import Path
from typing import Any, Optional, Tuple, Union
from pocketflow import Flow
from async_flow import order_pipeline
def build_mermaid(start):
ids, visited, lines = {}, set(), ["graph LR"]
ctr = 1
def get_id(n):
nonlocal ctr
return (
ids[n] if n in ids else (ids.setdefault(n, f"N{ctr}"), (ctr := ctr + 1))[0]
)
def link(a, b):
lines.append(f" {a} --> {b}")
def walk(node, parent=None):
if node in visited:
return parent and link(parent, get_id(node))
visited.add(node)
if isinstance(node, Flow):
node.start_node and parent and link(parent, get_id(node.start_node))
lines.append(
f"\n subgraph sub_flow_{get_id(node)}[{type(node).__name__}]"
)
node.start_node and walk(node.start_node)
for nxt in node.successors.values():
node.start_node and walk(nxt, get_id(node.start_node)) or (
parent and link(parent, get_id(nxt))
) or walk(nxt)
lines.append(" end\n")
else:
lines.append(f" {(nid := get_id(node))}['{type(node).__name__}']")
parent and link(parent, nid)
[walk(nxt, nid) for nxt in node.successors.values()]
walk(start)
return "\n".join(lines)
def flow_to_json(start):
"""Convert a flow to JSON format suitable for D3.js visualization.
This function walks through the flow graph and builds a structure with:
- nodes: All non-Flow nodes with their group memberships
- links: Connections between nodes within the same group
- group_links: Connections between different groups (for inter-flow connections)
- flows: Flow information for group labeling
Returns:
dict: A JSON-serializable dictionary with 'nodes' and 'links' arrays.
"""
nodes = []
links = []
group_links = [] # For connections between groups (Flow to Flow)
ids = {}
node_types = {}
flow_nodes = {} # Keep track of flow nodes
ctr = 1
def get_id(n):
nonlocal ctr
if n not in ids:
ids[n] = ctr
node_types[ctr] = type(n).__name__
if isinstance(n, Flow):
flow_nodes[ctr] = n # Store flow reference
ctr += 1
return ids[n]
def walk(node, parent=None, group=None, parent_group=None, action=None):
"""Recursively walk the flow graph to build the visualization data.
Args:
node: Current node being processed
parent: ID of the parent node that connects to this node
group: Group (Flow) ID this node belongs to
parent_group: Group ID of the parent node
action: Action label on the edge from parent to this node
"""
node_id = get_id(node)
# Add node if not already in nodes list and not a Flow
if not any(n["id"] == node_id for n in nodes) and not isinstance(node, Flow):
node_data = {
"id": node_id,
"name": node_types[node_id],
"group": group or 0, # Default group
}
nodes.append(node_data)
# Add link from parent if exists
if parent and not isinstance(node, Flow):
links.append(
{"source": parent, "target": node_id, "action": action or "default"}
)
# Process different types of nodes
if isinstance(node, Flow):
# This is a Flow node - it becomes a group container
flow_group = node_id # Use flow's ID as group for contained nodes
# Add a group-to-group link if this flow has a parent group
# This creates connections between nested flows
if parent_group is not None and parent_group != flow_group:
# Check if this link already exists
if not any(
l["source"] == parent_group and l["target"] == flow_group
for l in group_links
):
group_links.append(
{
"source": parent_group,
"target": flow_group,
"action": action or "default",
}
)
if node.start_node:
# Process the start node of this flow
walk(node.start_node, parent, flow_group, parent_group, action)
# Process successors of the flow's start node
for next_action, nxt in node.successors.items():
walk(
nxt,
get_id(node.start_node),
flow_group,
parent_group,
next_action,
)
else:
# Process successors for regular nodes
for next_action, nxt in node.successors.items():
if isinstance(nxt, Flow):
# This node connects to a flow - track the group relationship
flow_group_id = get_id(nxt)
walk(nxt, node_id, None, group, next_action)
else:
# Regular node-to-node connection
walk(nxt, node_id, group, parent_group, next_action)
# Start the traversal
walk(start)
# Post-processing: Generate group links based on node connections between different groups
# This ensures that when nodes in different groups are connected, we show a group-to-group
# link rather than a direct node-to-node link
node_groups = {n["id"]: n["group"] for n in nodes}
filtered_links = []
for link in links:
source_id = link["source"]
target_id = link["target"]
source_group = node_groups.get(source_id, 0)
target_group = node_groups.get(target_id, 0)
# If source and target are in different groups and both groups are valid
if source_group != target_group and source_group > 0 and target_group > 0:
# Add to group links if not already there
# This creates the dashed lines connecting group boxes
if not any(
gl["source"] == source_group and gl["target"] == target_group
for gl in group_links
):
group_links.append(
{
"source": source_group,
"target": target_group,
"action": link["action"],
}
)
# Skip adding this link to filtered_links - we don't want direct node connections across groups
else:
# Keep links within the same group
filtered_links.append(link)
return {
"nodes": nodes,
"links": filtered_links, # Use filtered links instead of all links
"group_links": group_links,
"flows": {str(k): v.__class__.__name__ for k, v in flow_nodes.items()},
}
def create_d3_visualization(
json_data,
output_dir="./viz",
filename="flow_viz",
html_title="PocketFlow Visualization",
):
"""Create a D3.js visualization from JSON data.
Args:
json_data: The JSON data for the visualization
output_dir: Directory to save the files
filename: Base filename (without extension)
html_title: Title for the HTML page
Returns:
str: Path to the HTML file
"""
# Create output directory if it doesn't exist
os.makedirs(output_dir, exist_ok=True)
# Save JSON data to file
json_path = os.path.join(output_dir, f"{filename}.json")
with open(json_path, "w") as f:
json.dump(json_data, f, indent=2)
# Create HTML file with D3.js visualization
html_content = r"""<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>TITLE_PLACEHOLDER</title>
<script src="https://d3js.org/d3.v7.min.js"></script>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
overflow: hidden;
}
svg {
width: 100vw;
height: 100vh;
}
.links path {
fill: none;
stroke: #999;
stroke-opacity: 0.6;
stroke-width: 1.5px;
}
.group-links path {
fill: none;
stroke: #333;
stroke-opacity: 0.8;
stroke-width: 2px;
stroke-dasharray: 5,5;
}
.nodes circle {
stroke: #fff;
stroke-width: 1.5px;
}
.node-labels {
font-size: 12px;
pointer-events: none;
}
.link-labels {
font-size: 10px;
fill: #666;
pointer-events: none;
}
.group-link-labels {
font-size: 11px;
font-weight: bold;
fill: #333;
pointer-events: none;
}
.group-container {
stroke: #333;
stroke-width: 1.5px;
stroke-dasharray: 5,5;
fill: rgba(200, 200, 200, 0.1);
rx: 10;
ry: 10;
}
.group-label {
font-size: 14px;
font-weight: bold;
pointer-events: none;
}
</style>
</head>
<body>
<svg id="graph"></svg>
<script>
// Load data from file
d3.json("FILENAME_PLACEHOLDER.json").then(data => {
const svg = d3.select("#graph");
const width = window.innerWidth;
const height = window.innerHeight;
// Define arrow markers for links
svg.append("defs").append("marker")
.attr("id", "arrowhead")
.attr("viewBox", "0 -5 10 10")
.attr("refX", 25) // Position the arrow away from the target node
.attr("refY", 0)
.attr("orient", "auto")
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("xoverflow", "visible")
.append("path")
.attr("d", "M 0,-5 L 10,0 L 0,5")
.attr("fill", "#999");
// Define thicker arrow markers for group links
svg.append("defs").append("marker")
.attr("id", "group-arrowhead")
.attr("viewBox", "0 -5 10 10")
.attr("refX", 3) // Position at the boundary of the group
.attr("refY", 0)
.attr("orient", "auto")
.attr("markerWidth", 8)
.attr("markerHeight", 8)
.attr("xoverflow", "visible")
.append("path")
.attr("d", "M 0,-5 L 10,0 L 0,5")
.attr("fill", "#333");
// Color scale for node groups
const color = d3.scaleOrdinal(d3.schemeCategory10);
// Process the data to identify groups
const groups = {};
data.nodes.forEach(node => {
if (node.group > 0) {
if (!groups[node.group]) {
// Use the flow name instead of generic "Group X"
const flowName = data.flows && data.flows[node.group] ? data.flows[node.group] : `Flow ${node.group}`;
groups[node.group] = {
id: node.group,
name: flowName,
nodes: [],
x: 0,
y: 0,
width: 0,
height: 0
};
}
groups[node.group].nodes.push(node);
}
});
// Create a force simulation
const simulation = d3.forceSimulation(data.nodes)
// Controls the distance between connected nodes
.force("link", d3.forceLink(data.links).id(d => d.id).distance(100))
// Controls how nodes repel each other - lower values bring nodes closer
.force("charge", d3.forceManyBody().strength(-30))
// Centers the entire graph in the SVG
.force("center", d3.forceCenter(width / 2, height / 2))
// Prevents nodes from overlapping - acts like a minimum distance
.force("collide", d3.forceCollide().radius(50));
// Group forces - create a force to keep nodes in the same group closer together
// This creates the effect of nodes clustering within their group boxes
const groupForce = alpha => {
for (let i = 0; i < data.nodes.length; i++) {
const node = data.nodes[i];
if (node.group > 0) {
const group = groups[node.group];
if (group && group.nodes.length > 1) {
// Calculate center of group
let centerX = 0, centerY = 0;
group.nodes.forEach(n => {
centerX += n.x || 0;
centerY += n.y || 0;
});
centerX /= group.nodes.length;
centerY /= group.nodes.length;
// Move nodes toward center
const k = alpha * 0.3; // Increased from 0.1 to 0.3
node.vx += (centerX - node.x) * k;
node.vy += (centerY - node.y) * k;
}
}
}
};
// Additional force to position groups in a more organized layout (like in the image)
// This arranges the groups horizontally/vertically based on their connections
const groupLayoutForce = alpha => {
// Get group centers
const groupCenters = Object.values(groups).map(g => {
return { id: g.id, cx: 0, cy: 0 };
});
// Calculate current center positions
Object.values(groups).forEach(g => {
if (g.nodes.length > 0) {
let cx = 0, cy = 0;
g.nodes.forEach(n => {
cx += n.x || 0;
cy += n.y || 0;
});
const groupCenter = groupCenters.find(gc => gc.id === g.id);
if (groupCenter) {
groupCenter.cx = cx / g.nodes.length;
groupCenter.cy = cy / g.nodes.length;
}
}
});
// Apply forces to position groups
const k = alpha * 0.05;
// Try to position groups in a more structured way
// Adjust these values to change the overall layout
for (let i = 0; i < data.group_links.length; i++) {
const link = data.group_links[i];
const source = groupCenters.find(g => g.id === link.source);
const target = groupCenters.find(g => g.id === link.target);
if (source && target) {
// Add a horizontal force to align groups
const desiredDx = 300; // Desired horizontal distance between linked groups
const dx = target.cx - source.cx;
const diff = desiredDx - Math.abs(dx);
// Apply forces to group nodes
groups[source.id].nodes.forEach(n => {
if (dx > 0) {
n.vx -= diff * k;
} else {
n.vx += diff * k;
}
});
groups[target.id].nodes.forEach(n => {
if (dx > 0) {
n.vx += diff * k;
} else {
n.vx -= diff * k;
}
});
}
}
};
simulation.force("group", groupForce);
simulation.force("groupLayout", groupLayoutForce);
// Create links with arrow paths instead of lines
const link = svg.append("g")
.attr("class", "links")
.selectAll("path")
.data(data.links)
.enter()
.append("path")
.attr("stroke-width", 2)
.attr("stroke", "#999")
.attr("marker-end", "url(#arrowhead)"); // Add the arrowhead marker
// Create group containers (drawn before nodes)
const groupContainers = svg.append("g")
.attr("class", "groups")
.selectAll("rect")
.data(Object.values(groups))
.enter()
.append("rect")
.attr("class", "group-container")
.attr("fill", d => d3.color(color(d.id)).copy({opacity: 0.2}));
// Create group links between flows
const groupLink = svg.append("g")
.attr("class", "group-links")
.selectAll("path")
.data(data.group_links || [])
.enter()
.append("path")
.attr("stroke-width", 2)
.attr("stroke", "#333")
.attr("marker-end", "url(#group-arrowhead)");
// Create group link labels
const groupLinkLabel = svg.append("g")
.attr("class", "group-link-labels")
.selectAll("text")
.data(data.group_links || [])
.enter()
.append("text")
.text(d => d.action)
.attr("font-size", "11px")
.attr("font-weight", "bold")
.attr("fill", "#333");
// Create group labels
const groupLabels = svg.append("g")
.attr("class", "group-labels")
.selectAll("text")
.data(Object.values(groups))
.enter()
.append("text")
.attr("class", "group-label")
.text(d => d.name) // Now using the proper flow name
.attr("fill", d => d3.color(color(d.id)).darker());
// Create link labels
const linkLabel = svg.append("g")
.attr("class", "link-labels")
.selectAll("text")
.data(data.links)
.enter()
.append("text")
.text(d => d.action)
.attr("font-size", "10px")
.attr("fill", "#666");
// Create nodes
const node = svg.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(data.nodes)
.enter()
.append("circle")
.attr("r", 15)
.attr("fill", d => color(d.group))
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
// Create node labels
const nodeLabel = svg.append("g")
.attr("class", "node-labels")
.selectAll("text")
.data(data.nodes)
.enter()
.append("text")
.text(d => d.name)
.attr("text-anchor", "middle")
.attr("dy", 25);
// Add tooltip on hover
node.append("title")
.text(d => d.name);
// Update positions on each tick
simulation.on("tick", () => {
// Update links with straight lines
link.attr("d", d => {
return `M${d.source.x},${d.source.y} L${d.target.x},${d.target.y}`;
});
// Update nodes
node
.attr("cx", d => d.x)
.attr("cy", d => d.y);
// Update node labels
nodeLabel
.attr("x", d => d.x)
.attr("y", d => d.y);
// Position link labels at midpoint
linkLabel
.attr("x", d => (d.source.x + d.target.x) / 2)
.attr("y", d => (d.source.y + d.target.y) / 2);
// Update group containers
groupContainers.each(function(d) {
// If there are nodes in this group
if (d.nodes.length > 0) {
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
// Find the bounding box for all nodes in the group
d.nodes.forEach(n => {
minX = Math.min(minX, n.x - 30);
minY = Math.min(minY, n.y - 30);
maxX = Math.max(maxX, n.x + 30);
maxY = Math.max(maxY, n.y + 40); // Extra space for labels
});
// Add padding
const padding = 20;
minX -= padding;
minY -= padding;
maxX += padding;
maxY += padding;
// Save group dimensions
d.x = minX;
d.y = minY;
d.width = maxX - minX;
d.height = maxY - minY;
d.centerX = minX + d.width / 2;
d.centerY = minY + d.height / 2;
// Set position and size of the group container
d3.select(this)
.attr("x", minX)
.attr("y", minY)
.attr("width", d.width)
.attr("height", d.height);
// Update group label position (top-left of group)
groupLabels.filter(g => g.id === d.id)
.attr("x", minX + 10)
.attr("y", minY + 20);
}
});
// Update group links between flows
groupLink.attr("d", d => {
const sourceGroup = groups[d.source];
const targetGroup = groups[d.target];
if (!sourceGroup || !targetGroup) return "";
// Find intersection points with group boundaries
// This ensures links connect to the group's border rather than its center
// Calculate centers of groups
const sx = sourceGroup.centerX;
const sy = sourceGroup.centerY;
const tx = targetGroup.centerX;
const ty = targetGroup.centerY;
// Calculate angle between centers - used to find intersection points
const angle = Math.atan2(ty - sy, tx - sx);
// Calculate intersection points with source group borders
// We cast a ray from center in the direction of the target
let sourceX, sourceY;
const cosA = Math.cos(angle);
const sinA = Math.sin(angle);
// Check intersection with horizontal borders (top and bottom)
const ts_top = (sourceGroup.y - sy) / sinA;
const ts_bottom = (sourceGroup.y + sourceGroup.height - sy) / sinA;
// Check intersection with vertical borders (left and right)
const ts_left = (sourceGroup.x - sx) / cosA;
const ts_right = (sourceGroup.x + sourceGroup.width - sx) / cosA;
// Use the closest positive intersection (first hit with the boundary)
let t_source = Infinity;
if (ts_top > 0) t_source = Math.min(t_source, ts_top);
if (ts_bottom > 0) t_source = Math.min(t_source, ts_bottom);
if (ts_left > 0) t_source = Math.min(t_source, ts_left);
if (ts_right > 0) t_source = Math.min(t_source, ts_right);
// Target group: Find intersection in the opposite direction
// We cast a ray from target center toward the source
let targetX, targetY;
const oppositeAngle = angle + Math.PI;
const cosOpp = Math.cos(oppositeAngle);
const sinOpp = Math.sin(oppositeAngle);
// Check intersections for target group
const tt_top = (targetGroup.y - ty) / sinOpp;
const tt_bottom = (targetGroup.y + targetGroup.height - ty) / sinOpp;
const tt_left = (targetGroup.x - tx) / cosOpp;
const tt_right = (targetGroup.x + targetGroup.width - tx) / cosOpp;
// Use the closest positive intersection
let t_target = Infinity;
if (tt_top > 0) t_target = Math.min(t_target, tt_top);
if (tt_bottom > 0) t_target = Math.min(t_target, tt_bottom);
if (tt_left > 0) t_target = Math.min(t_target, tt_left);
if (tt_right > 0) t_target = Math.min(t_target, tt_right);
// Calculate actual border points using parametric equation:
// point = center + t * direction
if (t_source !== Infinity) {
sourceX = sx + cosA * t_source;
sourceY = sy + sinA * t_source;
} else {
sourceX = sx;
sourceY = sy;
}
if (t_target !== Infinity) {
targetX = tx + cosOpp * t_target;
targetY = ty + sinOpp * t_target;
} else {
targetX = tx;
targetY = ty;
}
// Create a straight line between the border points
return `M${sourceX},${sourceY} L${targetX},${targetY}`;
});
// Update group link labels
groupLinkLabel.attr("x", d => {
const sourceGroup = groups[d.source];
const targetGroup = groups[d.target];
if (!sourceGroup || !targetGroup) return 0;
return (sourceGroup.centerX + targetGroup.centerX) / 2;
})
.attr("y", d => {
const sourceGroup = groups[d.source];
const targetGroup = groups[d.target];
if (!sourceGroup || !targetGroup) return 0;
return (sourceGroup.centerY + targetGroup.centerY) / 2 - 10;
});
});
// Drag functions
function dragstarted(event, d) {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
}
function dragended(event, d) {
if (!event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
});
</script>
</body>
</html>
"""
# Replace the placeholders with the actual values
html_content = html_content.replace("FILENAME_PLACEHOLDER", filename)
html_content = html_content.replace("TITLE_PLACEHOLDER", html_title)
# Write HTML to file
html_path = os.path.join(output_dir, f"{filename}.html")
with open(html_path, "w") as f:
f.write(html_content)
print(f"Visualization created at {html_path}")
return html_path
def find_free_port():
"""Find a free port on localhost."""
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(("", 0))
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
return s.getsockname()[1]
def start_http_server(directory, port=None):
"""Start an HTTP server in the given directory.
Args:
directory: Directory to serve files from
port: Port to use (finds a free port if None)
Returns:
tuple: (server_thread, port)
"""
if port is None:
port = find_free_port()
# Get the absolute path of the directory
directory = str(Path(directory).absolute())
# Change to the directory to serve files
os.chdir(directory)
# Create HTTP server
handler = http.server.SimpleHTTPRequestHandler
httpd = socketserver.TCPServer(("", port), handler)
# Start server in a separate thread
server_thread = threading.Thread(target=httpd.serve_forever)
server_thread.daemon = (
True # This makes the thread exit when the main program exits
)
server_thread.start()
print(f"Server started at http://localhost:{port}")
return server_thread, port
def serve_and_open_visualization(html_path, auto_open=True):
"""Serve the HTML file and open it in a browser.
Args:
html_path: Path to the HTML file
auto_open: Whether to automatically open the browser
Returns:
tuple: (server_thread, url)
"""
# Get the directory and filename
directory = os.path.dirname(os.path.abspath(html_path))
filename = os.path.basename(html_path)
# Start the server
server_thread, port = start_http_server(directory)
# Build the URL
url = f"http://localhost:{port}/{filename}"
# Open the URL in a browser
if auto_open:
print(f"Opening {url} in your browser...")
webbrowser.open(url)
else:
print(f"Visualization available at {url}")
return server_thread, url
def visualize_flow(
flow: Flow,
flow_name: str,
serve: bool = True,
auto_open: bool = True,
output_dir: str = "./viz",
html_title: Optional[str] = None,
) -> Union[str, Tuple[str, Any, str]]:
"""Helper function to visualize a flow with both mermaid and D3.js
Args:
flow: Flow object to visualize
flow_name: Name of the flow (used for filename and display)
serve: Whether to start a server for the visualization
auto_open: Whether to automatically open in browser
output_dir: Directory to save visualization files
html_title: Custom title for the HTML page (defaults to flow_name if None)
Returns:
str or tuple: Path to HTML file, or (path, server_thread, url) if serve=True
"""
print(f"\n--- {flow_name} Mermaid Diagram ---")
print(build_mermaid(start=flow))
print(f"\n--- {flow_name} D3.js Visualization ---")
json_data = flow_to_json(flow)
# Create the visualization
output_filename = f"{flow_name.lower().replace(' ', '_')}"
# Use flow_name as the HTML title if not specified
if html_title is None:
html_title = f"PocketFlow: {flow_name}"
html_path = create_d3_visualization(
json_data,
output_dir=output_dir,
filename=output_filename,
html_title=html_title,
)
# Serve and open if requested
if serve:
server_thread, url = serve_and_open_visualization(html_path, auto_open)
return html_path, server_thread, url
return html_path
def load_flow_from_module(module_path: str, flow_variable: str) -> Flow:
"""Dynamically load a flow from a module.
Args:
module_path: Path to the module (e.g., 'my_package.my_module')
flow_variable: Name of the flow variable in the module
Returns:
Flow: The loaded flow object
"""
try:
module = importlib.import_module(module_path)
return getattr(module, flow_variable)
except (ImportError, AttributeError) as e:
print(f"Error loading flow: {e}")
sys.exit(1)
# Example usage
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="Visualize a PocketFlow flow")
parser.add_argument(
"--module", default="async_flow", help="Module containing the flow"
)
parser.add_argument(
"--flow", default="order_pipeline", help="Flow variable name in the module"
)
parser.add_argument(
"--name", default="Flow Visualization", help="Name for the visualization"
)
parser.add_argument(
"--output-dir", default="./viz", help="Directory to save visualization files"
)
parser.add_argument("--no-serve", action="store_true", help="Don't start a server")
parser.add_argument(
"--no-open", action="store_true", help="Don't open browser automatically"
)
parser.add_argument("--title", help="Custom HTML title")
args = parser.parse_args()
# Load flow from the specified module
flow_obj = load_flow_from_module(args.module, args.flow)
# Visualize the flow
visualize_flow(
flow=flow_obj,
flow_name=args.name,
serve=not args.no_serve,
auto_open=not args.no_open,
output_dir=args.output_dir,
html_title=args.title,
)
# Keep server running if serving
if not args.no_serve:
try:
print("\nServer is running. Press Ctrl+C to stop...")
while True:
time.sleep(1)
except KeyboardInterrupt:
print("\nShutting down...")

View File

@ -0,0 +1,504 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>PocketFlow: Flow Visualization</title>
<script src="https://d3js.org/d3.v7.min.js"></script>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
overflow: hidden;
}
svg {
width: 100vw;
height: 100vh;
}
.links path {
fill: none;
stroke: #999;
stroke-opacity: 0.6;
stroke-width: 1.5px;
}
.group-links path {
fill: none;
stroke: #333;
stroke-opacity: 0.8;
stroke-width: 2px;
stroke-dasharray: 5,5;
}
.nodes circle {
stroke: #fff;
stroke-width: 1.5px;
}
.node-labels {
font-size: 12px;
pointer-events: none;
}
.link-labels {
font-size: 10px;
fill: #666;
pointer-events: none;
}
.group-link-labels {
font-size: 11px;
font-weight: bold;
fill: #333;
pointer-events: none;
}
.group-container {
stroke: #333;
stroke-width: 1.5px;
stroke-dasharray: 5,5;
fill: rgba(200, 200, 200, 0.1);
rx: 10;
ry: 10;
}
.group-label {
font-size: 14px;
font-weight: bold;
pointer-events: none;
}
</style>
</head>
<body>
<svg id="graph"></svg>
<script>
// Load data from file
d3.json("flow_visualization.json").then(data => {
const svg = d3.select("#graph");
const width = window.innerWidth;
const height = window.innerHeight;
// Define arrow markers for links
svg.append("defs").append("marker")
.attr("id", "arrowhead")
.attr("viewBox", "0 -5 10 10")
.attr("refX", 25) // Position the arrow away from the target node
.attr("refY", 0)
.attr("orient", "auto")
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("xoverflow", "visible")
.append("path")
.attr("d", "M 0,-5 L 10,0 L 0,5")
.attr("fill", "#999");
// Define thicker arrow markers for group links
svg.append("defs").append("marker")
.attr("id", "group-arrowhead")
.attr("viewBox", "0 -5 10 10")
.attr("refX", 3) // Position at the boundary of the group
.attr("refY", 0)
.attr("orient", "auto")
.attr("markerWidth", 8)
.attr("markerHeight", 8)
.attr("xoverflow", "visible")
.append("path")
.attr("d", "M 0,-5 L 10,0 L 0,5")
.attr("fill", "#333");
// Color scale for node groups
const color = d3.scaleOrdinal(d3.schemeCategory10);
// Process the data to identify groups
const groups = {};
data.nodes.forEach(node => {
if (node.group > 0) {
if (!groups[node.group]) {
// Use the flow name instead of generic "Group X"
const flowName = data.flows && data.flows[node.group] ? data.flows[node.group] : `Flow ${node.group}`;
groups[node.group] = {
id: node.group,
name: flowName,
nodes: [],
x: 0,
y: 0,
width: 0,
height: 0
};
}
groups[node.group].nodes.push(node);
}
});
// Create a force simulation
const simulation = d3.forceSimulation(data.nodes)
// Controls the distance between connected nodes
.force("link", d3.forceLink(data.links).id(d => d.id).distance(100))
// Controls how nodes repel each other - lower values bring nodes closer
.force("charge", d3.forceManyBody().strength(-30))
// Centers the entire graph in the SVG
.force("center", d3.forceCenter(width / 2, height / 2))
// Prevents nodes from overlapping - acts like a minimum distance
.force("collide", d3.forceCollide().radius(50));
// Group forces - create a force to keep nodes in the same group closer together
// This creates the effect of nodes clustering within their group boxes
const groupForce = alpha => {
for (let i = 0; i < data.nodes.length; i++) {
const node = data.nodes[i];
if (node.group > 0) {
const group = groups[node.group];
if (group && group.nodes.length > 1) {
// Calculate center of group
let centerX = 0, centerY = 0;
group.nodes.forEach(n => {
centerX += n.x || 0;
centerY += n.y || 0;
});
centerX /= group.nodes.length;
centerY /= group.nodes.length;
// Move nodes toward center
const k = alpha * 0.3; // Increased from 0.1 to 0.3
node.vx += (centerX - node.x) * k;
node.vy += (centerY - node.y) * k;
}
}
}
};
// Additional force to position groups in a more organized layout (like in the image)
// This arranges the groups horizontally/vertically based on their connections
const groupLayoutForce = alpha => {
// Get group centers
const groupCenters = Object.values(groups).map(g => {
return { id: g.id, cx: 0, cy: 0 };
});
// Calculate current center positions
Object.values(groups).forEach(g => {
if (g.nodes.length > 0) {
let cx = 0, cy = 0;
g.nodes.forEach(n => {
cx += n.x || 0;
cy += n.y || 0;
});
const groupCenter = groupCenters.find(gc => gc.id === g.id);
if (groupCenter) {
groupCenter.cx = cx / g.nodes.length;
groupCenter.cy = cy / g.nodes.length;
}
}
});
// Apply forces to position groups
const k = alpha * 0.05;
// Try to position groups in a more structured way
// Adjust these values to change the overall layout
for (let i = 0; i < data.group_links.length; i++) {
const link = data.group_links[i];
const source = groupCenters.find(g => g.id === link.source);
const target = groupCenters.find(g => g.id === link.target);
if (source && target) {
// Add a horizontal force to align groups
const desiredDx = 300; // Desired horizontal distance between linked groups
const dx = target.cx - source.cx;
const diff = desiredDx - Math.abs(dx);
// Apply forces to group nodes
groups[source.id].nodes.forEach(n => {
if (dx > 0) {
n.vx -= diff * k;
} else {
n.vx += diff * k;
}
});
groups[target.id].nodes.forEach(n => {
if (dx > 0) {
n.vx += diff * k;
} else {
n.vx -= diff * k;
}
});
}
}
};
simulation.force("group", groupForce);
simulation.force("groupLayout", groupLayoutForce);
// Create links with arrow paths instead of lines
const link = svg.append("g")
.attr("class", "links")
.selectAll("path")
.data(data.links)
.enter()
.append("path")
.attr("stroke-width", 2)
.attr("stroke", "#999")
.attr("marker-end", "url(#arrowhead)"); // Add the arrowhead marker
// Create group containers (drawn before nodes)
const groupContainers = svg.append("g")
.attr("class", "groups")
.selectAll("rect")
.data(Object.values(groups))
.enter()
.append("rect")
.attr("class", "group-container")
.attr("fill", d => d3.color(color(d.id)).copy({opacity: 0.2}));
// Create group links between flows
const groupLink = svg.append("g")
.attr("class", "group-links")
.selectAll("path")
.data(data.group_links || [])
.enter()
.append("path")
.attr("stroke-width", 2)
.attr("stroke", "#333")
.attr("marker-end", "url(#group-arrowhead)");
// Create group link labels
const groupLinkLabel = svg.append("g")
.attr("class", "group-link-labels")
.selectAll("text")
.data(data.group_links || [])
.enter()
.append("text")
.text(d => d.action)
.attr("font-size", "11px")
.attr("font-weight", "bold")
.attr("fill", "#333");
// Create group labels
const groupLabels = svg.append("g")
.attr("class", "group-labels")
.selectAll("text")
.data(Object.values(groups))
.enter()
.append("text")
.attr("class", "group-label")
.text(d => d.name) // Now using the proper flow name
.attr("fill", d => d3.color(color(d.id)).darker());
// Create link labels
const linkLabel = svg.append("g")
.attr("class", "link-labels")
.selectAll("text")
.data(data.links)
.enter()
.append("text")
.text(d => d.action)
.attr("font-size", "10px")
.attr("fill", "#666");
// Create nodes
const node = svg.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(data.nodes)
.enter()
.append("circle")
.attr("r", 15)
.attr("fill", d => color(d.group))
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
// Create node labels
const nodeLabel = svg.append("g")
.attr("class", "node-labels")
.selectAll("text")
.data(data.nodes)
.enter()
.append("text")
.text(d => d.name)
.attr("text-anchor", "middle")
.attr("dy", 25);
// Add tooltip on hover
node.append("title")
.text(d => d.name);
// Update positions on each tick
simulation.on("tick", () => {
// Update links with straight lines
link.attr("d", d => {
return `M${d.source.x},${d.source.y} L${d.target.x},${d.target.y}`;
});
// Update nodes
node
.attr("cx", d => d.x)
.attr("cy", d => d.y);
// Update node labels
nodeLabel
.attr("x", d => d.x)
.attr("y", d => d.y);
// Position link labels at midpoint
linkLabel
.attr("x", d => (d.source.x + d.target.x) / 2)
.attr("y", d => (d.source.y + d.target.y) / 2);
// Update group containers
groupContainers.each(function(d) {
// If there are nodes in this group
if (d.nodes.length > 0) {
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
// Find the bounding box for all nodes in the group
d.nodes.forEach(n => {
minX = Math.min(minX, n.x - 30);
minY = Math.min(minY, n.y - 30);
maxX = Math.max(maxX, n.x + 30);
maxY = Math.max(maxY, n.y + 40); // Extra space for labels
});
// Add padding
const padding = 20;
minX -= padding;
minY -= padding;
maxX += padding;
maxY += padding;
// Save group dimensions
d.x = minX;
d.y = minY;
d.width = maxX - minX;
d.height = maxY - minY;
d.centerX = minX + d.width / 2;
d.centerY = minY + d.height / 2;
// Set position and size of the group container
d3.select(this)
.attr("x", minX)
.attr("y", minY)
.attr("width", d.width)
.attr("height", d.height);
// Update group label position (top-left of group)
groupLabels.filter(g => g.id === d.id)
.attr("x", minX + 10)
.attr("y", minY + 20);
}
});
// Update group links between flows
groupLink.attr("d", d => {
const sourceGroup = groups[d.source];
const targetGroup = groups[d.target];
if (!sourceGroup || !targetGroup) return "";
// Find intersection points with group boundaries
// This ensures links connect to the group's border rather than its center
// Calculate centers of groups
const sx = sourceGroup.centerX;
const sy = sourceGroup.centerY;
const tx = targetGroup.centerX;
const ty = targetGroup.centerY;
// Calculate angle between centers - used to find intersection points
const angle = Math.atan2(ty - sy, tx - sx);
// Calculate intersection points with source group borders
// We cast a ray from center in the direction of the target
let sourceX, sourceY;
const cosA = Math.cos(angle);
const sinA = Math.sin(angle);
// Check intersection with horizontal borders (top and bottom)
const ts_top = (sourceGroup.y - sy) / sinA;
const ts_bottom = (sourceGroup.y + sourceGroup.height - sy) / sinA;
// Check intersection with vertical borders (left and right)
const ts_left = (sourceGroup.x - sx) / cosA;
const ts_right = (sourceGroup.x + sourceGroup.width - sx) / cosA;
// Use the closest positive intersection (first hit with the boundary)
let t_source = Infinity;
if (ts_top > 0) t_source = Math.min(t_source, ts_top);
if (ts_bottom > 0) t_source = Math.min(t_source, ts_bottom);
if (ts_left > 0) t_source = Math.min(t_source, ts_left);
if (ts_right > 0) t_source = Math.min(t_source, ts_right);
// Target group: Find intersection in the opposite direction
// We cast a ray from target center toward the source
let targetX, targetY;
const oppositeAngle = angle + Math.PI;
const cosOpp = Math.cos(oppositeAngle);
const sinOpp = Math.sin(oppositeAngle);
// Check intersections for target group
const tt_top = (targetGroup.y - ty) / sinOpp;
const tt_bottom = (targetGroup.y + targetGroup.height - ty) / sinOpp;
const tt_left = (targetGroup.x - tx) / cosOpp;
const tt_right = (targetGroup.x + targetGroup.width - tx) / cosOpp;
// Use the closest positive intersection
let t_target = Infinity;
if (tt_top > 0) t_target = Math.min(t_target, tt_top);
if (tt_bottom > 0) t_target = Math.min(t_target, tt_bottom);
if (tt_left > 0) t_target = Math.min(t_target, tt_left);
if (tt_right > 0) t_target = Math.min(t_target, tt_right);
// Calculate actual border points using parametric equation:
// point = center + t * direction
if (t_source !== Infinity) {
sourceX = sx + cosA * t_source;
sourceY = sy + sinA * t_source;
} else {
sourceX = sx;
sourceY = sy;
}
if (t_target !== Infinity) {
targetX = tx + cosOpp * t_target;
targetY = ty + sinOpp * t_target;
} else {
targetX = tx;
targetY = ty;
}
// Create a straight line between the border points
return `M${sourceX},${sourceY} L${targetX},${targetY}`;
});
// Update group link labels
groupLinkLabel.attr("x", d => {
const sourceGroup = groups[d.source];
const targetGroup = groups[d.target];
if (!sourceGroup || !targetGroup) return 0;
return (sourceGroup.centerX + targetGroup.centerX) / 2;
})
.attr("y", d => {
const sourceGroup = groups[d.source];
const targetGroup = groups[d.target];
if (!sourceGroup || !targetGroup) return 0;
return (sourceGroup.centerY + targetGroup.centerY) / 2 - 10;
});
});
// Drag functions
function dragstarted(event, d) {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
}
function dragended(event, d) {
if (!event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
});
</script>
</body>
</html>

View File

@ -0,0 +1,99 @@
{
"nodes": [
{
"id": 3,
"name": "ValidatePayment",
"group": 2
},
{
"id": 4,
"name": "ProcessPayment",
"group": 2
},
{
"id": 5,
"name": "PaymentConfirmation",
"group": 2
},
{
"id": 7,
"name": "CheckStock",
"group": 6
},
{
"id": 8,
"name": "ReserveItems",
"group": 6
},
{
"id": 9,
"name": "UpdateInventory",
"group": 6
},
{
"id": 11,
"name": "CreateLabel",
"group": 10
},
{
"id": 12,
"name": "AssignCarrier",
"group": 10
},
{
"id": 13,
"name": "SchedulePickup",
"group": 10
}
],
"links": [
{
"source": 3,
"target": 4,
"action": "default"
},
{
"source": 4,
"target": 5,
"action": "default"
},
{
"source": 7,
"target": 8,
"action": "default"
},
{
"source": 8,
"target": 9,
"action": "default"
},
{
"source": 11,
"target": 12,
"action": "default"
},
{
"source": 12,
"target": 13,
"action": "default"
}
],
"group_links": [
{
"source": 2,
"target": 6,
"action": "default"
},
{
"source": 6,
"target": 10,
"action": "default"
}
],
"flows": {
"1": "OrderFlow",
"2": "AsyncFlow",
"6": "AsyncFlow",
"10": "AsyncFlow"
}
}

View File

@ -89,7 +89,151 @@ graph LR
end
```
## 2. Call Stack Debugging
## 2. Interactive D3.js Visualization
For more complex flows, a static diagram may not be sufficient. We provide a D3.js-based interactive visualization that allows for dragging nodes, showing group boundaries for flows, and connecting flows at their boundaries.
### Converting Flow to JSON
First, we convert the PocketFlow graph to JSON format suitable for D3.js:
```python
def flow_to_json(start):
"""Convert a flow to JSON format suitable for D3.js visualization.
This function walks through the flow graph and builds a structure with:
- nodes: All non-Flow nodes with their group memberships
- links: Connections between nodes within the same group
- group_links: Connections between different groups (for inter-flow connections)
- flows: Flow information for group labeling
"""
nodes = []
links = []
group_links = [] # For connections between groups (Flow to Flow)
ids = {}
node_types = {}
flow_nodes = {} # Keep track of flow nodes
ctr = 1
def get_id(n):
nonlocal ctr
if n not in ids:
ids[n] = ctr
node_types[ctr] = type(n).__name__
if isinstance(n, Flow):
flow_nodes[ctr] = n # Store flow reference
ctr += 1
return ids[n]
def walk(node, parent=None, group=None, parent_group=None, action=None):
# Traverse the flow graph recursively
# ...implementation details...
# Start the traversal
walk(start)
# Post-processing: Generate group links based on node connections between different groups
node_groups = {n["id"]: n["group"] for n in nodes}
filtered_links = []
for link in links:
source_id = link["source"]
target_id = link["target"]
source_group = node_groups.get(source_id, 0)
target_group = node_groups.get(target_id, 0)
# If source and target are in different groups and both groups are valid
if source_group != target_group and source_group > 0 and target_group > 0:
# Add to group links if not already there
# Skip adding this link to filtered_links - we don't want direct node connections across groups
else:
# Keep links within the same group
filtered_links.append(link)
return {
"nodes": nodes,
"links": filtered_links, # Use filtered links instead of all links
"group_links": group_links,
"flows": {str(k): v.__class__.__name__ for k, v in flow_nodes.items()},
}
```
### Creating the Visualization
Then, we generate an HTML file with D3.js visualization:
```python
def create_d3_visualization(json_data, output_dir="./viz", filename="flow_viz"):
"""Create a D3.js visualization from JSON data.
This generates an HTML file with an interactive visualization where:
- Nodes are represented as circles
- Flows are shown as dashed rectangles (groups)
- Inter-group connections are shown as dashed lines connecting at group boundaries
- Node and group labels are displayed
- Nodes can be dragged to reorganize the layout
"""
# Create output directory
os.makedirs(output_dir, exist_ok=True)
# Save JSON data to file
json_path = os.path.join(output_dir, f"{filename}.json")
with open(json_path, "w") as f:
json.dump(json_data, f, indent=2)
# Generate HTML with D3.js visualization
# ...HTML template with D3.js code...
# Write HTML to file
html_path = os.path.join(output_dir, f"{filename}.html")
with open(html_path, "w") as f:
f.write(html_content)
print(f"Visualization created at {html_path}")
return html_path
```
### Convenience Function
A convenience function to visualize flows:
```python
def visualize_flow(flow, flow_name):
"""Helper function to visualize a flow with both mermaid and D3.js"""
print(f"\n--- {flow_name} Mermaid Diagram ---")
print(build_mermaid(start=flow))
print(f"\n--- {flow_name} D3.js Visualization ---")
json_data = flow_to_json(flow)
create_d3_visualization(
json_data, filename=f"{flow_name.lower().replace(' ', '_')}"
)
```
### Usage Example
```python
from visualize import visualize_flow
# Create a complex flow with nested subflows
# ...flow definition...
# Generate visualization
visualize_flow(data_science_flow, "Data Science Flow")
```
This generates:
1. A mermaid diagram in the console
2. A JSON file with the flow structure
3. An HTML file with the interactive D3.js visualization
The D3.js visualization offers several advantages:
- **Interactivity**: Nodes can be dragged to reorganize the layout
- **Group visualization**: Flows are shown as groups with their own boundaries
- **Inter-group connections**: Links between groups connect at boundaries for cleaner visualization
- **Action labels**: Edge labels show transition actions
## 3. Call Stack Debugging
It would be useful to print the Node call stacks for debugging. This can be achieved by inspecting the runtime call stack: