Dataflow Graph Renderer Implementation Plan — Phase 5: Frontend — Logical View#
Goal: Cytoscape.js rendering of the logical dataflow graph with coloured circular nodes, edge annotations, and function region boxes.
Architecture: Three TypeScript modules: main.ts (WebSocket client, cytoscape init, graph updates), style.ts (stylesheet definitions), layout.ts (dagre layout config). The frontend receives graph_update JSON from the backend WebSocket and renders it using cytoscape.js with the dagre hierarchical layout.
Tech Stack: TypeScript, cytoscape.js 3.30+, cytoscape-dagre 2.5+, esbuild
Scope: 8 phases from original design (this is phase 5 of 8)
Codebase verified: 2026-02-23
Acceptance Criteria Coverage#
This phase implements and tests:
dataflow-renderer.AC1: Logical view renders dataflow graph#
- dataflow-renderer.AC1.1 Success: Valid dfasm source renders as a directed graph with nodes and edges
- dataflow-renderer.AC1.2 Success: Nodes are circular with opcode mnemonic labels centred inside
- dataflow-renderer.AC1.3 Success: Nodes are coloured by opcode category (arithmetic=blue, logic=green, comparison=amber, routing=purple, memory=orange, io=teal, config=grey)
- dataflow-renderer.AC1.4 Success: Edges show port annotations (L/R) at target end and branch labels (T/F) at source end for routing ops
- dataflow-renderer.AC1.5 Success: Function regions render as dashed bounding boxes around their child nodes
Task 1: Create dfgraph/frontend/src/types.ts — shared type definitions#
Verifies: (supports all AC1.x — defines the JSON contract)
Files:
- Create:
/home/orual/Projects/or1-design/dfgraph/frontend/src/types.ts
Implementation:
Define TypeScript interfaces matching the JSON structure produced by dfgraph/graph_json.py (Phase 3). These types are used by all frontend modules.
export interface GraphNode {
id: string;
opcode: string;
category: string;
colour: string;
const: number | null;
pe: number | null;
iram_offset: number | null;
ctx: number | null;
has_error: boolean;
loc: SourceLoc;
}
export interface SourceLoc {
line: number;
column: number;
end_line: number | null;
end_column: number | null;
}
export interface AddrInfo {
offset: number;
port: string;
pe: number | null;
}
export interface GraphEdge {
source: string;
target: string;
port: string;
source_port: string | null;
has_error: boolean;
addr?: AddrInfo;
}
export interface GraphRegion {
tag: string;
kind: string;
node_ids: string[];
}
export interface GraphError {
line: number;
column: number;
category: string;
message: string;
suggestions: string[];
}
export interface GraphUpdate {
type: "graph_update";
stage: string;
nodes: GraphNode[];
edges: GraphEdge[];
regions: GraphRegion[];
errors: GraphError[];
parse_error: string | null;
metadata: {
stage: string;
pe_count: number;
sm_count: number;
};
}
Verification:
cd /home/orual/Projects/or1-design/dfgraph/frontend
npx esbuild src/types.ts --bundle --outfile=/dev/null --format=esm --target=es2020
Expected: No type errors.
Commit: feat: add TypeScript type definitions for graph JSON
Task 2: Create dfgraph/frontend/src/style.ts — cytoscape stylesheet#
Verifies: dataflow-renderer.AC1.2, dataflow-renderer.AC1.3, dataflow-renderer.AC1.4, dataflow-renderer.AC1.5
Files:
- Create:
/home/orual/Projects/or1-design/dfgraph/frontend/src/style.ts
Implementation:
Export a cytoscape stylesheet array. Key styling rules:
-
Nodes (AC1.2): shape
ellipse,width/heightsized to label with padding, centered label viatext-valign: centerandtext-halign: center,labelfromdata(label). -
Category colours (AC1.3):
background-colorfromdata(colour)— the backend sets the colour per node based on opcode category. The colours are: arithmetic=#4a90d9 (blue), logic=#4caf50 (green), comparison=#ff9800 (amber), routing=#9c27b0 (purple), memory=#ff5722 (orange), config=#9e9e9e (grey). -
Edge annotations (AC1.4):
- Port labels (L/R) at target end via
target-label: data(targetLabel)withtarget-text-offset - Branch labels (T/F) at source end for routing ops via
source-label: data(sourceLabel)withsource-text-offset - Constant values shown as node labels (already part of the opcode label)
- Port labels (L/R) at target end via
-
Function regions (AC1.5): compound parent nodes with
$node > nodeselector — dashed border, light background, label at top. -
Error styling: nodes with
.errorclass get red dashed border. Edges with.errorclass get red dashed line.
import cytoscape from "cytoscape";
export const stylesheet: cytoscape.Stylesheet[] = [
{
selector: "node",
style: {
shape: "ellipse",
width: "label",
height: "label",
padding: "12px",
"text-valign": "center",
"text-halign": "center",
label: "data(label)",
"font-size": 11,
"font-family": "monospace",
color: "#fff",
"text-outline-width": 0,
"background-color": "data(colour)",
"border-width": 2,
"border-color": "data(colour)",
},
},
{
selector: "$node > node",
style: {
shape: "roundrectangle",
"border-style": "dashed",
"border-width": 2,
"border-color": "#888",
"background-color": "rgba(200, 200, 200, 0.08)",
padding: "24px",
"text-valign": "top",
"text-halign": "center",
label: "data(label)",
"font-size": 12,
color: "#666",
},
},
{
selector: "edge",
style: {
"curve-style": "bezier",
"target-arrow-shape": "triangle",
"target-arrow-color": "#999",
"line-color": "#999",
width: 2,
"target-label": "data(targetLabel)",
"target-text-offset": 18,
"target-text-margin-y": -10,
"source-label": "data(sourceLabel)",
"source-text-offset": 18,
"source-text-margin-y": -10,
"font-size": 9,
"font-family": "monospace",
color: "#666",
"text-background-color": "#fff",
"text-background-opacity": 0.8,
"text-background-padding": "2px",
},
},
{
selector: "node.error",
style: {
"border-style": "dashed",
"border-width": 3,
"border-color": "#e53935",
},
},
{
selector: "edge.error",
style: {
"line-style": "dashed",
"line-color": "#e53935",
"target-arrow-color": "#e53935",
},
},
];
Verification:
cd /home/orual/Projects/or1-design/dfgraph/frontend
npx esbuild src/style.ts --bundle --outfile=/dev/null --format=esm --target=es2020
Expected: No errors.
Commit: feat: add cytoscape stylesheet for logical view
Task 3: Create dfgraph/frontend/src/layout.ts — dagre layout configuration#
Verifies: dataflow-renderer.AC1.1, dataflow-renderer.AC1.5
Files:
- Create:
/home/orual/Projects/or1-design/dfgraph/frontend/src/layout.ts
Implementation:
Export a function that returns the dagre layout options. The layout should be top-to-bottom hierarchical with reasonable spacing for small-to-medium graphs (5–50 nodes).
export function logicalLayout(): object {
return {
name: "dagre",
rankDir: "TB",
nodeSep: 60,
rankSep: 80,
edgeSep: 20,
animate: false,
};
}
Verification:
cd /home/orual/Projects/or1-design/dfgraph/frontend
npx esbuild src/layout.ts --bundle --outfile=/dev/null --format=esm --target=es2020
Expected: No errors.
Commit: feat: add dagre layout configuration
Task 4: Rewrite dfgraph/frontend/src/main.ts — WebSocket client and graph rendering#
Verifies: dataflow-renderer.AC1.1, dataflow-renderer.AC1.2, dataflow-renderer.AC1.3, dataflow-renderer.AC1.4, dataflow-renderer.AC1.5
Files:
- Modify:
/home/orual/Projects/or1-design/dfgraph/frontend/src/main.ts(replace Phase 1 placeholder)
Implementation:
Replace the minimal placeholder from Phase 1 with the full WebSocket client and graph rendering logic.
Key responsibilities:
- Initialize cytoscape with the stylesheet and an empty graph
- Connect to WebSocket at
ws://${location.host}/ws - On
graph_updatemessage, convert JSON to cytoscape elements and re-render - Handle reconnection on WebSocket close
Converting backend JSON to cytoscape elements:
- Each
GraphNodebecomes a cytoscape node element withdata: { id, label, colour, category, ... } - The label should include the opcode mnemonic, plus constant value if present (e.g., "const\n42")
- Each
GraphEdgebecomes a cytoscape edge element withdata: { source, target, targetLabel, sourceLabel }targetLabel= edge port ("L" or "R")sourceLabel= branch label for routing ops — check if source node is a routing op, and ifsource_portis set, use "T" for L and "F" for R
- Each
GraphRegionwithkind: "function"becomes a compound parent node, and itsnode_idsnodes getparentset to the region'stag - Nodes with
has_error: trueget theerrorclass - Edges with
has_error: trueget theerrorclass
import cytoscape from "cytoscape";
import dagre from "cytoscape-dagre";
import type { GraphUpdate, GraphNode, GraphEdge, GraphRegion } from "./types";
import { stylesheet } from "./style";
import { logicalLayout } from "./layout";
cytoscape.use(dagre);
const ROUTING_CATEGORY = "routing";
const cy = cytoscape({
container: document.getElementById("graph"),
style: stylesheet,
elements: [],
});
function buildLabel(node: GraphNode): string {
if (node.const !== null) {
return `${node.opcode}\n${node.const}`;
}
return node.opcode;
}
function buildElements(
update: GraphUpdate
): cytoscape.ElementDefinition[] {
const elements: cytoscape.ElementDefinition[] = [];
const regionParents = new Map<string, string>();
for (const region of update.regions) {
if (region.kind === "function") {
elements.push({
data: { id: region.tag, label: region.tag },
});
for (const nodeId of region.node_ids) {
regionParents.set(nodeId, region.tag);
}
}
}
for (const node of update.nodes) {
const el: cytoscape.ElementDefinition = {
data: {
id: node.id,
label: buildLabel(node),
colour: node.colour,
category: node.category,
pe: node.pe,
iram_offset: node.iram_offset,
ctx: node.ctx,
},
classes: node.has_error ? "error" : undefined,
};
const parent = regionParents.get(node.id);
if (parent) {
el.data.parent = parent;
}
elements.push(el);
}
for (const edge of update.edges) {
const sourceNode = update.nodes.find((n) => n.id === edge.source);
let sourceLabel: string | undefined;
if (sourceNode && sourceNode.category === ROUTING_CATEGORY && edge.source_port) {
sourceLabel = edge.source_port === "L" ? "T" : "F";
}
elements.push({
data: {
id: `${edge.source}->${edge.target}:${edge.port}`,
source: edge.source,
target: edge.target,
targetLabel: edge.port,
sourceLabel: sourceLabel ?? "",
},
classes: edge.has_error ? "error" : undefined,
});
}
return elements;
}
function renderGraph(update: GraphUpdate): void {
cy.batch(() => {
cy.elements().remove();
cy.add(buildElements(update));
});
cy.layout(logicalLayout()).run();
cy.fit(undefined, 40);
}
function connect(): void {
const protocol = location.protocol === "https:" ? "wss:" : "ws:";
const ws = new WebSocket(`${protocol}//${location.host}/ws`);
ws.onmessage = (event: MessageEvent) => {
const update: GraphUpdate = JSON.parse(event.data);
if (update.type === "graph_update") {
renderGraph(update);
}
};
ws.onclose = () => {
setTimeout(connect, 2000);
};
}
connect();
Verification:
cd /home/orual/Projects/or1-design/dfgraph/frontend
npm run build
ls -la dist/bundle.js
Expected: dist/bundle.js is created without errors.
Commit: feat: implement WebSocket client and logical view rendering
Task 5: Manual verification of the logical view#
Files: None (manual testing)
Step 1: Create a test dfasm file
Create a temporary test file (e.g., /tmp/test_render.dfasm) with a simple program. Use a valid program from the existing test suite. A minimal example:
@system pe=2 sm=1
$main {
&a = add @const_1 @const_2
&b = sub &a @const_3
&out = pass &b
&const_1 = const 10
&const_2 = const 20
&const_3 = const 5
}
(Adapt this to match actual dfasm syntax — check tests/test_e2e.py for known-working examples.)
Step 2: Build the frontend
cd /home/orual/Projects/or1-design/dfgraph/frontend
npm run build
Step 3: Start the server
cd /home/orual/Projects/or1-design
python -m dfgraph /tmp/test_render.dfasm
Expected: Browser opens showing an interactive graph.
Step 4: Verify visual elements
Check visually:
- Nodes are circular with opcode labels (AC1.2)
- Nodes have category-appropriate colours (AC1.3)
- Edges have arrowheads and port labels (AC1.4)
- Function region (
$main) has a dashed bounding box (AC1.5)
Step 5: Run full test suite
cd /home/orual/Projects/or1-design
python -m pytest tests/ -v
Expected: All tests pass.