【问题标题】:d3.js - Add background rectangle on force directed diagram groupsd3.js - 在力有向图组上添加背景矩形
【发布时间】:2021-09-16 12:50:45
【问题描述】:

我想在第2组中添加一个背景矩形,想法是添加一个g元素并将所有第2组节点附加到g元素,然后使用g元素bbox绘制一个矩形。

但我不知道如何将现有节点移动到 g 元素! (也许不可能?)。

示例代码如下:

var graph = {
  nodes:[
    {id: "A",name:'AAAA', group: 1},
    {id: "B", name:'BBBB',group: 2},
    {id: "C", name:'CCCC',group: 2},
    {id: "D", name:'DDDD',group: 2},
    {id: "E", name:'EEEE',group: 2},
    {id: "F", name:'FFFF',group: 3},
    {id: "G", name:'GGGG',group: 3},
    {id: "H", name:'HHHH',group: 3},
    {id: "I", name:'IIII',group: 3}
  ],
  links:[
    {source: "A", target: "B", value: 1},
    {source: "A", target: "C", value: 1},
    {source: "A", target: "D", value: 1},
    {source: "A", target: "E", value: 1},
    {source: "A", target: "F", value: 1},
    {source: "A", target: "G", value: 1},
    {source: "A", target: "H", value: 1},
    {source: "A", target: "I", value: 1},
  ]
};

var width = 400
var height = 200
var svg = d3.select('body').append('svg')
.attr('width',width)
.attr('height',height)
.style('border','1px solid red')

var color = d3.scaleOrdinal(d3.schemeCategory10);

var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) { return d.id; }).distance(100))
.force("charge", d3.forceManyBody())
.force("x", d3.forceX(function(d){
  if(d.group === 2){
    return width/3
  } else if (d.group === 3){
    return 2*width/3
  } else {
    return width/2 
  }
}))
.force("y", d3.forceY(height/2))
.force("center", d3.forceCenter(width / 2, height / 2));

var g = svg.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(graph.nodes)
.enter()

var w = 80
var txts = g.append('text')
.attr('class','text')
.attr('text-anchor','middle')
.attr("dominant-baseline", "central")
.attr('fill','black')
.text(d => d.name)
.each((d,i,n) => {
  var bbox = d3.select(n[i]).node().getBBox()
  var margin = 4
  bbox.x -= margin
  bbox.y -= margin
  bbox.width += 2*margin
  bbox.height += 2*margin
  if (bbox.width < w) {
    bbox.width = w
  }
  d.bbox = bbox
})

var node = g
.insert('rect','text')
.attr('stroke','black')
.attr('width', d => d.bbox.width)
.attr('height',d => d.bbox.height)
.attr("fill", function(d) { return color(d.group); })
.attr('fill-opacity',0.3)
.call(d3.drag()
      .on("start", dragstarted)
      .on("drag", dragged)
      .on("end", dragended));

var link = svg.append("g")
.attr("class", "links")
.attr('stroke','black')
.selectAll("line")
.data(graph.links)
.enter().append("path")
.attr("stroke-width", function(d) { return Math.sqrt(d.value); });

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

simulation.force("link")
  .links(graph.links);

function ticked() {
  link
    .attr("d", function(d) { 
    var ax = d.source.x
    var ay = d.source.y
    var bx = d.target.x
    var by = d.target.y
    if (bx < ax) {
      ax -= w/2
      bx += w/2
    }else{
      ax += w/2
      bx -= w/2
    }
    var path = ['M',ax,ay,'L',bx,by]
    return path.join(' ')
  })

  txts.attr('x',d => d.x)
    .attr('y',d => d.y)

  node
    .attr("x", function(d) { return d.x - d.bbox.width/2; })
    .attr("y", function(d) { return d.y - d.bbox.height/2; });

}

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

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

function dragended(event,d) {
  if (!event.active) simulation.alphaTarget(0);
  d.fx = null;
  d.fy = null;
}
&lt;script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.min.js"&gt;&lt;/script&gt;

