no message

This commit is contained in:
KuroSago 2025-04-28 18:27:09 +08:00
parent 9ccaa418a2
commit f0bc11705a

View File

@ -14,8 +14,9 @@ import {
} from 'd3-force'
/**
* 力导向图布局子节点全方位环形包围父节点
* 力导向图布局
* 使用 D3.js d3-force 模块实现高质量的力导向算法
* 支持一次性布局和渐进式动画布局
*/
class ForceDirected extends Base {
constructor(opt = {}) {
@ -25,13 +26,13 @@ class ForceDirected extends Base {
this.forceConfig = {
// 基础参数
idealDistance: 100, // 理想连接距离
repulsionStrength: 800, // 节点间斥力强度
attractionStrength: 0.3, // 节点间引力强度
centerStrength: 0.05, // 中心引力强度
collideRadius: 1.2, // 碰撞半径系数
repulsionStrength: 100, // 节点间斥力强度
attractionStrength: 0.5, // 节点间引力强度
centerStrength: 0.08, // 中心引力强度
collideRadius: 1.3, // 碰撞半径系数
// 迭代参数
iterations: 120, // 默认迭代次数
iterations: 300, // 默认迭代次数(一次性布局时使用)
alphaDecay: 0.0228, // alpha衰减率
velocityDecay: 0.4, // 速度衰减率(阻尼)
@ -40,19 +41,29 @@ class ForceDirected extends Base {
initialExpandAngle: Math.PI * 2, // 初始展开角度范围
// 布局调优参数
layerDistance: 100, // 层级间距
siblingDistance: 30, // 兄弟节点间距
layerDistance: 50, // 层级间距
siblingDistance: 50, // 兄弟节点间距
descendantEffect: 0.15, // 子孙节点对父节点的引力影响
nodeSize: 1.2, // 节点大小影响系数
textSize: 0.5, // 文本大小影响系数
// 连线参数
linkStyle: 'curve', // 连线样式curve(曲线)、straight(直线)
linkCurvature: 0.4, // 连线曲率
linkCurvature: 0.5, // 连线曲率
// 树形结构约束
treeSpread: 0.8, // 树形展开程度
verticalCorrection: 0.5 // 垂直方向修正系数
treeSpread: 1.2, // 树形展开程度
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.isAnimating = false
this.animationFrameId = null
this.animationStep = 0
this.animationStartPositions = new Map()
this.animationTargetPositions = new Map()
// 节点ID映射用于提高查询性能
this.nodeIdMap = new Map()
}
/**
* 执行布局
*/
doLayout(callback) {
// 取消正在进行的动画(如果有)
this.cancelAnimation()
const task = [
// 1. 计算节点基础信息
() => {
@ -82,11 +106,21 @@ class ForceDirected extends Base {
},
// 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)
@ -125,10 +159,13 @@ class ForceDirected extends Base {
true,
0
)
// 清除节点ID映射
this.nodeIdMap.clear()
}
/**
* 应用力导向布局算法
* 应用力导向布局算法一次性完成所有迭代
*/
applyForceDirected() {
// 1. 准备节点和连接数据
@ -137,18 +174,202 @@ class ForceDirected extends Base {
// 2. 设置初始布局(树形结构作为初始布局效果更佳)
this.setInitialPositions()
// 3. 运行力导向模拟
this.runForceSimulation()
// 3. 计算迭代次数(可自适应)
const iterations = this.calculateIterations()
// 4. 更新节点最终位置
// 4. 运行力导向模拟
this.runForceSimulation(iterations)
// 5. 更新节点最终位置
this.updateNodePositions()
// 5. 布局后处理(避免重叠等)
// 6. 布局后处理(避免重叠等)
this.postLayoutAdjustment()
// 非第一次布局后重置标志
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力导向布局需要的数据结构
@ -188,7 +409,9 @@ class ForceDirected extends Base {
// 是否固定位置
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
}
// 将节点添加到数组和映射
this.d3Nodes.push(d3Node)
this.nodeIdMap.set(node.id, d3Node)
// 为非根节点创建与父节点的连接
if (!node.isRoot) {
@ -219,6 +444,33 @@ class ForceDirected extends Base {
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 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 sizeFactor = Math.sqrt(
(node.width * node.height + node.parent.width * node.parent.height) / 2
) / 100
const nodeSize = Math.sqrt(node.width * node.height)
const parentSize = Math.sqrt(node.parent.width * node.parent.height)
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() {
// 使用自定义的树形初始布局可以加速力导向算法收敛
// 使用树形初始布局可以加速力导向算法收敛
// 按层级组织节点
const nodesByLevel = {}
@ -277,8 +542,7 @@ class ForceDirected extends Base {
// 从根节点开始
const rootNode = this.d3Nodes.find(n => n.node.isRoot)
const rootX = rootNode.x
const rootY = rootNode.y
if (!rootNode) return
// 对每个层级进行处理
Object.keys(nodesByLevel).forEach(level => {
@ -288,7 +552,7 @@ class ForceDirected extends Base {
// 在每个层级中,按照父节点分组
const nodesByParent = {}
nodesAtLevel.forEach(node => {
const parentId = this.d3Links.find(link => link.target === node.id)?.source
const parentId = this.findParentId(node.id)
if (parentId !== undefined) {
if (!nodesByParent[parentId]) {
nodesByParent[parentId] = []
@ -299,33 +563,41 @@ class ForceDirected extends Base {
// 对于每组有共同父节点的节点,按径向布局排列
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
// 如果此父节点只有单个子节点,以特定偏移放置
if (count === 1) {
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
// 如果节点未固定位置
if (!child.fixed) {
child.x = parent.x + Math.cos(angle) * radius
child.y = parent.y + Math.sin(angle) * radius
}
// 应用位置
child.x = parent.x + Math.cos(angle) * radius
child.y = parent.y + Math.sin(angle) * radius
} else {
// 多个子节点,在父节点周围环形布局
const angleStep = this.forceConfig.initialExpandAngle / count
// 根据节点重要度排序
childNodes.sort((a, b) => b.importance - a.importance)
// 分配位置
childNodes.forEach((child, i) => {
if (child.fixed) return // 跳过固定位置的节点
if (child.fixed) return
// 计算径向位置
const angle = angleStep * i
// 半径随层级和节点数增加
// 半径根据层级、节点重要度和大小调整
const radius = this.forceConfig.layerDistance *
(child.depth + child.textFactor * 0.1) *
this.forceConfig.initialRadius
child.depth *
this.forceConfig.initialRadius *
(0.8 + 0.4 * child.importance / 10)
// 应用位置
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力导向模拟
*/
runForceSimulation() {
runForceSimulation(iterations) {
// 创建力导向模拟器
this.simulation = forceSimulation(this.d3Nodes)
// 设置衰减参数
@ -360,74 +660,107 @@ class ForceDirected extends Base {
// 节点间斥力
.force('charge', forceManyBody()
.strength(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
})
.strength(d => this.calculateRepulsionStrength(d))
.distanceMax(1000)) // 限制斥力的最大作用距离
// 碰撞力 - 避免节点重叠
.force('collide', forceCollide()
.radius(d => {
// 碰撞半径 = 节点大小 + 边距
const nodeSize = Math.max(d.node.width, d.node.height) / 2
return nodeSize * this.forceConfig.collideRadius
})
.strength(0.8)
.radius(d => this.calculateCollideRadius(d))
.strength(0.7 + 0.3 * this.forceConfig.stabilizationFactor)
.iterations(2))
// 沿X轴的力 - 使子节点倾向于在父节点两侧分布
.force('x', forceX().strength(d => {
if (d.node.isRoot || !d.node.parent) return 0
// 获取父节点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
}))
// 沿X轴的力 - 使子节点倾向于分散
.force('x', forceX().strength(d => this.calculateXForceStrength(d))
.x(d => this.calculateXTarget(d)))
// 沿Y轴的力 - 使子节点倾向于在父节点下方
.force('y', forceY().strength(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
}))
// 沿Y轴的力 - 使子节点倾向于有层次感
.force('y', forceY().strength(d => this.calculateYForceStrength(d))
.y(d => this.calculateYTarget(d)))
// 一次性运行所有迭代
for (let i = 0; i < this.forceConfig.iterations; i++) {
for (let i = 0; i < iterations; i++) {
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() {
// 获取所有非根节点
const nodes = this.d3Nodes.filter(d => !d.node.isRoot && !d.fixed)
.map(d => d.node)
const nodes = this.d3Nodes
.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++) {
let hasAdjusted = false
@ -519,6 +853,8 @@ class ForceDirected extends Base {
* 分离重叠的节点
*/
separateNodes(nodeA, nodeB) {
if (nodeA.hasCustomPosition() || nodeB.hasCustomPosition()) return
// 计算两个节点中心点
const centerAx = nodeA.left + nodeA.width / 2
const centerAy = nodeA.top + nodeA.height / 2
@ -528,7 +864,7 @@ class ForceDirected extends Base {
// 计算两节点之间的向量
const dx = centerBx - centerAx
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) {
// 如果节点中心重合,则添加微小的随机偏移
@ -563,6 +899,7 @@ class ForceDirected extends Base {
const moveY = dy * factor / 2
// 移动节点(考虑移动距离的权重分配)
// 有更多子节点的节点移动较少
const weightA = nodeA.children ? nodeA.children.length : 0
const weightB = nodeB.children ? nodeB.children.length : 0
const totalWeight = weightA + weightB + 2
@ -581,7 +918,7 @@ class ForceDirected extends Base {
* 绘制连接线 - 从父节点到子节点
*/
renderLine(node, lines, style) {
// 根据配置选择连线风格
// 根据线风格选择相应的渲染方法
if (this.forceConfig.linkStyle === 'straight') {
this.renderLineStraight(node, lines, style)
} else {
@ -631,10 +968,8 @@ class ForceDirected extends Base {
const distance = Math.sqrt(dx * dx + dy * dy)
// 计算控制点
// 控制点根据连线方向和曲率系数计算
const controlDistance = distance * curvature
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 cy1 = y1 + Math.sin(angle) * distance * 0.3
@ -648,83 +983,65 @@ class ForceDirected extends Base {
}
/**
* 渲染展开/收起按钮
* 渲染展开收起按钮
*/
renderExpandBtn(node, btn) {
const { width, height, expandBtnSize } = node
let { translateX, translateY } = btn.transform()
// 在力导向布局中统一将展开按钮放在节点右侧
// 计算按钮位置 - 在力导向布局中统一放在右侧
const _x = width
const _y = height / 2
// 位置未改变无需更新
// 位置没有变化则返回
if (_x === translateX && _y === translateY) {
return
}
// 计算偏移量并应用
const x = _x - translateX
const y = _y - translateY
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) {
// 在力导向布局中,展开按钮统一在节点右侧
// 在力导向布局中,统一将展开按钮放在节点右侧
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