0

I am working on a temporal network graph, with pie charts as nodes. Along with the nodes/links changing over time, the pie charts are supposed to change. It works fine without incorporating the changing pie slices. When I am incorporating the changing slices I get this weird behaviour where the nodes/pies restart from their initial position every time the (time) slider moves, which makes the whole thing jitter rather severely.

Each nodes data looks like:

{
"id": "Mike",
"start": "2022-08-09",
"location": [12.34, -56.74],
"received": [{
    "giver": "Susan",
    "receiver": "Mike",
    "user_timestamp": "2022-08-09",
    "message": "thanks!",
    "location": [3.1415, 9.6535]
}, {
    "giver": "Joe",
    "receiver": "Mike",
    "user_timestamp": "2022-08-11",
    "message": "so cool!",
    "location": [27.18, 2.818]
}]

}

The received array holds all the data pertinent to the pie - each slice is the same size, and there as many slices as their are elements in the array. I am changing the pie slices by filtering the received array based on the user_timestamp and the slider position. Another issue I'm having is that the pie slices are not updating properly when slices are added ... should be equal sized slices

rangeSlider.on("onchange", (val) => {

        currentValue = Math.ceil(timeScale(val));
        
        // this filters entire node/pie
        const filteredNodes = userNetworkData.nodes.filter(
          (d) => new Date(d.start) <= val
        );
        
        // filter the received array in each node 
        const filteredNodesReceived = filteredNodes.map((d) => {
          return {
            ...d,
            received: d.received.filter((r) => new Date(r.user_timestamp) <= val),
          };
        });
        

        const filteredLinks = userNetworkData.links.filter(
          (d) => new Date(d.start) <= val
        );
        
        // remove edge if either source or target is not present
        const filteredLinksFiltered = filteredLinks.filter(
          (d) => filteredNodesReceived.some(o => o.id == d.source.id) && filteredNodesReceived.some(o => o.id == d.target.id)
        );

        // point to new source, target structure
        const filteredLinksMapped = filteredLinksFiltered.map((d) => {
          return {
              ...d,
              source: filteredNodesReceived.find(x => x.id==d.source.id),
              target: filteredNodesReceived.find(x => x.id==d.target.id)
              
          };
        });

        update(filteredNodesReceived, filteredLinksMapped);

      });

The way its set up I am using userNetworkData to hold the data in some static version so I can bring it back after I have removed it. Maybe that doesn't make sense. I have tried updating the x,y,vx,vy each instance of userNetworkData.nodes on slider change but the same jittering occurs.

filteredLinksMapped is my attempt to re-associate the links with the nodes (which now have a different amount of elements in the received array).

The relevant parts of update(nodes,links):

function update(nodes, links) {

        node = node
          .data(nodes, (d) => d.id)
          .join(
            (enter) => enter.append("g").call(drag(simulation))
          );

        paths = node
          .selectAll("path")
          .data(function (d, i) {
            return pie(d.received);
          })
          .join(
          (enter)=>
          enter.append("svg:path")
          .attr("class", "path")
          .attr("d", arc)
          .attr("opacity", 1)
          // for making each pie slice visible
          .attr("stroke", function (d) {
            return color(d.data.receiver);
          })
          .attr("stroke-width", radius * 0.2)
          .attr("fill", function (d, i) {
            return color(d.data.giver);
          })
          .attr("cursor", "pointer")
          .on("mousemove", function (event, d) {
            //tooltips bit
            div.transition().duration(200).style("opacity", 0.9);
            // this bit puts the tooltip near the slice of the pie chart
            div
              .html(parse_message(d.data))
              .style("left", event.pageX + 20 + "px")
              .style("top", event.pageY - 28 + "px");
          })
          .on("mouseout", function (d) {
            div.transition().duration(500).style("opacity", 0);
          })
          )
        
        
        
        link = link
          .data(links, (d) => [d.source, d.target])
          .join("line")
          .attr("stroke-width", radius * 0.3)
          .attr("stroke", (d) => color(d.source.id));
        
          
        simulation.nodes(nodes);
        simulation.force("link").links(links,function(d){return d.id;});
        simulation.alpha(0.5).tick();
        simulation.restart();
        ticked();

      }

I am initializing my selections and simulation outside of update like so:

     const simulation = d3.forceSimulation()
        .force("charge", d3.forceManyBody())
        .force(
          "link",
          d3.forceLink().id((d) => d.id)
        )
        // .force("collide", d3.forceCollide().radius(2*radius  ).iterations(3))
        .force(
          "y",
          d3.forceY((d) => projection(d.location)[1])
        )
        .force(
          "x",
          d3.forceX((d) => projection(d.location)[0])
        )
        .on("tick", ticked);


      let link = svg.append("g").attr("class", "links").selectAll("line");

      let node = svg.append("g").attr("class", "nodes").selectAll("g");

Note I am forcing the nodes towards the coordinates corresponding to their lat/lon as I am moving towards transitioning between a "map" view and a network view.

Unfortunately i'm having trouble getting to work on codepen, i'll keep trying but hopefully that's enough.

Michael Higgins
  • 321
  • 3
  • 7

1 Answers1

0

The problem was with the way I was copying over the x,y,vx,vy values from the node selection to the original data (which I would use to filter/add to my selection). Here is what I settled on.

const node_pos_vel = node.data().map(d=> (({ x,y,vx,vy,id }) => ({ x,y,vx,vy,id}))(d))
const node_pos_vel_map = new Map(node_pos_vel.map(d => [d.id, d]));
userNetworkData.nodes = userNetworkData.nodes.map(d => Object.assign(d, node_pos_vel_map.get(d.id)));

The first line is just taking the subset of the object that I want to update. See How to get a subset of a javascript object's properties for how it works.

The last line replaces just the values x,y,vx,vy values for each instance in userNetworkData.nodes when it is a part of the nodes currently in the DOM.

This was inspired by https://observablehq.com/@d3/temporal-force-directed-graph but the difference between their case (where they copy over all the data from node.data() is that I cannot copy over the received array, as the time filter is changing it, and I need to hold a full copy of it.

Michael Higgins
  • 321
  • 3
  • 7