gpt4 book ai didi

javascript - D3.js 高亮相关节点/链接

转载 作者:行者123 更新时间:2023-12-04 17:14:26 26 4
gpt4 key购买 nike

问题:我想根据链接类型淡化/突出显示整个依赖链。

为此,我使用了 mouseEnter 事件,该事件当前存储所有链接和节点。此外,我淡化所有节点和链接,只突出显示那些被过滤为相关节点和链接的节点。它需要再次检查所有相关的节点和链接,如果它们也有来自 need 类型的连接。只要找到依赖关系就必须这样做。我想不出合适的算法。

示例:

为了更好地理解,我创建了一个啤酒成分依赖项,它看起来像一个星星。出于这些目的,我的版本很好。 但是 第二条链,关于汽车 -> 车轮 -> 轮胎 -> 橡胶和 radio 让我头疼。 radio 是一个“use”依赖项,这意味着它对链不是强制性的,不应该被突出显示。

预期结果:

如果光标在 car 上,所有具有“需要”依赖关系的连接节点都应该突出显示,其余的应该淡出。

对于那些想帮助我的人,如果有任何不清楚的地方,请不要犹豫。

<!DOCTYPE html>
<html>

<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- d3.js framework -->
<script src="https://d3js.org/d3.v6.js"></script>
<!-- fontawesome stylesheet https://fontawesome.com/ -->
<script src="https://kit.fontawesome.com/39094309d6.js" crossorigin="anonymous"></script>
</head>

<style>
body {
height: 100%;
background: #e6e7ee;
overflow: hidden;
margin: 0px;
}

.faded {
opacity: 0.1;
transition: 0.3s opacity;
}

.highlight {
opacity: 1;
}
</style>

<body>
<svg id="svg"></svg>

<script>
var graph = {
"nodes": [
{
"id": 0,
"name": "beer",
},
{
"id": 1,
"name": "water",
},
{
"id": 2,
"name": "hop",
},
{
"id": 3,
"name": "malt",
},
{
"id": 4,
"name": "yeast",
},
{
"id": 10,
"name": "car",
},
{
"id": 11,
"name": "wheels",
},
{
"id": 12,
"name": "tires",
},
{
"id": 13,
"name": "rubber",
},
{
"id": 14,
"name": "radio",
}
],
"links": [
{
"source": 0,
"target": 1,
"type": "need"
},
{
"source": 0,
"target": 2,
"type": "need"
},
{
"source": 0,
"target": 3,
"type": "need"
},
{
"source": 0,
"target": 4,
"type": "need"
},
{
"source": 10,
"target": 11,
"type": "need"
},
{
"source": 11,
"target": 12,
"type": "need"
},
{
"source": 12,
"target": 13,
"type": "need"
},
{
"source": 10,
"target": 14,
"type": "use"
}

]
}

var svg = d3.select("svg")
.attr("class", "canvas")
.attr("width", window.innerWidth)
.attr("height", window.innerHeight)
.call(d3.zoom().on("zoom", function (event) {
svg.attr("transform", event.transform)
}))
.append("g")

// append markers to svg
svg.append("defs").append("marker")
.attr("id", "arrowhead")
.attr("viewBox", "-0 -5 10 10")
.attr("refX", 8)
.attr("refY", 0)
.attr("orient", "auto")
.attr("markerWidth", 50)
.attr("markerHeight", 50)
.attr("xoverflow", "visible")
.append("svg:path")
.attr("d", "M 0,-1 L 2 ,0 L 0,1")
.attr("fill", "black")
.style("stroke", "none")

var linksContainer = svg.append("g").attr("class", linksContainer)
var nodesContainer = svg.append("g").attr("class", nodesContainer)

var force = d3.forceSimulation()
.force("link", d3.forceLink().id(function (d) {
return d.id
}).distance(80))
.force("charge", d3.forceManyBody().strength(-100))
.force("center", d3.forceCenter(window.innerWidth / 2, window.innerHeight / 2))
.force("collision", d3.forceCollide().radius(90))

initialize()

