feat: 战略地图贝塞尔因果链+节点KPI可视化(P0+P1)

This commit is contained in:
Hermes CI Fix
2026-08-15 22:08:09 +08:00
parent 778a5d9c79
commit 603d6ae5cf
8 changed files with 645 additions and 183 deletions
+91
View File
@@ -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 }
}