diff --git a/dfgraph/frontend/index.html b/dfgraph/frontend/index.html
index 15c780d..9189be8 100644
--- a/dfgraph/frontend/index.html
+++ b/dfgraph/frontend/index.html
@@ -9,12 +9,15 @@
body { font-family: system-ui, sans-serif; height: 100vh; display: flex; flex-direction: column; }
#toolbar { padding: 8px 16px; background: #1a1a2e; color: #eee; display: flex; align-items: center; gap: 12px; }
#toolbar h1 { font-size: 14px; font-weight: 600; }
+ #view-toggle { padding: 6px 12px; background: #5c6bc0; color: #eee; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; font-weight: 500; }
+ #view-toggle:hover { background: #3f51b5; }
#graph { flex: 1; }
dfgraph
+
diff --git a/dfgraph/frontend/src/layout.ts b/dfgraph/frontend/src/layout.ts
index 1659fac..dc9c643 100644
--- a/dfgraph/frontend/src/layout.ts
+++ b/dfgraph/frontend/src/layout.ts
@@ -8,3 +8,14 @@ export function logicalLayout(): object {
animate: false,
};
}
+
+export function physicalLayout(): object {
+ return {
+ name: "dagre",
+ rankDir: "TB",
+ nodeSep: 40,
+ rankSep: 60,
+ edgeSep: 15,
+ animate: false,
+ };
+}
diff --git a/dfgraph/frontend/src/main.ts b/dfgraph/frontend/src/main.ts
index e6fe00c..e17d77e 100644
--- a/dfgraph/frontend/src/main.ts
+++ b/dfgraph/frontend/src/main.ts
@@ -2,7 +2,7 @@ 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";
+import { logicalLayout, physicalLayout } from "./layout";
cytoscape.use(dagre);
@@ -21,6 +21,13 @@ function buildLabel(node: GraphNode): string {
return node.opcode;
}
+function buildPhysicalLabel(node: GraphNode): string {
+ const parts = [node.opcode];
+ if (node.const !== null) parts.push(`=${node.const}`);
+ if (node.iram_offset !== null) parts.push(`\n[iram:${node.iram_offset}, ctx:${node.ctx}]`);
+ return parts.join("");
+}
+
function buildElements(
update: GraphUpdate
): cytoscape.ElementDefinition[] {
@@ -80,7 +87,77 @@ function buildElements(
return elements;
}
-function renderGraph(update: GraphUpdate): void {
+function buildPhysicalElements(update: GraphUpdate): cytoscape.ElementDefinition[] {
+ const elements: cytoscape.ElementDefinition[] = [];
+ const peNodes = new Map();
+
+ // Group nodes by PE
+ for (const node of update.nodes) {
+ if (node.pe !== null) {
+ if (!peNodes.has(node.pe)) peNodes.set(node.pe, []);
+ peNodes.get(node.pe)!.push(node.id);
+ }
+ }
+
+ // Create PE cluster parent nodes
+ for (const peId of peNodes.keys()) {
+ elements.push({
+ data: { id: `pe-${peId}`, label: `PE ${peId}` },
+ classes: "pe-cluster",
+ });
+ }
+
+ // Create operation nodes parented to PE clusters
+ for (const node of update.nodes) {
+ const el: cytoscape.ElementDefinition = {
+ data: {
+ id: node.id,
+ label: buildPhysicalLabel(node),
+ colour: node.colour,
+ category: node.category,
+ pe: node.pe,
+ iram_offset: node.iram_offset,
+ ctx: node.ctx,
+ },
+ classes: node.has_error ? "error" : undefined,
+ };
+ if (node.pe !== null) {
+ el.data.parent = `pe-${node.pe}`;
+ }
+ elements.push(el);
+ }
+
+ // Create edges with cross-PE / intra-PE classification
+ const nodePeMap = new Map();
+ for (const node of update.nodes) {
+ nodePeMap.set(node.id, node.pe);
+ }
+
+ for (const edge of update.edges) {
+ const sourcePe = nodePeMap.get(edge.source);
+ const targetPe = nodePeMap.get(edge.target);
+ const isCrossPe = sourcePe !== null && targetPe !== null && sourcePe !== targetPe;
+
+ elements.push({
+ data: {
+ id: `${edge.source}->${edge.target}:${edge.port}`,
+ source: edge.source,
+ target: edge.target,
+ targetLabel: edge.port,
+ sourceLabel: "",
+ },
+ classes: (isCrossPe ? "cross-pe" : "intra-pe") + (edge.has_error ? " error" : ""),
+ });
+ }
+
+ return elements;
+}
+
+type ViewMode = "logical" | "physical";
+let currentView: ViewMode = "logical";
+let latestUpdate: GraphUpdate | null = null;
+
+function renderLogical(update: GraphUpdate): void {
cy.batch(() => {
cy.elements().remove();
cy.add(buildElements(update));
@@ -89,6 +166,53 @@ function renderGraph(update: GraphUpdate): void {
cy.fit(undefined, 40);
}
+function renderPhysical(update: GraphUpdate): void {
+ cy.batch(() => {
+ cy.elements().remove();
+ cy.add(buildPhysicalElements(update));
+ });
+ cy.layout(physicalLayout()).run();
+ cy.fit(undefined, 40);
+}
+
+function renderUpdate(update: GraphUpdate): void {
+ latestUpdate = update;
+ if (currentView === "physical" && update.stage !== "allocate") {
+ currentView = "logical";
+ const toggleBtn = document.getElementById("view-toggle");
+ if (toggleBtn) {
+ toggleBtn.textContent = "Physical View";
+ }
+ }
+ if (currentView === "logical") {
+ renderLogical(update);
+ } else {
+ renderPhysical(update);
+ }
+}
+
+function setupToggleButton(): void {
+ const toggleBtn = document.getElementById("view-toggle");
+ if (toggleBtn) {
+ toggleBtn.addEventListener("click", () => {
+ if (!latestUpdate) return;
+ if (currentView === "logical") {
+ if (latestUpdate.stage !== "allocate") {
+ // Physical view not available
+ return;
+ }
+ currentView = "physical";
+ toggleBtn.textContent = "Logical View";
+ renderPhysical(latestUpdate);
+ } else {
+ currentView = "logical";
+ toggleBtn.textContent = "Physical View";
+ renderLogical(latestUpdate);
+ }
+ });
+ }
+}
+
function connect(): void {
const protocol = location.protocol === "https:" ? "wss:" : "ws:";
const ws = new WebSocket(`${protocol}//${location.host}/ws`);
@@ -96,7 +220,7 @@ function connect(): void {
ws.onmessage = (event: MessageEvent) => {
const update: GraphUpdate = JSON.parse(event.data);
if (update.type === "graph_update") {
- renderGraph(update);
+ renderUpdate(update);
}
};
@@ -105,4 +229,5 @@ function connect(): void {
};
}
+setupToggleButton();
connect();
diff --git a/dfgraph/frontend/src/style.ts b/dfgraph/frontend/src/style.ts
index 5133139..137cfb0 100644
--- a/dfgraph/frontend/src/style.ts
+++ b/dfgraph/frontend/src/style.ts
@@ -74,4 +74,36 @@ export const stylesheet: cytoscape.Stylesheet[] = [
"target-arrow-color": "#e53935",
},
},
+ {
+ selector: "node.pe-cluster",
+ style: {
+ shape: "roundrectangle",
+ "border-width": 2,
+ "border-color": "#5c6bc0",
+ "background-color": "rgba(92, 107, 192, 0.06)",
+ padding: "20px",
+ "text-valign": "top",
+ "text-halign": "center",
+ label: "data(label)",
+ "font-size": 13,
+ "font-weight": "bold",
+ color: "#5c6bc0",
+ },
+ },
+ {
+ selector: "edge.cross-pe",
+ style: {
+ width: 3,
+ "line-color": "#5c6bc0",
+ "target-arrow-color": "#5c6bc0",
+ },
+ },
+ {
+ selector: "edge.intra-pe",
+ style: {
+ width: 1.5,
+ "line-color": "#bbb",
+ "target-arrow-color": "#bbb",
+ },
+ },
];