function initialize() {

link = linksContainer.selectAll(".link")
.data(graph.links)
.join("line")
.attr("class", "link")
.attr('marker-end', 'url(#arrowhead)')
.style("display", "block")
.style("stroke", "black")
.style("stroke-width", 1)

linkPaths = linksContainer.selectAll(".linkPath")
.data(graph.links)
.join("path")
.style("pointer-events", "none")
.attr("class", "linkPath")
.attr("fill-opacity", 1)
.attr("stroke-opacity", 1)
.attr("id", function (d, i) { return "linkPath" + i })
.style("display", "block")

linkLabels = linksContainer.selectAll(".linkLabel")
.data(graph.links)
.join("text")
.style("pointer-events", "none")
.attr("class", "linkLabel")
.attr("id", function (d, i) { return "linkLabel" + i })
.attr("font-size", 16)
.attr("fill", "black")
.text("")

linkLabels
.append("textPath")
.attr('xlink:href', function (d, i) { return '#linkPath' + i })
.style("text-anchor", "middle")
.style("pointer-events", "none")
.attr("startOffset", "50%")
.text(function (d) { return d.type })

node = nodesContainer.selectAll(".node")
.data(graph.nodes, d => d.id)
.join("g")
.attr("class", "node")
.call(d3.drag()
.on("start", dragStarted)
.on("drag", dragged)
.on("end", dragEnded)
)

node.selectAll("circle")
.data(d => [d])
.join("circle")
.attr("r", 30)
.style("fill", "whitesmoke")
.on("mouseenter", mouseEnter)
.on("mouseleave", mouseLeave)

node.selectAll("text")
.data(d => [d])
.join("text")
.style("class", "icon")
.attr("font-family", "FontAwesome")
.attr("dominant-baseline", "central")
.attr("text-anchor", "middle")
.attr("font-size", 20)
.attr("fill", "black")
.attr("pointer-events", "none")
.attr("dy", "-1em")
.text(function (d) {
return d.name
})
node.append("text")
.attr("dominant-baseline", "central")
.attr("text-anchor", "middle")
.attr("font-size", 13)
.attr("fill", "black")
.attr("pointer-events", "none")
.attr("dy", "0.5em")
.text(function (d) {
return d.id
})

force
.nodes(graph.nodes)
.on("tick", ticked);

force
.force("link")
.links(graph.links)
}

function mouseEnter(event, d) {
const selNodes = node.selectAll("circle")
const selLink = link
const selLinkLabel = linkLabels
const selText = node.selectAll("text")
const related = []
const relatedLinks = []

related.push(d)
force.force('link').links().forEach((link) => {
if (link.source === d || link.target === d) {
relatedLinks.push(link)
if (related.indexOf(link.source) === -1) { related.push(link.source) }
if (related.indexOf(link.target) === -1) { related.push(link.target) }
}
})
selNodes.classed('faded', true)
selNodes.filter((dNodes) => related.indexOf(dNodes) > -1)
.classed('highlight', true)
selLink.classed('faded', true)
selLink.filter((dLink) => dLink.source === d || dLink.target === d)
.classed('highlight', true)
selLinkLabel.classed('faded', true)
selLinkLabel.filter((dLinkLabel) => dLinkLabel.source === d || dLinkLabel.target === d)
.classed('highlight', true)
selText.classed('faded', true)
selText.filter((dText) => related.indexOf(dText) > -1)
.classed('highlight', true)

force.alphaTarget(0.0001).restart()
}

function mouseLeave(event, d) {
const selNodes = node.selectAll("circle")
const selLink = link
const selLinkLabel = linkLabels
const selText = node.selectAll("text")

selNodes.classed('faded', false)
selNodes.classed('highlight', false)
selLink.classed('faded', false)
selLink.classed('highlight', false)
selLinkLabel.classed('faded', false)
selLinkLabel.classed('highlight', false)
selText.classed('faded', false)
selText.classed('highlight', false)

force.restart()
}

function ticked() {
// update link positions
link
.attr("x1", function (d) {
return d.source.x;
})
.attr("y1", function (d) {
return d.source.y;
})
.attr("x2", function (d) {
return d.target.x;
})
.attr("y2", function (d) {
return d.target.y;
});

// update node positions
node
.attr("transform", function (d) {
return "translate(" + d.x + ", " + d.y + ")";
});

linkPaths.attr('d', function (d) {
return 'M ' + d.source.x + ' ' + d.source.y + ' L ' + d.target.x + ' ' + d.target.y;
});

linkLabels.attr('transform', function (d) {
if (d.target.x < d.source.x) {
var bbox = this.getBBox();

rx = bbox.x + bbox.width / 2;
ry = bbox.y + bbox.height / 2;
return 'rotate(180 ' + rx + ' ' + ry + ')';
}
else {
return 'rotate(0)';
}
});

}

