带嵌套圆的 D3 力定向图节点

D3 force directed graph nodes with nested circles

在我的力导向图中,我希望我的每个节点都有:

我做错了什么?

    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>