Skip to content

Commit ff44aff

Browse files
committed
Reduce number of inits: only keep the model that computes it, not the ones that map into the variable
1 parent 27f4011 commit ff44aff

7 files changed

Lines changed: 1214 additions & 159 deletions

File tree

frontend/src/App.tsx

Lines changed: 784 additions & 135 deletions
Large diffs are not rendered by default.

frontend/src/DependencyEdge.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ export function DependencyEdge({
3333
offset: 28,
3434
});
3535

36+
if (!data) {
37+
return <BaseEdge id={id} path={path} markerEnd={markerEnd} style={style} interactionWidth={18} />;
38+
}
39+
3640
const label = data?.label;
3741
const renamed = data?.sourceVariable && data?.targetVariable && data.sourceVariable !== data.targetVariable;
3842
const isCallEdge = data?.kind === "hard_dependency" && !data.sourcePort && !data.targetPort;

frontend/src/ModelNode.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
import { Handle, Position, type Node, type NodeProps } from "@xyflow/react";
22
import { Clock3, GitBranch, Layers3, Link2, PhoneCall } from "lucide-react";
33
import type { GraphPort, RuntimeGraphNodeData } from "./types";
4+
import { nodeWidth } from "./nodeSizing";
45

56
type ModelFlowNode = Node<RuntimeGraphNodeData, "model">;
67

