Reputation: 113
There is an arbitrary data set, which has to be visualized by d3.forceSimulation() method.
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.
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
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