D3.JS Attract one node by another in force simulation

40 Views Asked by At

There is an arbitrary data set, which has to be visualized by d3.forceSimulation() method. enter image description here

These nodes are intractable, you can choose parent and children circles. The task is to attract children node (blue) by parent one (red), while both are chosen. And, eventually, rearrange others.

enter image description here

I have looked through d3.forceSimulation docs and could find any clues how to do it.

//parent: red, children: blue
    
    let svg, width = 800, height = 400, radius, nodes, x, y, simulation;
    let parent = null, children = null;
    
    let data = [
      
        {id: 0, size: 0.5 },
        {id: 1, size: 0.25 },
        {id: 2, size: 0.125 },
        {id: 3, size: 0.75 },
        {id: 4, size: 0.8 },
        {id: 5, size: 0.4 },
        {id: 6, size: 0.25 },
        {id: 7, size: 0.5 }
        
    ];
    
    const tick = () => { nodes.attr("cx", d_ => d_.x ).attr("cy", d_ => d_.y ) }
    
    svg = d3.select("body").append("svg").attr("viewBox", `0 0 ${width} ${height}`);
    
    let background = svg.append("rect").attr("width", width).attr("height", height).attr("fill", "#444444");
         
    radius = d3.scaleLinear().domain([0.0, 1.0]).range([32, 64]);
    
    x = d3.scaleLinear().domain([0, data.length]).range([64, width - 64]);
    y = d3.scaleLinear().domain([0, data.length]).range([64, height - 64]);
                
    simulation = d3.forceSimulation()
    .force("x", d3.forceX(d_ => { return x(d_.id); }).strength(0.1))
    .force("y",  d3.forceY(height / 2).strength(0.05))
    .force("collide", d3.forceCollide().radius(d => radius(Number(d.size)) + 10))
    .alpha(1).restart();
    
    nodes = svg.selectAll(null)
    .data(data)
    .enter()
    .append("circle")
    .attr("r", d_ => { return radius(d_.size); })
    .attr("fill", "#FFFFFF")
    .on("mouseover", (event_, d_) => { if(parent !== d_.id && children !== d_.id) { d3.select(event_.currentTarget).attr("fill", "#888888"); } })
    .on("mouseout", (event_, d_) => { if(parent !== d_.id && children !== d_.id) { d3.select(event_.currentTarget).attr("fill", "#FFFFFF"); } })
    .on("click", (event_, d_) => {
       
        if(parent == null) { parent = d_.id; d3.select(event_.currentTarget).attr("fill", "#FF0000"); }
        else if(parent != d_.id) { 
            
            
            children = d_.id; d3.select(event_.currentTarget).attr("fill", "#0000FF");
            
            //attrackt blue node by red
            
            //...
                                 
        }
                
    });
    
    simulation.nodes(data).on("tick", tick);
<script src="https://d3js.org/d3.v7.min.js"></script>

1

There are 1 best solutions below

0
toowren On

Here is my solution based on re-indexing nodes (id2).

let svg, width = 800, height = 400, radius, nodes, x, y, simulation;
let parent = null, child = null;

let data = [
  
    {id: 0, id2: 0, size: 0.5 },
    {id: 1, id2: 1, size: 0.25 },
    {id: 2, id2: 2, size: 0.125 },
    {id: 3, id2: 3,  size: 0.75 },
    {id: 4, id2: 4,  size: 0.8 },
    {id: 5, id2: 5,  size: 0.4 },
    {id: 6, id2: 6,  size: 0.25 },
    {id: 7, id2: 7,  size: 0.5 }
    
];

const tick = () => { nodes.attr("transform", d_ => `translate(${d_.x},${d_.y})`); } //nodes.attr("cx", d_ => d_.x ).attr("cy", d_ => d_.y ) }

const attract = () => {
    
    let element = data[child];
    data.splice(child, 1);
    
    if(child > parent) { data.splice(parent + 1, 0, element); } else { data.splice(parent - 1, 0, element); }
    
    data.forEach((d_, i_) => { d_.id2 = i_; })
    
    parent = null; child = null;
    d3.selectAll("circle").attr("fill", "#FFFFFF");
    
    simulation.force("x", d3.forceX(d_ => { return x(d_.id2); }).strength(0.2))
    .force("y",  d3.forceY(height / 2).strength(0.05))
    .force("collide", d3.forceCollide().radius(d => radius(Number(d.size)) + 10))
    .alpha(1).restart();

}

svg = d3.select("body").append("svg").attr("viewBox", `0 0 ${width} ${height}`);

let background = svg.append("rect").attr("width", width).attr("height", height).attr("fill", "#444444");
     
radius = d3.scaleLinear().domain([0.0, 1.0]).range([32, 64]);

x = d3.scaleLinear().domain([0, data.length]).range([64, width - 64]);
y = d3.scaleLinear().domain([0, data.length]).range([64, height - 64]);
            
simulation = d3.forceSimulation()
.force("x", d3.forceX(d_ => { return x(d_.id2); }).strength(0.2))
.force("y",  d3.forceY(height / 2).strength(0.05))
.force("collide", d3.forceCollide().radius(d => radius(Number(d.size)) + 10))
.alpha(1).restart();

nodes = svg.selectAll(null)
.data(data)
.enter()
.append("g");

nodes.append("circle")
.attr("id", d_ => "node_" + d_.id)
.attr("r", d_ => { return radius(d_.size); })
.attr("fill", "#FFFFFF")
.on("mouseover", (event_, d_) => { if(parent !== d_.id2 && child !== d_.id2) { d3.select(event_.currentTarget).attr("fill", "#888888"); } })
.on("mouseout", (event_, d_) => { if(parent !== d_.id2 && child !== d_.id2) { d3.select(event_.currentTarget).attr("fill", "#FFFFFF"); } })
.on("click", (event_, d_) => {
   
    if(parent == null) { parent = d_.id2; d3.select(event_.currentTarget).attr("fill", "#FF0000"); }
    
    else if(parent != d_.id2) { 
        
        child = d_.id2; d3.select(event_.currentTarget).attr("fill", "#0000FF");
        
        attract();
                             
    }
            
});

nodes.append("text").attr("class", "non-selectable").attr("text-anchor", "middle").attr("alignment-baseline", "middle").text(d_ => d_.id);

simulation.nodes(data).on("tick", tick);
<script src="https://d3js.org/d3.v7.min.js"></script>