diff --git a/__pycache__/dax_parser.cpython-313.pyc b/__pycache__/dax_parser.cpython-313.pyc index 57c2e5b..ee35d03 100644 Binary files a/__pycache__/dax_parser.cpython-313.pyc and b/__pycache__/dax_parser.cpython-313.pyc differ diff --git a/__pycache__/model_connector.cpython-313.pyc b/__pycache__/model_connector.cpython-313.pyc index 097d5eb..7c11d07 100644 Binary files a/__pycache__/model_connector.cpython-313.pyc and b/__pycache__/model_connector.cpython-313.pyc differ diff --git a/model_connector.py b/model_connector.py index 110a057..811a327 100644 --- a/model_connector.py +++ b/model_connector.py @@ -1,5 +1,5 @@ from sys import path -path.append('D:\\Personal_Files\\document\\GitHub\\PBI-MEASURE-GRAPH\\MSNET') +path.append('D:\\Personal_Files\\document\\GitHub\\PBI-MEASURE-CALLGRAPH\\MSNET') import pyadomd import pandas as pd diff --git a/static/css/style.css b/static/css/style.css index 4543cb2..3d4ed98 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -13,7 +13,7 @@ body { } .container { - max-width: 1400px; + max-width: 96%; margin: 0 auto; padding: 20px; } @@ -34,6 +34,34 @@ h2 { font-size: 1.5rem; } +/* Main content layout - new */ +.main-content { + display: flex; + gap: 10px; +} + +.visualization-container { + background-color: white; + padding: 20px; + border-radius: 5px; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); + margin-bottom: 20px; + flex: 4; +} + +.measure-details { + background-color: white; + padding: 20px; + border-radius: 5px; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); + flex: 1; + min-width: 30%; + max-width: 40%; + height: fit-content; + position: sticky; + top: 20px; +} + /* Connection panel */ .connection-panel { background-color: white; @@ -89,15 +117,6 @@ button:hover { color: #d83b01; } -/* Visualization container */ -.visualization-container { - background-color: white; - padding: 20px; - border-radius: 5px; - box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); - margin-bottom: 20px; -} - .controls { display: flex; justify-content: space-between; @@ -111,10 +130,58 @@ button:hover { #graph-container { width: 100%; - height: 600px; + height: 500px; border: 1px solid #ddd; border-radius: 4px; overflow: hidden; + margin-bottom: 20px; +} + +/* 选中节点相关图 */ +#graph-container-detail { + width: 100%; + height: 300px; + border: 1px solid #ddd; + border-radius: 4px; + overflow: hidden; + margin-top: 20px; + position: relative; +} + +#graph-container-detail h3 { + margin: 10px; + color: #0078d4; + font-size: 1.2rem; + pointer-events: none; + position: absolute; + z-index: 5; + background-color: rgba(255, 255, 255, 0.7); + padding: 5px; + border-radius: 3px; +} + +/* 重置按钮 */ +.reset-btn { + position: absolute; + top: 10px; + right: 10px; + width: 30px; + height: 30px; + border-radius: 50%; + background-color: rgba(255, 255, 255, 0.8); + border: 1px solid #ddd; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + z-index: 10; + padding: 0; +} + +.reset-btn:hover { + background-color: #0078d4; + color: white; + border-color: #0078d4; } /* Graph styles */ @@ -129,14 +196,18 @@ button:hover { fill: #333; } +/* 高亮节点样式 */ .node.highlighted circle { fill: #ffb900; + stroke-width: 3px; } .node.selected circle { fill: #107c10; + stroke-width: 3px; } +/* 连线样式 */ .link { stroke: #999; stroke-opacity: 0.6; @@ -149,14 +220,40 @@ button:hover { stroke-width: 2px; } -/* Measure details */ -.measure-details { - background-color: white; - padding: 20px; - border-radius: 5px; - box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); +/* 搜索匹配的节点文本样式 */ +.node.search-match text { + font-weight: bold; + font-size: 14px; + fill: #d83b01; } +/* 暗淡效果 */ +.node.dimmed circle { + fill-opacity: 0.3; +} + +.node.dimmed text { + opacity: 0.3; +} + +.link.dimmed { + stroke-opacity: 0.2; +} + +/* 箭头标记样式 */ +.arrow { + fill: #999; +} + +.arrow.highlighted { + fill: #ffb900; +} + +.arrow.dimmed { + fill-opacity: 0.2; +} + +/* Measure details */ #measure-name { font-size: 1.2rem; font-weight: 600; @@ -186,3 +283,81 @@ button:hover { z-index: 10; pointer-events: none; } + +/* 力模型参数 - 使节点更分散 */ +.graph-svg { + width: 100%; + height: 100%; +} + +/* 水平布局的节点 */ +.detail-node.reference circle { + fill: #0078d4; +} + +.detail-node.referenced circle { + fill: #107c10; +} + +.detail-node.selected circle { + fill: #ffb900; +} + +.detail-link { + stroke: #999; + stroke-opacity: 0.6; + stroke-width: 1.5px; +} + +/* 详情图中的节点文本 */ +.detail-node text { + font-size: 13px; + font-weight: 500; +} + +.detail-node.referenced text { + fill: #107c10; +} + +.detail-node.reference text { + fill: #0078d4; +} + +.detail-node.selected text { + fill: #d83b01; + font-weight: bold; +} + +/* 左右布局的连接表单 */ +.connection-form { + display: flex; + gap: 20px; + margin-bottom: 15px; +} + +.connection-form .form-group { + flex: 1; +} + +/* 详情图交互样式 */ +.detail-node { + cursor: pointer; + transition: all 0.2s ease; +} + +.detail-node circle:hover { + stroke: #333; + stroke-width: 2px; + filter: brightness(1.2); + transform: scale(1.1); +} + +.detail-node text:hover { + font-weight: bold; +} + +/* 拖动状态样式 */ +.detail-node.dragging circle { + stroke: #d83b01; + stroke-width: 3px; +} diff --git a/static/js/app.js b/static/js/app.js index b1e5e42..21a1108 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -8,6 +8,7 @@ const databaseInput = document.getElementById('database'); const connectBtn = document.getElementById('connect-btn'); const connectionStatus = document.getElementById('connection-status'); const resetBtn = document.getElementById('reset-btn'); +const resetDetailBtn = document.getElementById('reset-detail-btn'); const searchInput = document.getElementById('search-input'); const measureName = document.getElementById('measure-name'); const measureExpression = document.getElementById('measure-expression'); @@ -23,13 +24,20 @@ document.addEventListener('DOMContentLoaded', () => { // Connect button connectBtn.addEventListener('click', connectToModel); - // Reset button + // Reset button for main graph resetBtn.addEventListener('click', () => { if (graph) { graph.resetView(); } }); + // Reset button for detail graph + resetDetailBtn.addEventListener('click', () => { + if (graph) { + graph.resetDetailView(); + } + }); + // Search input searchInput.addEventListener('input', (e) => { if (graph) { diff --git a/static/js/graph.js b/static/js/graph.js index fc2249b..a8a612e 100644 --- a/static/js/graph.js +++ b/static/js/graph.js @@ -17,8 +17,12 @@ class Graph { this.zoom = null; this.tooltip = null; this.selectedNode = null; + this.detailGraph = null; // 详情图实例 this.init(); + + // 初始化详情图 + this.initDetailGraph(); } /** @@ -31,6 +35,48 @@ class Graph { .attr('height', this.height) .attr('class', 'graph-svg'); + // 添加箭头定义 + const defs = this.svg.append('defs'); + + // 普通箭头 + defs.append('marker') + .attr('id', 'arrow') + .attr('viewBox', '0 -5 10 10') + .attr('refX', 20) + .attr('refY', 0) + .attr('markerWidth', 6) + .attr('markerHeight', 6) + .attr('orient', 'auto') + .append('path') + .attr('d', 'M0,-5L10,0L0,5') + .attr('class', 'arrow'); + + // 高亮箭头 + defs.append('marker') + .attr('id', 'arrow-highlighted') + .attr('viewBox', '0 -5 10 10') + .attr('refX', 20) + .attr('refY', 0) + .attr('markerWidth', 6) + .attr('markerHeight', 6) + .attr('orient', 'auto') + .append('path') + .attr('d', 'M0,-5L10,0L0,5') + .attr('class', 'arrow highlighted'); + + // 暗淡箭头 + defs.append('marker') + .attr('id', 'arrow-dimmed') + .attr('viewBox', '0 -5 10 10') + .attr('refX', 20) + .attr('refY', 0) + .attr('markerWidth', 6) + .attr('markerHeight', 6) + .attr('orient', 'auto') + .append('path') + .attr('d', 'M0,-5L10,0L0,5') + .attr('class', 'arrow dimmed'); + // Create zoom behavior this.zoom = d3.zoom() .scaleExtent([0.1, 4]) @@ -60,12 +106,88 @@ class Graph { }); } + /** + * 初始化详情图 + */ + initDetailGraph() { + const detailContainer = d3.select('#graph-container-detail'); + const width = detailContainer.node().getBoundingClientRect().width; + const height = detailContainer.node().getBoundingClientRect().height; + + // 创建SVG + this.detailSvg = detailContainer.append('svg') + .attr('width', width) + .attr('height', height) + .attr('class', 'graph-svg'); + + // 添加箭头定义 + const defs = this.detailSvg.append('defs'); + + defs.append('marker') + .attr('id', 'detail-arrow') + .attr('viewBox', '0 -5 10 10') + .attr('refX', 20) + .attr('refY', 0) + .attr('markerWidth', 6) + .attr('markerHeight', 6) + .attr('orient', 'auto') + .append('path') + .attr('d', 'M0,-5L10,0L0,5') + .attr('class', 'arrow'); + + // 创建详情图的缩放行为 + this.detailZoom = d3.zoom() + .scaleExtent([0.1, 4]) + .on('zoom', (event) => { + this.detailG.attr('transform', event.transform); + }); + + // 应用缩放到SVG + this.detailSvg.call(this.detailZoom); + + // 创建详情图的容器组 + this.detailG = this.detailSvg.append('g'); + + // 添加标题 + detailContainer.select('h3') + .text('Selected Measure Details'); + + // 处理窗口大小调整 + window.addEventListener('resize', () => { + const newWidth = detailContainer.node().getBoundingClientRect().width; + const newHeight = detailContainer.node().getBoundingClientRect().height; + this.detailSvg.attr('width', newWidth).attr('height', newHeight); + if (this.detailSimulation) { + this.detailSimulation.alpha(0.3).restart(); + } + }); + + // 初始化时调用resetDetailView方法,确保详情图在可视范围内 + // 使用setTimeout确保DOM已完全渲染 + setTimeout(() => { + this.resetDetailView(); + }, 300); + } + /** * Set the graph data * @param {Object} data - The graph data with nodes and links */ setData(data) { this.data = data; + + // 计算每个节点的引用次数 + this.data.nodes.forEach(node => { + node.referenceCount = 0; // 被引用次数 + }); + + this.data.links.forEach(link => { + // 对目标节点(被引用的节点)的引用次数+1 + const targetNode = this.data.nodes.find(node => node.id === link.target); + if (targetNode) { + targetNode.referenceCount = (targetNode.referenceCount || 0) + 1; + } + }); } /** @@ -77,13 +199,17 @@ class Graph { // Clear previous graph this.g.selectAll('*').remove(); - // Create links + // 排序节点:被引用次数多的在左侧 + this.data.nodes.sort((a, b) => b.referenceCount - a.referenceCount); + + // Create links with arrows const links = this.g.append('g') .attr('class', 'links') .selectAll('line') .data(this.data.links) .enter().append('line') - .attr('class', 'link'); + .attr('class', 'link') + .attr('marker-end', 'url(#arrow)'); // 添加箭头 // Create nodes const nodes = this.g.append('g') @@ -99,7 +225,7 @@ class Graph { // Add circles to nodes nodes.append('circle') - .attr('r', 8); + .attr('r', d => Math.max(8, Math.min(12, d.referenceCount + 6))); // 根据引用次数调整大小 // Add text labels to nodes nodes.append('text') @@ -129,12 +255,19 @@ class Graph { this.selectNode(d); }); - // Create simulation + // 布局力模型 - 调整参数使节点更分散 this.simulation = d3.forceSimulation(this.data.nodes) - .force('link', d3.forceLink(this.data.links).id(d => d.id).distance(100)) - .force('charge', d3.forceManyBody().strength(-300)) + .force('link', d3.forceLink(this.data.links).id(d => d.id).distance(150)) // 增加连接距离 + .force('charge', d3.forceManyBody().strength(-500)) // 增加排斥力 .force('center', d3.forceCenter(this.width / 2, this.height / 2)) - .force('collision', d3.forceCollide().radius(30)) + .force('collision', d3.forceCollide().radius(40)) // 增加碰撞半径 + // 水平定位力:根据引用次数排列,增加分散度 + .force('x', d3.forceX().x(d => { + // 引用次数越多,越靠左 + const maxCount = Math.max(...this.data.nodes.map(n => n.referenceCount || 0)); + const scaleFactor = this.width * 0.9; // 增加宽度因子 + return this.width / 2 - (scaleFactor * (d.referenceCount / maxCount)) + (scaleFactor / 2); + }).strength(0.2)) // 减小力强度,允许更自由的布局 .on('tick', () => { links .attr('x1', d => d.source.x) @@ -182,56 +315,344 @@ class Graph { /** * Select a node and highlight its connections - * @param {Object|null} node - The node to select, or null to deselect + * @param {Object} node - The selected node */ selectNode(node) { this.selectedNode = node; - // Show measure details + // Update node classes based on selection + this.g.selectAll('.node') + .classed('selected', d => d === node) + .classed('highlighted', d => { + if (!node) return false; + // Check if this node is connected to the selected node + return this.data.links.some(link => + (link.source.id === node.id && link.target.id === d.id) || + (link.target.id === node.id && link.source.id === d.id) + ); + }) + .classed('dimmed', d => { + if (!node) return false; + if (d === node) return false; + // Dim nodes that are not connected to the selected node + return !this.data.links.some(link => + (link.source.id === node.id && link.target.id === d.id) || + (link.target.id === node.id && link.source.id === d.id) + ); + }); + + // Update link classes based on selection + this.g.selectAll('.link') + .classed('highlighted', link => + node && (link.source.id === node.id || link.target.id === node.id) + ) + .classed('dimmed', link => + node && !(link.source.id === node.id || link.target.id === node.id) + ) + .attr('marker-end', link => { + if (!node) return 'url(#arrow)'; + if (link.source.id === node.id || link.target.id === node.id) { + return 'url(#arrow-highlighted)'; + } + return 'url(#arrow-dimmed)'; + }); + + // Display measure details if (window.appFunctions && window.appFunctions.showMeasureDetails) { window.appFunctions.showMeasureDetails(node); } - // Reset all nodes and links - this.g.selectAll('.node').classed('selected', false).classed('highlighted', false); - this.g.selectAll('.link').classed('highlighted', false); + // 更新详情图 + this.renderDetailGraph(node); + } + + /** + * 渲染选中节点相关的详情图 + * @param {Object} selectedNode - 选中的节点 + */ + renderDetailGraph(selectedNode) { + // 清除之前的详情图 + this.detailG.selectAll('*').remove(); - if (!node) return; + if (!selectedNode || !this.data) { + d3.select('#graph-container-detail h3').text('Selected Measure Details'); + return; + } - // Get connected nodes - const connectedNodes = new Set(); - const connectedLinks = new Set(); + d3.select('#graph-container-detail h3').text(`"${selectedNode.id}" Details`); - // Find incoming links (where this node is the target) + // 找出与选中节点相关的所有节点和连接 + const relatedNodes = []; + const relatedLinks = []; + + // 添加选中的节点 + relatedNodes.push({ + ...selectedNode, + type: 'selected' + }); + + // 找出引用这个节点的其他节点(出现在source位置) this.data.links.forEach(link => { - if (link.target.id === node.id) { - connectedNodes.add(link.source.id); - connectedLinks.add(link); + if (link.target.id === selectedNode.id) { + // 找到引用这个节点的其他节点 + const sourceNode = this.data.nodes.find(n => n.id === link.source.id); + if (sourceNode) { + relatedNodes.push({ + ...sourceNode, + type: 'reference' // 引用节点 + }); + + relatedLinks.push({ + source: link.source.id, + target: selectedNode.id + }); + } + } else if (link.source.id === selectedNode.id) { + // 找到被这个节点引用的其他节点 + const targetNode = this.data.nodes.find(n => n.id === link.target.id); + if (targetNode) { + relatedNodes.push({ + ...targetNode, + type: 'referenced' // 被引用节点 + }); + + relatedLinks.push({ + source: selectedNode.id, + target: link.target.id + }); + } } }); - // Find outgoing links (where this node is the source) - this.data.links.forEach(link => { - if (link.source.id === node.id) { - connectedNodes.add(link.target.id); - connectedLinks.add(link); - } + // 获取详情图容器的边界 + const containerBounds = d3.select('#graph-container-detail').node().getBoundingClientRect(); + const width = containerBounds.width; + const height = containerBounds.height; + + // 绘制连接线 + const links = this.detailG.append('g') + .attr('class', 'detail-links') + .selectAll('line') + .data(relatedLinks) + .enter().append('line') + .attr('class', 'detail-link') + .attr('marker-end', 'url(#detail-arrow)'); + + // 绘制节点 + const nodes = this.detailG.append('g') + .attr('class', 'detail-nodes') + .selectAll('g') + .data(relatedNodes) + .enter().append('g') + .attr('class', d => `detail-node ${d.type}`) + .call(d3.drag() // 添加拖拽功能 + .on('start', this.detailDragStarted.bind(this)) + .on('drag', this.detailDragged.bind(this)) + .on('end', this.detailDragEnded.bind(this))); + + // 添加节点圆形 + nodes.append('circle') + .attr('r', 10) + .attr('class', d => d.type); + + // 添加节点文本,根据节点类型调整文本位置 + nodes.append('text') + .attr('dx', d => { + if (d.type === 'referenced') return -12; // 被引用节点文本在左侧 + return 12; // 选中节点和引用节点文本在右侧 + }) + .attr('dy', '.35em') + .attr('text-anchor', d => { + if (d.type === 'referenced') return 'end'; // 被引用节点文本右对齐 + return 'start'; // 选中节点和引用节点文本左对齐 + }) + .text(d => d.id); + + // 设置布局 - 被引用节点在左侧,引用节点在右侧 + this.detailSimulation = d3.forceSimulation(relatedNodes) + .force('link', d3.forceLink(relatedLinks).id(d => d.id).distance(120)) // 减小连接距离 + .force('charge', d3.forceManyBody().strength(-300)) // 减小排斥力 + .force('center', d3.forceCenter(width / 2, height / 2)) + .force('collision', d3.forceCollide().radius(30)) // 减小碰撞半径 + // 水平布局力 - 被引用节点在左侧,引用节点在右侧 + .force('x', d3.forceX().x(d => { + if (d.type === 'selected') return width / 2; // 选中节点在中间 + if (d.type === 'referenced') return width * 0.25; // 被引用节点在左侧 + if (d.type === 'reference') return width * 0.75; // 引用节点在右侧 + return width / 2; + }).strength(0.5)) // 减小力度让节点可以更自由分布 + .on('tick', () => { + links + .attr('x1', d => d.source.x) + .attr('y1', d => d.source.y) + .attr('x2', d => d.target.x) + .attr('y2', d => d.target.y); + + nodes.attr('transform', d => `translate(${d.x},${d.y})`); + }); + + // 添加双击事件来重置详情图视图 + this.detailSvg.on('dblclick', () => { + this.resetDetailView(); }); - // Highlight the selected node - this.g.selectAll('.node') - .filter(d => d.id === node.id) - .classed('selected', true); + // 添加点击事件到详情图中的节点 + nodes.on('click', (event, d) => { + // 防止事件冒泡 + event.stopPropagation(); + + // 在主图中查找该节点 + const mainNode = this.data.nodes.find(n => n.id === d.id); + if (mainNode) { + // 在主图中选中该节点 + this.selectNode(mainNode); + + // 将主图视图移动到该节点位置 + this.centerViewOnNode(mainNode); + } + }) + .on('mouseover', (event, d) => { + // 显示点击提示 + this.tooltip.transition() + .duration(200) + .style('opacity', .9); + + let tooltipText = '点击跳转到主图中的该节点'; + if (d.type === 'selected') { + tooltipText = '当前选中的节点'; + } + + this.tooltip.html(tooltipText) + .style('left', (event.pageX + 10) + 'px') + .style('top', (event.pageY - 28) + 'px'); + }) + .on('mouseout', () => { + // 隐藏提示 + this.tooltip.transition() + .duration(500) + .style('opacity', 0); + }); - // Highlight connected nodes - this.g.selectAll('.node') - .filter(d => connectedNodes.has(d.id)) - .classed('highlighted', true); + // 在力学模拟稳定后重置视图,确保等待图表完全渲染 + this.detailSimulation.on('end', () => { + // 延迟一点执行重置,确保图表完全渲染 + setTimeout(() => { + this.resetDetailView(); + }, 200); + }); + } + + /** + * 将主图的视图中心移动到指定节点 + * @param {Object} node - 要居中显示的节点 + */ + centerViewOnNode(node) { + if (!node || !node.x || !node.y) return; - // Highlight connected links - this.g.selectAll('.link') - .filter(d => connectedLinks.has(d)) - .classed('highlighted', true); + // 获取当前缩放级别 + const currentTransform = d3.zoomTransform(this.svg.node()); + const scale = currentTransform.k; + + // 计算需要的平移量,使节点位于视图中心 + const x = node.x; + const y = node.y; + const translate = [this.width / 2 - scale * x, this.height / 2 - scale * y]; + + // 应用变换,添加动画效果 + this.svg.transition() + .duration(750) + .call(this.zoom.transform, d3.zoomIdentity + .translate(translate[0], translate[1]) + .scale(scale)); + } + + /** + * 详情图拖拽开始事件处理 + */ + detailDragStarted(event, d) { + if (!event.active) this.detailSimulation.alphaTarget(0.3).restart(); + d.fx = d.x; + d.fy = d.y; + // 添加拖动样式 + d3.select(event.sourceEvent.target.parentNode).classed('dragging', true); + } + + /** + * 详情图拖拽中事件处理 + */ + detailDragged(event, d) { + d.fx = event.x; + d.fy = event.y; + } + + /** + * 详情图拖拽结束事件处理 + */ + detailDragEnded(event, d) { + if (!event.active) this.detailSimulation.alphaTarget(0); + d.fx = null; + d.fy = null; + // 移除拖动样式 + d3.select(event.sourceEvent.target.parentNode).classed('dragging', false); + } + + /** + * 重置详情图视图 + */ + resetDetailView() { + if (!this.detailG || !this.detailZoom) return; + + try { + // 获取详情图容器的边界 + const containerBounds = d3.select('#graph-container-detail').node().getBoundingClientRect(); + const width = containerBounds.width; + const height = containerBounds.height; + + // 获取图表内容的边界框 + const bounds = this.detailG.node().getBBox(); + + // 如果边界框有效且有宽高 + if (bounds && bounds.width > 0 && bounds.height > 0) { + const dx = bounds.width; + const dy = bounds.height; + const x = bounds.x + dx / 2; + const y = bounds.y + dy / 2; + + // 计算缩放比例以适应图形 + const scale = 0.8 / Math.max(dx / width, dy / height); + const translate = [width / 2 - scale * x, height / 2 - scale * y]; + + // 应用变换 + this.detailSvg.transition() + .duration(750) + .call(this.detailZoom.transform, d3.zoomIdentity + .translate(translate[0], translate[1]) + .scale(scale)); + } else { + // 如果没有有效边界框,仅重置到中心 + this.detailSvg.transition() + .duration(750) + .call(this.detailZoom.transform, d3.zoomIdentity + .translate(width / 2, height / 2) + .scale(1)); + } + } catch (error) { + console.log('重置详情图视图出错:', error); + // 发生错误时,尝试简单重置 + try { + const containerBounds = d3.select('#graph-container-detail').node().getBoundingClientRect(); + const width = containerBounds.width; + const height = containerBounds.height; + + this.detailSvg.transition() + .duration(750) + .call(this.detailZoom.transform, d3.zoomIdentity + .translate(width / 2, height / 2) + .scale(1)); + } catch (e) { + console.error('备用重置也失败:', e); + } + } } /** @@ -239,18 +660,26 @@ class Graph { * @param {string} query - The search query */ searchNodes(query) { + // 移除之前的搜索匹配标记 + this.g.selectAll('.node').classed('search-match', false); + if (!query) { - // Reset all nodes if query is empty + // 如果查询为空,恢复所有节点的正常显示 this.g.selectAll('.node').style('opacity', 1); return; } const lowerQuery = query.toLowerCase(); - // Filter nodes based on the query - this.g.selectAll('.node').style('opacity', d => { - return d.id.toLowerCase().includes(lowerQuery) ? 1 : 0.2; - }); + // 标记匹配的节点 + this.g.selectAll('.node') + .each((d, i, nodes) => { + const node = d3.select(nodes[i]); + const isMatch = d.id.toLowerCase().includes(lowerQuery); + + // 为匹配的节点添加特殊类并使其文本加粗 + node.classed('search-match', isMatch); + }); } /** diff --git a/templates/index.html b/templates/index.html index 7be1385..4a06070 100644 --- a/templates/index.html +++ b/templates/index.html @@ -16,32 +16,81 @@