【问题标题】:"Hard" filtering of nodes in a d3,js force-directed graphd3,js 强制导向图中节点的“硬”过滤
【发布时间】:2018-07-05 21:17:08
【问题描述】:

我有一个力导向图,其中包含(大量)节点以及它们之间的许多链接。我想以交互方式应用过滤器,以便只保留一部分节点(以及它们之间的任何链接)。但是,由于图表很大,我想从模拟中删除任何被过滤掉的节点,而不是仅仅隐藏它们(以便生成的图表具有更好的性能)。因此,我希望创建一个过滤的节点列表作为新数组,并仅使用这些节点重新初始化模拟。同样适用于边缘 - 我还没有这样做,但我可能需要以编程方式确定要保留哪些边缘并在使用新节点 边缘重新初始化图形之前进行相同类型的过滤.我想保留原始节点/边数组以允许“重置”回到起始状态。

我已经整理了一个简单的示例,此时它只是进行一些硬编码过滤,但我正在努力使用过滤后的数组重新初始化模拟。它似乎正在从模拟中删除过滤后的节点(圆圈“三”不再可拖动),但它仍然显示在渲染图中。

到目前为止,我对“重置”逻辑的尝试似乎确实有效。

我做错了什么?有没有更好的方法来实现这一目标? (d3.js v3)

我的示例代码如下:

var links = [{
    source: 0,
    target: 1,
    type: "c"
  },
  {
    source: 1,
    target: 2,
    type: "d"
  },
  {
    source: 2,
    target: 0,
    type: "d"
  }
];

var nodes = [{
    name: "one",
    type: "a"
  },
  {
    name: "two",
    type: "a"
  },
  {
    name: "three",
    type: "b"
  }
];

var width = 300;
var height = 300;

var force = d3.layout.force()
  .nodes(nodes)
  .links(links)
  .size([width, height])
  .linkDistance(200)
  .charge(-400)
  .on("tick", tick)
  .start();

var svg = d3.select("#graph").append("svg")
  .attr("width", width)
  .attr("height", height);

function colours(n) {
  var colours = ["#3366cc", "#dc3912", "#ff9900", "#109618", "#990099", "#0099c6", "#dd4477", "#66aa00", "#b82e2e", "#316395",
    "#994499", "#22aa99", "#aaaa11", "#6633cc", "#e67300", "#8b0707", "#651067", "#329262", "#5574a6", "#3b3eac"
  ];
  return colours[n % colours.length];
}

var path = svg.append("g").selectAll("path")
  .data(force.links())
  .enter().append("line")
  .attr('class', 'link')
  .attr('stroke', function(d, i) {
    return colours(i);
  })

var circles = svg.append("g");
var circle = circles.selectAll("circle")
  .data(force.nodes())
  .enter().append("circle")
  .attr("r", 8)
  .attr('class', 'circle')
  .attr('fill', function(d, i) {
    return colours(i + 3);
  })
  .call(force.drag);

var text = svg.append("g").selectAll("text")
  .data(force.nodes())
  .enter().append("text")
  .attr("x", 14)
  .attr("y", ".31em")
  .text(function(d) {
    return d.name;
  });

function tick() {
  path.attr({
    x1: function(d) {
      return d.source.x;
    },
    y1: function(d) {
      return d.source.y;
    },
    x2: function(d) {
      return d.target.x;
    },
    y2: function(d) {
      return d.target.y;
    }
  });
  circle.attr("transform", transform);
  text.attr("transform", transform);
}

function transform(d) {
  return "translate(" + d.x + "," + d.y + ")";
}

var nodeText = "";

function nodeTypeA(node) {
  return (node.type == "a");
}

function linkTypeC(link) {
  return (link.type == "c");
}

function applyFilter() {
  force.nodes(nodes.filter(nodeTypeA));
  force.links(links.filter(linkTypeC));
  circle.data(force.nodes());
  text.data(force.nodes());
  path.data(force.links());
  d3.selectAll("circle").each(
    function(d) {
      console.log(d.name);
    }
  );
  console.log("");
}

function resetFilter() {
  force.nodes(nodes);
  force.links(links);
  circle.data(force.nodes());
  text.data(force.nodes());
  path.data(force.links());
  d3.selectAll("circle").each(
    function(d) {
      console.log(d.name);
    }
  )
  console.log("");
}
#buttons {
  position: absolute;
  top: 10px;
  left: 20px;
  height: 100px;
  width: 400px;
  z-index: 99;
}

#graph {
  position: absolute;
  top: 50px;
  left: 20px;
  height: 300px;
  width: 300px;
  z-index: 98;
}
<script src="https://d3js.org/d3.v3.min.js"></script>
<!DOCTYPE html>
<meta charset="utf-8">
<body>
  <div id="root">
    <div id="buttons">
      <button id="filter" onclick="applyFilter()">Apply</button>
      <button id="reset" onclick="resetFilter()">Reset</button>
    </div>
    <div id="graph">
    </div>
  </div>