function dragStarted(event, d) {
if (!event.active) force.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;

PosX = d.x
PosY = d.y
}

function dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
}

function dragEnded(event, d) {
if (!event.active) force.alphaTarget(0);
d.fx = undefined;
d.fy = undefined;
}

</script>
</body>

</html>

最佳答案

要让淡入淡出/高光运行,您可以考虑几个问题:


首先,注意 force方法改变了 links graph 中的数组.

例如,您的第一个链接是这样开始的:

{
"source": 0,
"target": 1,
"type": "need"
}

但变成这样:

{
"index": 0
"source": {
"id": 0
"index": 0
"name": "beer"
"vx": 0.036971029563580046
"vy": 0.04369386654517388
"x": 394.1514674087123
"y": 220.18458726626062
},
"target": {
"id": 1
"index": 1
"name": "water"
"vx": -0.021212609689083086
"vy": 0.01105162589441528
"x": 568.911363724937
"y": 177.07991527420614
},
"type": "need"
}

因此您需要一个递归函数,但如果您引用 link.source,您将得到空数组- 相反,您需要引用 link.source.id因为这就是force已根据上述示例更新了您的图形对象。

这是一个相当冗长的递归函数,它返回给定节点 ID 的所有节点和链接,这些节点和链接由给定 type 的链接链接。 :

function nodesByTypeAfterForce(nodeId, sieved, type) {

// get the links for the node per the type
const newLinks = graph.links
.filter(link => link.type === type && link.source.id === nodeId);

// get the linked nodes to nodeId from the links
const newNodes = newLinks
.map(link => graph.nodes.find(newNode => newNode.id === link.target.id));

// concatenate new nodes and links
(sieved.links = sieved.links || []).push(...newLinks);
(sieved.nodes = sieved.nodes || []).push(...newNodes);

// recursively visit linked nodes until exhausted options
newNodes.forEach(node => nodesByTypeAfterForce(node.id, sieved, type));

// return indices relevant nodes and links
return {
nodes: sieved.nodes.map(node => node.index),
links: sieved.links.map(link => link.index)
};
}

注意该函数返回一个 index 的数组这是一个属性 force分配给每个节点和链接。这使得稍后对淡入淡出/突出显示的过滤更加明确。


现在,在 mouseEnter你可以通过调用这个函数来开始,只返回由某个 type 的链接链接在一起的节点。并通过 d初始化搜索:

function mouseEnter(event, d) {

// sub graph for the hovered node
const sieved = nodesByTypeAfterForce(d.id, {nodes: [d]}, "need");
//...
}

它取代了 related 的构造在您的 OP 中与 type 无关的数组.虽然这很容易解决,但您当前 mouseEnter 中的另一个问题是你有这些线。

selLink
.filter((dLink) => dLink.source === d || dLink.target === d)

selLinkLabel
.filter((dLinkLabel) => dLinkLabel.source === d || dLinkLabel.target === d)
.classed('highlight', true)

这导致链接(和链接标签)突出显示到任何链接节点,而不仅仅是由 type 链接的节点。 (例如 need )。

所以,我建议你用这个代码块替换(我移动了所有行以将所有内容淡化到它们自己的部分):

// only highlight from sieved
node.selectAll("circle")
.filter(node => sieved.nodes.indexOf(node.index) > -1)
.classed('highlight', true)
link
.filter(link => sieved.links.indexOf(link.index) > -1)
.classed('highlight', true)
linkLabels
.filter(link => sieved.links.indexOf(link.index) > -1)
.classed('highlight', true)
node.selectAll("text")
.filter(node => sieved.nodes.indexOf(node.index) > -1)
.classed('highlight', true)

现在根据 index 仅突出显示节点和链接从 nodeByTypeAfterForce 返回上面的功能。


工作示例在下面,其中 nodeByTypeAfterForcegraph 的定义之后被丢弃唯一的其他编辑在 mouseEnter 中:

<!DOCTYPE html>
<html>

<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- d3.js framework -->
<script src="https://d3js.org/d3.v6.js"></script>
<!-- fontawesome stylesheet https://fontawesome.com/ -->
<script src="https://kit.fontawesome.com/39094309d6.js" crossorigin="anonymous"></script>
</head>

