toowren
toowren

Reputation: 113

D3.JS Attract one node by another in force simulation

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>

Upvotes: 0

Views: 58

Answers (1)

toowren
toowren

Reputation: 113

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>

Upvotes: 0

Related Questions