fix: 实体关系图优化:优化拓展临时节点时,动画展示方式(由起点向终点沿直线进行动画展示);
This commit is contained in:
@@ -74,6 +74,8 @@ export default {
|
||||
defaultMargin: 2, // 图像与箭头的距离
|
||||
rootNode: null,
|
||||
isClicking: false,
|
||||
isDraggingEntityNode: false,
|
||||
defaultArrowSize: 3, // 箭头大小
|
||||
/* 自己实现stack操作 */
|
||||
stackData: {
|
||||
undo: [], // 后退
|
||||
@@ -96,6 +98,36 @@ export default {
|
||||
this.rightBox.loading = false
|
||||
}
|
||||
},
|
||||
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
|
||||
const canvasHeight = document.body.clientHeight - 100
|
||||
@@ -224,6 +256,26 @@ export default {
|
||||
case nodeType.tempNode: {
|
||||
// 先画个白底圆环,避免 link 穿过不好看
|
||||
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.closePath()
|
||||
ctx.fillStyle = nodeStyle.fillStyle
|
||||
@@ -232,6 +284,7 @@ export default {
|
||||
ctx.globalAlpha = 0.7
|
||||
ctx.drawImage(node.img, node.x - nodeStyle.iconWidth / 2, node.y - nodeStyle.iconHeight / 2, nodeStyle.iconWidth, nodeStyle.iconHeight)
|
||||
ctx.globalAlpha = 1
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -240,8 +293,8 @@ export default {
|
||||
const start = link.source
|
||||
const end = link.target
|
||||
let width = 1 // 线宽
|
||||
const arrowSize = 3 // 箭头大小
|
||||
const shortenedLength = end.type === nodeType.rootNode ? 23 : 20 // link 末端缩短长度,root节点特殊处理
|
||||
const arrowSize = this.defaultArrowSize // 箭头大小
|
||||
const shortenedLength = this.getShortenedLength(end) // link 末端缩短长度,root节点特殊处理
|
||||
|
||||
// 计算箭头角度
|
||||
const dx = end.x - start.x
|
||||
@@ -254,26 +307,41 @@ export default {
|
||||
|
||||
// 绘制线
|
||||
let color
|
||||
ctx.beginPath()
|
||||
if (link.type === linkType.temp) {
|
||||
ctx.setLineDash([2, 2])
|
||||
color = 'rgba(119,131,145,0.2)' // 虚线颜色
|
||||
} else {
|
||||
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 {
|
||||
color = 'rgba(119,131,145,0.3)' // 普通线颜色
|
||||
const startX = start.x
|
||||
const startY = start.y
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(startX, startY)
|
||||
if (end.realEndX) {
|
||||
end.drawEndX = lineEndX
|
||||
end.drawEndY = lineEndY
|
||||
}
|
||||
}
|
||||
ctx.moveTo(start.x, start.y)
|
||||
ctx.lineTo(lineEndX, lineEndY)
|
||||
const nodeCoordinate = this.getCoordinate(start, end)
|
||||
if (nodeCoordinate && !this.isDraggingEntityNode) {
|
||||
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.lineWidth = width
|
||||
ctx.stroke()
|
||||
|
||||
// 绘制箭头
|
||||
ctx.save() // 保存当前状态以便之后恢复
|
||||
ctx.translate(arrowEndX, arrowEndY) // 将坐标原点移动到箭头末端
|
||||
ctx.rotate(angle) // 根据链接方向旋转坐标系
|
||||
@@ -285,35 +353,35 @@ export default {
|
||||
ctx.fillStyle = color
|
||||
ctx.fill()
|
||||
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)
|
||||
|
||||
ctx.lineWidth = 0.5
|
||||
}
|
||||
} else {
|
||||
ctx.beginPath()
|
||||
if (this.clickNode && (this.clickNode.id === link.source.id || this.clickNode.id === link.target.id)) {
|
||||
ctx.strokeStyle = link.clickColor
|
||||
ctx.moveTo(start.x, start.y)
|
||||
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 {
|
||||
ctx.strokeStyle = link.color
|
||||
color = 'rgba(119,131,145,0.3)' // 普通线颜色
|
||||
}
|
||||
|
||||
const lineDash = link.target.type === nodeType.tempNode ? [2, 2] : []
|
||||
ctx.setLineDash(lineDash)
|
||||
ctx.moveTo(link.source.x, link.source.y)
|
||||
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.lineTo(lineEndX, lineEndY)
|
||||
ctx.strokeStyle = color
|
||||
ctx.lineWidth = width
|
||||
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如果您有依赖于画布的不断重绘的自定义动态对象,建议关闭此选项。
|
||||
.onNodeHover(node => {
|
||||
@@ -342,8 +410,14 @@ export default {
|
||||
}
|
||||
const toAddNodes = []
|
||||
const toAddLinks = []
|
||||
const keyCount = Object.keys(node.data.relatedEntities).length
|
||||
let keyCount = 0
|
||||
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) {
|
||||
// 若已有同级同类型的listNode,不生成此tempNode
|
||||
const neighbors = node.getNeighbors(this.graph.graphData())
|
||||
@@ -360,15 +434,20 @@ export default {
|
||||
this.getIconUrl(k, false, false)
|
||||
)
|
||||
// 临时节点的初始固定坐标为其对应的entity节点坐标,展示直线动画
|
||||
// tempNode.fx = node.x
|
||||
// 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 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,旋转到临时节点的角度
|
||||
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)
|
||||
toAddNodes.push(tempNode)
|
||||
toAddLinks.push(tempLink)
|
||||
@@ -478,7 +557,6 @@ export default {
|
||||
}, 200)
|
||||
}
|
||||
})
|
||||
|
||||
.d3Force('link', d3.forceLink().id(link => link.id)
|
||||
.distance(link => {
|
||||
if (link.source.type === nodeType.rootNode) {
|
||||
@@ -497,6 +575,7 @@ export default {
|
||||
const { nodes, links } = this.graph.graphData()
|
||||
// 拖动entity节点时,如果此entity节点有临时节点,则同时移动临时节点
|
||||
if (node.type === nodeType.entityNode) {
|
||||
this.isDraggingEntityNode = true
|
||||
// 查询点击entity节点对应的list节点
|
||||
const fromX = node.sourceNode.preDragX ? node.sourceNode.preDragX : node.sourceNode.x
|
||||
const fromY = node.sourceNode.preDragY ? node.sourceNode.preDragY : node.sourceNode.y
|
||||
@@ -568,6 +647,7 @@ export default {
|
||||
})
|
||||
.cooldownTime(2000)// 到时间后,才执行onEngineStop
|
||||
.onNodeDragEnd((node, translate) => { // 修复拖动节点
|
||||
this.isDraggingEntityNode = false
|
||||
node.fx = node.x
|
||||
node.fy = node.y
|
||||
// 拖拽结束时,把所有临时节点的的angle置为null,便于拖动其它节点时的计算
|
||||
@@ -583,14 +663,17 @@ export default {
|
||||
const tempNodeGroup = this.graph.graphData().nodes.filter(node => node.type === nodeType.tempNode)
|
||||
if (tempNodeGroup.length > 0) {
|
||||
const keyCount = tempNodeGroup.length
|
||||
tempNodeGroup.forEach((tempNode, i) => {
|
||||
let nodeIndex = 0// 临时节点(需要total大于0的,所以不可以用循环中的i)角度计算index
|
||||
tempNodeGroup.forEach((tempNode) => {
|
||||
const tempNodeSource = tempNode.sourceNode
|
||||
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 finalTempNodePoint = this.pointOfLine({ x: tempNodeSource.x, y: tempNodeSource.y }, tempNodePoint, this.defaultLinkDistance)// start,end,lineLength
|
||||
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
|
||||
if (tempNodePoint.x && tempNodePoint.y) {
|
||||
tempNode.fx = finalTempNodePoint.x
|
||||
tempNode.fy = finalTempNodePoint.y
|
||||
// tempNode.fx = finalTempNodePoint.x
|
||||
// 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) {
|
||||
switch (nodeCount) {
|
||||
case 1:
|
||||
|
||||
Reference in New Issue
Block a user