# All files / src/components/dag useDagLayout.ts

70.73% Statements 29/41
41.66% Branches 5/12
66.66% Functions 4/6
71.79% Lines 28/39

Press n or j to go to the next uncovered block, b, p or k for the previous block.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140

    3x

    3x

    3x

    3x

    3x

    3x

    6x
    6x
    3x

    3x
    3x
    3x

    5x

    3x
    3x
    5x

    3x
    5x

    3x

    3x

    5x

    5x

    3x

    3x
    5x
    5x

    5x

    3x

     import { useMemo } from "react";
    import dagre from "@dagrejs/dagre";
    import type { Node, Edge } from "@xyflow/react";
    import type { TaskData } from "../../hooks/types.js";

    /** Width of each task node in the DAG layout (pixels). */
    const NODE_WIDTH: number = 220;
    /** Height of each task node in the DAG layout (pixels). */
    const NODE_HEIGHT: number = 70;
    /** Horizontal separation between sibling nodes (pixels). */
    const NODE_SEPARATION: number = 40;
    /** Vertical separation between rank levels (pixels). */
    const RANK_SEPARATION: number = 60;

    /** Edge type identifier for parent→child (hierarchy) edges. */
    const EDGE_TYPE_HIERARCHY: string = "hierarchy";
    /** Edge type identifier for dependency edges. */
    const EDGE_TYPE_DEPENDENCY: string = "dependency";

    /** Data attached to each React Flow task node. */
    export interface TaskNodeData extends Record<string, unknown> {
      task: TaskData;
      childCount: number;
      doneChildCount: number;
      hasDependencies: boolean;
    }

    /** Result of the DAG layout computation. */
    export interface DagLayoutResult {
      nodes: Node<TaskNodeData>[];
      edges: Edge[];
    }

    /**
     * Computes a dagre-based DAG layout from a flat list of tasks.
     * Produces positioned React Flow nodes and edges for both hierarchy
     * (parent→child) and dependency relationships.
     */
    export function useDagLayout(tasks: TaskData[]): DagLayoutResult {
      return useMemo(() => {
        if (tasks.length === 0) {
          return { nodes: [], edges: [] };
        }

        // Enable multigraph so hierarchy and dependency edges between the same
        // pair of nodes are both preserved in the layout graph.
        const graph = new dagre.graphlib.Graph({ multigraph: true });
        graph.setDefaultEdgeLabel(() => ({}));
        graph.setGraph({
          rankdir: "TB",
          nodesep: NODE_SEPARATION,
          ranksep: RANK_SEPARATION,
        });

        const taskById = new Map(tasks.map((t) => [t.id, t]));

        // Precompute children per parent to avoid O(n^2) lookups when building nodes.
        const childrenByParent = new Map<string, TaskData[]>();
        for (const task of tasks) {
          Iif (task.parentTaskId && taskById.has(task.parentTaskId)) {
            const siblings = childrenByParent.get(task.parentTaskId) || [];
            siblings.push(task);
            childrenByParent.set(task.parentTaskId, siblings);
          }
        }

        // Add nodes
        for (const task of tasks) {
          graph.setNode(task.id, { width: NODE_WIDTH, height: NODE_HEIGHT });
        }

        // Add edges
        const edges: Edge[] = [];

        for (const task of tasks) {
          // Parent → child edges
          Iif (task.parentTaskId && taskById.has(task.parentTaskId)) {
            const edgeId = `hierarchy-${task.parentTaskId}-${task.id}`;
            graph.setEdge(task.parentTaskId, task.id, {}, edgeId);
            edges.push({
              id: edgeId,
              source: task.parentTaskId,
              target: task.id,
              type: "smoothstep",
              data: { edgeType: EDGE_TYPE_HIERARCHY },
              style: { stroke: "var(--accent-green)", strokeWidth: 2 },
              animated: false,
            });
          }

          // Dependency edges
          for (const depId of task.dependsOn) {
            Iif (taskById.has(depId)) {
              const edgeId = `dependency-${depId}-${task.id}`;
              graph.setEdge(depId, task.id, {}, edgeId);
              edges.push({
                id: edgeId,
                source: depId,
                target: task.id,
                type: "smoothstep",
                data: { edgeType: EDGE_TYPE_DEPENDENCY },
                style: {
                  stroke: "var(--text-tertiary)",
                  strokeWidth: 1.5,
                  strokeDasharray: "6 3",
                },
                animated: false,
              });
            }
          }
        }

        // Run dagre layout
        dagre.layout(graph);

        // Map dagre positions to React Flow nodes
        const nodes: Node<TaskNodeData>[] = tasks.map((task) => {
          const nodeWithPosition = graph.node(task.id) as { x: number; y: number };
          const children = childrenByParent.get(task.id) || [];

          return {
            id: task.id,
            type: "task",
            position: {
              x: nodeWithPosition.x - NODE_WIDTH / 2,
              y: nodeWithPosition.y - NODE_HEIGHT / 2,
            },
            data: {
              task,
              childCount: children.length,
              doneChildCount: children.filter((c) => c.status === "complete").length,
              hasDependencies: task.dependsOn.length > 0,
            },
          };
        });

        return { nodes, edges };
      }, [tasks]);
    }