fix: 实体关系图优化:优化拓展临时节点时,动画展示方式(由起点向终点沿直线进行动画展示);

This commit is contained in:
hanyuxia
2024-07-12 16:31:38 +08:00
parent 3fa14fb69f
commit 41a76ca91f

View File

@@ -74,6 +74,8 @@ export default {
defaultMargin: 2, // 图像与箭头的距离 defaultMargin: 2, // 图像与箭头的距离
rootNode: null, rootNode: null,
isClicking: false, isClicking: false,
isDraggingEntityNode: false,
defaultArrowSize: 3, // 箭头大小
/* 自己实现stack操作 */ /* 自己实现stack操作 */
stackData: { stackData: {
undo: [], // 后退 undo: [], // 后退
@@ -96,7 +98,37 @@ export default {
this.rightBox.loading = false this.rightBox.loading = false
} }
}, },
initForceGraph(initialData) { getCoordinate (startNode, endNode) {
const startX = startNode.x
const startY = startNode.y
const endX = endNode.drawEndX
const endY = endNode.drawEndY
if (endX && startX) {
const diffVal = Math.round(Math.sqrt(Math.pow((endX - startX), 2) + Math.pow((endY - startY), 2)))
const step = 3
const i = endNode.nextI ? endNode.nextI : 0
if (i < diffVal / step) {
endNode.nextI = i + 1
const dx = endX - startX
const dy = endY - startY
const angle = Math.atan2(dy, dx) // 计算与x轴的角度弧度
const cx = startX + step * Math.cos(angle) * i
const cy = this.linearInterpolate([startX, startY], [endX, endY]).getY(cx)
return {
x: cx,
y: cy
}
} else {
return null
}
} else {
return null
}
},
getShortenedLength (end) {
return end.type === nodeType.rootNode ? 23 : 20 // link 末端缩短长度,root节点特殊处理
},
initForceGraph (initialData) {
let hoverNode = null let hoverNode = null
const canvasHeight = document.body.clientHeight - 100 const canvasHeight = document.body.clientHeight - 100
this.graph = ForceGraph()(document.getElementById('entityGraph')) this.graph = ForceGraph()(document.getElementById('entityGraph'))
@@ -224,6 +256,26 @@ export default {
case nodeType.tempNode: { case nodeType.tempNode: {
// 先画个白底圆环,避免 link 穿过不好看 // 先画个白底圆环,避免 link 穿过不好看
ctx.beginPath() ctx.beginPath()
const tempNodeDistance = this.getShortenedLength(node)// 临时节点展示动画时临时节点的初始位置距离entity节点的距离
// 计算箭头角度
const dx = node.x - node.sourceNode.x
const dy = node.y - node.sourceNode.y
const angle = Math.atan2(dy, dx) // 计算与x轴的角度弧度
const startNodeX = node.sourceNode.x + tempNodeDistance * Math.cos(angle)
const startNodeY = node.sourceNode.y + tempNodeDistance * Math.sin(angle)
node.drawEndX = node.realEndX
node.drawEndY = node.realEndY
const nodeCoordinate = this.getCoordinate({ x: startNodeX, y: startNodeY }, node)
if (nodeCoordinate && !this.isDraggingEntityNode) {
ctx.arc(nodeCoordinate.x, nodeCoordinate.y, nodeStyle.innerR, 0, 2 * Math.PI, false)
ctx.closePath()
ctx.fillStyle = nodeStyle.fillStyle
ctx.fill()
// 画透明度0.7的图标
ctx.globalAlpha = 0.7
ctx.drawImage(node.img, nodeCoordinate.x - nodeStyle.iconWidth / 2, nodeCoordinate.y - nodeStyle.iconHeight / 2, nodeStyle.iconWidth, nodeStyle.iconHeight)
ctx.globalAlpha = 1
} else {
ctx.arc(node.x, node.y, nodeStyle.innerR, 0, 2 * Math.PI, false) ctx.arc(node.x, node.y, nodeStyle.innerR, 0, 2 * Math.PI, false)
ctx.closePath() ctx.closePath()
ctx.fillStyle = nodeStyle.fillStyle ctx.fillStyle = nodeStyle.fillStyle
@@ -232,6 +284,7 @@ export default {
ctx.globalAlpha = 0.7 ctx.globalAlpha = 0.7
ctx.drawImage(node.img, node.x - nodeStyle.iconWidth / 2, node.y - nodeStyle.iconHeight / 2, nodeStyle.iconWidth, nodeStyle.iconHeight) ctx.drawImage(node.img, node.x - nodeStyle.iconWidth / 2, node.y - nodeStyle.iconHeight / 2, nodeStyle.iconWidth, nodeStyle.iconHeight)
ctx.globalAlpha = 1 ctx.globalAlpha = 1
}
break break
} }
} }
@@ -240,8 +293,8 @@ export default {
const start = link.source const start = link.source
const end = link.target const end = link.target
let width = 1 // 线宽 let width = 1 // 线宽
const arrowSize = 3 // 箭头大小 const arrowSize = this.defaultArrowSize // 箭头大小
const shortenedLength = end.type === nodeType.rootNode ? 23 : 20 // link 末端缩短长度,root节点特殊处理 const shortenedLength = this.getShortenedLength(end) // link 末端缩短长度,root节点特殊处理
// 计算箭头角度 // 计算箭头角度
const dx = end.x - start.x const dx = end.x - start.x
@@ -254,26 +307,41 @@ export default {
// 绘制线 // 绘制线
let color let color
ctx.beginPath()
if (link.type === linkType.temp) { if (link.type === linkType.temp) {
ctx.setLineDash([2, 2]) ctx.setLineDash([2, 2])
color = 'rgba(119,131,145,0.2)' // 虚线颜色 color = 'rgba(119,131,145,0.2)' // 虚线颜色
} else { const startX = start.x
ctx.setLineDash([]) const startY = start.y
if (this.clickNode.id === link.source.id || this.clickNode.id === link.target.id) { ctx.beginPath()
color = 'rgba(119,131,145,0.9)' // 高亮线颜色 ctx.moveTo(startX, startY)
width = 1.2 if (end.realEndX) {
} else { end.drawEndX = lineEndX
color = 'rgba(119,131,145,0.3)' // 普通线颜色 end.drawEndY = lineEndY
} }
} const nodeCoordinate = this.getCoordinate(start, end)
ctx.moveTo(start.x, start.y) if (nodeCoordinate && !this.isDraggingEntityNode) {
ctx.lineTo(lineEndX, lineEndY) ctx.lineTo(nodeCoordinate.x, nodeCoordinate.y)// 避免出现线和箭头之间出现空白部分
ctx.strokeStyle = color
ctx.lineWidth = width
ctx.stroke()
ctx.save() // 保存当前状态以便之后恢复
ctx.translate(nodeCoordinate.x + arrowSize * Math.cos(angle), nodeCoordinate.y + arrowSize * Math.sin(angle)) // 将坐标原点移动到箭头末端
ctx.rotate(angle) // 根据链接方向旋转坐标系
ctx.beginPath()
ctx.moveTo(0, 0)
ctx.lineTo(-arrowSize, arrowSize) // 绘制箭头的一边
ctx.lineTo(-arrowSize, -arrowSize) // 绘制箭头的另一边
ctx.closePath()
ctx.fillStyle = color
ctx.fill()
ctx.restore() // 恢复之前保存的状态
} else {
ctx.lineTo(lineEndX, lineEndY)// 避免出现线和箭头之间出现空白部分
ctx.strokeStyle = color ctx.strokeStyle = color
ctx.lineWidth = width ctx.lineWidth = width
ctx.stroke() ctx.stroke()
// 绘制箭头
ctx.save() // 保存当前状态以便之后恢复 ctx.save() // 保存当前状态以便之后恢复
ctx.translate(arrowEndX, arrowEndY) // 将坐标原点移动到箭头末端 ctx.translate(arrowEndX, arrowEndY) // 将坐标原点移动到箭头末端
ctx.rotate(angle) // 根据链接方向旋转坐标系 ctx.rotate(angle) // 根据链接方向旋转坐标系
@@ -285,35 +353,35 @@ export default {
ctx.fillStyle = color ctx.fillStyle = color
ctx.fill() ctx.fill()
ctx.restore() // 恢复之前保存的状态 ctx.restore() // 恢复之前保存的状态
/* if (link.source.x !== undefined && link.source.y !== undefined && link.target.x !== undefined && link.target.y !== undefined) { }
const nodeStyle = this.getNodeStyle(link.target.type, link.target.data.entityType) } else {
ctx.lineWidth = 0.5
ctx.beginPath() ctx.beginPath()
if (this.clickNode && (this.clickNode.id === link.source.id || this.clickNode.id === link.target.id)) { ctx.moveTo(start.x, start.y)
ctx.strokeStyle = link.clickColor ctx.setLineDash([])
if (this.clickNode.id === link.source.id || this.clickNode.id === link.target.id) {
color = 'rgba(119,131,145,0.9)' // 高亮线颜色
width = 1.2
} else { } else {
ctx.strokeStyle = link.color color = 'rgba(119,131,145,0.3)' // 普通线颜色
} }
const lineDash = link.target.type === nodeType.tempNode ? [2, 2] : [] ctx.lineTo(lineEndX, lineEndY)
ctx.setLineDash(lineDash) ctx.strokeStyle = color
ctx.moveTo(link.source.x, link.source.y) ctx.lineWidth = width
const xy = this.findCircleLineIntersection(link.target.x, link.target.y, nodeStyle.innerR / 2, link.source.x, link.source.y)
ctx.lineTo(xy[0].x, xy[0].y)
this.drawArrow(ctx, link.source.x, link.source.y, xy[0].x, xy[0].y, 40, 3)
// 圆角三角形
if (this.clickNode && (this.clickNode.id === link.source.id || this.clickNode.id === link.target.id)) {
ctx.fillStyle = link.clickColor
} else {
ctx.fillStyle = link.color
}
ctx.fill()
ctx.closePath()
ctx.stroke() ctx.stroke()
} */
ctx.save() // 保存当前状态以便之后恢复
ctx.translate(arrowEndX, arrowEndY) // 将坐标原点移动到箭头末端
ctx.rotate(angle) // 根据链接方向旋转坐标系
ctx.beginPath()
ctx.moveTo(0, 0)
ctx.lineTo(-arrowSize, arrowSize) // 绘制箭头的一边
ctx.lineTo(-arrowSize, -arrowSize) // 绘制箭头的另一边
ctx.closePath()
ctx.fillStyle = color
ctx.fill()
ctx.restore() // 恢复之前保存的状态
}
}) })
.autoPauseRedraw(false) // keep redrawing after engine has stopped如果您有依赖于画布的不断重绘的自定义动态对象建议关闭此选项。 .autoPauseRedraw(false) // keep redrawing after engine has stopped如果您有依赖于画布的不断重绘的自定义动态对象建议关闭此选项。
.onNodeHover(node => { .onNodeHover(node => {
@@ -342,8 +410,14 @@ export default {
} }
const toAddNodes = [] const toAddNodes = []
const toAddLinks = [] const toAddLinks = []
const keyCount = Object.keys(node.data.relatedEntities).length let keyCount = 0
Object.keys(node.data.relatedEntities).forEach((k, i) => { Object.keys(node.data.relatedEntities).forEach((k, i) => {
if (node.data.relatedEntities[k].total) {
keyCount++
}
})
let nodeIndex = 0// 临时节点需要total大于0的所以不可以用循环中的i角度计算index
Object.keys(node.data.relatedEntities).forEach((k) => {
if (node.data.relatedEntities[k].total) { if (node.data.relatedEntities[k].total) {
// 若已有同级同类型的listNode不生成此tempNode // 若已有同级同类型的listNode不生成此tempNode
const neighbors = node.getNeighbors(this.graph.graphData()) const neighbors = node.getNeighbors(this.graph.graphData())
@@ -360,15 +434,20 @@ export default {
this.getIconUrl(k, false, false) this.getIconUrl(k, false, false)
) )
// 临时节点的初始固定坐标为其对应的entity节点坐标展示直线动画 // 临时节点的初始固定坐标为其对应的entity节点坐标展示直线动画
// tempNode.fx = node.x const tempNodePoint = this.pointOfRotate({ x: node.sourceNode.x, y: node.sourceNode.y }, { x: node.x, y: node.y }, this.getTempNodeAngle(keyCount, nodeIndex++))// 临时节点固定角度为以entity节点为centerlist节点为from旋转到临时节点的角度
// tempNode.fy = node.y
const tempNodePoint = this.pointOfRotate({ x: node.sourceNode.x, y: node.sourceNode.y }, { x: node.x, y: node.y }, this.getTempNodeAngle(keyCount, i))// 临时节点固定角度为以entity节点为centerlist节点为from旋转到临时节点的角度
const finalTempNodePoint = this.pointOfLine({ x: node.x, y: node.y }, tempNodePoint, this.defaultLinkDistance)// start,end,lineLength const finalTempNodePoint = this.pointOfLine({ x: node.x, y: node.y }, tempNodePoint, this.defaultLinkDistance)// start,end,lineLength
if (tempNodePoint.x && tempNodePoint.y) {
tempNode.fx = (node.x + finalTempNodePoint.x) / 2
tempNode.fy = (node.y + finalTempNodePoint.y) / 2
}
if (tempNodePoint.x && tempNodePoint.y) {
const tempNodeDistance = this.getShortenedLength(tempNode) + this.defaultArrowSize// 临时节点展示动画时临时节点的初始位置距离entity节点的距离
// 计算箭头角度
const dx = finalTempNodePoint.x - node.x
const dy = finalTempNodePoint.y - node.y
const angle = Math.atan2(dy, dx) // 计算与x轴的角度弧度
tempNode.fx = node.x + tempNodeDistance * Math.cos(angle)
tempNode.fy = node.y + tempNodeDistance * Math.sin(angle)
tempNode.realEndX = finalTempNodePoint.x
tempNode.realEndY = finalTempNodePoint.y
}
const tempLink = new Link(node, tempNode, linkType.temp) const tempLink = new Link(node, tempNode, linkType.temp)
toAddNodes.push(tempNode) toAddNodes.push(tempNode)
toAddLinks.push(tempLink) toAddLinks.push(tempLink)
@@ -478,7 +557,6 @@ export default {
}, 200) }, 200)
} }
}) })
.d3Force('link', d3.forceLink().id(link => link.id) .d3Force('link', d3.forceLink().id(link => link.id)
.distance(link => { .distance(link => {
if (link.source.type === nodeType.rootNode) { if (link.source.type === nodeType.rootNode) {
@@ -497,6 +575,7 @@ export default {
const { nodes, links } = this.graph.graphData() const { nodes, links } = this.graph.graphData()
// 拖动entity节点时如果此entity节点有临时节点则同时移动临时节点 // 拖动entity节点时如果此entity节点有临时节点则同时移动临时节点
if (node.type === nodeType.entityNode) { if (node.type === nodeType.entityNode) {
this.isDraggingEntityNode = true
// 查询点击entity节点对应的list节点 // 查询点击entity节点对应的list节点
const fromX = node.sourceNode.preDragX ? node.sourceNode.preDragX : node.sourceNode.x const fromX = node.sourceNode.preDragX ? node.sourceNode.preDragX : node.sourceNode.x
const fromY = node.sourceNode.preDragY ? node.sourceNode.preDragY : node.sourceNode.y const fromY = node.sourceNode.preDragY ? node.sourceNode.preDragY : node.sourceNode.y
@@ -568,6 +647,7 @@ export default {
}) })
.cooldownTime(2000)// 到时间后才执行onEngineStop .cooldownTime(2000)// 到时间后才执行onEngineStop
.onNodeDragEnd((node, translate) => { // 修复拖动节点 .onNodeDragEnd((node, translate) => { // 修复拖动节点
this.isDraggingEntityNode = false
node.fx = node.x node.fx = node.x
node.fy = node.y node.fy = node.y
// 拖拽结束时把所有临时节点的的angle置为null,便于拖动其它节点时的计算 // 拖拽结束时把所有临时节点的的angle置为null,便于拖动其它节点时的计算
@@ -583,14 +663,17 @@ export default {
const tempNodeGroup = this.graph.graphData().nodes.filter(node => node.type === nodeType.tempNode) const tempNodeGroup = this.graph.graphData().nodes.filter(node => node.type === nodeType.tempNode)
if (tempNodeGroup.length > 0) { if (tempNodeGroup.length > 0) {
const keyCount = tempNodeGroup.length const keyCount = tempNodeGroup.length
tempNodeGroup.forEach((tempNode, i) => { let nodeIndex = 0// 临时节点需要total大于0的所以不可以用循环中的i角度计算index
tempNodeGroup.forEach((tempNode) => {
const tempNodeSource = tempNode.sourceNode const tempNodeSource = tempNode.sourceNode
if (tempNodeSource) { if (tempNodeSource) {
const tempNodePoint = this.pointOfRotate({ x: tempNodeSource.sourceNode.x, y: tempNodeSource.sourceNode.y }, { x: tempNodeSource.x, y: tempNodeSource.y }, this.getTempNodeAngle(keyCount, i))// 临时节点固定角度为以entity节点为centerlist节点为from旋转到临时节点的角度 const tempNodePoint = this.pointOfRotate({ x: tempNodeSource.sourceNode.x, y: tempNodeSource.sourceNode.y }, { x: tempNodeSource.x, y: tempNodeSource.y }, this.getTempNodeAngle(keyCount, nodeIndex++))// 临时节点固定角度为以entity节点为centerlist节点为from旋转到临时节点的角度
const finalTempNodePoint = this.pointOfLine({ x: tempNodeSource.x, y: tempNodeSource.y }, tempNodePoint, this.defaultLinkDistance)// start,end,lineLength // const finalTempNodePoint = this.pointOfLine({ x: tempNodeSource.x, y: tempNodeSource.y }, tempNodePoint, this.defaultLinkDistance)// start,end,lineLength
if (tempNodePoint.x && tempNodePoint.y) { if (tempNodePoint.x && tempNodePoint.y) {
tempNode.fx = finalTempNodePoint.x // tempNode.fx = finalTempNodePoint.x
tempNode.fy = finalTempNodePoint.y // tempNode.fy = finalTempNodePoint.y
tempNode.fx = tempNode.realEndX
tempNode.fy = tempNode.realEndY
} }
} }
}) })
@@ -599,6 +682,17 @@ export default {
} }
}) })
}, },
/*
* 线性插值: 已知两点坐标计算已知x或y的C点坐标且C点在AB连线上
*
* */
linearInterpolate (coord1, coord2) {
const scale = (coord1[0] - coord2[0]) / (coord1[1] - coord2[1])
return {
getY: (x) => (x - coord1[0]) / scale + coord1[1],
getX: (y) => (y - coord1[1]) / scale + coord1[0]
}
},
getTempNodeAngle (nodeCount, i) { getTempNodeAngle (nodeCount, i) {
switch (nodeCount) { switch (nodeCount) {
case 1: case 1: