no message
This commit is contained in:
parent
9ccaa418a2
commit
f0bc11705a
@ -14,8 +14,9 @@ import {
|
|||||||
} from 'd3-force'
|
} from 'd3-force'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 力导向图布局,子节点全方位环形包围父节点
|
* 力导向图布局
|
||||||
* 使用 D3.js 的 d3-force 模块实现高质量的力导向算法
|
* 使用 D3.js 的 d3-force 模块实现高质量的力导向算法
|
||||||
|
* 支持一次性布局和渐进式动画布局
|
||||||
*/
|
*/
|
||||||
class ForceDirected extends Base {
|
class ForceDirected extends Base {
|
||||||
constructor(opt = {}) {
|
constructor(opt = {}) {
|
||||||
@ -25,13 +26,13 @@ class ForceDirected extends Base {
|
|||||||
this.forceConfig = {
|
this.forceConfig = {
|
||||||
// 基础参数
|
// 基础参数
|
||||||
idealDistance: 100, // 理想连接距离
|
idealDistance: 100, // 理想连接距离
|
||||||
repulsionStrength: 800, // 节点间斥力强度
|
repulsionStrength: 100, // 节点间斥力强度
|
||||||
attractionStrength: 0.3, // 节点间引力强度
|
attractionStrength: 0.5, // 节点间引力强度
|
||||||
centerStrength: 0.05, // 中心引力强度
|
centerStrength: 0.08, // 中心引力强度
|
||||||
collideRadius: 1.2, // 碰撞半径系数
|
collideRadius: 1.3, // 碰撞半径系数
|
||||||
|
|
||||||
// 迭代参数
|
// 迭代参数
|
||||||
iterations: 120, // 默认迭代次数
|
iterations: 300, // 默认迭代次数(一次性布局时使用)
|
||||||
alphaDecay: 0.0228, // alpha衰减率
|
alphaDecay: 0.0228, // alpha衰减率
|
||||||
velocityDecay: 0.4, // 速度衰减率(阻尼)
|
velocityDecay: 0.4, // 速度衰减率(阻尼)
|
||||||
|
|
||||||
@ -40,19 +41,29 @@ class ForceDirected extends Base {
|
|||||||
initialExpandAngle: Math.PI * 2, // 初始展开角度范围
|
initialExpandAngle: Math.PI * 2, // 初始展开角度范围
|
||||||
|
|
||||||
// 布局调优参数
|
// 布局调优参数
|
||||||
layerDistance: 100, // 层级间距
|
layerDistance: 50, // 层级间距
|
||||||
siblingDistance: 30, // 兄弟节点间距
|
siblingDistance: 50, // 兄弟节点间距
|
||||||
descendantEffect: 0.15, // 子孙节点对父节点的引力影响
|
descendantEffect: 0.15, // 子孙节点对父节点的引力影响
|
||||||
nodeSize: 1.2, // 节点大小影响系数
|
nodeSize: 1.2, // 节点大小影响系数
|
||||||
textSize: 0.5, // 文本大小影响系数
|
textSize: 0.5, // 文本大小影响系数
|
||||||
|
|
||||||
// 连线参数
|
// 连线参数
|
||||||
linkStyle: 'curve', // 连线样式:curve(曲线)、straight(直线)
|
linkStyle: 'curve', // 连线样式:curve(曲线)、straight(直线)
|
||||||
linkCurvature: 0.4, // 连线曲率
|
linkCurvature: 0.5, // 连线曲率
|
||||||
|
|
||||||
// 树形结构约束
|
// 树形结构约束
|
||||||
treeSpread: 0.8, // 树形展开程度
|
treeSpread: 1.2, // 树形展开程度
|
||||||
verticalCorrection: 0.5 // 垂直方向修正系数
|
verticalCorrection: 0.5, // 垂直方向修正系数
|
||||||
|
|
||||||
|
// 动画参数
|
||||||
|
enableAnimation: false, // 是否启用动画布局
|
||||||
|
animationDuration: 800, // 动画持续时间(毫秒)
|
||||||
|
animationSteps: 60, // 动画步数
|
||||||
|
|
||||||
|
// 高级参数
|
||||||
|
adaptiveIterations: true, // 自适应迭代次数
|
||||||
|
overlapAvoidanceIterations: 0, // 避免重叠的迭代次数
|
||||||
|
stabilizationFactor: 0.2 // 稳定因子(减少震荡)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 合并自定义配置
|
// 合并自定义配置
|
||||||
@ -69,12 +80,25 @@ class ForceDirected extends Base {
|
|||||||
|
|
||||||
// 初始布局计算标志
|
// 初始布局计算标志
|
||||||
this.isInitialLayout = true
|
this.isInitialLayout = true
|
||||||
|
|
||||||
|
// 动画相关变量
|
||||||
|
this.isAnimating = false
|
||||||
|
this.animationFrameId = null
|
||||||
|
this.animationStep = 0
|
||||||
|
this.animationStartPositions = new Map()
|
||||||
|
this.animationTargetPositions = new Map()
|
||||||
|
|
||||||
|
// 节点ID映射(用于提高查询性能)
|
||||||
|
this.nodeIdMap = new Map()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 执行布局
|
* 执行布局
|
||||||
*/
|
*/
|
||||||
doLayout(callback) {
|
doLayout(callback) {
|
||||||
|
// 取消正在进行的动画(如果有)
|
||||||
|
this.cancelAnimation()
|
||||||
|
|
||||||
const task = [
|
const task = [
|
||||||
// 1. 计算节点基础信息
|
// 1. 计算节点基础信息
|
||||||
() => {
|
() => {
|
||||||
@ -82,11 +106,21 @@ class ForceDirected extends Base {
|
|||||||
},
|
},
|
||||||
// 2. 应用力导向布局
|
// 2. 应用力导向布局
|
||||||
() => {
|
() => {
|
||||||
this.applyForceDirected()
|
if (this.forceConfig.enableAnimation) {
|
||||||
|
// 使用动画布局
|
||||||
|
this.applyAnimatedForceDirected(callback)
|
||||||
|
// 由于动画是异步的,这里直接返回,不触发回调
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
// 使用一次性布局
|
||||||
|
this.applyForceDirected()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
// 3. 完成布局后回调
|
// 3. 完成布局后回调(仅当不使用动画时执行)
|
||||||
() => {
|
() => {
|
||||||
callback(this.root)
|
if (!this.forceConfig.enableAnimation) {
|
||||||
|
callback(this.root)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
asyncRun(task)
|
asyncRun(task)
|
||||||
@ -125,10 +159,13 @@ class ForceDirected extends Base {
|
|||||||
true,
|
true,
|
||||||
0
|
0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 清除节点ID映射
|
||||||
|
this.nodeIdMap.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 应用力导向布局算法
|
* 应用力导向布局算法(一次性完成所有迭代)
|
||||||
*/
|
*/
|
||||||
applyForceDirected() {
|
applyForceDirected() {
|
||||||
// 1. 准备节点和连接数据
|
// 1. 准备节点和连接数据
|
||||||
@ -137,18 +174,202 @@ class ForceDirected extends Base {
|
|||||||
// 2. 设置初始布局(树形结构作为初始布局效果更佳)
|
// 2. 设置初始布局(树形结构作为初始布局效果更佳)
|
||||||
this.setInitialPositions()
|
this.setInitialPositions()
|
||||||
|
|
||||||
// 3. 运行力导向模拟
|
// 3. 计算迭代次数(可自适应)
|
||||||
this.runForceSimulation()
|
const iterations = this.calculateIterations()
|
||||||
|
|
||||||
// 4. 更新节点最终位置
|
// 4. 运行力导向模拟
|
||||||
|
this.runForceSimulation(iterations)
|
||||||
|
|
||||||
|
// 5. 更新节点最终位置
|
||||||
this.updateNodePositions()
|
this.updateNodePositions()
|
||||||
|
|
||||||
// 5. 布局后处理(避免重叠等)
|
// 6. 布局后处理(避免重叠等)
|
||||||
this.postLayoutAdjustment()
|
this.postLayoutAdjustment()
|
||||||
|
|
||||||
// 非第一次布局后重置标志
|
// 非第一次布局后重置标志
|
||||||
this.isInitialLayout = false
|
this.isInitialLayout = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算适合当前节点数的迭代次数
|
||||||
|
*/
|
||||||
|
calculateIterations() {
|
||||||
|
if (!this.forceConfig.adaptiveIterations) {
|
||||||
|
return this.forceConfig.iterations
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据节点数量动态调整迭代次数
|
||||||
|
const nodeCount = this.d3Nodes.length
|
||||||
|
|
||||||
|
if (nodeCount <= 10) {
|
||||||
|
return Math.min(this.forceConfig.iterations, 100)
|
||||||
|
} else if (nodeCount <= 50) {
|
||||||
|
return Math.min(this.forceConfig.iterations, 200)
|
||||||
|
} else if (nodeCount <= 200) {
|
||||||
|
return Math.min(this.forceConfig.iterations, 300)
|
||||||
|
} else {
|
||||||
|
return Math.min(this.forceConfig.iterations, 500)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用渐进式动画力导向布局
|
||||||
|
*/
|
||||||
|
applyAnimatedForceDirected(callback) {
|
||||||
|
// 1. 准备节点和连接数据
|
||||||
|
this.prepareD3Data()
|
||||||
|
|
||||||
|
// 2. 设置初始布局
|
||||||
|
this.setInitialPositions()
|
||||||
|
|
||||||
|
// 3. 保存当前节点位置作为动画起点
|
||||||
|
this.saveStartPositions()
|
||||||
|
|
||||||
|
// 4. 运行力导向模拟
|
||||||
|
this.runForceSimulation(this.calculateIterations())
|
||||||
|
|
||||||
|
// 5. 保存模拟后的位置作为动画终点
|
||||||
|
this.saveTargetPositions()
|
||||||
|
|
||||||
|
// 6. 执行动画
|
||||||
|
this.startLayoutAnimation(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存动画起始位置
|
||||||
|
*/
|
||||||
|
saveStartPositions() {
|
||||||
|
this.animationStartPositions.clear()
|
||||||
|
|
||||||
|
walk(
|
||||||
|
this.root,
|
||||||
|
null,
|
||||||
|
(node) => {
|
||||||
|
// 只处理展开的节点
|
||||||
|
if (node.isRoot || (node.parent && node.parent.getData('expand'))) {
|
||||||
|
// 存储起始位置
|
||||||
|
this.animationStartPositions.set(node.id, {
|
||||||
|
left: node.left,
|
||||||
|
top: node.top
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存动画目标位置
|
||||||
|
*/
|
||||||
|
saveTargetPositions() {
|
||||||
|
this.animationTargetPositions.clear()
|
||||||
|
|
||||||
|
// 将D3模拟结果作为目标位置
|
||||||
|
this.d3Nodes.forEach(d3Node => {
|
||||||
|
const node = d3Node.node
|
||||||
|
|
||||||
|
if (!node.hasCustomPosition()) {
|
||||||
|
// 计算目标位置(从中心点坐标转换为左上角坐标)
|
||||||
|
const targetLeft = d3Node.x - node.width / 2
|
||||||
|
const targetTop = d3Node.y - node.height / 2
|
||||||
|
|
||||||
|
this.animationTargetPositions.set(node.id, {
|
||||||
|
left: targetLeft,
|
||||||
|
top: targetTop
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 重置节点到起始位置
|
||||||
|
for (const [id, position] of this.animationStartPositions) {
|
||||||
|
const node = this.d3Nodes.find(n => n.node.id === id)?.node
|
||||||
|
if (node && !node.hasCustomPosition()) {
|
||||||
|
node.left = position.left
|
||||||
|
node.top = position.top
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始布局动画
|
||||||
|
*/
|
||||||
|
startLayoutAnimation(callback) {
|
||||||
|
// 初始化动画参数
|
||||||
|
this.isAnimating = true
|
||||||
|
this.animationStep = 0
|
||||||
|
const totalSteps = this.forceConfig.animationSteps
|
||||||
|
const stepDuration = this.forceConfig.animationDuration / totalSteps
|
||||||
|
|
||||||
|
// 定义动画帧函数
|
||||||
|
const animate = () => {
|
||||||
|
if (!this.isAnimating || this.animationStep >= totalSteps) {
|
||||||
|
// 动画结束,执行最终布局调整
|
||||||
|
this.postLayoutAdjustment()
|
||||||
|
|
||||||
|
// 调用回调函数
|
||||||
|
callback && callback(this.root)
|
||||||
|
|
||||||
|
// 重置动画状态
|
||||||
|
this.isAnimating = false
|
||||||
|
this.animationFrameId = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 增加步数
|
||||||
|
this.animationStep++
|
||||||
|
|
||||||
|
// 计算当前进度(0-1之间)
|
||||||
|
const progress = this.animationStep / totalSteps
|
||||||
|
|
||||||
|
// 应用缓动函数 - 先慢后快再慢
|
||||||
|
const easedProgress = this.easeInOutCubic(progress)
|
||||||
|
|
||||||
|
// 更新每个节点的位置
|
||||||
|
this.d3Nodes.forEach(d3Node => {
|
||||||
|
const node = d3Node.node
|
||||||
|
if (!node.hasCustomPosition()) {
|
||||||
|
const startPos = this.animationStartPositions.get(node.id)
|
||||||
|
const targetPos = this.animationTargetPositions.get(node.id)
|
||||||
|
|
||||||
|
if (startPos && targetPos) {
|
||||||
|
// 线性插值计算当前位置
|
||||||
|
node.left = startPos.left + (targetPos.left - startPos.left) * easedProgress
|
||||||
|
node.top = startPos.top + (targetPos.top - startPos.top) * easedProgress
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 触发重绘
|
||||||
|
this.mindMap.emit('vie:change')
|
||||||
|
|
||||||
|
// 安排下一帧
|
||||||
|
this.animationFrameId = setTimeout(animate, stepDuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始动画
|
||||||
|
animate()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓动函数:先慢后快再慢
|
||||||
|
*/
|
||||||
|
easeInOutCubic(t) {
|
||||||
|
return t < 0.5
|
||||||
|
? 4 * t * t * t
|
||||||
|
: 1 - Math.pow(-2 * t + 2, 3) / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消正在进行的动画
|
||||||
|
*/
|
||||||
|
cancelAnimation() {
|
||||||
|
if (this.isAnimating && this.animationFrameId) {
|
||||||
|
clearTimeout(this.animationFrameId)
|
||||||
|
this.animationFrameId = null
|
||||||
|
this.isAnimating = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 准备D3力导向布局需要的数据结构
|
* 准备D3力导向布局需要的数据结构
|
||||||
@ -188,7 +409,9 @@ class ForceDirected extends Base {
|
|||||||
// 是否固定位置
|
// 是否固定位置
|
||||||
fixed: node.hasCustomPosition(),
|
fixed: node.hasCustomPosition(),
|
||||||
// 文本长度因子
|
// 文本长度因子
|
||||||
textFactor: (node.getData('text') || '').length * this.forceConfig.textSize
|
textFactor: (node.getData('text') || '').length * this.forceConfig.textSize,
|
||||||
|
// 节点重要度评分
|
||||||
|
importance: this.calculateNodeImportance(node)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果节点位置已自定义,则固定它
|
// 如果节点位置已自定义,则固定它
|
||||||
@ -197,7 +420,9 @@ class ForceDirected extends Base {
|
|||||||
d3Node.fy = nodeY
|
d3Node.fy = nodeY
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 将节点添加到数组和映射
|
||||||
this.d3Nodes.push(d3Node)
|
this.d3Nodes.push(d3Node)
|
||||||
|
this.nodeIdMap.set(node.id, d3Node)
|
||||||
|
|
||||||
// 为非根节点创建与父节点的连接
|
// 为非根节点创建与父节点的连接
|
||||||
if (!node.isRoot) {
|
if (!node.isRoot) {
|
||||||
@ -219,6 +444,33 @@ class ForceDirected extends Base {
|
|||||||
return { nodes: this.d3Nodes, links: this.d3Links }
|
return { nodes: this.d3Nodes, links: this.d3Links }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算节点重要度 - 影响布局决策
|
||||||
|
*/
|
||||||
|
calculateNodeImportance(node) {
|
||||||
|
// 基础重要度分数
|
||||||
|
let score = 1
|
||||||
|
|
||||||
|
// 节点层级影响
|
||||||
|
score *= Math.pow(0.9, node.layerIndex)
|
||||||
|
|
||||||
|
// 子节点数量影响
|
||||||
|
const childrenCount = node.children ? node.children.length : 0
|
||||||
|
score *= (1 + childrenCount / 10)
|
||||||
|
|
||||||
|
// 文本长度影响
|
||||||
|
const textLength = (node.getData('text') || '').length
|
||||||
|
score *= (1 + Math.min(1, textLength / 50))
|
||||||
|
|
||||||
|
// 扩展属性影响
|
||||||
|
if (node.getData('priority')) {
|
||||||
|
score *= 1.5
|
||||||
|
}
|
||||||
|
|
||||||
|
// 缩放到合理范围 0.1-10
|
||||||
|
return Math.max(0.1, Math.min(10, score))
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 计算连接强度 - 影响节点间吸引力
|
* 计算连接强度 - 影响节点间吸引力
|
||||||
*/
|
*/
|
||||||
@ -237,7 +489,12 @@ class ForceDirected extends Base {
|
|||||||
const childrenCount = node.children ? node.children.length : 0
|
const childrenCount = node.children ? node.children.length : 0
|
||||||
const childrenFactor = Math.min(1.5, 1 + childrenCount / 20)
|
const childrenFactor = Math.min(1.5, 1 + childrenCount / 20)
|
||||||
|
|
||||||
return strength * depthFactor * childrenFactor * (1 + textFactor)
|
// 节点面积因子
|
||||||
|
const size = node.width * node.height
|
||||||
|
const sizeFactor = Math.min(1.5, 1 + Math.sqrt(size) / 200)
|
||||||
|
|
||||||
|
// 综合计算
|
||||||
|
return strength * depthFactor * childrenFactor * (1 + textFactor) * sizeFactor
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -251,19 +508,27 @@ class ForceDirected extends Base {
|
|||||||
const layerFactor = 1 + (node.layerIndex - 1) * 0.2
|
const layerFactor = 1 + (node.layerIndex - 1) * 0.2
|
||||||
|
|
||||||
// 根据节点大小调整距离
|
// 根据节点大小调整距离
|
||||||
const sizeFactor = Math.sqrt(
|
const nodeSize = Math.sqrt(node.width * node.height)
|
||||||
(node.width * node.height + node.parent.width * node.parent.height) / 2
|
const parentSize = Math.sqrt(node.parent.width * node.parent.height)
|
||||||
) / 100
|
const sizeFactor = Math.sqrt(nodeSize * parentSize) / 100
|
||||||
|
|
||||||
|
// 根据子节点数量调整
|
||||||
|
const childrenCount = node.parent.children ? node.parent.children.length : 0
|
||||||
|
const childrenFactor = Math.max(0.7, Math.min(1.2, Math.log(childrenCount + 1) / Math.log(10)))
|
||||||
|
|
||||||
|
// 根据文本长度调整
|
||||||
|
const textLength = (node.getData('text') || '').length
|
||||||
|
const textFactor = Math.min(1.3, 1 + textLength / 100)
|
||||||
|
|
||||||
// 综合计算理想距离
|
// 综合计算理想距离
|
||||||
return distance * layerFactor * (1 + sizeFactor)
|
return distance * layerFactor * (1 + sizeFactor) * childrenFactor * textFactor
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置节点的初始位置 - 使用简单的径向布局作为力导向的初始位置
|
* 设置节点的初始位置 - 使用树形布局作为力导向的初始位置
|
||||||
*/
|
*/
|
||||||
setInitialPositions() {
|
setInitialPositions() {
|
||||||
// 使用自定义的树形初始布局可以加速力导向算法收敛
|
// 使用树形初始布局可以加速力导向算法收敛
|
||||||
|
|
||||||
// 按层级组织节点
|
// 按层级组织节点
|
||||||
const nodesByLevel = {}
|
const nodesByLevel = {}
|
||||||
@ -277,8 +542,7 @@ class ForceDirected extends Base {
|
|||||||
|
|
||||||
// 从根节点开始
|
// 从根节点开始
|
||||||
const rootNode = this.d3Nodes.find(n => n.node.isRoot)
|
const rootNode = this.d3Nodes.find(n => n.node.isRoot)
|
||||||
const rootX = rootNode.x
|
if (!rootNode) return
|
||||||
const rootY = rootNode.y
|
|
||||||
|
|
||||||
// 对每个层级进行处理
|
// 对每个层级进行处理
|
||||||
Object.keys(nodesByLevel).forEach(level => {
|
Object.keys(nodesByLevel).forEach(level => {
|
||||||
@ -288,7 +552,7 @@ class ForceDirected extends Base {
|
|||||||
// 在每个层级中,按照父节点分组
|
// 在每个层级中,按照父节点分组
|
||||||
const nodesByParent = {}
|
const nodesByParent = {}
|
||||||
nodesAtLevel.forEach(node => {
|
nodesAtLevel.forEach(node => {
|
||||||
const parentId = this.d3Links.find(link => link.target === node.id)?.source
|
const parentId = this.findParentId(node.id)
|
||||||
if (parentId !== undefined) {
|
if (parentId !== undefined) {
|
||||||
if (!nodesByParent[parentId]) {
|
if (!nodesByParent[parentId]) {
|
||||||
nodesByParent[parentId] = []
|
nodesByParent[parentId] = []
|
||||||
@ -299,33 +563,41 @@ class ForceDirected extends Base {
|
|||||||
|
|
||||||
// 对于每组有共同父节点的节点,按径向布局排列
|
// 对于每组有共同父节点的节点,按径向布局排列
|
||||||
Object.entries(nodesByParent).forEach(([parentId, childNodes]) => {
|
Object.entries(nodesByParent).forEach(([parentId, childNodes]) => {
|
||||||
const parent = this.d3Nodes.find(n => n.id === Number(parentId))
|
const parent = this.nodeIdMap.get(Number(parentId))
|
||||||
|
if (!parent) return
|
||||||
|
|
||||||
const count = childNodes.length
|
const count = childNodes.length
|
||||||
|
|
||||||
// 如果此父节点只有单个子节点,以特定偏移放置
|
// 如果此父节点只有单个子节点,以特定偏移放置
|
||||||
if (count === 1) {
|
if (count === 1) {
|
||||||
const child = childNodes[0]
|
const child = childNodes[0]
|
||||||
const angle = Math.random() * Math.PI * 2 // 随机角度
|
if (child.fixed) return
|
||||||
|
|
||||||
|
// 根据节点ID选择一个稳定的随机角度
|
||||||
|
const angle = ((child.id * 10007) % 360) * (Math.PI / 180)
|
||||||
const radius = this.forceConfig.layerDistance * child.depth * this.forceConfig.initialRadius
|
const radius = this.forceConfig.layerDistance * child.depth * this.forceConfig.initialRadius
|
||||||
|
|
||||||
// 如果节点未固定位置
|
// 应用位置
|
||||||
if (!child.fixed) {
|
child.x = parent.x + Math.cos(angle) * radius
|
||||||
child.x = parent.x + Math.cos(angle) * radius
|
child.y = parent.y + Math.sin(angle) * radius
|
||||||
child.y = parent.y + Math.sin(angle) * radius
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// 多个子节点,在父节点周围环形布局
|
// 多个子节点,在父节点周围环形布局
|
||||||
const angleStep = this.forceConfig.initialExpandAngle / count
|
const angleStep = this.forceConfig.initialExpandAngle / count
|
||||||
|
|
||||||
|
// 根据节点重要度排序
|
||||||
|
childNodes.sort((a, b) => b.importance - a.importance)
|
||||||
|
|
||||||
|
// 分配位置
|
||||||
childNodes.forEach((child, i) => {
|
childNodes.forEach((child, i) => {
|
||||||
if (child.fixed) return // 跳过固定位置的节点
|
if (child.fixed) return
|
||||||
|
|
||||||
// 计算径向位置
|
// 计算径向位置
|
||||||
const angle = angleStep * i
|
const angle = angleStep * i
|
||||||
// 半径随层级和节点数增加
|
// 半径根据层级、节点重要度和大小调整
|
||||||
const radius = this.forceConfig.layerDistance *
|
const radius = this.forceConfig.layerDistance *
|
||||||
(child.depth + child.textFactor * 0.1) *
|
child.depth *
|
||||||
this.forceConfig.initialRadius
|
this.forceConfig.initialRadius *
|
||||||
|
(0.8 + 0.4 * child.importance / 10)
|
||||||
|
|
||||||
// 应用位置
|
// 应用位置
|
||||||
child.x = parent.x + Math.cos(angle) * radius
|
child.x = parent.x + Math.cos(angle) * radius
|
||||||
@ -334,12 +606,40 @@ class ForceDirected extends Base {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 添加微小随机偏移,避免初始完全对称
|
||||||
|
this.addRandomOffset()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加微小随机偏移
|
||||||
|
*/
|
||||||
|
addRandomOffset() {
|
||||||
|
this.d3Nodes.forEach(d3Node => {
|
||||||
|
if (!d3Node.fixed && !d3Node.node.isRoot) {
|
||||||
|
// 添加微小随机偏移,基于节点ID确保稳定性
|
||||||
|
const seed = d3Node.id * 10007
|
||||||
|
const offsetX = (((seed % 100) / 100) - 0.5) * 5
|
||||||
|
const offsetY = ((((seed / 100) % 100) / 100) - 0.5) * 5
|
||||||
|
|
||||||
|
d3Node.x += offsetX
|
||||||
|
d3Node.y += offsetY
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 找到节点的父节点ID
|
||||||
|
*/
|
||||||
|
findParentId(nodeId) {
|
||||||
|
const link = this.d3Links.find(link => link.target === nodeId)
|
||||||
|
return link ? link.source : undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 运行D3力导向模拟
|
* 运行D3力导向模拟
|
||||||
*/
|
*/
|
||||||
runForceSimulation() {
|
runForceSimulation(iterations) {
|
||||||
// 创建力导向模拟器
|
// 创建力导向模拟器
|
||||||
this.simulation = forceSimulation(this.d3Nodes)
|
this.simulation = forceSimulation(this.d3Nodes)
|
||||||
// 设置衰减参数
|
// 设置衰减参数
|
||||||
@ -360,74 +660,107 @@ class ForceDirected extends Base {
|
|||||||
|
|
||||||
// 节点间斥力
|
// 节点间斥力
|
||||||
.force('charge', forceManyBody()
|
.force('charge', forceManyBody()
|
||||||
.strength(d => {
|
.strength(d => this.calculateRepulsionStrength(d))
|
||||||
// 节点斥力随深度、子节点数量和文本长度调整
|
|
||||||
const depthFactor = Math.pow(0.8, d.depth)
|
|
||||||
const sizeFactor = d.size / 50 + 1
|
|
||||||
const childrenFactor = d.node.children ? d.node.children.length / 5 + 1 : 1
|
|
||||||
|
|
||||||
return -this.forceConfig.repulsionStrength * depthFactor * sizeFactor * childrenFactor
|
|
||||||
})
|
|
||||||
.distanceMax(1000)) // 限制斥力的最大作用距离
|
.distanceMax(1000)) // 限制斥力的最大作用距离
|
||||||
|
|
||||||
// 碰撞力 - 避免节点重叠
|
// 碰撞力 - 避免节点重叠
|
||||||
.force('collide', forceCollide()
|
.force('collide', forceCollide()
|
||||||
.radius(d => {
|
.radius(d => this.calculateCollideRadius(d))
|
||||||
// 碰撞半径 = 节点大小 + 边距
|
.strength(0.7 + 0.3 * this.forceConfig.stabilizationFactor)
|
||||||
const nodeSize = Math.max(d.node.width, d.node.height) / 2
|
|
||||||
return nodeSize * this.forceConfig.collideRadius
|
|
||||||
})
|
|
||||||
.strength(0.8)
|
|
||||||
.iterations(2))
|
.iterations(2))
|
||||||
|
|
||||||
// 沿X轴的力 - 使子节点倾向于在父节点两侧分布
|
// 沿X轴的力 - 使子节点倾向于分散
|
||||||
.force('x', forceX().strength(d => {
|
.force('x', forceX().strength(d => this.calculateXForceStrength(d))
|
||||||
if (d.node.isRoot || !d.node.parent) return 0
|
.x(d => this.calculateXTarget(d)))
|
||||||
|
|
||||||
// 获取父节点D3数据
|
|
||||||
const parentD3 = this.d3Nodes.find(n => n.id === d.node.parent.id)
|
|
||||||
if (!parentD3) return 0
|
|
||||||
|
|
||||||
// 水平分布的力,使子节点分散在父节点两侧
|
|
||||||
return 0.02 * this.forceConfig.treeSpread
|
|
||||||
}).x(d => {
|
|
||||||
// 如果是根节点或无父节点,返回当前位置
|
|
||||||
if (d.node.isRoot || !d.node.parent) return d.x
|
|
||||||
|
|
||||||
// 获取父节点位置
|
|
||||||
const parentD3 = this.d3Nodes.find(n => n.id === d.node.parent.id)
|
|
||||||
if (!parentD3) return d.x
|
|
||||||
|
|
||||||
// 应用随机偏移以打破对称性
|
|
||||||
const offset = (d.id % 2 === 0 ? 1 : -1) * this.forceConfig.siblingDistance *
|
|
||||||
(1 + d.node.getData('text').length / 20)
|
|
||||||
return parentD3.x + offset
|
|
||||||
}))
|
|
||||||
|
|
||||||
// 沿Y轴的力 - 使子节点倾向于在父节点下方
|
// 沿Y轴的力 - 使子节点倾向于有层次感
|
||||||
.force('y', forceY().strength(d => {
|
.force('y', forceY().strength(d => this.calculateYForceStrength(d))
|
||||||
// 对根节点无影响
|
.y(d => this.calculateYTarget(d)))
|
||||||
if (d.node.isRoot) return 0
|
|
||||||
|
|
||||||
// 垂直方向吸引力,与层级和子节点数相关
|
|
||||||
return 0.04 * this.forceConfig.verticalCorrection * (1 + d.depth * 0.1)
|
|
||||||
}).y(d => {
|
|
||||||
if (d.node.isRoot || !d.node.parent) return d.y
|
|
||||||
|
|
||||||
// 获取父节点位置
|
|
||||||
const parentD3 = this.d3Nodes.find(n => n.id === d.node.parent.id)
|
|
||||||
if (!parentD3) return d.y
|
|
||||||
|
|
||||||
// 向下偏移,层级越深偏移越大
|
|
||||||
const levelOffset = d.depth * this.forceConfig.layerDistance * 0.6
|
|
||||||
return parentD3.y + levelOffset
|
|
||||||
}))
|
|
||||||
|
|
||||||
// 一次性运行所有迭代
|
// 一次性运行所有迭代
|
||||||
for (let i = 0; i < this.forceConfig.iterations; i++) {
|
for (let i = 0; i < iterations; i++) {
|
||||||
this.simulation.tick()
|
this.simulation.tick()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算节点斥力强度
|
||||||
|
*/
|
||||||
|
calculateRepulsionStrength(d) {
|
||||||
|
// 节点斥力随深度、子节点数量和文本长度调整
|
||||||
|
const depthFactor = Math.pow(0.8, d.depth)
|
||||||
|
const sizeFactor = d.size / 50 + 1
|
||||||
|
const importanceFactor = d.importance / 5 + 0.5
|
||||||
|
|
||||||
|
return -this.forceConfig.repulsionStrength * depthFactor * sizeFactor * importanceFactor
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算节点碰撞半径
|
||||||
|
*/
|
||||||
|
calculateCollideRadius(d) {
|
||||||
|
// 碰撞半径 = 节点大小 + 边距
|
||||||
|
const nodeSize = Math.max(d.node.width, d.node.height) / 2
|
||||||
|
return nodeSize * this.forceConfig.collideRadius * Math.sqrt(d.importance / 5 + 0.5)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算X轴力的强度
|
||||||
|
*/
|
||||||
|
calculateXForceStrength(d) {
|
||||||
|
if (d.node.isRoot || !d.node.parent) return 0
|
||||||
|
|
||||||
|
// 水平方向的力,使子节点分散
|
||||||
|
return 0.03 * this.forceConfig.treeSpread * (1 - this.forceConfig.stabilizationFactor * 0.5)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算Y轴力的强度
|
||||||
|
*/
|
||||||
|
calculateYForceStrength(d) {
|
||||||
|
// 对根节点无影响
|
||||||
|
if (d.node.isRoot) return 0
|
||||||
|
|
||||||
|
// 垂直方向吸引力,与层级和重要度相关
|
||||||
|
return 0.04 * this.forceConfig.verticalCorrection * (1 + d.depth * 0.1) * (1 - d.importance / 20)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算X轴目标位置
|
||||||
|
*/
|
||||||
|
calculateXTarget(d) {
|
||||||
|
// 如果是根节点或无父节点,返回当前位置
|
||||||
|
if (d.node.isRoot || !d.node.parent) return d.x
|
||||||
|
|
||||||
|
// 获取父节点位置
|
||||||
|
const parentD3 = this.nodeIdMap.get(d.node.parent.id)
|
||||||
|
if (!parentD3) return d.x
|
||||||
|
|
||||||
|
// 应用偏移以打破对称性(使用节点ID确保稳定性)
|
||||||
|
const offset = ((d.id % 2 === 0 ? 1 : -1) *
|
||||||
|
this.forceConfig.siblingDistance *
|
||||||
|
(1 + d.textFactor * 2) *
|
||||||
|
(0.5 + d.importance / 10))
|
||||||
|
|
||||||
|
return parentD3.x + offset
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算Y轴目标位置
|
||||||
|
*/
|
||||||
|
calculateYTarget(d) {
|
||||||
|
if (d.node.isRoot || !d.node.parent) return d.y
|
||||||
|
|
||||||
|
// 获取父节点位置
|
||||||
|
const parentD3 = this.nodeIdMap.get(d.node.parent.id)
|
||||||
|
if (!parentD3) return d.y
|
||||||
|
|
||||||
|
// 向下偏移,层级越深偏移越大
|
||||||
|
const levelOffset = d.depth * this.forceConfig.layerDistance *
|
||||||
|
(0.6 + 0.4 * (d.importance / 10))
|
||||||
|
|
||||||
|
return parentD3.y + levelOffset
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新节点位置数据
|
* 更新节点位置数据
|
||||||
@ -457,11 +790,12 @@ class ForceDirected extends Base {
|
|||||||
*/
|
*/
|
||||||
resolveOverlapping() {
|
resolveOverlapping() {
|
||||||
// 获取所有非根节点
|
// 获取所有非根节点
|
||||||
const nodes = this.d3Nodes.filter(d => !d.node.isRoot && !d.fixed)
|
const nodes = this.d3Nodes
|
||||||
.map(d => d.node)
|
.filter(d => !d.node.isRoot && !d.fixed)
|
||||||
|
.map(d => d.node)
|
||||||
|
|
||||||
// 向外移动来解决重叠
|
// 向外移动来解决重叠
|
||||||
const iterations = 5
|
const iterations = this.forceConfig.overlapAvoidanceIterations
|
||||||
for (let iter = 0; iter < iterations; iter++) {
|
for (let iter = 0; iter < iterations; iter++) {
|
||||||
let hasAdjusted = false
|
let hasAdjusted = false
|
||||||
|
|
||||||
@ -519,6 +853,8 @@ class ForceDirected extends Base {
|
|||||||
* 分离重叠的节点
|
* 分离重叠的节点
|
||||||
*/
|
*/
|
||||||
separateNodes(nodeA, nodeB) {
|
separateNodes(nodeA, nodeB) {
|
||||||
|
if (nodeA.hasCustomPosition() || nodeB.hasCustomPosition()) return
|
||||||
|
|
||||||
// 计算两个节点中心点
|
// 计算两个节点中心点
|
||||||
const centerAx = nodeA.left + nodeA.width / 2
|
const centerAx = nodeA.left + nodeA.width / 2
|
||||||
const centerAy = nodeA.top + nodeA.height / 2
|
const centerAy = nodeA.top + nodeA.height / 2
|
||||||
@ -528,7 +864,7 @@ class ForceDirected extends Base {
|
|||||||
// 计算两节点之间的向量
|
// 计算两节点之间的向量
|
||||||
const dx = centerBx - centerAx
|
const dx = centerBx - centerAx
|
||||||
const dy = centerBy - centerAy
|
const dy = centerBy - centerAy
|
||||||
const distance = Math.sqrt(dx * dx + dy * dy)
|
const distance = Math.max(1, Math.sqrt(dx * dx + dy * dy))
|
||||||
|
|
||||||
if (distance < 1) {
|
if (distance < 1) {
|
||||||
// 如果节点中心重合,则添加微小的随机偏移
|
// 如果节点中心重合,则添加微小的随机偏移
|
||||||
@ -563,6 +899,7 @@ class ForceDirected extends Base {
|
|||||||
const moveY = dy * factor / 2
|
const moveY = dy * factor / 2
|
||||||
|
|
||||||
// 移动节点(考虑移动距离的权重分配)
|
// 移动节点(考虑移动距离的权重分配)
|
||||||
|
// 有更多子节点的节点移动较少
|
||||||
const weightA = nodeA.children ? nodeA.children.length : 0
|
const weightA = nodeA.children ? nodeA.children.length : 0
|
||||||
const weightB = nodeB.children ? nodeB.children.length : 0
|
const weightB = nodeB.children ? nodeB.children.length : 0
|
||||||
const totalWeight = weightA + weightB + 2
|
const totalWeight = weightA + weightB + 2
|
||||||
@ -581,7 +918,7 @@ class ForceDirected extends Base {
|
|||||||
* 绘制连接线 - 从父节点到子节点
|
* 绘制连接线 - 从父节点到子节点
|
||||||
*/
|
*/
|
||||||
renderLine(node, lines, style) {
|
renderLine(node, lines, style) {
|
||||||
// 根据配置选择连线风格
|
// 根据线条风格选择相应的渲染方法
|
||||||
if (this.forceConfig.linkStyle === 'straight') {
|
if (this.forceConfig.linkStyle === 'straight') {
|
||||||
this.renderLineStraight(node, lines, style)
|
this.renderLineStraight(node, lines, style)
|
||||||
} else {
|
} else {
|
||||||
@ -631,10 +968,8 @@ class ForceDirected extends Base {
|
|||||||
const distance = Math.sqrt(dx * dx + dy * dy)
|
const distance = Math.sqrt(dx * dx + dy * dy)
|
||||||
|
|
||||||
// 计算控制点
|
// 计算控制点
|
||||||
// 控制点根据连线方向和曲率系数计算
|
|
||||||
const controlDistance = distance * curvature
|
|
||||||
const angle = Math.atan2(dy, dx)
|
const angle = Math.atan2(dy, dx)
|
||||||
const perpendicular = angle + Math.PI / 2
|
const controlDistance = distance * curvature
|
||||||
|
|
||||||
const cx1 = x1 + Math.cos(angle) * distance * 0.3
|
const cx1 = x1 + Math.cos(angle) * distance * 0.3
|
||||||
const cy1 = y1 + Math.sin(angle) * distance * 0.3
|
const cy1 = y1 + Math.sin(angle) * distance * 0.3
|
||||||
@ -648,83 +983,65 @@ class ForceDirected extends Base {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 渲染展开/收起按钮
|
* 渲染展开收起按钮
|
||||||
*/
|
*/
|
||||||
renderExpandBtn(node, btn) {
|
renderExpandBtn(node, btn) {
|
||||||
const { width, height, expandBtnSize } = node
|
const { width, height, expandBtnSize } = node
|
||||||
let { translateX, translateY } = btn.transform()
|
let { translateX, translateY } = btn.transform()
|
||||||
|
|
||||||
// 在力导向布局中,统一将展开按钮放在节点右侧
|
// 计算按钮位置 - 在力导向布局中统一放在右侧
|
||||||
const _x = width
|
const _x = width
|
||||||
const _y = height / 2
|
const _y = height / 2
|
||||||
|
|
||||||
// 位置未改变无需更新
|
// 位置没有变化则返回
|
||||||
if (_x === translateX && _y === translateY) {
|
if (_x === translateX && _y === translateY) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算偏移量并应用
|
|
||||||
const x = _x - translateX
|
const x = _x - translateX
|
||||||
const y = _y - translateY
|
const y = _y - translateY
|
||||||
btn.translate(x, y)
|
btn.translate(x, y)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 渲染概要节点
|
* 渲染展开收起按钮的隐藏占位元素
|
||||||
*/
|
|
||||||
renderGeneralization(list) {
|
|
||||||
list.forEach(item => {
|
|
||||||
const {
|
|
||||||
top,
|
|
||||||
bottom,
|
|
||||||
left,
|
|
||||||
right,
|
|
||||||
generalizationLineMargin,
|
|
||||||
generalizationNodeMargin
|
|
||||||
} = this.getNodeGeneralizationRenderBoundaries(item, 'h')
|
|
||||||
|
|
||||||
// 计算中心点
|
|
||||||
const centerX = (left + right) / 2
|
|
||||||
const centerY = (top + bottom) / 2
|
|
||||||
|
|
||||||
// 计算概要节点方向
|
|
||||||
const nodeX = item.node.left + item.node.width / 2
|
|
||||||
const nodeY = item.node.top + item.node.height / 2
|
|
||||||
|
|
||||||
// 计算方向向量
|
|
||||||
const dx = centerX - nodeX
|
|
||||||
const dy = centerY - nodeY
|
|
||||||
const distance = Math.sqrt(dx * dx + dy * dy)
|
|
||||||
const nx = dx / distance
|
|
||||||
const ny = dy / distance
|
|
||||||
|
|
||||||
// 计算概要线起止点
|
|
||||||
const x1 = centerX + nx * generalizationLineMargin
|
|
||||||
const y1 = centerY + ny * generalizationLineMargin
|
|
||||||
const x2 = centerX - nx * generalizationLineMargin
|
|
||||||
const y2 = centerY - ny * generalizationLineMargin
|
|
||||||
|
|
||||||
// 控制点 - 垂直于方向向量
|
|
||||||
const cx = (x1 + x2) / 2 + ny * 30
|
|
||||||
const cy = (y1 + y2) / 2 - nx * 30
|
|
||||||
|
|
||||||
// 绘制概要线
|
|
||||||
const path = `M ${x1},${y1} Q ${cx},${cy} ${x2},${y2}`
|
|
||||||
item.generalizationLine.plot(path)
|
|
||||||
|
|
||||||
// 定位概要节点
|
|
||||||
item.generalizationNode.left = x2 - item.generalizationNode.width / 2
|
|
||||||
item.generalizationNode.top = y2 - item.generalizationNode.height / 2
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 渲染展开收起按钮的占位元素
|
|
||||||
*/
|
*/
|
||||||
renderExpandBtnRect(rect, expandBtnSize, width, height, node) {
|
renderExpandBtnRect(rect, expandBtnSize, width, height, node) {
|
||||||
// 在力导向布局中,展开按钮统一在节点右侧
|
// 在力导向布局中,统一将展开按钮放在节点右侧
|
||||||
rect.size(expandBtnSize, height).x(width).y(0)
|
rect.size(expandBtnSize, height).x(width).y(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 创建概要节点
|
||||||
|
renderGeneralization(list) {
|
||||||
|
list.forEach(item => {
|
||||||
|
let isLeft = item.node.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT
|
||||||
|
let {
|
||||||
|
top,
|
||||||
|
bottom,
|
||||||
|
left,
|
||||||
|
right,
|
||||||
|
generalizationLineMargin,
|
||||||
|
generalizationNodeMargin
|
||||||
|
} = this.getNodeGeneralizationRenderBoundaries(item, 'h')
|
||||||
|
let x = isLeft
|
||||||
|
? left - generalizationLineMargin
|
||||||
|
: right + generalizationLineMargin
|
||||||
|
let x1 = x
|
||||||
|
let y1 = top
|
||||||
|
let x2 = x
|
||||||
|
let y2 = bottom
|
||||||
|
let cx = x1 + (isLeft ? -20 : 20)
|
||||||
|
let cy = y1 + (y2 - y1) / 2
|
||||||
|
let path = `M ${x1},${y1} Q ${cx},${cy} ${x2},${y2}`
|
||||||
|
item.generalizationLine.plot(path)
|
||||||
|
item.generalizationNode.left =
|
||||||
|
x +
|
||||||
|
(isLeft ? -generalizationNodeMargin : generalizationNodeMargin) -
|
||||||
|
(isLeft ? item.generalizationNode.width : 0)
|
||||||
|
item.generalizationNode.top =
|
||||||
|
top + (bottom - top - item.generalizationNode.height) / 2
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ForceDirected
|
export default ForceDirected
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user