| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189 |
- // 统一的高亮/置灰工具
- import * as d3 from 'd3'
- /**
- * 获取节点ID(兼容 d3.hierarchy 和普通对象)
- */
- export function getNodeId(d) {
- return d.data?.id || d.id
- }
- /**
- * 获取边的 source 和 target ID
- */
- 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 }
- }
- /**
- * 检查边是否在游走的边集合中
- */
- function isEdgeInWalked(d, walkedEdgeSet) {
- if (!walkedEdgeSet) return false
- 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 节点的路径
- * 支持嵌套:锁定路径 + 新 hover 路径同时显示
- * @param {D3Selection} nodeSelection - 节点选择集
- * @param {D3Selection} linkSelection - 边选择集
- * @param {D3Selection} labelSelection - 标签选择集(可选)
- * @param {Set} pathNodes - 当前 hover 路径上的节点ID集合
- * @param {Set} lockedPathNodes - 锁定路径上的节点ID集合(可选)
- */
- export function applyHoverHighlight(nodeSelection, linkSelection, labelSelection, pathNodes, lockedPathNodes = null) {
- // 合并路径:锁定 + hover
- const allPathNodes = new Set([...pathNodes])
- if (lockedPathNodes) {
- for (const id of lockedPathNodes) {
- allPathNodes.add(id)
- }
- }
- // 节点:不在任何路径中的置灰,只在锁定路径中的半透明
- nodeSelection
- .classed('dimmed', d => !allPathNodes.has(getNodeId(d)))
- .classed('locked-path', d => {
- const id = getNodeId(d)
- return lockedPathNodes && lockedPathNodes.has(id) && !pathNodes.has(id)
- })
- // 边:不在任何路径中的置灰,只在锁定路径中的半透明
- linkSelection
- .classed('dimmed', l => {
- const { srcId, tgtId } = getLinkIds(l)
- return !allPathNodes.has(srcId) || !allPathNodes.has(tgtId)
- })
- .classed('locked-path', l => {
- if (!lockedPathNodes) return false
- const { srcId, tgtId } = getLinkIds(l)
- const inLocked = lockedPathNodes.has(srcId) && lockedPathNodes.has(tgtId)
- const inHover = pathNodes.has(srcId) && pathNodes.has(tgtId)
- return inLocked && !inHover
- })
- if (labelSelection) {
- labelSelection
- .classed('dimmed', l => {
- const { srcId, tgtId } = getLinkIds(l)
- return !allPathNodes.has(srcId) || !allPathNodes.has(tgtId)
- })
- .classed('locked-path', l => {
- if (!lockedPathNodes) return false
- const { srcId, tgtId } = getLinkIds(l)
- const inLocked = lockedPathNodes.has(srcId) && lockedPathNodes.has(tgtId)
- const inHover = pathNodes.has(srcId) && pathNodes.has(tgtId)
- return inLocked && !inHover
- })
- }
- }
- /**
- * 清除 hover 效果
- */
- export function clearHoverHighlight(nodeSelection, linkSelection, labelSelection) {
- nodeSelection.classed('dimmed', false).classed('locked-path', false)
- linkSelection.classed('dimmed', false).classed('locked-path', false)
- if (labelSelection) {
- labelSelection.classed('dimmed', false).classed('locked-path', false)
- }
- }
- /**
- * 应用高亮/置灰效果
- * @param {SVGElement} svgEl - SVG DOM 元素
- * @param {Set} highlightedIds - 高亮的节点ID集合
- * @param {Set} walkedEdgeSet - 游走的边集合(格式:"sourceId->targetId")
- * @param {string} selectedId - 选中的节点ID
- */
- export function applyHighlight(svgEl, highlightedIds, walkedEdgeSet = null, selectedId = null) {
- if (!svgEl) return
- const svg = d3.select(svgEl)
- const hasHighlight = highlightedIds.size > 0
- // 所有节点:在 highlightedIds 中的保持,否则置灰
- svg.selectAll('.tree-node, .match-node, .graph-node, .walked-node')
- .classed('selected', d => {
- const nodeId = getNodeId(d)
- if (selectedId) {
- return nodeId === selectedId
- } else if (hasHighlight) {
- return highlightedIds.has(nodeId)
- }
- return false
- })
- .classed('dimmed', d => hasHighlight && !highlightedIds.has(getNodeId(d)))
- // 所有边:在 walkedEdgeSet 中的保持,否则置灰
- svg.selectAll('.tree-link, .graph-link, .graph-link-label, .match-link, .match-score, .walked-link, .walked-score')
- .classed('dimmed', function(d) {
- if (!hasHighlight) return false
- if (d && walkedEdgeSet) {
- return !isEdgeInWalked(d, walkedEdgeSet)
- }
- if (d) {
- const { srcId, tgtId } = getLinkIds(d)
- return !highlightedIds.has(srcId) || !highlightedIds.has(tgtId)
- }
- return true
- })
- // 选中节点的边框
- svg.selectAll('.match-node').each(function(d) {
- const isSelected = d.id === selectedId
- d3.select(this).select('.tree-shape')
- .attr('stroke', isSelected ? '#fff' : 'rgba(255,255,255,0.5)')
- .attr('stroke-width', isSelected ? 2 : 1)
- })
- }
|