</body>
<div id="node_details">
</div>
</body>

【问题讨论】:

  • 你能把这个例子放在一个 sn-p (或 plunkr, jsfiddle...) 中,以便于执行和编辑吗?参考文献stackoverflow.blog/2014/09/16/…
  • 当然。将示例移至 sn-p。

标签: javascript d3.js


【解决方案1】:

对于您的过滤器和重置功能,您可以更新选择的数据,但不要使用进入或退出选择来添加/删除新元素。

您在最初添加元素时确实使用了输入选择,例如:

var path = svg.append("g").selectAll("path")
 .data(force.links())
 .enter().append("line")
 ...

但是在更新时您只需使用:

path.data(force.links());

您仍然需要指定要添加的内容以及添加方式,就像最初添加节点时所做的那样。

退出节点非常简单,我们只需在应用过滤器时将.exit().remove() 添加到上述行。 .exit() 选择选择中不再在数据数组中具有对应项的元素。 .remove() 只是将它们从 DOM 中移除:

var links = [
    {source: 0, target: 1, type: "c"},
    {source: 1, target: 2, type: "d"},
    {source: 2, target: 0, type: "d"}
];

var nodes = [
    {name: "one", type: "a"},
    {name: "two", type: "a"},
    {name: "three", type: "b"}
];


var width = 300;
var height = 300;

var force = d3.layout.force()
    .nodes(nodes)
    .links(links)
    .size([width, height])
    .linkDistance(200)
    .charge(-400)
    .on("tick", tick)
    .start();

var svg = d3.select("#graph").append("svg")
  .attr("width", width)
  .attr("height", height);

function colours(n) {
  var colours = ["#3366cc", "#dc3912", "#ff9900", "#109618", "#990099", "#0099c6", "#dd4477", "#66aa00", "#b82e2e", "#316395",
                 "#994499", "#22aa99", "#aaaa11", "#6633cc", "#e67300", "#8b0707", "#651067", "#329262", "#5574a6", "#3b3eac"];
  return colours[n % colours.length];
}

var path = svg.append("g").selectAll("path")
  .data(force.links())
  .enter().append("line")
  .attr('class', 'link')
  .attr('stroke', function(d, i) {
    return colours(i);
  })

var circles = svg.append("g");
var circle = circles.selectAll("circle")
  .data(force.nodes())
  .enter().append("circle")
  .attr("r", 8)
  .attr('class', 'circle')
  .attr('fill', function(d, i) {
    return colours(i + 3);
  })
  .call(force.drag);

var text = svg.append("g").selectAll("text")
  .data(force.nodes())
  .enter().append("text")
  .attr("x", 14)
  .attr("y", ".31em")
  .text(function(d) {
    return d.name;
  });

function tick() {
  path.attr({
    x1: function(d) {
      return d.source.x;
    },
    y1: function(d) {
      return d.source.y;
    },
    x2: function(d) {
      return d.target.x;
    },
    y2: function(d) {
      return d.target.y;
    }
  });
  circle.attr("transform", transform);
  text.attr("transform", transform);
}

function transform(d) {
  return "translate(" + d.x + "," + d.y + ")";
}

var nodeText = "";

function nodeTypeA(node) {
    return (node.type == "a");
}

function linkTypeC(link) {
    return (link.type == "c");
}

function applyFilter() {
    force.nodes(nodes.filter(nodeTypeA));
    force.links(links.filter(linkTypeC));
    
    circle.data(force.nodes()).exit().remove();
    text.data(force.nodes()).exit().remove();
    path.data(force.links()).exit().remove();
    
    d3.selectAll("circle").each(
        function(d) {
            console.log(d.name);
        }
    );
    console.log("");
}

function resetFilter() {
    force.nodes(nodes);
    force.links(links);
    circle.data(force.nodes());
    text.data(force.nodes());
    path.data(force.links());
    d3.selectAll("circle").each(
        function(d) {
            console.log(d.name);
        }
    )
    console.log("");
}
#buttons {
    position: absolute;
    top: 10px;
    left: 20px;
    height: 100px;
    width: 400px;
    z-index: 99;
}

#graph {
    position: absolute;
    top: 50px;
    left: 20px;
    height: 300px;
    width: 300px;
    z-index: 98;
}
<div id="root">
    <div id="buttons">
      <button id="filter"  onclick="applyFilter()">Apply</button>
      <button id="reset" onclick="resetFilter()">Reset</button>
    </div>
    <div id="graph">
    </div>
</div>
</body>
<script src="https://d3js.org/d3.v3.min.js"></script>

我们可以复制您在重置函数中用于初始输入的代码以输入元素(以及一些小的修改),但这有点重复 - 我们将有两段代码执行相同的操作。

相反,让我们将进入和退出放入更新函数中。更新函数会从力布局中获取节点和链接,并根据需要进入/退出:

var links = [
    {source: 0, target: 1, type: "c"},
    {source: 1, target: 2, type: "d"},
    {source: 2, target: 0, type: "d"}
];

