初步实现功能

This commit is contained in:
chenwu 2025-03-16 01:32:50 +08:00
parent 382be91e63
commit 98b5b9855e
7 changed files with 742 additions and 81 deletions

View File

@ -1,5 +1,5 @@
from sys import path 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 pyadomd
import pandas as pd import pandas as pd

View File

@ -13,7 +13,7 @@ body {
} }
.container { .container {
max-width: 1400px; max-width: 96%;
margin: 0 auto; margin: 0 auto;
padding: 20px; padding: 20px;
} }
@ -34,6 +34,34 @@ h2 {
font-size: 1.5rem; 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 */
.connection-panel { .connection-panel {
background-color: white; background-color: white;
@ -89,15 +117,6 @@ button:hover {
color: #d83b01; 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 { .controls {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@ -111,10 +130,58 @@ button:hover {
#graph-container { #graph-container {
width: 100%; width: 100%;
height: 600px; height: 500px;
border: 1px solid #ddd; border: 1px solid #ddd;
border-radius: 4px; border-radius: 4px;
overflow: hidden; 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 */ /* Graph styles */
@ -129,14 +196,18 @@ button:hover {
fill: #333; fill: #333;
} }
/* 高亮节点样式 */
.node.highlighted circle { .node.highlighted circle {
fill: #ffb900; fill: #ffb900;
stroke-width: 3px;
} }
.node.selected circle { .node.selected circle {
fill: #107c10; fill: #107c10;
stroke-width: 3px;
} }
/* 连线样式 */
.link { .link {
stroke: #999; stroke: #999;
stroke-opacity: 0.6; stroke-opacity: 0.6;
@ -149,14 +220,40 @@ button:hover {
stroke-width: 2px; stroke-width: 2px;
} }
/* Measure details */ /* 搜索匹配的节点文本样式 */
.measure-details { .node.search-match text {
background-color: white; font-weight: bold;
padding: 20px; font-size: 14px;
border-radius: 5px; fill: #d83b01;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
} }
/* 暗淡效果 */
.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 { #measure-name {
font-size: 1.2rem; font-size: 1.2rem;
font-weight: 600; font-weight: 600;
@ -186,3 +283,81 @@ button:hover {
z-index: 10; z-index: 10;
pointer-events: none; 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;
}

View File

@ -8,6 +8,7 @@ const databaseInput = document.getElementById('database');
const connectBtn = document.getElementById('connect-btn'); const connectBtn = document.getElementById('connect-btn');
const connectionStatus = document.getElementById('connection-status'); const connectionStatus = document.getElementById('connection-status');
const resetBtn = document.getElementById('reset-btn'); const resetBtn = document.getElementById('reset-btn');
const resetDetailBtn = document.getElementById('reset-detail-btn');
const searchInput = document.getElementById('search-input'); const searchInput = document.getElementById('search-input');
const measureName = document.getElementById('measure-name'); const measureName = document.getElementById('measure-name');
const measureExpression = document.getElementById('measure-expression'); const measureExpression = document.getElementById('measure-expression');
@ -23,13 +24,20 @@ document.addEventListener('DOMContentLoaded', () => {
// Connect button // Connect button
connectBtn.addEventListener('click', connectToModel); connectBtn.addEventListener('click', connectToModel);
// Reset button // Reset button for main graph
resetBtn.addEventListener('click', () => { resetBtn.addEventListener('click', () => {
if (graph) { if (graph) {
graph.resetView(); graph.resetView();
} }
}); });
// Reset button for detail graph
resetDetailBtn.addEventListener('click', () => {
if (graph) {
graph.resetDetailView();
}
});
// Search input // Search input
searchInput.addEventListener('input', (e) => { searchInput.addEventListener('input', (e) => {
if (graph) { if (graph) {

View File

@ -17,8 +17,12 @@ class Graph {
this.zoom = null; this.zoom = null;
this.tooltip = null; this.tooltip = null;
this.selectedNode = null; this.selectedNode = null;
this.detailGraph = null; // 详情图实例
this.init(); this.init();
// 初始化详情图
this.initDetailGraph();
} }
/** /**
@ -31,6 +35,48 @@ class Graph {
.attr('height', this.height) .attr('height', this.height)
.attr('class', 'graph-svg'); .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 // Create zoom behavior
this.zoom = d3.zoom() this.zoom = d3.zoom()
.scaleExtent([0.1, 4]) .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 * Set the graph data
* @param {Object} data - The graph data with nodes and links * @param {Object} data - The graph data with nodes and links
*/ */
setData(data) { setData(data) {
this.data = 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 // Clear previous graph
this.g.selectAll('*').remove(); 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') const links = this.g.append('g')
.attr('class', 'links') .attr('class', 'links')
.selectAll('line') .selectAll('line')
.data(this.data.links) .data(this.data.links)
.enter().append('line') .enter().append('line')
.attr('class', 'link'); .attr('class', 'link')
.attr('marker-end', 'url(#arrow)'); // 添加箭头
// Create nodes // Create nodes
const nodes = this.g.append('g') const nodes = this.g.append('g')
@ -99,7 +225,7 @@ class Graph {
// Add circles to nodes // Add circles to nodes
nodes.append('circle') nodes.append('circle')
.attr('r', 8); .attr('r', d => Math.max(8, Math.min(12, d.referenceCount + 6))); // 根据引用次数调整大小
// Add text labels to nodes // Add text labels to nodes
nodes.append('text') nodes.append('text')
@ -129,12 +255,19 @@ class Graph {
this.selectNode(d); this.selectNode(d);
}); });
// Create simulation // 布局力模型 - 调整参数使节点更分散
this.simulation = d3.forceSimulation(this.data.nodes) this.simulation = d3.forceSimulation(this.data.nodes)
.force('link', d3.forceLink(this.data.links).id(d => d.id).distance(100)) .force('link', d3.forceLink(this.data.links).id(d => d.id).distance(150)) // 增加连接距离
.force('charge', d3.forceManyBody().strength(-300)) .force('charge', d3.forceManyBody().strength(-500)) // 增加排斥力
.force('center', d3.forceCenter(this.width / 2, this.height / 2)) .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', () => { .on('tick', () => {
links links
.attr('x1', d => d.source.x) .attr('x1', d => d.source.x)
@ -182,56 +315,344 @@ class Graph {
/** /**
* Select a node and highlight its connections * 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) { selectNode(node) {
this.selectedNode = 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) { if (window.appFunctions && window.appFunctions.showMeasureDetails) {
window.appFunctions.showMeasureDetails(node); window.appFunctions.showMeasureDetails(node);
} }
// Reset all nodes and links // 更新详情图
this.g.selectAll('.node').classed('selected', false).classed('highlighted', false); this.renderDetailGraph(node);
this.g.selectAll('.link').classed('highlighted', false); }
if (!node) return; /**
* 渲染选中节点相关的详情图
* @param {Object} selectedNode - 选中的节点
*/
renderDetailGraph(selectedNode) {
// 清除之前的详情图
this.detailG.selectAll('*').remove();
// Get connected nodes if (!selectedNode || !this.data) {
const connectedNodes = new Set(); d3.select('#graph-container-detail h3').text('Selected Measure Details');
const connectedLinks = new Set(); return;
}
// Find incoming links (where this node is the target) d3.select('#graph-container-detail h3').text(`"${selectedNode.id}" Details`);
// 找出与选中节点相关的所有节点和连接
const relatedNodes = [];
const relatedLinks = [];
// 添加选中的节点
relatedNodes.push({
...selectedNode,
type: 'selected'
});
// 找出引用这个节点的其他节点出现在source位置
this.data.links.forEach(link => { this.data.links.forEach(link => {
if (link.target.id === node.id) { if (link.target.id === selectedNode.id) {
connectedNodes.add(link.source.id); // 找到引用这个节点的其他节点
connectedLinks.add(link); 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 => { const containerBounds = d3.select('#graph-container-detail').node().getBoundingClientRect();
if (link.source.id === node.id) { const width = containerBounds.width;
connectedNodes.add(link.target.id); const height = containerBounds.height;
connectedLinks.add(link);
} // 绘制连接线
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})`);
}); });
// Highlight the selected node // 添加双击事件来重置详情图视图
this.g.selectAll('.node') this.detailSvg.on('dblclick', () => {
.filter(d => d.id === node.id) this.resetDetailView();
.classed('selected', true); });
// Highlight connected nodes // 添加点击事件到详情图中的节点
this.g.selectAll('.node') nodes.on('click', (event, d) => {
.filter(d => connectedNodes.has(d.id)) // 防止事件冒泡
.classed('highlighted', true); event.stopPropagation();
// Highlight connected links // 在主图中查找该节点
this.g.selectAll('.link') const mainNode = this.data.nodes.find(n => n.id === d.id);
.filter(d => connectedLinks.has(d)) if (mainNode) {
.classed('highlighted', true); // 在主图中选中该节点
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);
});
// 在力学模拟稳定后重置视图,确保等待图表完全渲染
this.detailSimulation.on('end', () => {
// 延迟一点执行重置,确保图表完全渲染
setTimeout(() => {
this.resetDetailView();
}, 200);
});
}
/**
* 将主图的视图中心移动到指定节点
* @param {Object} node - 要居中显示的节点
*/
centerViewOnNode(node) {
if (!node || !node.x || !node.y) return;
// 获取当前缩放级别
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,17 +660,25 @@ class Graph {
* @param {string} query - The search query * @param {string} query - The search query
*/ */
searchNodes(query) { searchNodes(query) {
// 移除之前的搜索匹配标记
this.g.selectAll('.node').classed('search-match', false);
if (!query) { if (!query) {
// Reset all nodes if query is empty // 如果查询为空,恢复所有节点的正常显示
this.g.selectAll('.node').style('opacity', 1); this.g.selectAll('.node').style('opacity', 1);
return; return;
} }
const lowerQuery = query.toLowerCase(); const lowerQuery = query.toLowerCase();
// Filter nodes based on the query // 标记匹配的节点
this.g.selectAll('.node').style('opacity', d => { this.g.selectAll('.node')
return d.id.toLowerCase().includes(lowerQuery) ? 1 : 0.2; .each((d, i, nodes) => {
const node = d3.select(nodes[i]);
const isMatch = d.id.toLowerCase().includes(lowerQuery);
// 为匹配的节点添加特殊类并使其文本加粗
node.classed('search-match', isMatch);
}); });
} }

View File

@ -16,6 +16,7 @@
<div class="connection-panel"> <div class="connection-panel">
<h2>Connect to Analysis Services</h2> <h2>Connect to Analysis Services</h2>
<div class="connection-form">
<div class="form-group"> <div class="form-group">
<label for="server">Server:</label> <label for="server">Server:</label>
<input type="text" id="server" placeholder="localhost"> <input type="text" id="server" placeholder="localhost">
@ -24,10 +25,12 @@
<label for="database">Database:</label> <label for="database">Database:</label>
<input type="text" id="database" placeholder="AdventureWorks"> <input type="text" id="database" placeholder="AdventureWorks">
</div> </div>
</div>
<button id="connect-btn">Connect</button> <button id="connect-btn">Connect</button>
<div id="connection-status"></div> <div id="connection-status"></div>
</div> </div>
<div class="main-content">
<div class="visualization-container"> <div class="visualization-container">
<div class="controls"> <div class="controls">
<button id="reset-btn">Reset View</button> <button id="reset-btn">Reset View</button>
@ -36,6 +39,51 @@
</div> </div>
</div> </div>
<div id="graph-container"></div> <div id="graph-container"></div>
<div id="graph-container-detail">
<h3>Selected Measure Details</h3>
<button id="reset-detail-btn" class="reset-btn" title="Reset view">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 12a9 9 0 1 0 18 0 9 9 0 0 0-18 0z"></path>
<path d="M17 12H7"></path>
<path d="M12 17V7"></path>
</svg>
</button>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
setTimeout(function() {
const resetDetailBtn = document.getElementById('reset-detail-btn');
if (resetDetailBtn) {
resetDetailBtn.click();
}
}, 800);
// 添加一个MutationObserver来监视图表内容变化
const detailContainer = document.getElementById('graph-container-detail');
if (detailContainer) {
const observer = new MutationObserver(function(mutations) {
const hasContentChanges = mutations.some(mutation =>
mutation.type === 'childList' &&
(mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0)
);
if (hasContentChanges) {
setTimeout(function() {
const resetDetailBtn = document.getElementById('reset-detail-btn');
if (resetDetailBtn) {
resetDetailBtn.click();
}
}, 500);
}
});
observer.observe(detailContainer, {
childList: true,
subtree: true
});
}
});
</script>
</div> </div>
<div class="measure-details"> <div class="measure-details">
@ -44,6 +92,7 @@
<pre id="measure-expression"></pre> <pre id="measure-expression"></pre>
</div> </div>
</div> </div>
</div>
<script src="{{ url_for('static', filename='js/graph.js') }}"></script> <script src="{{ url_for('static', filename='js/graph.js') }}"></script>
<script src="{{ url_for('static', filename='js/app.js') }}"></script> <script src="{{ url_for('static', filename='js/app.js') }}"></script>