/** * 因果链图算法 — 供 ConnectionLines / MapCanvas 共用 * 连线数据: { from: 'financial-0', to: 'customer-1', effect?: 'positive'|'negative', label?: string } */ export interface ChainResult { /** 链上节点 key 集合 */ nodes: string[] /** 链上连线索引集合 */ connIdxs: number[] } interface AdjNode { out: Set in: Set } function buildAdjacency(connections: any[]): Map { const adj = new Map() for (const c of connections) { if (!c?.from || !c?.to) continue if (!adj.has(c.from)) adj.set(c.from, { out: new Set(), in: new Set() }) if (!adj.has(c.to)) adj.set(c.to, { out: new Set(), in: new Set() }) adj.get(c.from)!.out.add(c.to) adj.get(c.to)!.in.add(c.from) } return adj } /** 从 start 沿 out 边 BFS(下游) */ function descendants(adj: Map, start: string): Set { const seen = new Set([start]) const queue = [start] while (queue.length) { const cur = queue.shift()! for (const next of adj.get(cur)?.out ?? []) { if (!seen.has(next)) { seen.add(next) queue.push(next) } } } return seen } /** 从 start 沿 in 边 BFS(上游) */ function ancestors(adj: Map, start: string): Set { const seen = new Set([start]) const queue = [start] while (queue.length) { const cur = queue.shift()! for (const prev of adj.get(cur)?.in ?? []) { if (!seen.has(prev)) { seen.add(prev) queue.push(prev) } } } return seen } /** * 某条连线的完整因果链:源节点的所有下游 + 目标节点的所有上游 * (即"从因到果"整条路径上的节点与连线) */ export function getChainForConnection(connections: any[], connIdx: number): ChainResult | null { const conn = connections[connIdx] if (!conn?.from || !conn?.to) return null const adj = buildAdjacency(connections) const nodeSet = new Set([...descendants(adj, conn.from), ...ancestors(adj, conn.to)]) const connIdxs: number[] = [] connections.forEach((c, i) => { if (c?.from && c?.to && nodeSet.has(c.from) && nodeSet.has(c.to)) connIdxs.push(i) }) return { nodes: [...nodeSet], connIdxs } } /** * 某节点的相关因果链:其上游(原因)+ 下游(结果)全部节点与连线 */ export function getChainForNode(connections: any[], nodeKey: string): ChainResult | null { const adj = buildAdjacency(connections) if (!adj.has(nodeKey)) return null const nodeSet = new Set([...descendants(adj, nodeKey), ...ancestors(adj, nodeKey)]) if (nodeSet.size <= 1) return { nodes: [nodeKey], connIdxs: [] } const connIdxs: number[] = [] connections.forEach((c, i) => { if (c?.from && c?.to && nodeSet.has(c.from) && nodeSet.has(c.to)) connIdxs.push(i) }) return { nodes: [...nodeSet], connIdxs } }