<style>
body {
height: 100%;
background: #e6e7ee;
overflow: hidden;
margin: 0px;
}

.faded {
opacity: 0.1;
transition: 0.3s opacity;
}

.highlight {
opacity: 1;
}
</style>

<body>
<svg id="svg"></svg>

<script>
var graph = {
"nodes": [
{
"id": 0,
"name": "beer",
},
{
"id": 1,
"name": "water",
},
{
"id": 2,
"name": "hop",
},
{
"id": 3,
"name": "malt",
},
{
"id": 4,
"name": "yeast",
},
{
"id": 10,
"name": "car",
},
{
"id": 11,
"name": "wheels",
},
{
"id": 12,
"name": "tires",
},
{
"id": 13,
"name": "rubber",
},
{
"id": 14,
"name": "radio",
}
],
"links": [
{
"source": 0,
"target": 1,
"type": "need"
},
{
"source": 0,
"target": 2,
"type": "need"
},
{
"source": 0,
"target": 3,
"type": "need"
},
{
"source": 0,
"target": 4,
"type": "need"
},
{
"source": 10,
"target": 11,
"type": "need"
},
{
"source": 11,
"target": 12,
"type": "need"
},
{
"source": 12,
"target": 13,
"type": "need"
},
{
"source": 10,
"target": 14,
"type": "use"
}

]
}


function nodesByTypeAfterForce(nodeId, sieved, type) {

// get the links for the node per the type
const newLinks = graph.links
.filter(link => link.type === type && link.source.id === nodeId);

// get the linked nodes to nodeId from the links
const newNodes = newLinks
.map(link => graph.nodes.find(newNode => newNode.id === link.target.id));

// concatenate new nodes and links
(sieved.links = sieved.links || []).push(...newLinks);
(sieved.nodes = sieved.nodes || []).push(...newNodes);

// recursively visit linked nodes until exhausted options
newNodes.forEach(node => nodesByTypeAfterForce(node.id, sieved, type));

// return indices relevant nodes and links
return {
nodes: sieved.nodes.map(node => node.index),
links: sieved.links.map(link => link.index)
};
}

var svg = d3.select("svg")
.attr("class", "canvas")
.attr("width", window.innerWidth)
.attr("height", window.innerHeight)
.call(d3.zoom().on("zoom", function (event) {
svg.attr("transform", event.transform)
}))
.append("g")

// append markers to svg
svg.append("defs").append("marker")
.attr("id", "arrowhead")
.attr("viewBox", "-0 -5 10 10")
.attr("refX", 8)
.attr("refY", 0)
.attr("orient", "auto")
.attr("markerWidth", 50)
.attr("markerHeight", 50)
.attr("xoverflow", "visible")
.append("svg:path")
.attr("d", "M 0,-1 L 2 ,0 L 0,1")
.attr("fill", "black")
.style("stroke", "none")

var linksContainer = svg.append("g").attr("class", linksContainer)
var nodesContainer = svg.append("g").attr("class", nodesContainer)

var force = d3.forceSimulation()
.force("link", d3.forceLink().id(function (d) {
return d.id
}).distance(80))
.force("charge", d3.forceManyBody().strength(-100))
.force("center", d3.forceCenter(window.innerWidth / 2, window.innerHeight / 2))
.force("collision", d3.forceCollide().radius(90))

initialize()

