我正在做一个图形可视化玩具项目。我希望能够给它一个边缘列表或邻接列表作为输入(在textarea中),并在svg元素中显示相应的图形。被描述为边缘列表的图有效,但是邻接列表以某种方式为所有节点提供了相同的索引。
最近几天,我一直在为一个问题打招呼。我给出了相同的图形,当描述为边缘列表时,它行得通...
,当被描述为邻接列表时,它给出以下内容:
这里是接受文本输入并将其转换为邻接/边缘列表的部分:
export const getAdjacencyListFromText = (text: string) => {
const tokenizedLines = getLines(text)
.map(line => getTokensFromLine(line).map(token => Number(token)))
.filter(tokens => tokens.length >= 2)
const adjacencyList = tokenizedLines.reduce<AdjacencyList>((acc: any, current: any) => {
const source = current[0]
const neighbors = current.slice(1)
acc[source] = neighbors
return acc
}, [])
return adjacencyList
}
export const getEdgeListFromText = (text: string) =>
getLines(text)
.map(line => getTokensFromLine(line).map(e => Number(e)))
.filter(lineTokens => lineTokens.length >= 2)
.map(tokens => {
const source = tokens[0]
const target = tokens[1]
const weight = tokens[2]
if (tokens.length === 2) return { source, target }
else if (tokens.length === 3) return { source, target, weight }
}) as EdgeList
创建图形对象的部分:
export const getGraphFromAdjacencyList = (adjacencyList: AdjacencyList) => {
const nodes = adjacencyList.reduce<G.Node>((acc: G.Node[], current: AdjacencyListEntry, index: number) => {
current.forEach(node => acc.push({ id: node }))
if (!acc.some(el => el.id === index)) acc.push({ id: index })
return acc
}, []) as G.Node[]
const links = nodes.reduce<G.Link>((acc: G.Link[], currentNode: G.Node) => {
const neighbors = adjacencyList[currentNode.id]
if (neighbors === undefined) return acc
const newLinks = neighbors
.map(neighborId => ({ source: currentNode.id, target: neighborId }))
.filter(link => !acc.some(l => l.source === link.source && l.target === link.target))
return acc = acc.concat(newLinks)
}, [])
return { nodes, links }
}
export const getGraphFromEdgeList = (edgeList: EdgeList) => {
const nodes = edgeList.reduce<G.Node>((acc: G.Node[], currentEdge) => {
const { source, target } = currentEdge
if (!acc.some(n => n.id === source)) {
acc.push({ id: source })
}
if (!acc.some(n => n.id === target)) {
acc.push({ id: target })
}
return acc
}, [])
return {
links: edgeList,
nodes,
}
}
以及我将图形提供给d3的部分:
componentDidUpdate() {
const { links: edgeData, nodes: nodeData } = this.props.graph
// newNodes and newLinks is done so that old nodes and links keep their position
// goal is better visuals
const previousNodes = this.simulation.nodes()
const newNodes = nodeData.map(node => {
const existingNode = _(previousNodes).find((n: any) => n.id === node.id)
if (existingNode !== undefined) {
return existingNode
} else {
return node
}
})
const previousLinks = this.simulation.force("link").links()
const newLinks = edgeData.map(edge => {
const existingLink = _(previousLinks).find((l: any) => l.source.id === edge.source && l.target.id === edge.target)
if (existingLink !== undefined) {
return existingLink
} else {
return edge
}
})
const line = this.edgeLayer.selectAll("g").data(newLinks)
const node = this.nodeLayer.selectAll("g").data(newNodes)
node.exit().remove()
line.exit().remove()
this.node = this.createNode(node)
this.edge = this.createEdge(line)
this.simulation = this.simulation.nodes(newNodes)
this.simulation.force("link").links(newLinks).id(d => d.id)
this.simulation.on("tick", this.simulationTick)
this.simulation.alphaTarget(1)
this.simulation.restart()
}
邻接列表 中的图形对象是这样的:
{
nodes: [ { id: 2 }, { id: 1 }, { id: 3 } ],
links: [ { source: 2, target: 3 }, { source: 1, target: 2 } ] }
}
边缘列表 中的图形对象是这样的:
{
nodes: [ { id: 1 }, { id: 2 }, { id: 3 } ],
links: [ { source: 1, target: 2 }, { source: 1, target: 3 } ]
}
唯一的区别是节点数组中节点的顺序。 因此,在为边缘列表和邻接列表实用程序功能编写测试用例一整个下午之后,我决定先对节点进行排序,然后再将其分配给d3 ...,它可以正常工作...
为什么订单很重要? 我去检查了文档,到目前为止(我到目前为止看到的任何地方)都没有提到与节点顺序有关的任何重要性。我很困惑。
答案 0 :(得分:0)
好的,所以我的图表在EdgeList案例中工作的原因并非偶然。
似乎我应该在selectAll
函数中使用tick
来代替select
函数中的节点和链接。
我的getAdjacencyListFromText
有时也曾经提供重复的节点/链接,所以我删除了该错误。
结果:
simulationTick
simulationTick = () => {
const { width, height } = this.props
const keepBounded = (p: Point) => ({
...p,
x: Math.max(0, Math.min(p.x, width)),
y: Math.max(0, Math.min(p.y, height)),
})
const processPoint = (p: Point) => keepBounded(p)
const setEdgeAttributes = edge =>
edge
.attr("x1", d => processPoint(d.source).x)
.attr("y1", d => processPoint(d.source).y)
.attr("x2", d => processPoint(d.target).x)
.attr("y2", d => processPoint(d.target).y)
const setNodeAttributes = node =>
node.attr("transform", d => {
const p = processPoint(d)
return "translate(" + p.x + ", " + p.y + ")"
})
setEdgeAttributes(this.edge.select("line"))
setNodeAttributes(this.node)
}
在createNode
函数内部,将selectAll
更改为select
:
createNode = node => {
const nodeGroup = node
const nodeGroupEnter = nodeGroup.enter().append("g")
// here
const nodeCircle = nodeGroup.select("circle")
const nodeLabel = nodeGroup.select("text")
// here
const nodeCircleEnter = nodeGroupEnter.append("circle")
nodeCircleEnter(...)
const nodeLabelEnter = nodeGroupEnter.append("text")
nodeLabelEnter(...)
return nodeGroupEnter.merge(nodeGroup).attr("id", d => d.id)
}
与createEdge
相同:
createEdge = edge => {
const edgeGroup = edge
const edgeGroupEnter = edgeGroup.enter().append("g")
// here
const edgeLine = edgeGroup.select("line")
const edgeLabel = edgeGroup.select("text")
// here
const edgeLineEnter = edgeGroupEnter.append("line")
edgeLineEnter (...)
const edgeLabelEnter = edgeGroupEnter.append("text")
edgeLabelEnter (...)
return edgeGroupEnter.merge(edgeGroup)
}