feat(FN-3082): restructure dependency graph plugin with modular architectur
Implements a modular dependency graph feature for the Fusion dashboard plugin, replacing the monolithic `DependencyGraphView` component with a factored architecture: graph types and filtering (Step 1), a data hook (Step 2), auto-layout engine (Step 3), SVG edge rendering (Step 4), an interaction hoo Fusion-Task-Id: FN-3082
This commit is contained in:
91
plugins/fusion-plugin-dependency-graph/src/layout.ts
Normal file
91
plugins/fusion-plugin-dependency-graph/src/layout.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import type { GraphData, GraphPosition } from "./types";
|
||||
|
||||
export interface LayoutOptions {
|
||||
nodeWidth?: number;
|
||||
nodeHeight?: number;
|
||||
horizontalGap?: number;
|
||||
verticalGap?: number;
|
||||
}
|
||||
|
||||
const DEFAULT_LAYOUT_OPTIONS: Required<LayoutOptions> = {
|
||||
nodeWidth: 280,
|
||||
nodeHeight: 100,
|
||||
horizontalGap: 40,
|
||||
verticalGap: 80,
|
||||
};
|
||||
|
||||
export function computeAutoLayout(
|
||||
graphData: GraphData,
|
||||
options?: LayoutOptions,
|
||||
): Map<string, GraphPosition> {
|
||||
const settings = { ...DEFAULT_LAYOUT_OPTIONS, ...options };
|
||||
const nodeIds = graphData.nodes.map((node) => node.task.id);
|
||||
if (nodeIds.length === 0) return new Map();
|
||||
|
||||
const dependentsByDependency = new Map<string, string[]>();
|
||||
const inDegree = new Map<string, number>();
|
||||
|
||||
for (const id of nodeIds) {
|
||||
inDegree.set(id, 0);
|
||||
dependentsByDependency.set(id, []);
|
||||
}
|
||||
|
||||
for (const edge of graphData.edges) {
|
||||
if (!inDegree.has(edge.source) || !inDegree.has(edge.target)) continue;
|
||||
dependentsByDependency.get(edge.target)?.push(edge.source);
|
||||
inDegree.set(edge.source, (inDegree.get(edge.source) ?? 0) + 1);
|
||||
}
|
||||
|
||||
const queue = nodeIds.filter((id) => (inDegree.get(id) ?? 0) === 0);
|
||||
const topologicalOrder: string[] = [];
|
||||
|
||||
while (queue.length > 0) {
|
||||
const current = queue.shift()!;
|
||||
topologicalOrder.push(current);
|
||||
for (const dependent of dependentsByDependency.get(current) ?? []) {
|
||||
const nextInDegree = (inDegree.get(dependent) ?? 0) - 1;
|
||||
inDegree.set(dependent, nextInDegree);
|
||||
if (nextInDegree === 0) queue.push(dependent);
|
||||
}
|
||||
}
|
||||
|
||||
for (const id of nodeIds) {
|
||||
if (!topologicalOrder.includes(id)) topologicalOrder.push(id);
|
||||
}
|
||||
|
||||
const depthByNode = new Map<string, number>();
|
||||
for (const id of topologicalOrder) {
|
||||
const parents = graphData.edges.filter((edge) => edge.source === id).map((edge) => edge.target);
|
||||
let depth = 0;
|
||||
for (const parent of parents) {
|
||||
depth = Math.max(depth, (depthByNode.get(parent) ?? 0) + 1);
|
||||
}
|
||||
depthByNode.set(id, depth);
|
||||
}
|
||||
|
||||
const layers = new Map<number, string[]>();
|
||||
for (const id of nodeIds) {
|
||||
const depth = depthByNode.get(id) ?? 0;
|
||||
const layer = layers.get(depth) ?? [];
|
||||
layer.push(id);
|
||||
layers.set(depth, layer);
|
||||
}
|
||||
|
||||
const positions = new Map<string, GraphPosition>();
|
||||
const sortedDepths = Array.from(layers.keys()).sort((a, b) => a - b);
|
||||
|
||||
for (const depth of sortedDepths) {
|
||||
const layer = layers.get(depth) ?? [];
|
||||
layer.sort();
|
||||
const layerWidth = layer.length * settings.nodeWidth + Math.max(0, layer.length - 1) * settings.horizontalGap;
|
||||
const startX = -layerWidth / 2;
|
||||
|
||||
layer.forEach((id, index) => {
|
||||
const x = startX + index * (settings.nodeWidth + settings.horizontalGap);
|
||||
const y = depth * (settings.nodeHeight + settings.verticalGap);
|
||||
positions.set(id, { x, y });
|
||||
});
|
||||
}
|
||||
|
||||
return positions;
|
||||
}
|
||||
Reference in New Issue
Block a user