How to make a convex hull covering differently sized nodes in D3

南笙酒味 提交于 2019-12-11 20:28:19

问题


I've run across another limitation of the convex hull implementation of D3: it does not adjust to the size of the nodes. Well, I didn't expect that it would do so automatically, but this seems like a fairly standard aspect of the data visualizations people are using D3.js, so I expected to find a fix for it. I have not found such a fix.

One obvious and unsatisfactory solution is to find the size of the largest node of the group and set that as the stroke-width of the hull. Unfortunately that looks terrible then the node sizes are highly variable.

I've tried to extend the thinking of inserting fake points to account for groups with 1 or 2 members. In this case I added four points per node that are located at the N,S,E, and W boundaries of the node.

var groupPath = function(d) {
var fakePoints = [];  // This adjusts convex hulls for groups with fewer than 3 nodes by adding virtual nodes.
if (d.length == 1 || d.length == 2) {
   fakePoints = [ [d[0].x + 0.001, d[0].y - 0.001],
                  [d[0].x - 0.001, d[0].y + 0.001],
                  [d[0].x - 0.001, d[0].y + 0.001]]; }     
   return "M" + 
   d3.geom.hull(d.map(function(i) { return [
        [(i.x), (i.y + (2 + (4 * i.propertyValue)))],
        [(i.x + (2 + (4 * i.propertyValue))), (i.y)],
        [(i.x), (i.y - (2 + (4 * i.propertyValue)))],
        [(i.x - (2 + (4 * i.propertyValue))), (i.y) ]]; })
      .concat(fakePoints))  //do not forget to append the fakePoints to the input data
      .join("L") 
      + "Z";
};

...in which (2 + (4 * i.propertyValue)) is the radius of the node. Then I made the the stroke width just the distance from the edge of the node to the outer edge of the convex hull...the padding. This seemed like a good idea, but it TOTALLY doesn't work. The result is very puzzling because it doesn't even create the convex hull of those points...and the order matters. You can see what it actually does in this JSFiddle.

I first thought the fakepoints were causing the problem, and maybe they are in some way, but if you remove them this the convex hull doesn't work for groups with 1 or 2 nodes...which is weird because I thought I was already adding 4 points to the input of the convex hull. One (likely) possibility is that I am not adding these four points in the right way, but I don't know what way, different from this, is the correct way.

Maybe there is an easy fix for this for somebody who better understands how D3 is creating the convex hulls. That is, maybe somebody can see what is wrong with my method and help me fix it, or maybe somebody already has a completely better way to do this.


回答1:


The problem with my previous attempt was caused by using the map function for the four virtual points inside the d3.geom.hull function. This created nested arrays of points rather than a simple list of points. I couldn't find a "flatten" function for javascript, so I put the points together outside the d3.geom.hull function using forEach() and just fed them in like this:

var groupPath = function(d) {
    var fakePoints = [];  
    d.forEach(function(element) { fakePoints = fakePoints.concat([  // "0.7071" is the sine and cosine of 45 degree for corner points.
       [(element.x), (element.y + (2 + (4 * element.propertyValue)))],
       [(element.x + 0.7071 * (2 + (4 * element.propertyValue))), (element.y + 0.7071 * (2 + (4 * element.propertyValue)))],
       [(element.x + (2 + (4 * element.propertyValue))), (element.y)],
       [(element.x + 0.7071 * (2 + (4 * element.propertyValue))), (element.y - 0.7071 * (2 + (4 * element.propertyValue)))],
       [(element.x), (element.y - (2 + (4 * element.propertyValue)))],
       [(element.x - 0.7071 * (2 + (4 * element.propertyValue))), (element.y - 0.7071 * (2 + (4 * element.propertyValue)))],
       [(element.x - (2 + (4 * element.propertyValue))), (element.y)],
       [(element.x - 0.7071 * (2 + (4 * element.propertyValue))), (element.y + 0.7071 * (2 + (4 * element.propertyValue)))]
       ]); 
    })
    return "M" + d3.geom.hull( fakePoints ).join("L") + "Z";
};

As you can see, this works by adding 8 points for every node so that the convex hull is roundish from every direction. Doing so also removes the need to add fakePoints to account for groups with 1 or 2 members. Although it's still not as nice looking as the normal examples which do not adapt to node size, the aesthetics can probably be improved for your case if that's necessary.

This JSFiddle has a working example with multiple layers of groups such that each group's convex hull wraps around and adapts to the size of the nodes. That visualization still needs work on other fronts (such as redrawing the edges over the hulls and making the link lengths adaptive to group membership), but those are other questions. This one is solved, but I'm still open to seeing a better answer or an improved version of this one.



来源:https://stackoverflow.com/questions/33708351/how-to-make-a-convex-hull-covering-differently-sized-nodes-in-d3

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!