feat: 战略地图贝塞尔因果链+节点KPI可视化(P0+P1)
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* 因果链图算法 — 供 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<string>
|
||||
in: Set<string>
|
||||
}
|
||||
|
||||
function buildAdjacency(connections: any[]): Map<string, AdjNode> {
|
||||
const adj = new Map<string, AdjNode>()
|
||||
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<string, AdjNode>, start: string): Set<string> {
|
||||
const seen = new Set<string>([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<string, AdjNode>, start: string): Set<string> {
|
||||
const seen = new Set<string>([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<string>([...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<string>([...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 }
|
||||
}
|
||||
Reference in New Issue
Block a user