d3,js力导向图中的节点的“硬”过滤

时间:2018-07-05 21:17:09

标签: javascript d3.js

我有一个力导向图,其中有(大量)节点,并且它们之间有许多链接。我想交互式地应用过滤器,以便仅保留节点的一个子集(以及它们之间的任何链接)。但是,由于该图很大,因此我想从仿真中删除,而不是仅仅隐藏掉所有被过滤掉的节点(这样结果图就具有更好的性能)。因此,我希望将过滤后的节点列表创建为新数组,并仅使用这些节点重新初始化仿真。同样适用于边缘-我尚未执行此操作,但是我可能需要以编程方式确定要保留的边缘并进行相同类型的过滤,然后使用新的节点重新初始化图形。我想保留原始的节点/边数组,以允许“重置”回到初始状态。

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

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

我在做什么错?有没有更好的方法来实现这一目标? (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>

1 个答案:

答案 0 :(得分:2)

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

最初添加元素时确实要使用回车选择,例如:

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>

与原始版本的更改:过滤器和重置函数在设置力的节点和链接(并且最初使用update函数绘制节点)后调用update函数。添加节点时,力会重置(要重新激活模拟,就好像它已经冷却了,不会打勾,节点也不会正确放置)。

最后,文本,圆圈和线条分别位于父项g中,名称分别为texts circleslines。刻度功能已被修改,可以重新选择每个刻度g的每个父级的子级,尽管您可以进行不同的优化。

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