highlight.js 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  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. * @param {string} selectedNodeId - 选中节点ID(保持选中样式)
  76. */
  77. export function applyHoverHighlight(nodeSelection, linkSelection, labelSelection, pathNodes, lockedPathNodes = null, selectedNodeId = null) {
  78. // 合并路径:锁定 + hover
  79. const allPathNodes = new Set([...pathNodes])
  80. if (lockedPathNodes) {
  81. for (const id of lockedPathNodes) {
  82. allPathNodes.add(id)
  83. }
  84. }
  85. // 节点:不在任何路径中的置灰,只在锁定路径中的半透明,保持选中节点样式
  86. nodeSelection
  87. .classed('dimmed', d => !allPathNodes.has(getNodeId(d)))
  88. .classed('locked-path', d => {
  89. const id = getNodeId(d)
  90. return lockedPathNodes && lockedPathNodes.has(id) && !pathNodes.has(id)
  91. })
  92. .classed('selected', d => selectedNodeId && getNodeId(d) === selectedNodeId)
  93. // 边:不在任何路径中的置灰,只在锁定路径中的半透明
  94. linkSelection
  95. .classed('dimmed', l => {
  96. const { srcId, tgtId } = getLinkIds(l)
  97. return !allPathNodes.has(srcId) || !allPathNodes.has(tgtId)
  98. })
  99. .classed('locked-path', l => {
  100. if (!lockedPathNodes) return false
  101. const { srcId, tgtId } = getLinkIds(l)
  102. const inLocked = lockedPathNodes.has(srcId) && lockedPathNodes.has(tgtId)
  103. const inHover = pathNodes.has(srcId) && pathNodes.has(tgtId)
  104. return inLocked && !inHover
  105. })
  106. if (labelSelection) {
  107. labelSelection
  108. .classed('dimmed', l => {
  109. const { srcId, tgtId } = getLinkIds(l)
  110. return !allPathNodes.has(srcId) || !allPathNodes.has(tgtId)
  111. })
  112. .classed('locked-path', l => {
  113. if (!lockedPathNodes) return false
  114. const { srcId, tgtId } = getLinkIds(l)
  115. const inLocked = lockedPathNodes.has(srcId) && lockedPathNodes.has(tgtId)
  116. const inHover = pathNodes.has(srcId) && pathNodes.has(tgtId)
  117. return inLocked && !inHover
  118. })
  119. }
  120. }
  121. /**
  122. * 清除 hover 效果
  123. */
  124. export function clearHoverHighlight(nodeSelection, linkSelection, labelSelection) {
  125. nodeSelection
  126. .classed('dimmed', false)
  127. .classed('locked-path', false)
  128. .classed('highlighted', false)
  129. .classed('selected', false)
  130. linkSelection
  131. .classed('dimmed', false)
  132. .classed('locked-path', false)
  133. .classed('highlighted', false)
  134. if (labelSelection) {
  135. labelSelection
  136. .classed('dimmed', false)
  137. .classed('locked-path', false)
  138. .classed('highlighted', false)
  139. }
  140. }
  141. /**
  142. * 应用高亮/置灰效果
  143. * @param {SVGElement} svgEl - SVG DOM 元素
  144. * @param {Set} highlightedIds - 高亮的节点ID集合
  145. * @param {Set} walkedEdgeSet - 游走的边集合(格式:"sourceId->targetId")
  146. * @param {string} selectedId - 选中的节点ID
  147. */
  148. export function applyHighlight(svgEl, highlightedIds, walkedEdgeSet = null, selectedId = null) {
  149. if (!svgEl) return
  150. const svg = d3.select(svgEl)
  151. const hasHighlight = highlightedIds.size > 0
  152. // 所有节点:先清除残留的 hover 相关类,然后应用高亮
  153. svg.selectAll('.tree-node, .match-node, .graph-node, .walked-node')
  154. .classed('highlighted', false)
  155. .classed('locked-path', false)
  156. .classed('selected', d => {
  157. const nodeId = getNodeId(d)
  158. if (selectedId) {
  159. return nodeId === selectedId
  160. } else if (hasHighlight) {
  161. return highlightedIds.has(nodeId)
  162. }
  163. return false
  164. })
  165. .classed('dimmed', d => hasHighlight && !highlightedIds.has(getNodeId(d)))
  166. // 所有边:先清除残留的 hover 相关类,然后应用高亮
  167. svg.selectAll('.tree-link, .graph-link, .graph-link-label, .match-link, .match-score, .walked-link, .walked-score')
  168. .classed('highlighted', false)
  169. .classed('locked-path', false)
  170. .classed('dimmed', function(d) {
  171. if (!hasHighlight) return false
  172. if (d && walkedEdgeSet) {
  173. return !isEdgeInWalked(d, walkedEdgeSet)
  174. }
  175. if (d) {
  176. const { srcId, tgtId } = getLinkIds(d)
  177. return !highlightedIds.has(srcId) || !highlightedIds.has(tgtId)
  178. }
  179. return true
  180. })
  181. // 选中节点的边框
  182. svg.selectAll('.match-node').each(function(d) {
  183. const isSelected = d.id === selectedId
  184. d3.select(this).select('.tree-shape')
  185. .attr('stroke', isSelected ? '#fff' : 'rgba(255,255,255,0.5)')
  186. .attr('stroke-width', isSelected ? 2 : 1)
  187. })
  188. }