function initialize() {

link = linksContainer.selectAll(".link")
.data(graph.links)
.join("line")
.attr("class", "link")
.attr('marker-end', 'url(#arrowhead)')
.style("display", "block")
.style("stroke", "black")
.style("stroke-width", 1)

linkPaths = linksContainer.selectAll(".linkPath")
.data(graph.links)
.join("path")
.style("pointer-events", "none")
.attr("class", "linkPath")
.attr("fill-opacity", 1)
.attr("stroke-opacity", 1)
.attr("id", function (d, i) { return "linkPath" + i })
.style("display", "block")

linkLabels = linksContainer.selectAll(".linkLabel")
.data(graph.links)
.join("text")
.style("pointer-events", "none")
.attr("class", "linkLabel")
.attr("id", function (d, i) { return "linkLabel" + i })
.attr("font-size", 16)
.attr("fill", "black")
.text("")

linkLabels
.append("textPath")
.attr('xlink:href', function (d, i) { return '#linkPath' + i })
.style("text-anchor", "middle")
.style("pointer-events", "none")
.attr("startOffset", "50%")
.text(function (d) { return d.type })

node = nodesContainer.selectAll(".node")
.data(graph.nodes, d => d.id)
.join("g")
.attr("class", "node")
.call(d3.drag()
.on("start", dragStarted)
.on("drag", dragged)
.on("end", dragEnded)
)

node.selectAll("circle")
.data(d => [d])
.join("circle")
.attr("r", 30)
.style("fill", "whitesmoke")
.on("mouseenter", mouseEnter)
.on("mouseleave", mouseLeave)

node.selectAll("text")
.data(d => [d])
.join("text")
.style("class", "icon")
.attr("font-family", "FontAwesome")
.attr("dominant-baseline", "central")
.attr("text-anchor", "middle")
.attr("font-size", 20)
.attr("fill", "black")
.attr("pointer-events", "none")
.attr("dy", "-1em")
.text(function (d) {
return d.name
})
node.append("text")
.attr("dominant-baseline", "central")
.attr("text-anchor", "middle")
.attr("font-size", 13)
.attr("fill", "black")
.attr("pointer-events", "none")
.attr("dy", "0.5em")
.text(function (d) {
return d.id
})

force
.nodes(graph.nodes)
.on("tick", ticked);

force
.force("link")
.links(graph.links)
}

function mouseEnter(event, d) {

// sub graph for the hovered node
const sieved = nodesByTypeAfterForce(d.id, {nodes: [d]}, "need");

// fade everything
node.selectAll("circle").classed('faded', true)
node.selectAll("circle").classed('highlight', false)
link.classed('faded', true)
link.classed('highlight', false)
linkLabels.classed('faded', true)
linkLabels.classed('highlight', false)
node.selectAll("text").classed('faded', true)
node.selectAll("text").classed('highlight', false)

// only highlight from sieved
node.selectAll("circle")
.filter(node => sieved.nodes.indexOf(node.index) > -1)
.classed('highlight', true)
link
.filter(link => sieved.links.indexOf(link.index) > -1)
.classed('highlight', true)
linkLabels
.filter(link => sieved.links.indexOf(link.index) > -1)
.classed('highlight', true)
node.selectAll("text")
.filter(node => sieved.nodes.indexOf(node.index) > -1)
.classed('highlight', true)

force.alphaTarget(0.0001).restart()
}

function mouseLeave(event, d) {
const selNodes = node.selectAll("circle")
const selLink = link
const selLinkLabel = linkLabels
const selText = node.selectAll("text")

selNodes.classed('faded', false)
selNodes.classed('highlight', false)
selLink.classed('faded', false)
selLink.classed('highlight', false)
selLinkLabel.classed('faded', false)
selLinkLabel.classed('highlight', false)
selText.classed('faded', false)
selText.classed('highlight', false)

force.restart()
}

function ticked() {
// update link positions
link
.attr("x1", function (d) {
return d.source.x;
})
.attr("y1", function (d) {
return d.source.y;
})
.attr("x2", function (d) {
return d.target.x;
})
.attr("y2", function (d) {
return d.target.y;
});

// update node positions
node
.attr("transform", function (d) {
return "translate(" + d.x + ", " + d.y + ")";
});

linkPaths.attr('d', function (d) {
return 'M ' + d.source.x + ' ' + d.source.y + ' L ' + d.target.x + ' ' + d.target.y;
});

linkLabels.attr('transform', function (d) {
if (d.target.x < d.source.x) {
var bbox = this.getBBox();

rx = bbox.x + bbox.width / 2;
ry = bbox.y + bbox.height / 2;
return 'rotate(180 ' + rx + ' ' + ry + ')';
}
else {
return 'rotate(0)';
}
});

}

function dragStarted(event, d) {
if (!event.active) force.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;

PosX = d.x
PosY = d.y
}

function dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
}

function dragEnded(event, d) {
if (!event.active) force.alphaTarget(0);
d.fx = undefined;
d.fy = undefined;
}

</script>
</body>

</html>

如果您定义这样的链接,您将遇到堆栈溢出,例如用“酵母”:

"links": [ // Yeast need yeast ??
{
"source": 4,
"target": 4,
"type": "need"
},
...
]

因此 nodesByTypeAfterForce 中需要一些额外的逻辑以适应自引用链接。

关于javascript - D3.js 高亮相关节点/链接,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/68939086/

26 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com