I have SVG nodes that I defined as <g> elements that contain one <circle> and one <text> inside them.
Here's an examples HTML:
<g class="nodes">
<g>
<circle r="63"></circle>
<text class="true">Croatia</text>
</g>
(...)
I am animating them in d3js (v6) as to achieve a circular packing effect, using forces. All is working well, I just am not at all able to define the initial coordinates for my elements. The elements starting position is top right, and I would like them to be in the center of my SVG area when the animation starts.
I have tried giving my node <g> elements "x, y" attributes, as well as transform: translate them before the animation starts, to no avail.
I also tried initializing each object "x" and "y" values, before turning them into nodes, as so:
arrayOfCountries.forEach((country) => {
country["x"] = 300;
country["y"] = 700;
});
var simulation = d3
.forceSimulation()
//add nodes
.nodes(arrayOfCountries);
I have created an animations with:
simulation.on("tick", tickActions);
and animated it using:
function tickActions() {
//update g transform:
node.attr("transform", function (d) {
return "translate(" + [d.x, d.y] + ")";
});
}
Any idea how I could make sure the nodes start animating from center outwards?
Here's an outline of my code (I'm using Vue JS 3):
renderCountries() {
// #graph3 is my main SVG
var svg = d3.select("#graph3");
// Clear the previous map, if any
svg.selectAll("*").remove();
// create somewhere to put the force directed graph
let width = +svg.attr("width");
let height = +svg.attr("height");
let arrayOfCountries = this.euCountryList.values;
arrayOfCountries.forEach((country) => {
country["x"] = 300;
country["y"] = 700;
});
var simulation = d3
.forceSimulation()
//add nodes
.nodes(arrayOfCountries);
// add forces
// we're going to add a charge to each node
// also going to add a centering force
simulation
.force("charge_force", d3.forceManyBody().strength(-200))
.force("center_force", d3.forceCenter(width / 2, height / 2))
.force(
"x",
d3
.forceX()
.x(function (d) {
let lon = parseFloat(d[2].replace(",", ".").replace(" ", ""));
// shift 20 units so as to avoid negative numbers
let displayLon = (lon + 20) * 22;
return displayLon;
})
.strength(5)
)
.force(
"y",
d3
.forceY()
.y(function (d) {
let lat = parseFloat(d[1].replace(",", ".").replace(" ", ""));
let displayLat = height - lat * 10;
// console.log("LAT: " + displayLat);
return displayLat;
})
.strength(5)
)
.force(
"collide",
d3
.forceCollide()
.strength(0.01)
.radius(function (d) {
if (d[14]) {
let radius = parseFloat(
d[14].replace(",", ".").replace(" ", "")
);
return radius;
} else {
return 50;
}
})
.iterations(35)
); // Force that avoids circle overlapping;
// add tick instructions
simulation.on("tick", tickActions);
(...)
// draw circles
var node = svg
.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(arrayOfCountries)
.enter()
.append("g");
var circles = node
.append("circle")
.attr("r", function (d) {
if (d[14]) {
let radius = parseFloat(d[14].replace(",", ".").replace(" ", ""));
// Bail if radius is Not A Number; possibly a presedential election in a monarchy, eg, Spain.
if (isNaN(radius)) {
return 0;
}
if (radius > 35) {
return radius / 1.1;
} else {
// define a minimum radius
return 35;
}
// No data for abstention in country. Hide country in map.
} else {
return 0;
}
})
.attr("fill", "black")
.attr("filter", "url(#blurMe)")
.attr("ref", function (d) {
return d[0];
})
// Access country's caption using Vue template Refs
.attr("id", function (d) {
return d[0] + "-euparliament";
});
// eslint-disable-next-line no-unused-vars
var labels = node
.append("text")
.text(function (d) {
if (d[14] && d[14] != "nodata" && d[14] != "—") {
return d[3];
}
})
.attr("class", function (d) {
if (d[14]) {
let abstentionism = parseFloat(
d[14].replace(",", ".").replace(" ", "")
);
if (abstentionism) {
if (abstentionism < 35) {
return "small-country";
// return short name
}
}
}
return true;
})
.attr("text-anchor", "middle")
.attr("dominant-baseline", "middle")
.attr("pointer-events", "none")
// .attr("x", function (d) {
// return -(d.abstentionism / 2);
// })
.attr("y", 0)
.attr("fill", "red");
function tickActions() {
//update g transform:
node.attr("transform", function (d) {
return "translate(" + [d.x, d.y] + ")";
});
}
}
You say that
but it's not exactly clear where you've done that.
Typically, the nodes should be a list of objects. The force simulation will reset any x/y properties of those objects, but you can set them to some initial position if we like.
Often, I set the initial positions randomly, rather than the default upper left position for all nodes. The code to create
nnodes inside an SVG with widthwand heighthin this fashion might look something like so:If you prefer to start the nodes all from the middle, you just do
You can see this in action in this Observable notebook, if you like.