4

d3.js实现力导向图圈选框选

今天给大家带来的是如何在2D可视化图形中加入通过鼠标拖动圈选功能,以力导向图为例。

最终效果

http://jsrun.net/5TqKp

代码解析

我们是要在节点的上方绘制一个矩形覆盖节点的视觉效果,但是为了和原来的节点拖动不冲突,就需要对事件的target做判断。
当鼠标在空白区域时才能圈选。还不明白的话,返回电脑桌面试一下拖动鼠标。

clipboard.png

首先还是先画一个力导向图

数据
var nodes = [{
  value: "66666666",
  type: "home",
  index: "0"
},
{
  value: "11111111111",
  type: "phone",
  index: "1"
},
{
  value: "22222222222",
  type: "phone",
  index: "2"
},
{
  value: "33333333333",
  type: "phone",
  index: "3"
},
{
  value: "44444444444",
  type: "phone",
  index: "4"
},
{
  value: "55555555555",
  type: "phone",
  index: "5"
},
{
  value: "aaa",
  type: "weixin",
  index: "6"
},
{
  value: "bbb",
  type: "weixin",
  index: "7"
},
{
  value: "ccc",
  type: "weixin",
  index: "8"
},
{
  value: "ddd",
  type: "weixin",
  index: "9"
},
{
  value: "eee",
  type: "weixin",
  index: "10"
},
{
  value: "fff",
  type: "weixin",
  index: "11"
},
];
var links = [{
  source: 0,
  target: 1
},
{
  source: 0,
  target: 2
},
{
  source: 0,
  target: 3
},
{
  source: 0,
  target: 4
},
{
  source: 0,
  target: 5
},
{
  source: 2,
  target: 6
},
{
  source: 2,
  target: 7
},
{
  source: 2,
  target: 8
},
{
  source: 3,
  target: 9
},
{
  source: 3,
  target: 10
},
{
  source: 3,
  target: 11
},
]

绘制力导向图

var svg = d3.select("#forceMap").append("svg")
  .attr("width", width)
  .attr("height", height)
  .attr("id", "forceSvg");
var mapG = svg.append("g")
  .attr("id", "forceGroup");

var force = d3.layout.force()
  .nodes(nodes)
  .links(links)
  .size([width, height])
  .linkDistance(100)
  .charge([ - 1250])
  .gravity(0.5)
  .friction(0.5);
force.start();
var linkG = mapG.selectAll(".link")
  .data(links)
  .enter()
  .append("line")
  .attr("class", "link")
  .attr("stroke", "#ccc");
var nodeG = mapG.selectAll(".node")
  .data(nodes)
  .enter()
  .append("circle")
  .attr("class", "node unselected")//加入新的class,给选中和未选中做判断
  .attr("r", 8)
  .attr("fill", function(d) {
  switch (d.type) {
  case "home":
    return "red";
    break;
  case "phone":
    return "blue";
    break;
  case "weixin":
    return "green";
    break;
  }
})
  .call(force.drag);

force.on("tick", function() {
  linkG.attr("x1", function(d) {
    return d.source.x;
  })
    .attr("y1", function(d) {
    return d.source.y;
  })
    .attr("x2", function(d) {
    return d.target.x;
  })
    .attr("y2", function(d) {
    return d.target.y;
  });

  nodeG.attr("cx", function(d) {
    return d.x
  })
    .attr("cy", function(d) {
    return d.y
  });
});

这里和以前是有区别的,就是在绘制node时加入了新的class=“unselected”,这里规定选中的node为selected,未选中为unselected。
两种样式在css里实现。

.unselected{
  opacity:0.3
}
.selected{
 opacity:1 
}

圈选功能

首先确定思路,通过获取鼠标按下时的坐标位置和最后鼠标左键抬起时的结束位置来确定圈选框的大小和绘制方式。
值得注意的是不同的拖动方向(左上,右上,左下,右下),绘制的方法有区别。
需要判断点击时是否鼠标在空白区域,防止和节点的拖动冲突。
做时间标记区别短暂点击和拖动。
var clickTime = "";
var startLoc = [];
var endLoc = [];
var flag = "";
function drawSquare() {

  var rect = svg.append("rect")
    .attr("width", 0)
    .attr("height", 0)
    .attr("fill", "rgba(33,20,50,0.3)")
    .attr("stroke", "#ccc")
    .attr("stroke-width", "2px")
    .attr("transform", "translate(0,0)")
    .attr("id", "squareSelect");

  svg.on("mousedown", function() {

    clickTime = (new Date()).getTime();//mark start time
    flag = true;//以flag作为可执行圈选的标记
    rect.attr("transform", "translate(" + d3.event.layerX + "," + d3.event.layerY + ")");
    startLoc = [d3.event.layerX, d3.event.layerY];

  });

  svg.on("mousemove", function() {
    //判断事件target
    if (d3.event.target.localName == "svg" && flag == true || d3.event.target.localName == "rect" && flag == true) {

      var width = d3.event.layerX - startLoc[0];
      var height = d3.event.layerY - startLoc[1];
      if (width < 0) {
        rect.attr("transform", "translate(" + d3.event.layerX + "," + startLoc[1] + ")");
      }
      if (height < 0) {
        rect.attr("transform", "translate(" + startLoc[0] + "," + d3.event.layerY + ")");
      }
      if (height < 0 && width < 0) {
        rect.attr("transform", "translate(" + d3.event.layerX + "," + d3.event.layerY + ")");
      }
      rect.attr("width", Math.abs(width)).attr("height", Math.abs(height))
    }

  })
  
  svg.on("mouseup", function(){
            if(flag == true){
                flag = false;
                endLoc = [d3.event.layerX, d3.event.layerY];
                var leftTop = [];
                var rightBottom = []
                if(endLoc[0]>=startLoc[0]){
                    leftTop[0] = startLoc[0];
                    rightBottom[0] = endLoc[0];
                }else{
                    leftTop[0] = endLoc[0];
                    rightBottom[0] = startLoc[0];
                }

                if(endLoc[1]>=startLoc[1]){
                    leftTop[1] = startLoc[1];
                    rightBottom[1] = endLoc[1];
                }else{
                    leftTop[1] = endLoc[1];
                    rightBottom[1] = startLoc[1];
                }

                  //最后通过和node的坐标比较,确定哪些点在圈选范围
                var nodes = d3.selectAll(".node").attr("temp", function(d){
                    if(d.x<rightBottom[0] && d.x>leftTop[0] && d.y>leftTop[1] && d.y<rightBottom[1]){
                        
                            d3.select(this).attr("class","node selected");
                    }
                })
                rect.attr("width",0).attr("height",0);
            }
            var times = (new Date()).getTime()-clickTime;
            if (times<100 && d3.event.target.id !== "squareSelect") {
                   var nodes = d3.selectAll(".node").attr("class", "node unselected")
                }
        
        })

}
drawSquare();

到这里就完成了一个简单的圈选功能。

总结

完成了圈选之后,在项目中,我们通常需要拿到圈选到的元素的属性显示出来或者保存,这些就很简单了,只需要遍历有selected的元素即可。
这里的方法不仅适用于力导向图,也同样适用于相似的树状图等。
如果你的图带缩放(Zoom),那就会复杂一点,因为圈选方法里面的对范围内节点的计算和scale有关系,需要把scale和translate加入计算。

但在有坐标系的柱状图和折线图这类图中实现圈选,这里建议使用v4中的brush方法,今后的文章中会详细介绍。


dlwbill
97 声望9 粉丝

web前端 数据可视化 D3 echarts Vue