var nodes = [
    {name: "one", type: "a"},
    {name: "two", type: "a"},
    {name: "three", type: "b"}
];


var width = 300;
var height = 300;

var force = d3.layout.force()
    .nodes(nodes)
    .links(links)
    .size([width, height])
    .linkDistance(200)
    .charge(-400)
    .on("tick", tick)
    .start();

var svg = d3.select("#graph").append("svg")
  .attr("width", width)
  .attr("height", height);

function colours(n) {
  var colours = ["#3366cc", "#dc3912", "#ff9900", "#109618", "#990099", "#0099c6", "#dd4477", "#66aa00", "#b82e2e", "#316395",
                 "#994499", "#22aa99", "#aaaa11", "#6633cc", "#e67300", "#8b0707", "#651067", "#329262", "#5574a6", "#3b3eac"];
  return colours[n % colours.length];
}



var paths = svg.append("g");
var circles = svg.append("g");
var texts = svg.append("g");

update();

function tick() {
  paths.selectAll("line").attr({
    x1: function(d) {
      return d.source.x;
    },
    y1: function(d) {
      return d.source.y;
    },
    x2: function(d) {
      return d.target.x;
    },
    y2: function(d) {
      return d.target.y;
    }
  });
  circles.selectAll("circle").attr("transform", transform);
  texts.selectAll("text").attr("transform", transform);
}

function transform(d) {
  return "translate(" + d.x + "," + d.y + ")";
}

var nodeText = "";

function nodeTypeA(node) {
    return (node.type == "a");
}

function linkTypeC(link) {
    return (link.type == "c");
}

function applyFilter() {
    force.nodes(nodes.filter(nodeTypeA));
    force.links(links.filter(linkTypeC));
	
	update();
}
function resetFilter() {
    force.nodes(nodes);
    force.links(links);
	
	update();
	force.start(); // start the force layout again.
}

function update() {   

  // update the data for the lines:
  var path = paths.selectAll("line")
    .data(force.links());
  
  // enter new lines:
  path.enter().append("line")
    .attr('class', 'link')
    .attr('stroke', function(d, i) {
      return colours(i);
  })
  
  // exit unneeded lines:
  path.exit().transition().style("opacity",0).remove();

  // update the data for the circles:
  var circle = circles.selectAll("circle")
    .data(force.nodes());
  
  // enter new circles:
  circle.enter().append("circle")
    .attr("r", 8)
    .attr('class', 'circle')
    .attr('fill', function(d, i) {
      return colours(i + 3);
    })
    .call(force.drag);
  
  // remove unneeded circles:
  circle.exit().transition().style("opacity",0).remove();

  // update the text data:
  var text = texts.selectAll("text")
    .data(force.nodes());
  
  // enter new text
  text.enter().append("text")
    .attr("x", 14)
    .attr("y", ".31em")
    .text(function(d) {
      return d.name;
    });
  
  // exit old text:
  text.exit().transition().style("opacity",0).remove();
}
#buttons {
    position: absolute;
    top: 10px;
    left: 20px;
    height: 100px;
    width: 400px;
    z-index: 99;
}

#graph {
    position: absolute;
    top: 50px;
    left: 20px;
    height: 300px;
    width: 300px;
    z-index: 98;
}
<script src="https://d3js.org/d3.v3.min.js"></script>
<div id="root">
    <div id="buttons">
      <button id="filter"  onclick="applyFilter()">Apply</button>
      <button id="reset" onclick="resetFilter()">Reset</button>
    </div>
    <div id="graph">
    </div>
</div>
</body>



    <div id="node_details">
    </div>

与原来的变化:filter 和 reset 函数在设置 force 的节点和链接后调用 update 函数(并且节点最初是使用 update 函数绘制的)。添加节点时会重置力(要重新激活模拟,就像它已经冷却一样,不会调用刻度并且节点不会正确定位)。

最后,文本、圆圈和线条位于父 g 选择中,分别命名为 texts circleslines。 tick 函数已被修改为在每个刻度上重新选择每个父 g 的子代 - 尽管您可以以不同的方式优化它。

最后一点,可能值得查看为数据指定标识符或指定数据中的节点/链接属性 - 当删除/添加链接/节点着色和/或按索引设置属性时可能会出现问题。

【讨论】:

  • 太好了。谢谢你。主要问题很有意义,并提供了一些有用的附加建议。我对此很陌生,所以我很感谢您的帮助。
  • @WarrenBlumenow,另外,特别是如果性能是一个问题,我会看一下基于画布的力 - 使用力布局,这允许您完全跳过进入/更新/退出周期,同时提高性能并保持简单的拖动功能。这是一个可拖动的画布示例:bl.ocks.org/mbostock/ad70335eeef6d167bc36fd3c04378048
猜你喜欢
  • 2013-01-14
  • 2017-08-27
  • 2013-10-17
  • 1970-01-01
  • 2018-02-18
  • 1970-01-01
  • 2021-04-03
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多