带嵌套圆的 D3 力定向图节点
D3 force directed graph nodes with nested circles
在我的力导向图中,我希望我的每个节点都有:
- 两个圆圈。
- 两个圆的大小必须相同。
- 圆圈需要彼此重叠,这样在视觉上我们只能看到一个。
- 下方的圆圈是红色填充的圆圈。
- 顶部的圆圈是填充 url 的圆圈。
我做错了什么?
const node = svg.append("g")
.attr("stroke", nodeStroke)
.attr("stroke-opacity", nodeStrokeOpacity)
.attr("stroke-width", nodeStrokeWidth)
.selectAll("circle")
.data(nodes)
.join("circle")
.style("fill", "red")
.attr("r", 30)
node.selectAll("circle")
.data(nodes)
.join("circle")
.style("fill", d => `url(#${d.id})`)
.attr("r", 30)
.call(drag(simulation))
编辑 2
在尝试将建议的解决方案应用到我的代码后,我成功地创建了我想要的圆圈。剩下的问题是我的圆圈似乎都堆叠在我的 svg 的中心,因为我的拖动(模拟)不再适用于圆圈。
下面是我的代码的屏幕截图和更详细的部分:
ForceGraph(
nodes, // an iterable of node objects (typically [{id}, …])
links // an iterable of link objects (typically [{src, target}, …])
){
var nodeId = d => d.id // given d in nodes, returns a unique identifier (string)
const nodeStrength = -450 // -1750
const linkDistance = 100
const linkStrokeOpacity = 1 // link stroke opacity
const linkStrokeWidth = 3 // given d in links, returns a stroke width in pixels
const linkStrokeLinecap = "round" // link stroke linecap
const linkStrength =1
var width = this.$refs.mapFrame.clientWidth // scale to parent container
var height = this.$refs.mapFrame.clientHeight // scale to parent container
const N = d3.map(nodes, nodeId);
// Replace the input nodes and links with mutable objects for the simulation.
nodes = nodes.map(n => Object.assign({}, n));
links = links.map(l => ({
orig: l,
//Object.assign({}, l)
source: l.src,
target: l.target
}));
// Construct the forces.
const forceNode = d3.forceManyBody();
const forceLink = d3.forceLink(links).id(({index: i}) => N[i]);
forceNode.strength(nodeStrength);
forceLink.strength(linkStrength);
forceLink.distance(linkDistance)
const simulation = d3.forceSimulation(nodes)
.force(link, forceLink)
.force("charge", forceNode)
.force("x", d3.forceX())
.force("y", d3.forceY())
.on("tick", ticked);
const svg = d3.create("svg")
.attr("id", "svgId")
.attr("preserveAspectRatio", "xMidYMid meet")
.attr("viewBox", [-width/2,-height/2, width,height])
.classed("svg-content-responsive", true)
const defs = svg.append('svg:defs');
defs.selectAll("pattern")
.data(nodes)
.join(
enter => {
// For every new <pattern>, set the constants and append an <image> tag
const patterns = enter
.append("pattern")
.attr("preserveAspectRatio", "none")
.attr("viewBox", [0,0, 100,100])
.attr("width", 1)
.attr("height", 1);
patterns
.append("image")
.attr("width", 80)
.attr("height", 80)
.attr("x", 10)
.attr("y", 10);
return patterns;
}
)
// For every <pattern>, set it to point to the correct
// URL and have the correct (company) ID
.attr("id", d => d.id)
.select("image")
.datum(d => {
return d;
})
.attr("xlink:href", d => {
return d.image
})
const link = svg.append("g")
.attr("stroke-opacity", linkStrokeOpacity)
.attr("stroke-width", linkStrokeWidth)
.attr("stroke-linecap", linkStrokeLinecap)
.selectAll("line")
.data(links)
.join("line")
;
link.attr("stroke", "white")
var node
var group = svg
.selectAll(".circle-group")
.data(nodes)
.join(enter => {
node = enter.append("g")
.attr("class", "circle-group");
node.append("circle")
.attr("class", "background") // classes aren't necessary here, but they can help with selections/styling
.style("fill", "red")
.attr("r", 30);
node.append("circle")
.attr("class", "foreground") // classes aren't necessary here, but they can help with selections/styling
.style("fill", d => `url(#${d.id})`)
.attr("r", 30)
}).call(drag(simulation))
function ticked() {
link
.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y);
node
.attr("cx", d => d.x)
.attr("cy", d => d.y);
}
function drag(simulation) {
function dragstarted(event) {
if (!event.active) simulation.alphaTarget(0.3).restart();
event.subject.fx = event.subject.x;
event.subject.fy = event.subject.y;
}
function dragged(event) {
event.subject.fx = event.x;
event.subject.fy = event.y;
}
function dragended(event) {
if (!event.active) simulation.alphaTarget(0);
event.subject.fx = null;
event.subject.fy = null;
}
return d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
}
return Object.assign(svg.node() );
}//forcegraph
这是预期的行为:
node
是一个 select 圆圈 - 你的代码块在这里:
svg.append("g")
.attr("stroke", nodeStroke)
.attr("stroke-opacity", nodeStrokeOpacity)
.attr("stroke-width", nodeStrokeWidth)
.selectAll("circle")
.data(nodes)
.join("circle")
.style("fill", "red")
.attr("r", 30)
returns最终是select个圆圈,也就是nodes
所指的。第二个 selectAll 语句试图将一个圆附加到这个 selection of circles 上。圆圈 SVG 元素不能包含子圆圈元素,因为这是无效语法,因此这些子圆圈中的 none 个将呈现。
如果我们打破这个链条,我们可以得到父 g
的 selection 来附加两组圆:
const g = svg.append("g")
.attr("stroke", nodeStroke)
.attr("stroke-opacity", nodeStrokeOpacity)
.attr("stroke-width", nodeStrokeWidth)
然后我们可以使用 g.selectAll() 来确保我们将圆圈附加到合法父级 g
。但是,如果我们不稍微调整您的代码,您将只会得到一组圆圈,因为第二个 selectAll() 语句将 select 在第一个 [=37] 之后添加的所有圆圈=]All() 语句:第一次这样做时没有圆圈,所有圆圈都是entered/appended。第二次执行此操作时,您有圆圈,数据数组中的每一项都有一个圆圈,因此不会附加新的圆圈。
您可以应用 class 名称来区分 select 所有语句:
.selectAll(".circleA")
.data(...)
.join("circle")
.attr("class", "circleA")
但是,如果您从未打算添加圈子一次并且不修改其数据,则可以使用 .selectAll("null")
。
正如 Andrew Reid 所暗示的那样,由于您想将多个圆圈添加到同一节点,因此您可能希望每个节点使用一个 svg 组元素(g
元素)来执行此操作。从那里您可以通过重复选择相同的组并附加到它来添加多个圈子。
无更新
例如,这是一个使用预填充图像的简单示例,由 Wikimedia Commons 提供。它适用于仅设置一次数据的情况:
var nodeStroke = 'black';
var nodeStrokeWidth = 5;
var nodeStrokeOpacity = 0.8;
var nodes = [{id: 'image-1' }, {id: 'image-2' }];
const svg = d3.select('svg')
.attr("stroke", nodeStroke)
.attr("stroke-opacity", nodeStrokeOpacity)
.attr("stroke-width", nodeStrokeWidth);
var group = svg
.selectAll()
.data(nodes)
.join("g")
.attr("class", "node")
.attr("transform", (d,i)=>`translate(${i * 100 + 50},100)`);
var background = group.append("circle")
.attr("class", "background") // classes aren't necessary here, but they can help with selections/styling
.style("fill", "red")
.attr("r", 30)
var foreground = group.append("circle")
.attr("class", "foreground") // classes aren't necessary here, but they can help with selections/styling
.style("fill", d => `url(#${d.id})`)
.attr("r", 30)
// can also start drag simulation here:
// group.call(drag(simulation));
<script src="https://d3js.org/d3.v7.min.js"></script>
<svg>
<defs>
<pattern id="image-1" x="30" y="30" patternUnits="userSpaceOnUse" height="60" width="60">
<image x="-44" y="-30" xlink:href="https://upload.wikimedia.org/wikipedia/commons/thumb/6/6a/Mona_Lisa.jpg/158px-Mona_Lisa.jpg"></image>
</pattern>
<pattern id="image-2" x="30" y="30" patternUnits="userSpaceOnUse" height="60" width="60">
<image x="-40" y="-30" xlink:href="https://upload.wikimedia.org/wikipedia/commons/thumb/9/90/Monet_w1709.jpg/163px-Monet_w1709.jpg"></image>
</pattern>
</defs>
</svg>
有更新
更新数据时,您只想向新元素添加圆圈。在这里,我们可以将圆圈附加到 join
(输入函数)的第一个函数参数中。
var nodeStroke = 'black';
var nodeStrokeWidth = 5;
var nodeStrokeOpacity = 0.8;
// if using a drag simulation, may need to have only one instance of it:
// var dragSimulation = drag(simulation);
const svg = d3.select('svg')
.attr("stroke", nodeStroke)
.attr("stroke-opacity", nodeStrokeOpacity)
.attr("stroke-width", nodeStrokeWidth);
function updateData(nodes) {
var group = svg
.selectAll(".circle-group")
.data(nodes)
.join(enter => {
var newNodes = enter.append("g")
.attr("class", "circle-group");
newNodes.append("circle")
.attr("class", "background") // classes aren't necessary here, but they can help with selections/styling
.style("fill", "red")
.attr("r", 30);
newNodes.append("circle")
.attr("class", "foreground") // classes aren't necessary here, but they can help with selections/styling
.style("fill", d => `url(#${d.id})`)
.attr("r", 30)
// if using the drag simulation:
// newNodes.call(dragSimulation);
return newNodes;
})
.attr("transform", (d,i)=>`translate(${i * 100 + 50},100)`);
}
var data = [{id: 'image-1' }, {id: 'image-2' }];
updateData(data);
setTimeout(function () {
data.push({id: 'image-1' });
updateData(data);
}, 2000);
<script src="https://d3js.org/d3.v7.min.js"></script>
<svg>
<defs>
<pattern id="image-1" x="30" y="30" patternUnits="userSpaceOnUse" height="60" width="60">
<image x="-44" y="-30" xlink:href="https://upload.wikimedia.org/wikipedia/commons/thumb/6/6a/Mona_Lisa.jpg/158px-Mona_Lisa.jpg"></image>
</pattern>
<pattern id="image-2" x="30" y="30" patternUnits="userSpaceOnUse" height="60" width="60">
<image x="-40" y="-30" xlink:href="https://upload.wikimedia.org/wikipedia/commons/thumb/9/90/Monet_w1709.jpg/163px-Monet_w1709.jpg"></image>
</pattern>
</defs>
</svg>
在我的力导向图中,我希望我的每个节点都有:
- 两个圆圈。
- 两个圆的大小必须相同。
- 圆圈需要彼此重叠,这样在视觉上我们只能看到一个。
- 下方的圆圈是红色填充的圆圈。
- 顶部的圆圈是填充 url 的圆圈。
我做错了什么?
const node = svg.append("g")
.attr("stroke", nodeStroke)
.attr("stroke-opacity", nodeStrokeOpacity)
.attr("stroke-width", nodeStrokeWidth)
.selectAll("circle")
.data(nodes)
.join("circle")
.style("fill", "red")
.attr("r", 30)
node.selectAll("circle")
.data(nodes)
.join("circle")
.style("fill", d => `url(#${d.id})`)
.attr("r", 30)
.call(drag(simulation))
编辑 2 在尝试将建议的解决方案应用到我的代码后,我成功地创建了我想要的圆圈。剩下的问题是我的圆圈似乎都堆叠在我的 svg 的中心,因为我的拖动(模拟)不再适用于圆圈。
下面是我的代码的屏幕截图和更详细的部分:
ForceGraph(
nodes, // an iterable of node objects (typically [{id}, …])
links // an iterable of link objects (typically [{src, target}, …])
){
var nodeId = d => d.id // given d in nodes, returns a unique identifier (string)
const nodeStrength = -450 // -1750
const linkDistance = 100
const linkStrokeOpacity = 1 // link stroke opacity
const linkStrokeWidth = 3 // given d in links, returns a stroke width in pixels
const linkStrokeLinecap = "round" // link stroke linecap
const linkStrength =1
var width = this.$refs.mapFrame.clientWidth // scale to parent container
var height = this.$refs.mapFrame.clientHeight // scale to parent container
const N = d3.map(nodes, nodeId);
// Replace the input nodes and links with mutable objects for the simulation.
nodes = nodes.map(n => Object.assign({}, n));
links = links.map(l => ({
orig: l,
//Object.assign({}, l)
source: l.src,
target: l.target
}));
// Construct the forces.
const forceNode = d3.forceManyBody();
const forceLink = d3.forceLink(links).id(({index: i}) => N[i]);
forceNode.strength(nodeStrength);
forceLink.strength(linkStrength);
forceLink.distance(linkDistance)
const simulation = d3.forceSimulation(nodes)
.force(link, forceLink)
.force("charge", forceNode)
.force("x", d3.forceX())
.force("y", d3.forceY())
.on("tick", ticked);
const svg = d3.create("svg")
.attr("id", "svgId")
.attr("preserveAspectRatio", "xMidYMid meet")
.attr("viewBox", [-width/2,-height/2, width,height])
.classed("svg-content-responsive", true)
const defs = svg.append('svg:defs');
defs.selectAll("pattern")
.data(nodes)
.join(
enter => {
// For every new <pattern>, set the constants and append an <image> tag
const patterns = enter
.append("pattern")
.attr("preserveAspectRatio", "none")
.attr("viewBox", [0,0, 100,100])
.attr("width", 1)
.attr("height", 1);
patterns
.append("image")
.attr("width", 80)
.attr("height", 80)
.attr("x", 10)
.attr("y", 10);
return patterns;
}
)
// For every <pattern>, set it to point to the correct
// URL and have the correct (company) ID
.attr("id", d => d.id)
.select("image")
.datum(d => {
return d;
})
.attr("xlink:href", d => {
return d.image
})
const link = svg.append("g")
.attr("stroke-opacity", linkStrokeOpacity)
.attr("stroke-width", linkStrokeWidth)
.attr("stroke-linecap", linkStrokeLinecap)
.selectAll("line")
.data(links)
.join("line")
;
link.attr("stroke", "white")
var node
var group = svg
.selectAll(".circle-group")
.data(nodes)
.join(enter => {
node = enter.append("g")
.attr("class", "circle-group");
node.append("circle")
.attr("class", "background") // classes aren't necessary here, but they can help with selections/styling
.style("fill", "red")
.attr("r", 30);
node.append("circle")
.attr("class", "foreground") // classes aren't necessary here, but they can help with selections/styling
.style("fill", d => `url(#${d.id})`)
.attr("r", 30)
}).call(drag(simulation))
function ticked() {
link
.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y);
node
.attr("cx", d => d.x)
.attr("cy", d => d.y);
}
function drag(simulation) {
function dragstarted(event) {
if (!event.active) simulation.alphaTarget(0.3).restart();
event.subject.fx = event.subject.x;
event.subject.fy = event.subject.y;
}
function dragged(event) {
event.subject.fx = event.x;
event.subject.fy = event.y;
}
function dragended(event) {
if (!event.active) simulation.alphaTarget(0);
event.subject.fx = null;
event.subject.fy = null;
}
return d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
}
return Object.assign(svg.node() );
}//forcegraph
这是预期的行为:
node
是一个 select 圆圈 - 你的代码块在这里:
svg.append("g")
.attr("stroke", nodeStroke)
.attr("stroke-opacity", nodeStrokeOpacity)
.attr("stroke-width", nodeStrokeWidth)
.selectAll("circle")
.data(nodes)
.join("circle")
.style("fill", "red")
.attr("r", 30)
returns最终是select个圆圈,也就是nodes
所指的。第二个 selectAll 语句试图将一个圆附加到这个 selection of circles 上。圆圈 SVG 元素不能包含子圆圈元素,因为这是无效语法,因此这些子圆圈中的 none 个将呈现。
如果我们打破这个链条,我们可以得到父 g
的 selection 来附加两组圆:
const g = svg.append("g")
.attr("stroke", nodeStroke)
.attr("stroke-opacity", nodeStrokeOpacity)
.attr("stroke-width", nodeStrokeWidth)
然后我们可以使用 g.selectAll() 来确保我们将圆圈附加到合法父级 g
。但是,如果我们不稍微调整您的代码,您将只会得到一组圆圈,因为第二个 selectAll() 语句将 select 在第一个 [=37] 之后添加的所有圆圈=]All() 语句:第一次这样做时没有圆圈,所有圆圈都是entered/appended。第二次执行此操作时,您有圆圈,数据数组中的每一项都有一个圆圈,因此不会附加新的圆圈。
您可以应用 class 名称来区分 select 所有语句:
.selectAll(".circleA")
.data(...)
.join("circle")
.attr("class", "circleA")
但是,如果您从未打算添加圈子一次并且不修改其数据,则可以使用 .selectAll("null")
。
正如 Andrew Reid 所暗示的那样,由于您想将多个圆圈添加到同一节点,因此您可能希望每个节点使用一个 svg 组元素(g
元素)来执行此操作。从那里您可以通过重复选择相同的组并附加到它来添加多个圈子。
无更新
例如,这是一个使用预填充图像的简单示例,由 Wikimedia Commons 提供。它适用于仅设置一次数据的情况:
var nodeStroke = 'black';
var nodeStrokeWidth = 5;
var nodeStrokeOpacity = 0.8;
var nodes = [{id: 'image-1' }, {id: 'image-2' }];
const svg = d3.select('svg')
.attr("stroke", nodeStroke)
.attr("stroke-opacity", nodeStrokeOpacity)
.attr("stroke-width", nodeStrokeWidth);
var group = svg
.selectAll()
.data(nodes)
.join("g")
.attr("class", "node")
.attr("transform", (d,i)=>`translate(${i * 100 + 50},100)`);
var background = group.append("circle")
.attr("class", "background") // classes aren't necessary here, but they can help with selections/styling
.style("fill", "red")
.attr("r", 30)
var foreground = group.append("circle")
.attr("class", "foreground") // classes aren't necessary here, but they can help with selections/styling
.style("fill", d => `url(#${d.id})`)
.attr("r", 30)
// can also start drag simulation here:
// group.call(drag(simulation));
<script src="https://d3js.org/d3.v7.min.js"></script>
<svg>
<defs>
<pattern id="image-1" x="30" y="30" patternUnits="userSpaceOnUse" height="60" width="60">
<image x="-44" y="-30" xlink:href="https://upload.wikimedia.org/wikipedia/commons/thumb/6/6a/Mona_Lisa.jpg/158px-Mona_Lisa.jpg"></image>
</pattern>
<pattern id="image-2" x="30" y="30" patternUnits="userSpaceOnUse" height="60" width="60">
<image x="-40" y="-30" xlink:href="https://upload.wikimedia.org/wikipedia/commons/thumb/9/90/Monet_w1709.jpg/163px-Monet_w1709.jpg"></image>
</pattern>
</defs>
</svg>
有更新
更新数据时,您只想向新元素添加圆圈。在这里,我们可以将圆圈附加到 join
(输入函数)的第一个函数参数中。
var nodeStroke = 'black';
var nodeStrokeWidth = 5;
var nodeStrokeOpacity = 0.8;
// if using a drag simulation, may need to have only one instance of it:
// var dragSimulation = drag(simulation);
const svg = d3.select('svg')
.attr("stroke", nodeStroke)
.attr("stroke-opacity", nodeStrokeOpacity)
.attr("stroke-width", nodeStrokeWidth);
function updateData(nodes) {
var group = svg
.selectAll(".circle-group")
.data(nodes)
.join(enter => {
var newNodes = enter.append("g")
.attr("class", "circle-group");
newNodes.append("circle")
.attr("class", "background") // classes aren't necessary here, but they can help with selections/styling
.style("fill", "red")
.attr("r", 30);
newNodes.append("circle")
.attr("class", "foreground") // classes aren't necessary here, but they can help with selections/styling
.style("fill", d => `url(#${d.id})`)
.attr("r", 30)
// if using the drag simulation:
// newNodes.call(dragSimulation);
return newNodes;
})
.attr("transform", (d,i)=>`translate(${i * 100 + 50},100)`);
}
var data = [{id: 'image-1' }, {id: 'image-2' }];
updateData(data);
setTimeout(function () {
data.push({id: 'image-1' });
updateData(data);
}, 2000);
<script src="https://d3js.org/d3.v7.min.js"></script>
<svg>
<defs>
<pattern id="image-1" x="30" y="30" patternUnits="userSpaceOnUse" height="60" width="60">
<image x="-44" y="-30" xlink:href="https://upload.wikimedia.org/wikipedia/commons/thumb/6/6a/Mona_Lisa.jpg/158px-Mona_Lisa.jpg"></image>
</pattern>
<pattern id="image-2" x="30" y="30" patternUnits="userSpaceOnUse" height="60" width="60">
<image x="-40" y="-30" xlink:href="https://upload.wikimedia.org/wikipedia/commons/thumb/9/90/Monet_w1709.jpg/163px-Monet_w1709.jpg"></image>
</pattern>
</defs>
</svg>