highlight.js 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. // 统一的高亮/置灰工具
  2. import * as d3 from 'd3'
  3. /**
  4. * 获取节点ID(兼容 d3.hierarchy 和普通对象)
  5. */
  6. export function getNodeId(d) {
  7. return d.data?.id || d.id
  8. }
  9. /**
  10. * 获取边的 source 和 target ID
  11. */
  12. export function getLinkIds(link) {
  13. const srcId = link.source?.id || link.source?.data?.id || link.source
  14. const tgtId = link.target?.id || link.target?.data?.id || link.target
  15. return { srcId, tgtId }
  16. }
  17. /**
  18. * 检查边是否在游走的边集合中
  19. */
  20. function isEdgeInWalked(d, walkedEdgeSet) {
  21. if (!walkedEdgeSet) return false
  22. const { srcId, tgtId } = getLinkIds(d)
  23. return walkedEdgeSet.has(`${srcId}->${tgtId}`) || walkedEdgeSet.has(`${tgtId}->${srcId}`)
  24. }
  25. /**
  26. * BFS 找从 start 到 end 的路径,返回路径上所有节点 ID 的 Set
  27. * @param {string} startId - 起点节点ID
  28. * @param {string} endId - 终点节点ID
  29. * @param {Array} links - 边数组,每个元素需要有 source 和 target
  30. */
  31. export function findPath(startId, endId, links) {
  32. // 构建邻接表(无向)
  33. const adj = new Map()
  34. for (const link of links) {
  35. const { srcId, tgtId } = getLinkIds(link)
  36. if (!adj.has(srcId)) adj.set(srcId, [])
  37. if (!adj.has(tgtId)) adj.set(tgtId, [])
  38. adj.get(srcId).push(tgtId)
  39. adj.get(tgtId).push(srcId)
  40. }
  41. // BFS
  42. const visited = new Set([startId])
  43. const parent = new Map()
  44. const queue = [startId]
  45. while (queue.length > 0) {
  46. const curr = queue.shift()
  47. if (curr === endId) break
  48. for (const neighbor of (adj.get(curr) || [])) {
  49. if (!visited.has(neighbor)) {
  50. visited.add(neighbor)
  51. parent.set(neighbor, curr)
  52. queue.push(neighbor)
  53. }
  54. }
  55. }
  56. // 回溯路径
  57. const pathNodes = new Set()
  58. if (visited.has(endId)) {
  59. let curr = endId
  60. while (curr) {
  61. pathNodes.add(curr)
  62. curr = parent.get(curr)
  63. }
  64. }
  65. return pathNodes
  66. }
  67. /**
  68. * 应用 hover 效果:高亮从起点到 hover 节点的路径
  69. * 支持嵌套:锁定路径 + 新 hover 路径同时显示
  70. * @param {D3Selection} nodeSelection - 节点选择集
  71. * @param {D3Selection} linkSelection - 边选择集
  72. * @param {D3Selection} labelSelection - 标签选择集(可选)
  73. * @param {Set} pathNodes - 当前 hover 路径上的节点ID集合
  74. * @param {Set} lockedPathNodes - 锁定路径上的节点ID集合(可选)
  75. */
  76. export function applyHoverHighlight(nodeSelection, linkSelection, labelSelection, pathNodes, lockedPathNodes = null) {
  77. // 合并路径:锁定 + hover
  78. const allPathNodes = new Set([...pathNodes])
  79. if (lockedPathNodes) {
  80. for (const id of lockedPathNodes) {
  81. allPathNodes.add(id)
  82. }
  83. }
  84. // 节点:不在任何路径中的置灰,只在锁定路径中的半透明
  85. nodeSelection
  86. .classed('dimmed', d => !allPathNodes.has(getNodeId(d)))
  87. .classed('locked-path', d => {
  88. const id = getNodeId(d)
  89. return lockedPathNodes && lockedPathNodes.has(id) && !pathNodes.has(id)
  90. })
  91. // 边:不在任何路径中的置灰,只在锁定路径中的半透明
  92. linkSelection
  93. .classed('dimmed', l => {
  94. const { srcId, tgtId } = getLinkIds(l)
  95. return !allPathNodes.has(srcId) || !allPathNodes.has(tgtId)
  96. })
  97. .classed('locked-path', l => {
  98. if (!lockedPathNodes) return false
  99. const { srcId, tgtId } = getLinkIds(l)
  100. const inLocked = lockedPathNodes.has(srcId) && lockedPathNodes.has(tgtId)
  101. const inHover = pathNodes.has(srcId) && pathNodes.has(tgtId)
  102. return inLocked && !inHover
  103. })
  104. if (labelSelection) {
  105. labelSelection
  106. .classed('dimmed', l => {
  107. const { srcId, tgtId } = getLinkIds(l)
  108. return !allPathNodes.has(srcId) || !allPathNodes.has(tgtId)
  109. })
  110. .classed('locked-path', l => {
  111. if (!lockedPathNodes) return false
  112. const { srcId, tgtId } = getLinkIds(l)
  113. const inLocked = lockedPathNodes.has(srcId) && lockedPathNodes.has(tgtId)
  114. const inHover = pathNodes.has(srcId) && pathNodes.has(tgtId)
  115. return inLocked && !inHover
  116. })
  117. }
  118. }
  119. /**
  120. * 清除 hover 效果
  121. */
  122. export function clearHoverHighlight(nodeSelection, linkSelection, labelSelection) {
  123. nodeSelection.classed('dimmed', false).classed('locked-path', false)
  124. linkSelection.classed('dimmed', false).classed('locked-path', false)
  125. if (labelSelection) {
  126. labelSelection.classed('dimmed', false).classed('locked-path', false)
  127. }
  128. }
  129. /**
  130. * 应用高亮/置灰效果
  131. * @param {SVGElement} svgEl - SVG DOM 元素
  132. * @param {Set} highlightedIds - 高亮的节点ID集合
  133. * @param {Set} walkedEdgeSet - 游走的边集合(格式:"sourceId->targetId")
  134. * @param {string} selectedId - 选中的节点ID
  135. */
  136. export function applyHighlight(svgEl, highlightedIds, walkedEdgeSet = null, selectedId = null) {
  137. if (!svgEl) return
  138. const svg = d3.select(svgEl)
  139. const hasHighlight = highlightedIds.size > 0
  140. // 所有节点:在 highlightedIds 中的保持,否则置灰
  141. svg.selectAll('.tree-node, .match-node, .graph-node, .walked-node')
  142. .classed('selected', d => {
  143. const nodeId = getNodeId(d)
  144. if (selectedId) {
  145. return nodeId === selectedId
  146. } else if (hasHighlight) {
  147. return highlightedIds.has(nodeId)
  148. }
  149. return false
  150. })
  151. .classed('dimmed', d => hasHighlight && !highlightedIds.has(getNodeId(d)))
  152. // 所有边:在 walkedEdgeSet 中的保持,否则置灰
  153. svg.selectAll('.tree-link, .graph-link, .graph-link-label, .match-link, .match-score, .walked-link, .walked-score')
  154. .classed('dimmed', function(d) {
  155. if (!hasHighlight) return false
  156. if (d && walkedEdgeSet) {
  157. return !isEdgeInWalked(d, walkedEdgeSet)
  158. }
  159. if (d) {
  160. const { srcId, tgtId } = getLinkIds(d)
  161. return !highlightedIds.has(srcId) || !highlightedIds.has(tgtId)
  162. }
  163. return true
  164. })
  165. // 选中节点的边框
  166. svg.selectAll('.match-node').each(function(d) {
  167. const isSelected = d.id === selectedId
  168. d3.select(this).select('.tree-shape')
  169. .attr('stroke', isSelected ? '#fff' : 'rgba(255,255,255,0.5)')
  170. .attr('stroke-width', isSelected ? 2 : 1)
  171. })
  172. }