【问题讨论】:

    标签: d3.js force-layout


    【解决方案1】:

    力模拟不使用 DOM 做任何事情。它只是计算节点应该在哪里,如何渲染它们,如果你渲染它们,取决于你。因此,将一些节点放在g 中而不是其他节点不是问题。例如,我们可以为组 2 添加一个g,遍历所有节点,如果它们来自组 2,则将它们从 DOM 中分离出来,然后将它们重新附加到新的g

    var parent = d3.select("g").append("g").lower();
    node.each(function(d) {
        if (d.group == 2) {
          d3.select(this).remove();
          parent.append((d)=>this);      
        }
      })
    

    那么我们需要做的就是创建一个背景矩形:

    var background = d3.select("g")
      .append("rect")
      .lower()  // so it is behind the nodes.
      ....
    

    并在勾选时使用g 的新边界框更新它,如下所示。

    var graph = {
      nodes:[
        {id: "A",name:'AAAA', group: 1},
        {id: "B", name:'BBBB',group: 2},
        {id: "C", name:'CCCC',group: 2},
        {id: "D", name:'DDDD',group: 2},
        {id: "E", name:'EEEE',group: 2},
        {id: "F", name:'FFFF',group: 3},
        {id: "G", name:'GGGG',group: 3},
        {id: "H", name:'HHHH',group: 3},
        {id: "I", name:'IIII',group: 3}
      ],
      links:[
        {source: "A", target: "B", value: 1},
        {source: "A", target: "C", value: 1},
        {source: "A", target: "D", value: 1},
        {source: "A", target: "E", value: 1},
        {source: "A", target: "F", value: 1},
        {source: "A", target: "G", value: 1},
        {source: "A", target: "H", value: 1},
        {source: "A", target: "I", value: 1},
      ]
    };
    
    var width = 400
    var height = 200
    var svg = d3.select('body').append('svg')
    .attr('width',width)
    .attr('height',height)
    .style('border','1px solid red')
    
    var color = d3.scaleOrdinal(d3.schemeCategory10);
    
    var simulation = d3.forceSimulation()
    .force("link", d3.forceLink().id(function(d) { return d.id; }).distance(100))
    .force("charge", d3.forceManyBody())
    .force("x", d3.forceX(function(d){
      if(d.group === 2){
        return width/3
      } else if (d.group === 3){
        return 2*width/3
      } else {
        return width/2 
      }
    }))
    .force("y", d3.forceY(height/2))
    .force("center", d3.forceCenter(width / 2, height / 2));
    
    var g = svg.append("g")
    .attr("class", "nodes")
    .selectAll("circle")
    .data(graph.nodes)
    .enter()
    
    var w = 80
    var txts = g.append('text')
    .attr('class','text')
    .attr('text-anchor','middle')
    .attr("dominant-baseline", "central")
    .attr('fill','black')
    .text(d => d.name)
    .each((d,i,n) => {
      var bbox = d3.select(n[i]).node().getBBox()
      var margin = 4
      bbox.x -= margin
      bbox.y -= margin
      bbox.width += 2*margin
      bbox.height += 2*margin
      if (bbox.width < w) {
        bbox.width = w
      }
      d.bbox = bbox
    })
    
    var node = g
    .insert('rect','text')
    .attr('stroke','black')
    .attr('width', d => d.bbox.width)
    .attr('height',d => d.bbox.height)
    .attr("fill", function(d) { return color(d.group); })
    .attr('fill-opacity',0.3)
    .call(d3.drag()
          .on("start", dragstarted)
          .on("drag", dragged)
          .on("end", dragended));
    
    // Start Changes 1/2
    var parent = d3.select("g").append("g").lower();
    node.each(function(d) {
        if (d.group == 2) {
          d3.select(this).remove();
          parent.append((d)=>this);      
        }
      })
    var background = d3.select("g")
      .append("rect")
      .lower()
      .attr("ry", 5)
      .attr("rx", 5)
      .attr("fill","#ccc")
      .attr("stroke","#999")
      .attr("stroke-width", 1);
    // End Changes 1/2
      
          
    
    var link = svg.append("g")
    .attr("class", "links")
    .attr('stroke','black')
    .selectAll("line")
    .data(graph.links)
    .enter().append("path")
    .attr("stroke-width", function(d) { return Math.sqrt(d.value); });
    
    simulation
      .nodes(graph.nodes)
      .on("tick", ticked);
    
    simulation.force("link")
      .links(graph.links);
    
    function ticked() {
      link
        .attr("d", function(d) { 
        var ax = d.source.x
        var ay = d.source.y
        var bx = d.target.x
        var by = d.target.y
        if (bx < ax) {
          ax -= w/2
          bx += w/2
        }else{
          ax += w/2
          bx -= w/2
        }
        var path = ['M',ax,ay,'L',bx,by]
        return path.join(' ')
      })
    
      txts.attr('x',d => d.x)
        .attr('y',d => d.y)
    
      node
        .attr("x", function(d) { return d.x - d.bbox.width/2; })
        .attr("y", function(d) { return d.y - d.bbox.height/2; });
        
    // Start changes 2/2
    var box = parent.node().getBBox() 
    
    background.attr("width", box.width+10)
      .attr("height",box.height+10)
      .attr("x", box.x-5)
      .attr("y", box.y-5);
    //End Changes 2/2
    
    }
    
    function dragstarted(event,d) {
      if (!event.active) simulation.alphaTarget(0.3).restart();
      d.fx = d.x;
      d.fy = d.y;
    }
    
    function dragged(event,d) {
      d.fx = event.x;
      d.fy = event.y;
    }
    
    function dragended(event,d) {
      if (!event.active) simulation.alphaTarget(0);
      d.fx = null;
      d.fy = null;
    }
    &lt;script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.js"&gt;&lt;/script&gt;

    如果您想要多个组,或者有动态数据,这种方法并不理想 - 需要对连接或数据结构进行一些修改以使更规范的方法起作用 - 我可能会重新审视今晚晚些时候有一个替代方案。就目前而言,这种解决方案可能是对您现有代码的侵入性最小的解决方案。

    【讨论】:

    • GAndrew Reid 很好的答案,如果我们可以为每个组添加背景矩形是个好主意。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-11-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多