fix: 实体关系图优化:优化拓展临时节点时,动画展示方式(由起点向终点沿直线进行动画展示);
This commit is contained in:
@@ -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节点为center,list节点为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节点为center,list节点为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节点为center,list节点为from,旋转到临时节点的角度
|
const tempNodePoint = this.pointOfRotate({ x: tempNodeSource.sourceNode.x, y: tempNodeSource.sourceNode.y }, { x: tempNodeSource.x, y: tempNodeSource.y }, this.getTempNodeAngle(keyCount, nodeIndex++))// 临时节点固定角度,为以entity节点为center,list节点为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:
|
||||||
|
|||||||
Reference in New Issue
Block a user