antvis / antvis/G6

使用 graph.removeNodeData([node.id]) 删除节点报错

Open
#7,466 1 comment 0 reactions 0 assignees View on GitHub
feature 💡 question 💬
Dominant language
TypeScript
Stars
12.3k
Forks
1.6k
PR merge metrics
No merged PRs in 30d

Description

> 请提供最小复现代码块❤️

_Originally posted by @yvonneyx in [#7457](https://github.com/antvis/G6/issues/7457#issuecomment-3454263802)_

```

import { ref, onMounted, onUnmounted } from 'vue'
import { Rect, Text } from '@antv/g'
import {
Badge,
BaseBehavior,
BaseNode,
BaseTransform,
CommonEvent,
CubicHorizontal,
ExtensionCategory,
Graph,
idOf,
NodeEvent,
register,
treeToGraphData,
} from '@antv/g6'

// 图表容器引用
const graphContainer = ref<HTMLDivElement>(null)
// G6 实例
let graph: any = null

const RootNodeStyle = {
labelFontSize: 24,
labelFontWeight: 600,
labelOffsetY: 10,
labelPlacement: 'center',
ports: [{ placement: 'right' }, { placement: 'left' }],
radius: 8,
labelWordWrap: true,
labelMaxWidth: 400,
labelMaxLines: 10,
labelLineHeight: 26,
}

const NodeStyle = {
labelFontSize: 16,
labelOffsetY: 2,
labelCursor: 'pointer',
labelBackgroundCursor: 'pointer',
labelPlacement: 'center',
ports: [{ placement: 'left' }, { placement: 'right' }],
radius: 8,
labelWordWrap: true,
labelMaxWidth: 400,
labelMaxLines: 10,
labelLineHeight: 18,
}

const TreeEvent = {
DELETE_NODE: 'delete-node',
}

let textShape
const measureText = (text) => {
if (!textShape) textShape = new Text({ style: text })
textShape.attr(text)

return textShape.getBBox().width
}

const getNodeWidth = (nodeData, isRoot) => {
const padding = 50
const nodeStyle = isRoot ? RootNodeStyle : NodeStyle
return (
measureText({
text: nodeData?.data?.text || '',
fontSize: nodeStyle.labelFontSize,
fontFamily: 'Gill Sans',
}) + padding
)
}

const getNodeSize = (nodeData, isRoot) => {
let width = getNodeWidth(nodeData, isRoot)

const baseWidth = 400

const num = Math.ceil(width / baseWidth)
let height = isRoot ? 48 : 32
if (num > 1) {
height = height * num
}
width = width < 350 ? width : 430

return [width, height]
}

class MindmapNode extends BaseNode {
static defaultStyleProps = {
showIcon: false,
}
constructor(options) {
Object.assign(options.style, MindmapNode.defaultStyleProps)
super(options)
}
getDeleteStyle(attributes) {
const { showIcon, color = '#000000' } = attributes
if (!showIcon) return false
const [width, height] = this.getSize(attributes)
const offsetX = 14
return {
backgroundFill: '#fff',
backgroundHeight: 22,
backgroundLineWidth: 2,
backgroundStroke: color,
backgroundWidth: 22,
cursor: 'pointer',
fill: '#333333',
fontFamily: 'iconfont',
fontSize: 12,
text: '\ue6cc',
textAlign: 'center',
x: width / 2,
y: height + offsetX,
}
}
drawDeleteShape(attributes, container) {
const addStyle: any = this.getDeleteStyle(attributes)
const btn = this.upsert('delete', Badge, addStyle, container)
this.forwardEvent(btn, CommonEvent.CLICK, (event) => {
event.stopPropagation()
this.context.graph.emit(TreeEvent.DELETE_NODE, {
id: this.id,
direction: attributes.direction,
})
})
}

forwardEvent(target, type, listener) {
if (target && !Reflect.has(target, '__bind__')) {
Reflect.set(target, '__bind__', true)
target.addEventListener(type, listener)
}
}

getKeyStyle(attributes) {
const [width, height] = this.getSize(attributes)
const keyShape = super.getKeyStyle(attributes)
return { width, height, ...keyShape }
}

drawKeyShape(attributes, container) {
const keyStyle = this.getKeyStyle(attributes)
return this.upsert('key', Rect, keyStyle, container)
}

render(attributes = this.parsedAttributes, container = this) {
super.render(attributes, container)
this.drawDeleteShape(attributes, container)
}
}

class MindmapEdge extends CubicHorizontal {
get rootId() {
return idOf(this.context.model.getRootsData()[0])
}
getKeyPath(attributes) {
const path = super.getKeyPath(attributes)
const isRoot = this.targetNode.id === this.rootId
const labelWidth = getNodeWidth(this.targetNode, isRoot)

const [, tp] = this.getEndpoints(attributes)
const sign =
this.sourceNode.getCenter()[0] < this.targetNode.getCenter()[0] ? 1 : -1

return [...path, ['L', tp[0] + labelWidth * sign, tp[1]]]
}
}

class CollapseExpandTree extends BaseBehavior {
constructor(context, options) {
super(context, options)
this.bindEvents()
}
update(options) {
this.unbindEvents()
super.update(options)
this.bindEvents()
}

bindEvents() {
const { graph } = this.context
graph.on(NodeEvent.POINTER_ENTER, this.showIcon)
graph.on(NodeEvent.POINTER_LEAVE, this.hideIcon)
graph.on(TreeEvent.DELETE_NODE, this.deleteNode)
}

unbindEvents() {
const { graph } = this.context
graph.off(NodeEvent.POINTER_ENTER, this.showIcon)
graph.off(NodeEvent.POINTER_LEAVE, this.hideIcon)
graph.off(TreeEvent.DELETE_NODE, this.deleteNode)
}

status = 'idle'

showIcon = (event) => {
this.setIcon(event, true)
}

hideIcon = (event) => {
this.setIcon(event, false)
}

setIcon = (event, show) => {
if (this.status !== 'idle') return
const { target } = event
const id = target.id
const { graph, element } = this.context
graph.updateNodeData([{ id, style: { showIcon: show } }])
element.draw({ animation: false, silence: true })
}

// 删除节点
deleteNode = async (event) => {
const { graph } = this.context
// 1. 判断节点是否存在
const node = graph.getNodeData(event.id)
console.log(node, 'node---')
if (!node) {
console.warn(`节点不存在: ${event.id}`)
return
}
// 2. 直接用 event.id 删除节点
await graph.removeNodeData([event.id])
// 3. 刷新视图
await graph.render()
}
}

class AssignColorByBranch extends BaseTransform {
static themeColor = '#8258F8'
static defaultOptions = {
colors: [
'#1783FF',
'#F08F56',
'#D580FF',
'#00C9C9',
'#7863FF',
'#DB9D0D',
'#60C42D',
'#FF80CA',
'#2491B3',
'#17C76F',
// ...Object.values(AssignColorByBranch.themeColor),
],
}

constructor(context, options) {
super(
context,
Object.assign({}, AssignColorByBranch.defaultOptions, options)
)
}
beforeDraw(input) {
const nodes = this.context.model.getNodeData()

if (nodes.length === 0) return input
let colorIndex = 0
const dfs = (nodeId, color) => {
const node = nodes.find((datum) => datum.id == nodeId)
if (!node) return

node.style ||= {}
node.style.color =
color || this.options.colors[colorIndex++ % this.options.colors.length]
node.children?.forEach((childId) => dfs(childId, node.style?.color))
}

nodes
.filter((node) => node.depth === 1)
.forEach((rootNode) => dfs(rootNode.id))

return input
}
}

register(ExtensionCategory.NODE, 'mindmap', MindmapNode)
register(ExtensionCategory.EDGE, 'mindmap', MindmapEdge)
register(ExtensionCategory.BEHAVIOR, 'collapse-expand-tree', CollapseExpandTree)
register(
ExtensionCategory.TRANSFORM,
'assign-color-by-branch',
AssignColorByBranch
)

// 初始化图表
const initGraph = async () => {
if (!graphContainer.value) return
console.log(treeListData.value, 'treeListData.value')
const rootId = treeListData.value.id
graph = new Graph({
container: graphContainer.value,
autoResize: true,
padding: 30,
autoFit: 'view',
zoomRange: [0.1, 10],
data: treeToGraphData(treeListData.value),
node: {
type: 'mindmap',
style: function (d) {
const isRoot = idOf(d) === rootId
const colors: any = d?.style?.color || '#000000'
return {
// direction,
labelText: d?.data?.text,
size: getNodeSize(d, isRoot),
labelFontFamily: 'Gill Sans',
// 通过设置节点标签背景来扩大交互区域 | Expand the interaction area by setting the node label background
labelBackground: true,
labelBackgroundFill: 'transparent',
labelPadding: [13, 60, 40, 20],
...(isRoot ? RootNodeStyle : NodeStyle),
fill: colors,
// fillOpacity: d?.depth > 1 ? 0.7 : 1,
labelFontWeight: d?.depth > 1 ? 400 : 600,
labelFill: d?.style?.color ? '#1a1a1a' : '#ffffff',
}
},

},
edge: {
type: 'mindmap',
style: {
lineWidth: 6,
stroke: function (data: any) {
return this.getNodeData(data.target).style.color || '#99ADD1'
},
curvePosition: 0,
curveOffset: 4,
},
},
layout: {
type: 'mindmap',
direction: 'LR',
getWidth: (node) => getNodeWidth(node, node.id === rootId),
getHGap: (node) => (node.collapsed ? 20 : 40),
getVGap: (node) => (node.collapsed ? 5 : 10),
getHeight: (node) => {
const baseH = getNodeSize(node, node.id === rootId)[1]
return baseH + 20
},
},
behaviors: ['drag-canvas', 'zoom-canvas', 'collapse-expand-tree'],
transforms: ['assign-color-by-branch'],
animation: false,
})

graph.render()
// 节点点击事件
graph.on('node:click', async (ev) => {
const { target } = ev
// console.log('点击边:', target, ev)
const nodeData = graph.getNodeData(target.id)
console.log('节点数据:', nodeData)
})
}

const treeListData: any = ref([])
onMounted(async () => {
treeListData.value = {
'id': 'KQ-lw8v6uj',
'data': {
'text': 'Web3重塑数字经济版图',
'level': 1,
'elementId': '60o3CG1t5A',
'slideId': 'KQ-lw8v6uj'
},
'children': [
{
'id': '54cE1x4Aqm',
'data': {
'level': 2,
'text': 'sss',
'elementId': '54cE1x4Aqm',
'isEmpty': false,
'slideId': 'HeL55xB96X',
'childrenSlideId': []
},
'index': 0,
'children': []
},
{
'id': '5CZf_miD6L',
'data': {
'level': 2,
'text': '添加章节标题',
'isEmpty': true,
'slideId': '-gVk5hF0fX',
'childrenSlideId': [
'-gVk5hF0fX'
]
},
'index': 2,
'children': []
},
]
}
await initGraph()
})

onUnmounted(() => {
// 销毁图表实例,释放资源
if (graph) {
graph.destroy()
graph = null
}
})

.graph-content {
padding: 30rem 23rem 70rem;
position: relative;
width: 100%;
height: 100%;
background: #fff;
}

```
我发现我删除节点的时候因为 这两个方法导致报错 Node not found for id
```
graph.on(NodeEvent.POINTER_ENTER, this.showIcon)
graph.on(NodeEvent.POINTER_LEAVE, this.hideIcon)
```

Contributor guide

No contributing guide indexed for this repository

Research direction

Start at CollapseExpandTree.bindEvents, the showIcon and hideIcon handlers, and deleteNode in the provided reproduction. Reproduce node removal while pointer-enter and pointer-leave listeners are registered, then trace which handler accesses the removed node. Done means removing a node no longer raises “Node not found for id” and the graph still renders correctly.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
frontend
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.