'D3 force directed graph, apply force to a "g" element

I am having trouble with my force directed graph. I had to make some changed to my nodes for design purposes and ever since, my forces stoped working. Each node is now a "g" element with two "circle" elements inside. One being the background of the node and the other being the partially transparent foreground.

Unlike before where I would apply ".call(drag(simulation))" to my node that used to be a "circle", I now need to apply it the the "g" element.

As seen on the screenshot, the nodes are not where they are supposed to be. They are detached from their respective links, and are all in the center of the map , on the top of each other.

Any clue on what I am doing wrong?

enter image description here


 ForceGraph(
    nodes, // an iterable of node objects (typically [{id}, …])
    links // an iterable of link objects (typically [{src, target}, …])
    ){
    var nodeId = d => d.id // given d in nodes, returns a unique identifier (string)
    const nodeStrength = -450 // -1750
    const linkDistance = 100
    const linkStrokeOpacity = 1 // link stroke opacity
    const linkStrokeWidth = 3 // given d in links, returns a stroke width in pixels
    const linkStrokeLinecap = "round" // link stroke linecap
    const linkStrength =1
    var width = this.$refs.mapFrame.clientWidth // scale to parent container
    var height = this.$refs.mapFrame.clientHeight // scale to parent container
 
    const N = d3.map(nodes, nodeId);
    

    // Replace the input nodes and links with mutable objects for the simulation.
    nodes = nodes.map(n => Object.assign({}, n));
    links = links.map(l => ({
        orig: l,
        //Object.assign({}, l)
        source: l.src,
        target: l.target
    }));
  

    // Construct the forces.
    const forceNode = d3.forceManyBody();
    const forceLink = d3.forceLink(links).id(({index: i}) => N[i]);
    forceNode.strength(nodeStrength);
    forceLink.strength(linkStrength);
    forceLink.distance(linkDistance)



    const simulation = d3.forceSimulation(nodes)
        .force(link, forceLink)
        .force("charge", forceNode)
        .force("x", d3.forceX())
        .force("y", d3.forceY())
        .on("tick", ticked);



    const svg = d3.create("svg")
    .attr("id", "svgId")
        .attr("preserveAspectRatio", "xMidYMid meet")
        .attr("viewBox", [-width/2,-height/2, width,height])
        .classed("svg-content-responsive", true)


    const defs = svg.append('svg:defs');
  

    defs.selectAll("pattern")
    .data(nodes)
    .join(
      enter => {
        // For every new <pattern>, set the constants and append an <image> tag
         const patterns = enter
          .append("pattern")
          .attr("preserveAspectRatio", "none")
          .attr("viewBox", [0,0, 100,100])
          .attr("width", 1)
          .attr("height", 1);
          
        patterns
          .append("image")
          .attr("width", 80)
          .attr("height", 80)
          .attr("x", 10)
          .attr("y", 10);
        return patterns;
      }
    )
    // For every <pattern>, set it to point to the correct
    // URL and have the correct (company) ID
    .attr("id", d => d.id)
    .select("image")
    .datum(d => {
      return d;
    })
    .attr("xlink:href", d => {
      return d.image
    })
    
 
    
     

    const link = svg.append("g")
        .attr("stroke-opacity", linkStrokeOpacity)
        .attr("stroke-width",  linkStrokeWidth)
        .attr("stroke-linecap", linkStrokeLinecap)
        .selectAll("line")
        .data(links)
        .join("line")
        ;
        link.attr("stroke", "white")
 
     
   
       

    

        

    var node = svg
        .selectAll(".circle-group")
        .data(nodes)
        .join(enter => {
         node = enter.append("g")        
        .attr("class", "circle-group");
         node.append("circle")
        .attr("class", "background") // classes aren't necessary here, but they can help with selections/styling
        .style("fill", "red")
        .attr("r", 30);
         node.append("circle")
        .attr("class", "foreground") // classes aren't necessary here, but they can help with selections/styling
        .style("fill", d => `url(#${d.id})`)
        .attr("r", 30)          
         
        })
        node.call(drag(simulation))
        
        


    function ticked() {
        link
        .attr("x1", d => d.source.x)
        .attr("y1", d => d.source.y)
        .attr("x2", d => d.target.x)
        .attr("y2", d => d.target.y);
        node
        //.transform("translate", d => "translate("+[d.x,d.y]+")"); // triggers error
         .attr("transform", d => "translate("+[d.x,d.y]+")");
        //.attr("cx", d => d.x)
        //.attr("cy", d => d.y);
    }


    function drag(simulation) {    
        function dragstarted(event) {
        if (!event.active) simulation.alphaTarget(0.3).restart();
        event.subject.fx = event.subject.x;
        event.subject.fy = event.subject.y;
        }
        
        function dragged(event) {
        event.subject.fx = event.x;
        event.subject.fy = event.y;
        }
        
        function dragended(event) {
        if (!event.active) simulation.alphaTarget(0);
        event.subject.fx = null;
        event.subject.fy = null;
        }

        return d3.drag()
        .on("start", dragstarted)
        .on("drag", dragged)
        .on("end", dragended);
    }



    return Object.assign(svg.node() );
    }//forcegraph

EDIT 1

I updated the ticked() function with what was suggested but ".transform("translate", d => "translate("+[d.x,d.y]+")");" triggered the following error :

  Mapping.vue?d90b:417 Uncaught TypeError: node.transform is not a function
    at Object.ticked (Mapping.vue?d90b:417:1)
    at Dispatch.call (dispatch.js?c68f:57:1)
    at step (simulation.js?5481:32:1)
    at timerFlush (timer.js?74f4:61:1)
    at wake (timer.js?74f4:71:1)  

So I changed it for ".attr("transform", d => "translate("+[d.x,d.y]+")");"

I don't get any error anymore but my nodes are still all in the center of the map as per the initial screenshot. I am not quite sure what I am doing wrong. Perhaps I need to call ".call(drag(simulation))" on each of the two circles instead of calling it on node?



Solution 1:[1]

g elements don't have cx or cy properties. Those are specific to circle elements (and ellipses). This is why your positioning does not work. However, both circle and g can use a transform for positioning. Instead of:

   node
    .attr("cx", d => d.x)
    .attr("cy", d => d.y);

You can use:

   node
    .transform("translate", d => "translate("+[d.x,d.y]+")");

In regards to your question title, d3 does not apply a force to the elements but rather the data itself. The forces continue to work regardless of whether you render the changes - as seen in your case by the links which move as they should.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Andrew Reid