初步实现功能
This commit is contained in:
parent
382be91e63
commit
98b5b9855e
Binary file not shown.
Binary file not shown.
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -16,32 +16,81 @@
|
||||
|
||||
<div class="connection-panel">
|
||||
<h2>Connect to Analysis Services</h2>
|
||||
<div class="form-group">
|
||||
<label for="server">Server:</label>
|
||||
<input type="text" id="server" placeholder="localhost">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="database">Database:</label>
|
||||
<input type="text" id="database" placeholder="AdventureWorks">
|
||||
<div class="connection-form">
|
||||
<div class="form-group">
|
||||
<label for="server">Server:</label>
|
||||
<input type="text" id="server" placeholder="localhost">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="database">Database:</label>
|
||||
<input type="text" id="database" placeholder="AdventureWorks">
|
||||
</div>
|
||||
</div>
|
||||
<button id="connect-btn">Connect</button>
|
||||
<div id="connection-status"></div>
|
||||
</div>
|
||||
|
||||
<div class="visualization-container">
|
||||
<div class="controls">
|
||||
<button id="reset-btn">Reset View</button>
|
||||
<div class="search-box">
|
||||
<input type="text" id="search-input" placeholder="Search measures...">
|
||||
<div class="main-content">
|
||||
<div class="visualization-container">
|
||||
<div class="controls">
|
||||
<button id="reset-btn">Reset View</button>
|
||||
<div class="search-box">
|
||||
<input type="text" id="search-input" placeholder="Search measures...">
|
||||
</div>
|
||||
</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 class="measure-details">
|
||||
<h2>Measure Details</h2>
|
||||
<div id="measure-name"></div>
|
||||
<pre id="measure-expression"></pre>
|
||||
</div>
|
||||
<div id="graph-container"></div>
|
||||
</div>
|
||||
|
||||
<div class="measure-details">
|
||||
<h2>Measure Details</h2>
|
||||
<div id="measure-name"></div>
|
||||
<pre id="measure-expression"></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user