|
|
@@ -4,17 +4,17 @@ import * as d3 from 'd3'
|
|
|
/**
|
|
|
* 获取节点ID(兼容 d3.hierarchy 和普通对象)
|
|
|
*/
|
|
|
-function getNodeId(d) {
|
|
|
+export function getNodeId(d) {
|
|
|
return d.data?.id || d.id
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 获取边的 source 和 target ID
|
|
|
*/
|
|
|
-function getEdgeIds(d) {
|
|
|
- const sourceId = d.source?.data?.id || d.source?.id || d.source
|
|
|
- const targetId = d.target?.data?.id || d.target?.id || d.target
|
|
|
- return { sourceId, targetId }
|
|
|
+export function getLinkIds(link) {
|
|
|
+ const srcId = link.source?.id || link.source?.data?.id || link.source
|
|
|
+ const tgtId = link.target?.id || link.target?.data?.id || link.target
|
|
|
+ return { srcId, tgtId }
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -22,9 +22,86 @@ function getEdgeIds(d) {
|
|
|
*/
|
|
|
function isEdgeInWalked(d, walkedEdgeSet) {
|
|
|
if (!walkedEdgeSet) return false
|
|
|
- const { sourceId, targetId } = getEdgeIds(d)
|
|
|
- // 检查正向和反向
|
|
|
- return walkedEdgeSet.has(`${sourceId}->${targetId}`) || walkedEdgeSet.has(`${targetId}->${sourceId}`)
|
|
|
+ const { srcId, tgtId } = getLinkIds(d)
|
|
|
+ return walkedEdgeSet.has(`${srcId}->${tgtId}`) || walkedEdgeSet.has(`${tgtId}->${srcId}`)
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * BFS 找从 start 到 end 的路径,返回路径上所有节点 ID 的 Set
|
|
|
+ * @param {string} startId - 起点节点ID
|
|
|
+ * @param {string} endId - 终点节点ID
|
|
|
+ * @param {Array} links - 边数组,每个元素需要有 source 和 target
|
|
|
+ */
|
|
|
+export function findPath(startId, endId, links) {
|
|
|
+ // 构建邻接表(无向)
|
|
|
+ const adj = new Map()
|
|
|
+ for (const link of links) {
|
|
|
+ const { srcId, tgtId } = getLinkIds(link)
|
|
|
+ if (!adj.has(srcId)) adj.set(srcId, [])
|
|
|
+ if (!adj.has(tgtId)) adj.set(tgtId, [])
|
|
|
+ adj.get(srcId).push(tgtId)
|
|
|
+ adj.get(tgtId).push(srcId)
|
|
|
+ }
|
|
|
+
|
|
|
+ // BFS
|
|
|
+ const visited = new Set([startId])
|
|
|
+ const parent = new Map()
|
|
|
+ const queue = [startId]
|
|
|
+
|
|
|
+ while (queue.length > 0) {
|
|
|
+ const curr = queue.shift()
|
|
|
+ if (curr === endId) break
|
|
|
+ for (const neighbor of (adj.get(curr) || [])) {
|
|
|
+ if (!visited.has(neighbor)) {
|
|
|
+ visited.add(neighbor)
|
|
|
+ parent.set(neighbor, curr)
|
|
|
+ queue.push(neighbor)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 回溯路径
|
|
|
+ const pathNodes = new Set()
|
|
|
+ if (visited.has(endId)) {
|
|
|
+ let curr = endId
|
|
|
+ while (curr) {
|
|
|
+ pathNodes.add(curr)
|
|
|
+ curr = parent.get(curr)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return pathNodes
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 应用 hover 效果:高亮从起点到 hover 节点的路径
|
|
|
+ * @param {D3Selection} nodeSelection - 节点选择集
|
|
|
+ * @param {D3Selection} linkSelection - 边选择集
|
|
|
+ * @param {D3Selection} labelSelection - 标签选择集(可选)
|
|
|
+ * @param {Set} pathNodes - 路径上的节点ID集合
|
|
|
+ */
|
|
|
+export function applyHoverHighlight(nodeSelection, linkSelection, labelSelection, pathNodes) {
|
|
|
+ nodeSelection.classed('dimmed', d => !pathNodes.has(getNodeId(d)))
|
|
|
+ linkSelection.classed('dimmed', l => {
|
|
|
+ const { srcId, tgtId } = getLinkIds(l)
|
|
|
+ return !pathNodes.has(srcId) || !pathNodes.has(tgtId)
|
|
|
+ })
|
|
|
+ if (labelSelection) {
|
|
|
+ labelSelection.classed('dimmed', l => {
|
|
|
+ const { srcId, tgtId } = getLinkIds(l)
|
|
|
+ return !pathNodes.has(srcId) || !pathNodes.has(tgtId)
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 清除 hover 效果
|
|
|
+ */
|
|
|
+export function clearHoverHighlight(nodeSelection, linkSelection, labelSelection) {
|
|
|
+ nodeSelection.classed('dimmed', false)
|
|
|
+ linkSelection.classed('dimmed', false)
|
|
|
+ if (labelSelection) {
|
|
|
+ labelSelection.classed('dimmed', false)
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -41,17 +118,12 @@ export function applyHighlight(svgEl, highlightedIds, walkedEdgeSet = null, sele
|
|
|
const hasHighlight = highlightedIds.size > 0
|
|
|
|
|
|
// 所有节点:在 highlightedIds 中的保持,否则置灰
|
|
|
- // selected 逻辑:
|
|
|
- // - 点击节点时(selectedId 有值):只有 selectedId 是 selected
|
|
|
- // - 点击边时(selectedId 为空但 highlightedIds 有值):highlightedIds 中的节点(边的两端)都是 selected
|
|
|
svg.selectAll('.tree-node, .match-node, .graph-node, .walked-node')
|
|
|
.classed('selected', d => {
|
|
|
const nodeId = getNodeId(d)
|
|
|
if (selectedId) {
|
|
|
- // 点击节点:只有被点击的节点是 selected
|
|
|
return nodeId === selectedId
|
|
|
} else if (hasHighlight) {
|
|
|
- // 点击边:边的两端节点都是 selected
|
|
|
return highlightedIds.has(nodeId)
|
|
|
}
|
|
|
return false
|
|
|
@@ -65,10 +137,9 @@ export function applyHighlight(svgEl, highlightedIds, walkedEdgeSet = null, sele
|
|
|
if (d && walkedEdgeSet) {
|
|
|
return !isEdgeInWalked(d, walkedEdgeSet)
|
|
|
}
|
|
|
- // fallback:检查两端节点是否都高亮
|
|
|
if (d) {
|
|
|
- const { sourceId, targetId } = getEdgeIds(d)
|
|
|
- return !highlightedIds.has(sourceId) || !highlightedIds.has(targetId)
|
|
|
+ const { srcId, tgtId } = getLinkIds(d)
|
|
|
+ return !highlightedIds.has(srcId) || !highlightedIds.has(tgtId)
|
|
|
}
|
|
|
return true
|
|
|
})
|