Create node cluster's focal points by data att

2019-05-28 01:37发布

问题:

I'm trying to force nodes into different clusters in force layout based on a certain attribute in the data like "group." I'm adapting the code from Mike Bostock's multi foci force layout example (code, example) and I've been successful in adding in my own data but I haven't been able to specify how many clusters there are and how to assign a node to a cluster.

I'm relatively new to d3 and JavaScript and I haven't been able to find many examples of multi foci applications. Here's my d3 code, any help or input is appreciated:

    var width = 960,
    height = 500;

    var fill = d3.scale.category10();


    d3.json("test.json" , function(error, json){

       var root = json.nodes[0];

       root.radius = 0;
       root.fixed = true;  

      var force = d3.layout.force()
         .nodes(json.nodes)
         .size([width, height])
         .gravity(0.06)
         .charge(function(d, i) { return i ? 0 : -2000; })
         .on("tick", tick)
         .start();

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

      var elem = svg.selectAll(".elem")
         .data(json.nodes)
         .enter()
         .append("g")
         .attr("class", "elem");

  elem.append("circle")
  .attr("cx", function(d) { return d.x; })
  .attr("cy", function(d) { return d.y; })
  .attr("r", 40)
  .style("fill", function(d, i) { return fill(i & 3); })
  .style("stroke", function(d, i) { return d3.rgb(fill(i & 3)).darker(2); })
  .call(force.drag)
  .on("mousedown", function() { d3.event.stopPropagation(); });

  elem.append("text")
  .text(function(d){ return d.name; });

  svg.style("opacity", 1e-6)
    .transition()
    .duration(1000)
    .style("opacity", 1);

  d3.select("body")
    .on("mousedown", mousedown);

I've specifically been trying to figure out how this tick function is working. I did some research and found that the "&" is a bitwise operator and I noticed that changing the number after it is what is changing how many clusters there are and which nodes are in each. But preferably I would like to be able to point to something like d.group here to specify the cluster.

 function tick(e) {

    // Push different nodes in different directions for clustering.
    var k = 6 * e.alpha;
    json.nodes.forEach(function(o, i) {

        o.y += i & 3 ? k : -k;
        o.x += i & 2 ? k : -k;      
    });

 var q = d3.geom.quadtree(json.nodes),
      i = 0,
      n = json.nodes.length;

 while (++i < n) q.visit(collide(json.nodes[i]));

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

 svg.selectAll("text")
     .attr("x", function(d) { return d.x; })
     .attr("y", function(d) { return d.y; });
 }

  function collide(node) {
  var r = node.radius + 16,
      nx1 = node.x - r,
      nx2 = node.x + r,
      ny1 = node.y - r,
      ny2 = node.y + r;
  return function(quad, x1, y1, x2, y2) {
    if (quad.point && (quad.point !== node)) {
      var x = node.x - quad.point.x,
          y = node.y - quad.point.y,
          l = Math.sqrt(x * x + y * y),
          r = node.radius + quad.point.radius;
      if (l < r) {
        l = (l - r) / l * .5;
        node.x -= x *= l;
        node.y -= y *= l;
        quad.point.x += x;
        quad.point.y += y;
      }
    }
    return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
  };
}

svg.on("mousemove", function() {
  var p1 = d3.mouse(this);
  root.px = p1[0];
  root.py = p1[1];
  force.resume();
}); 

function mousedown() {
  json.nodes.forEach(function(o, i) {
  o.x += (Math.random() - .5) * 40;
  o.y += (Math.random() - .5) * 40;
});

force.resume();
}
});

*Note that I have also implemented collision detection to repel nodes but I don't think this is affecting the clusters at the moment.

My data is currently stored in a json file called test.json:

 {
  "nodes":[
    {
        "name":     "Null",
        "radius":   40,
        "color":    "#ff0000",
        "gravity": 0.05,
        "group": 1
        },
        {
        "name":     "One",
        "radius":   40,
        "color":    "#ffff00",
        "gravity": 0.05,
        "group": 1
        },
        {
        "name":     "Two",
        "radius":   40,
        "color":    "#33cc33",
        "gravity": 0.2,
        "group": 1
        },
        {
        "name":     "Three",
        "radius":   40,
        "color":    "#3399ff",
        "gravity": 0.9,
        "group": 1
        },
        {
        "name":     "Four",
        "radius":   40,
        "color":    "#ffff00",
        "gravity": 0.05,
        "group": 6
        },
        {
        "name":     "Five",
        "radius":   40,
        "color":    "#33cc33",
        "gravity": 0.2,
        "group": 6
        },
        {
        "name":     "Six",
        "radius":   40,
        "color":    "#3399ff",
        "gravity": 0.9,
        "group": 6
        }             

  ]
}

回答1:

All the clustering work takes place here:

// Push different nodes in different directions for clustering.
var k = 6 * e.alpha;
json.nodes.forEach(function(o, i) {
    o.y += i & 3 ? k : -k;
    o.x += i & 2 ? k : -k;      
});

Admittedly, I don't get how it works in this particular example. It seems indirect and hard to understand. More generally, this is what you want to do in order to cluster:

force.on("tick", function(e) {
  var k = e.alpha * .1;
  nodes.forEach(function(node) {
    var center = ...; // here you want to set center to the appropriate [x,y] coords
    node.x += (center.x - node.x) * k;
    node.y += (center.y - node.y) * k;
  });

It's taken straight out of this example and you can view source to see the code.

In this code, it's easier to understand how, on tick, the nodes are pushed closer to a desired focal point. So now you need to come up with a way to map a node to a focal point based on its group param so that you fill in that var center = ...; line.

First, you need to get an inventory of all the groups in json.nodes. d3.nest() is good for that:

var groups = d3.nest()
  .key(function(d) { return d.group; })
  .map(json.nodes)

That will give you a mapping of groups to node. Since your example json has just 2 groups in it ("1" and "6"), it'll look like this:

{
  "1": [
    {
      "name": "Null",
      "radius": 40,
      "color": "#ff0000",
      "gravity": 0.05,
      "group": 1
    },
    {
      "name": "One",
      "radius": 40,
      "color": "#ffff00",
      "gravity": 0.05,
      "group": 1
    },
    {
      "name": "Two",
      "radius": 40,
      "color": "#33cc33",
      "gravity": 0.2,
      "group": 1
    },
    {
      "name": "Three",
      "radius": 40,
      "color": "#3399ff",
      "gravity": 0.9,
      "group": 1
    }
  ],
  "6": [
    {
      "name": "Four",
      "radius": 40,
      "color": "#ffff00",
      "gravity": 0.05,
      "group": 6
    },
    {
      "name": "Five",
      "radius": 40,
      "color": "#33cc33",
      "gravity": 0.2,
      "group": 6
    },
    {
      "name": "Six",
      "radius": 40,
      "color": "#3399ff",
      "gravity": 0.9,
      "group": 6
    }
  ]
}

Then you can loop over groups and assign each group a center point as you'd like. How you do that depends on what you're trying to achieve. Perhaps you'd like to distribute the focal points in a circle around the center of the screen. It's up to you... But the goal is to end up with something where groups["1"].center equals something like {x:123, y:456}, because then you can plug that back into the tick handler from above:

force.on("tick", function(e) {
  var k = e.alpha * .1;
  nodes.forEach(function(node) {
    var center = groups[node.group].center;
    node.x += (center.x - node.x) * k;
    node.y += (center.y - node.y) * k;
  });

And (hopefully) voila!