3

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] + ")";
  });
}
}
Fred Rocha
  • 450
  • 3
  • 16
  • `.force("center_force", d3.forceCenter(width / 2, height / 2))` should start the animation with nodes centered. How is `graph3` defined in your html ? – Robin Mackenzie Jun 14 '23 at 12:48

1 Answers1

1

You say that

I have tried giving "x, y" attributes

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 n nodes inside an SVG with width w and height h in this fashion might look something like so:

  let nodes = d3.range(n).map((i) => ({
    id: i,
    x: d3.randomUniform(0, w)(),
    y: d3.randomUniform(0, h)()
  }));

If you prefer to start the nodes all from the middle, you just do

x: w/2,
y: h/2,

You can see this in action in this Observable notebook, if you like.

Mark McClure
  • 4,862
  • 21
  • 34
  • Thanks for your answer @mark-mcclure! Sadly, this isn't working. I try setting "x" and "y" for each Array entry, but it seems it's being somewhere overwritten. I wonder if this is due to `forceX()`... I edited my answer to include an abridged version of the code, maybe that can assist in debugging this. – Fred Rocha Jun 08 '23 at 09:17