78
export function ModelNode({ data, selected }: NodeProps<ModelFlowNode>) {
89
const cyclic = Boolean(data.cyclic);
10+
const dimmed = Boolean(data.dimmed);
11+
const focused = Boolean(data.focused);
912
return (
10-
<section className={`model-node ${data.role} ${cyclic ? "cyclic" : ""} ${selected ? "selected" : ""}`} data-scale={data.scale}>
13+
<section
14+
className={`model-node ${data.role} ${cyclic ? "cyclic" : ""} ${selected ? "selected" : ""} ${focused ? "focused" : ""} ${dimmed ? "dimmed" : ""}`}
15+
data-scale={data.scale}
16+
style={{ width: nodeWidth(data) }}
17+
>
1118
<Handle className="call-handle call-target" id={`${data.id}:call-target`} type="target" position={Position.Left} />
1219
<Handle className="call-handle call-source" id={`${data.id}:call-source`} type="source" position={Position.Right} />
1320
<header className="node-header">
@@ -41,13 +48,14 @@ export function ModelNode({ data, selected }: NodeProps<ModelFlowNode>) {
4148

4249
function PortColumn({ title, ports, side, data }: { title: string; ports: GraphPort[]; side: "input" | "output"; data: RuntimeGraphNodeData }) {
4350
const highlighted = new Set(data.highlightedPortIds ?? []);
51+
const focused = new Set(data.focusedPortIds ?? []);
4452
const requiredInputs = new Set(data.requiredInputPortIds ?? []);
4553
return (
4654
<div className={`port-column ${side}`}>
4755
<div className="port-title">{title}</div>
4856
{ports.map((port) => (
4957
<div
50-
className={`port ${port.mappingMode ? "mapped" : ""} ${requiredInputs.has(port.id) ? "required-input" : ""} ${port.previousTimeStep ? "previous" : ""} ${highlighted.has(port.id) ? "highlighted" : ""} ${data.activePortId === port.id ? "active" : ""}`}
58+
className={`port ${port.mappingMode ? "mapped" : ""} ${requiredInputs.has(port.id) ? "required-input" : ""} ${port.previousTimeStep ? "previous" : ""} ${focused.has(port.id) ? "focused" : ""} ${highlighted.has(port.id) ? "highlighted" : ""} ${data.activePortId === port.id ? "active" : ""}`}
5159
key={port.id}
5260
data-default={`${requiredInputs.has(port.id) ? "Required initialization" : portValueLabel(port)}: ${port.default}`}
5361
aria-label={`${port.name}, ${side}, ${requiredInputs.has(port.id) ? "required initialization" : portValueLabel(port).toLowerCase()} ${port.default}`}

frontend/src/layout.ts

Lines changed: 62 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,19 @@
11
import ELK from "elkjs/lib/elk.bundled.js";
22
import type { Edge, Node } from "@xyflow/react";
33
import type { GraphEdgeData, GraphPort, RuntimeGraphNodeData } from "./types";
4+
import { nodeWidth } from "./nodeSizing";
45

56
const elk = new ELK();
6-
const NODE_WIDTH = 312;
7+
export type LayoutMode = "data_flow" | "compact" | "scale_grouped" | "call_stack";
78

8-
export async function layoutGraph(nodes: Node<RuntimeGraphNodeData>[], edges: Edge<GraphEdgeData>[]) {
9+
export async function layoutGraph(nodes: Node<RuntimeGraphNodeData>[], edges: Edge<GraphEdgeData>[], mode: LayoutMode = "data_flow") {
10+
const layoutEdges = mode === "call_stack" ? edges.filter((edge) => isCallEdge(edge.data)) : edges;
911
const graph = {
1012
id: "root",
11-
layoutOptions: {
12-
"elk.algorithm": "layered",
13-
"elk.direction": "RIGHT",
14-
"elk.spacing.nodeNode": "58",
15-
"elk.layered.spacing.nodeNodeBetweenLayers": "110",
16-
"elk.layered.nodePlacement.strategy": "BRANDES_KOEPF",
17-
"elk.layered.crossingMinimization.semiInteractive": "true",
18-
"elk.edgeRouting": "ORTHOGONAL",
19-
},
13+
layoutOptions: layoutOptions(mode),
2014
children: nodes.map((node) => ({
2115
id: node.id,
22-
width: NODE_WIDTH,
16+
width: nodeWidth(node.data),
2317
height: nodeHeight(node.data),
2418
ports: [
2519
elkCallPort(node.id, "target"),
@@ -31,7 +25,7 @@ export async function layoutGraph(nodes: Node<RuntimeGraphNodeData>[], edges: Ed
3125
"org.eclipse.elk.portConstraints": "FIXED_ORDER",
3226
},
3327
})),
34-
edges: edges.map((edge) => ({
28+
edges: layoutEdges.map((edge) => ({
3529
id: edge.id,
3630
sources: [edge.sourceHandle ?? edge.source],
3731
targets: [edge.targetHandle ?? edge.target],
@@ -40,11 +34,62 @@ export async function layoutGraph(nodes: Node<RuntimeGraphNodeData>[], edges: Ed
4034

4135
const result = await elk.layout(graph);
4236
const positions = new Map((result.children ?? []).map((child) => [child.id, { x: child.x ?? 0, y: child.y ?? 0 }]));
37+
const scaleOffsets = mode === "scale_grouped" ? scaleBandOffsets(nodes) : new Map<string, number>();
38+
39+
return nodes.map((node) => {
40+
const position = positions.get(node.id) ?? node.position;
41+
return {
42+
...node,
43+
position: {
44+
x: position.x,
45+
y: position.y + (scaleOffsets.get(node.data.scale) ?? 0),
46+
},
47+
};
48+
});
49+
}
50+
51+
function layoutOptions(mode: LayoutMode): Record<string, string> {
52+
if (mode === "compact") {
53+
return {
54+
"elk.algorithm": "layered",
55+
"elk.direction": "RIGHT",
56+
"elk.spacing.nodeNode": "28",
57+
"elk.layered.spacing.nodeNodeBetweenLayers": "52",
58+
"elk.layered.nodePlacement.strategy": "BRANDES_KOEPF",
59+
"elk.layered.crossingMinimization.semiInteractive": "true",
60+
"elk.edgeRouting": "ORTHOGONAL",
61+
};
62+
}
63+
64+
if (mode === "call_stack") {
65+
return {
66+
"elk.algorithm": "layered",
67+
"elk.direction": "DOWN",
68+
"elk.spacing.nodeNode": "46",
69+
"elk.layered.spacing.nodeNodeBetweenLayers": "76",
70+
"elk.layered.nodePlacement.strategy": "NETWORK_SIMPLEX",
71+
"elk.edgeRouting": "ORTHOGONAL",
72+
};
73+
}
74+
75+
return {
76+
"elk.algorithm": "layered",
77+
"elk.direction": "RIGHT",
78+
"elk.spacing.nodeNode": mode === "scale_grouped" ? "72" : "58",
79+
"elk.layered.spacing.nodeNodeBetweenLayers": mode === "scale_grouped" ? "130" : "110",
80+
"elk.layered.nodePlacement.strategy": "BRANDES_KOEPF",
81+
"elk.layered.crossingMinimization.semiInteractive": "true",
82+
"elk.edgeRouting": "ORTHOGONAL",
83+
};
84+
}
85+
86+
function scaleBandOffsets(nodes: Node<RuntimeGraphNodeData>[]) {
87+
const scales = [...new Set(nodes.map((node) => node.data.scale))].sort();
88+
return new Map(scales.map((scale, index) => [scale, index * 260]));
89+
}
4390

44-
return nodes.map((node) => ({
45-
...node,
46-
position: positions.get(node.id) ?? node.position,
47-
}));
91+
function isCallEdge(edge?: GraphEdgeData) {
92+
return edge?.kind === "hard_dependency" && !edge.sourcePort && !edge.targetPort;
4893
}
4994

5095
function nodeHeight(node: RuntimeGraphNodeData) {

frontend/src/nodeSizing.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import type { RuntimeGraphNodeData } from "./types";
2+
3+
const MIN_NODE_WIDTH = 312;
4+
const MAX_NODE_WIDTH = 620;
5+
const CARD_PADDING = 24;
6+
const GRID_GAP = 10;
7+
const PORT_HORIZONTAL_PADDING = 26;
8+
const MONO_CHAR_WIDTH = 8.1;
9+
10+
export function nodeWidth(node: RuntimeGraphNodeData) {
11+
const longestInput = longestPortName(node.inputs);
12+
const longestOutput = longestPortName(node.outputs);
13+
const inputWidth = portColumnWidth(longestInput);
14+
const outputWidth = portColumnWidth(longestOutput);
15+
return clamp(Math.ceil(CARD_PADDING + inputWidth + GRID_GAP + outputWidth), MIN_NODE_WIDTH, MAX_NODE_WIDTH);
16+
}
17+
18+
function longestPortName(ports: RuntimeGraphNodeData["inputs"]) {
19+
return ports.reduce((longest, port) => Math.max(longest, port.name.length), 0);
20+
}
21+
22+
function portColumnWidth(characters: number) {
23+
return Math.ceil(PORT_HORIZONTAL_PADDING + characters * MONO_CHAR_WIDTH);
24+
}
25+
26+
function clamp(value: number, min: number, max: number) {
27+
return Math.max(min, Math.min(max, value));
28+
}

0 commit comments

Comments
 (0)