4

I'm making an interactive bubble chart and I'm working on functionality to split the data into two groups which move to opposite sides of the screen. I'm using a centering force for my simulation because I think it gives a much nicer and more consistent display of the data than using forceX and forceY does. However, I'm having trouble with the splitting of my data.

I had the idea that, since you can pass an anonymous function as a parameter to forceX to determine whether a node moves left or right, you could theoretically do the same thing for the x value in a centering force. My center force code looks like this:

var forceCenterSplit = d3.forceCenter(function(d) {
            if (d[splitParameter] >= splitVal)
                return 3*width/4;
            else
                return width/4;
        }, height/2)

For comparison, here is the code for the forceX that accomplishes the same thing:

var forceXsplit = d3.forceX(function(d) {
                if (d[splitParameter] >= splitVal)
                    return 3*width/4;
                else
                    return width/4;
            }).strength(.05);    

Unfortunately, the console is saying "Unexpected value NaN parsing cx attribute." when I run the centering force and is pushing all of the data to cx = 0 (the default value).

Am I missing something basic here? Can you not pass an anonymous function as a parameter to the centering force? If not, is there a better way to do this?

Thanks!

Start state

After split

// nicer looking splitting forces that use forceCenter
        var forceCenterCombine = d3.forceCenter(width/2, height/2);

        var forceCenterSplit = d3.forceCenter(function(d) {
            if (d[splitParameter] >= splitVal)
                return 3*width/4;
            else
                return width/4;
        }, height/2);

        // simple splitting forces that only use forceX
        var forceXSplit = d3.forceX(function(d) {
            if (d[splitParameter] >= splitVal)
                return 3*width/4;
            else
                return width/4;
        }).strength(.05);

        var forceXCombine = d3.forceX(width/2).strength(.05);

        // collision force to stop the bubbles from hitting each other
        var forceCollide = d3.forceCollide(function(d){
            console.log("forceCollide");
            return radiusScale(d[radiusParam]) + 1;
        }).strength(.75)

        // This code is for the simulation that combines all the forces
        var simulation = d3.forceSimulation()
            .force("center", forceCenterCombine)
            .force("collide", forceCollide)
            .on('end', function(){console.log("Simulation ended!");});

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

var splitFlag = false;

        // dynamically divide the bubbles into two (or probably more later on) groups
        $scope.split = function() {
            // split them apart
            if (!splitFlag){
                console.log("splitForce");
                simulation.force("center", forceXSplit)
                    .force("y", d3.forceY(height/2).strength(.05))
                    .alphaTarget(.25)
                    .restart();
                splitFlag = true;
            }
            // bring them back together
            else {
                console.log("combineForce");
                simulation.force("center", forceCenterCombine)
                    .alphaTarget(.25)
                    .restart();
                splitFlag = false;
            }
        };

enter image description here

Paul Murray
  • 399
  • 2
  • 16
  • 2
    By the very nature of `d3.forceCenter`, that's not possible. The API says: *"The centering force translates nodes uniformly so that the mean position of **all nodes** (the center of mass if all nodes have equal weight) is at the given position ⟨x,y⟩."*, emphasis mine. Thus, there is no space for an accessor function here. `forceX` and `forceY`, on the other hand, *"set the coordinate accessor to the specified number or **function**"* (emphasis mine again). – Gerardo Furtado Jan 25 '17 at 04:17
  • Ah, that's annoying. As I mentioned in the original post, forceX and forceY are giving a much choppier display than forceCenter is - nodes colliding endlessly, getting stuck in clusters of other nodes, not moving as far as they're supposed to, etc. I'm assuming the only way I could have two different centers of mass would be to have to different simulations running on two different sets of nodes? Thanks for the reply by the way – Paul Murray Jan 25 '17 at 04:36
  • 1
    But, if you have 2 simulations, how would you use `collide` to avoid overlapping nodes? Unless the nodes are very separated one from the other. – Gerardo Furtado Jan 25 '17 at 04:42
  • The data itself should be far enough apart that it isn't an issue but I don't really like the idea of brute-forcing a solution when it doesn't seem like that's how forceCenter is supposed to work. For now, I think my best bet is just to play with the X and Y forces until I get something that I'm okay with, which is a bit tedious. It seems like being able to have multiple centers of mass in a simulation is something that would be be needed fairly often and should be available so maybe I need to submit a feature request. – Paul Murray Jan 25 '17 at 04:47
  • I added two images to show the state of the display before and after splitting. The centering force renders the data perfectly while forceX and forceY...not so much. Do you have any advice for how to improve the collision/transition? I've found that reducing the strength of the forces lessens the overlap but substantially increases the odds that a bubble will get stuck in the wrong place when the split happens. Neither problem is really acceptable for the final product. It seems like an issue of how collisions interact with centering forces vs X and Y forces but I don't really know what to do. – Paul Murray Jan 25 '17 at 04:55
  • 1
    It's hard to say anything without seeing your actual code, but you could increase the radius of the `collide` and change `velocityDecay`. – Gerardo Furtado Jan 25 '17 at 04:59
  • I've added the code for my simulation and split function. Any ideas? – Paul Murray Jan 25 '17 at 05:13
  • Well, looks like I errored my way into a working solution. In the second iteration of my split function (which was supposed to bring the nodes back together), I set the centering force back to its original value and set the Y force to null, and the nodes clustered perfectly around their separate centers of mass. Now I just need to write code to make it work in reverse. I posted a photo at the bottom of my original post showing the working split state – Paul Murray Jan 25 '17 at 05:21
  • 1
    I suggest you post *another* question. Right now, this question has a clear answer, which is "no". If you post another question regarding the use of forceX and forceY, with specific details, you'll get more attention. Meanwhile, I'll copy/paste my first comment as an answer. – Gerardo Furtado Jan 25 '17 at 05:33
  • 1
    @PaulMurray This looks somewhat similar to a problem I posted an answer to some days ago: [*"Collision Detection Lost After Toggle (d3v4)"*](/q/41479452). If not an answer to your question, it might well provide some interesting ideas. – altocumulus Jan 25 '17 at 10:52

1 Answers1

3

Unfortunately, the answer seems to be no.

Due to the very nature of d3.forceCenter, that ("pass an anonymous function as a parameter") is not possible. The API says:

The centering force translates nodes uniformly so that the mean position of all nodes (the center of mass if all nodes have equal weight) is at the given position ⟨x,y⟩. (emphasis mine)

Thus, there is no space for an accessor function here. forceX and forceY, on the other hand...

set the coordinate accessor to the specified number or function. (emphasis mine again)

... and may suit you best.

Gerardo Furtado
  • 100,839
  • 9
  • 121
  • 171