Search code examples
javascriptperformanced3.jsforce-layoutconvex-hull

Efficiently calculating convex hulls in D3 force-directed graph


I've got something similar to the force-directed graph example. The main difference is there is no force -- the layout is static except for user drag interactions. I've added code that draws convex hulls (as svg:path elements) around user-defined groups of nodes. As the nodes are moved (that is, .on("drag")) the code that calculates the hulls is fired. That means it's fired constantly when the nodes are dragged. There are typically 10 to 30 hulls; a node may be in one or more hull. See below:

Example of hull graph

I'm trying to figure out if there's a more efficient (higher performance) way to do what I'm doing. Keeping this at high-level for now:

On drag, update all hull graphics:

  1. For each hull, create an array of the coordinates of the constituent nodes.
  2. Replace each coordinate pair in the above array with 8 points that are some distance r away from the original point. This is to pad/dilate the hull so it's not actually touching any nodes.
  3. Feed the calculated coordinates to d3.geom.hull.

I'm getting about 50 fps in Chrome, which isn't bad at all, but it seems like a dreadfully inefficient setup.

My only clear idea is to store the ID(s) of the hull(s) in which a node is contained in the nodes array, and only update that/those hulls instead of all of the hulls. I'm also wondering about more efficient data structures to store the hull data (not the paths themselves). Currently the data structure looks like this:

hulls = {1:{nodeIDs:[1,2,3,4,5], name:"hull1"}, 2:{...}, ...};

Pardon the open-ended question, but I'm hoping there's some programming technique that I'm not familiar with that would work well here.


Solution

  • The most efficient approach would be to only recalculate the position of the 8-tuple of coordinates orbiting the node that is being dragged, then propagating that information in the parent hull(s) for redrawing.

    You can do this by making a few changes.

    • To each node's data structure, add the following elements.
      • References to all parent hulls (as you suggested)
      • The 8-tuple of "padding" coordinates orbiting that node (cache them)
    • On drag, consider only the node being dragged.
    • Update that node's 8-tuple.
    • Collect that node's parent hull's child nodes and collapse together their 8-tuples and feed them to hull (destroying the previous hull, if necessary).

    I'm not familar with d3.js's API, but I'm sure you can fill in any blanks that I've left.