Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

D3 new data at .data() makes svg to redraw instead of updating nodes position

Considering this working example, the data update makes the svg update without being re-drawn. The relevant original code section which makes this behave as explained:

// DATA JOIN
link = link.data(firstLinks ? graph.links1 : graph.links2);

// DATA JOIN
node = node.data(graph.nodes);

simulation.force("link")
    .links(firstLinks ? graph.links1 : graph.links2);

However, if the node and links data reference is new, even if the contents are the same, the svg is re-drawn on every update. The relevant changed code section which makes this behave as explained:

var new_graph_http_data = fetchNewData(); // unreferenced new data copy from graph

// DATA JOIN
link = link.data(firstLinks ? new_graph_http_data.links1 : new_graph_http_data.links2);

// DATA JOIN
node = node.data(new_graph_http_data.nodes);

simulation.force("link")
    .links(firstLinks ? new_graph_http_data.links1 : new_graph_http_data.links2);

Here, inside the function update(), the call update_local() will make the graph working as expected. The call update_http() will make the graph to be re-drawn.

// Local working vs fetch (clone)
function update() {
    /* Original working version */
//  update_local();

    /* Re-drawing version */
    update_http();
};

update();
<!DOCTYPE html>
<html>
<meta charset="UTF-8">
<style>
/*  Style Definitions  */
button {
  position: absolute;
  top: 1em;
  left: 1em;
}

.node {
    stroke: white;
    stroke-width: 2px;
}

.link {
    stroke: gray;
    stroke-width: 2px;
}

</style>
<button type="button" id="switch-btn">Switch Links</button>
<svg width="500" height="300"></svg>
<!--<script src="https://d3js.org/d3.v4.min.js"></script>-->
<script src="https://d3js.org/d3.v6.min.js"></script>
<script>
//  graph data store
// Part of original blocks-data.json
var graph = {
    "nodes": [
        { "id": "0", "group": "1" },
        { "id": "1", "group": "2" },
        { "id": "2", "group": "2" },
        { "id": "3", "group": "2" },
        { "id": "4", "group": "2" },
        { "id": "5", "group": "3" },
        { "id": "6", "group": "3" },
        { "id": "7", "group": "3" },
        { "id": "8", "group": "3" }
    ],
    "links1": [
        { "source": "0", "target": "1"},
        { "source": "0", "target": "2"},
        { "source": "0", "target": "3"},
        { "source": "0", "target": "4"},
        { "source": "1", "target": "5"},
        { "source": "2", "target": "6"},
        { "source": "3", "target": "7"},
        { "source": "4", "target": "8"},
        { "source": "1", "target": "8"},
        { "source": "2", "target": "5"},
        { "source": "3", "target": "6"},
        { "source": "4", "target": "7"}
    ],
    "links2": [
        { "source": "0", "target": "5"},
        { "source": "0", "target": "6"},
        { "source": "0", "target": "7"},
        { "source": "0", "target": "8"},
        { "source": "5", "target": "6"},
        { "source": "5", "target": "8"},
        { "source": "7", "target": "6"},
        { "source": "7", "target": "8"},
        { "source": "1", "target": "5"},
        { "source": "2", "target": "6"},
        { "source": "3", "target": "7"},
        { "source": "4", "target": "8"}
    ]
};

//  state variable for current link set
var firstLinks = true;

//  svg and sizing
var svg = d3.select("svg"),
    width = +svg.attr("width"),
    height = +svg.attr("height");

//  d3 color scheme
var color = d3.scaleOrdinal(d3.schemeCategory10);

// elements for data join
var link = svg.append("g").selectAll(".link"),
    node = svg.append("g").selectAll(".node");

//  simulation initialization
var simulation = d3.forceSimulation()
    .force("link", d3.forceLink()
        .id(function(d) { return d.id; }))
    .force("charge", d3.forceManyBody()
        .strength(function(d) { return -500;}))
    .force("center", d3.forceCenter(width / 2, height / 2));

//  button event handling
d3.select("#switch-btn").on("click", function() {
    firstLinks = !firstLinks;
    update();
});

//  follow v4 general update pattern
function update_local() {
    // Update link set based on current state
    // DATA JOIN
    link = link.data(firstLinks ? graph.links1 : graph.links2);
    
    // EXIT
    // Remove old links
    link.exit().remove();

    // ENTER
    // Create new links as needed.  
    link = link.enter().append("line")
        .attr("class", "link")
        .merge(link);

    // DATA JOIN
    node = node.data(graph.nodes);

    // EXIT
    node.exit().remove();

    // ENTER
    node = node.enter().append("circle")
        .attr("class", "node")
        .attr("r", 10)
        .attr("fill", function(d) {return color(d.group);})
        .call(d3.drag()
          .on("start", dragstarted)
          .on("drag", dragged)
          .on("end", dragended)
        )
        .merge(node);

    //  Set nodes, links, and alpha target for simulation
    simulation
        .nodes(graph.nodes)
        .on("tick", ticked);

    simulation.force("link")
        .links(firstLinks ? graph.links1 : graph.links2);

    simulation.alphaTarget(0.3).restart();
}

//  follow v4 general update pattern
function update_http() {
    var new_graph_http_data = fetchNewData();
    // Update link set based on current state
    // DATA JOIN
    link = link.data(firstLinks ? new_graph_http_data.links1 : new_graph_http_data.links2);
    
    // EXIT
    // Remove old links
    link.exit().remove();

    // ENTER
    // Create new links as needed.  
    link = link.enter().append("line")
        .attr("class", "link")
        .merge(link);

    // DATA JOIN
    node = node.data(new_graph_http_data.nodes);

    // EXIT
    node.exit().remove();

    // ENTER
    node = node.enter().append("circle")
        .attr("class", "node")
        .attr("r", 10)
        .attr("fill", function(d) {return color(d.group);})
        .call(d3.drag()
          .on("start", dragstarted)
          .on("drag", dragged)
          .on("end", dragended)
        )
        .merge(node);

    //  Set nodes, links, and alpha target for simulation
    simulation
        .nodes(new_graph_http_data.nodes)
        .on("tick", ticked);

    simulation.force("link")
        .links(firstLinks ? new_graph_http_data.links1 : new_graph_http_data.links2);

    simulation.alphaTarget(0.3).restart();
}

//  drag event handlers
function dragstarted(d) {
    if (!d3.event.active) simulation.alphaTarget(0.3).restart();
    d.fx = d.x;
    d.fy = d.y;
}

function dragged(d) {
    d.fx = d3.event.x;
    d.fy = d3.event.y;
}

function dragended(d) {
    if (!d3.event.active) simulation.alphaTarget(0);
    d.fx = null;
    d.fy = null;
}

//  tick event handler (nodes bound to container)
function ticked() {
    link
        .attr("x1", function(d) { return d.source.x; })
        .attr("y1", function(d) { return d.source.y; })
        .attr("x2", function(d) { return d.target.x; })
        .attr("y2", function(d) { return d.target.y; });

    node
        .attr("cx", function(d) { return d.x; })
        .attr("cy", function(d) { return d.y; });
}

// Value copying function
function fetchNewData() {
    var idx, tempStr;
    var obj = {
        "nodes": [],
        "links1": [],
        "links2": [],
    };

    for (idx in graph.nodes) {
        tempStr = JSON.stringify(graph.nodes[idx]);
        tempStr = JSON.parse(tempStr);
        obj.nodes.push(tempStr);
    }

    for (idx in graph.links1) {
        tempStr = JSON.stringify(graph.links1[idx]);
        tempStr = JSON.parse(tempStr);
        obj.links1.push(tempStr);
    }

    for (idx in graph.links2) {
        tempStr = JSON.stringify(graph.links2[idx]);
        tempStr = JSON.parse(tempStr);
        obj.links2.push(tempStr);
    }

    return obj;
}

</script>
</html>

Tested with D3 v4.13.0 and D3 v6.3.1 in Firefox 78.6.0esr and Chrome 86.0.4240.111. No errors are thrown on dev tools.

Thanks.

like image 750
mjmlopes Avatar asked Nov 07 '22 03:11

mjmlopes


1 Answers

While you have focused on the enter/update/exit cycle, the issue lies with d3-force: the objects that represent the nodes themselves store their own positional data.

D3-force modifies the datum of each node to include positional attributes. These attributes are what the tick function is using to place the nodes:

node
    .attr("cx", function(d) { return d.x; })
    .attr("cy", function(d) { return d.y; });
})

In addition to d.x and d.y, there are d.vx and d.vy properties. Should a node not have these properties, the force will create and assign them. If you remove all the nodes and replace them with new objects without x,y properties, the force will create and initialize those properties with no regard for the previous nodes (afterall, they're not part of the simulation anymore. Visually, this will make it look like all the old nodes/links were exited and new ones entered).

If you replace the objects with new objects by (re)loading nodes, and the new objects are intended to be located where the old nodes were, you could transfer the nodes's x,y,dx,dy properties if you wanted from the old nodes to the new objects, eg:

  var old = simulation.nodes()
  newGraph.nodes.forEach(function(node,i) {
       node.x = old[i].x;
       node.y = old[i].y;
       node.vx = old[i].vx;
       node.vy = old[i].vy;
  })

Update Fiddle

// Local working vs fetch (clone)
function update() {
    /* Original working version */
//  update_local();

    /* Re-drawing version */
    update_http();
};

update();
<!DOCTYPE html>
<html>
<meta charset="UTF-8">
<style>
/*  Style Definitions  */
button {
  position: absolute;
  top: 1em;
  left: 1em;
}

.node {
    stroke: white;
    stroke-width: 2px;
}

.link {
    stroke: gray;
    stroke-width: 2px;
}

</style>
<button type="button" id="switch-btn">Switch Links</button>
<svg width="500" height="300"></svg>
<!--<script src="https://d3js.org/d3.v4.min.js"></script>-->
<script src="https://d3js.org/d3.v6.min.js"></script>
<script>
//  graph data store
// Part of original blocks-data.json
var graph = {
    "nodes": [
        { "id": "0", "group": "1" },
        { "id": "1", "group": "2" },
        { "id": "2", "group": "2" },
        { "id": "3", "group": "2" },
        { "id": "4", "group": "2" },
        { "id": "5", "group": "3" },
        { "id": "6", "group": "3" },
        { "id": "7", "group": "3" },
        { "id": "8", "group": "3" }
    ],
    "links1": [
        { "source": "0", "target": "1"},
        { "source": "0", "target": "2"},
        { "source": "0", "target": "3"},
        { "source": "0", "target": "4"},
        { "source": "1", "target": "5"},
        { "source": "2", "target": "6"},
        { "source": "3", "target": "7"},
        { "source": "4", "target": "8"},
        { "source": "1", "target": "8"},
        { "source": "2", "target": "5"},
        { "source": "3", "target": "6"},
        { "source": "4", "target": "7"}
    ],
    "links2": [
        { "source": "0", "target": "5"},
        { "source": "0", "target": "6"},
        { "source": "0", "target": "7"},
        { "source": "0", "target": "8"},
        { "source": "5", "target": "6"},
        { "source": "5", "target": "8"},
        { "source": "7", "target": "6"},
        { "source": "7", "target": "8"},
        { "source": "1", "target": "5"},
        { "source": "2", "target": "6"},
        { "source": "3", "target": "7"},
        { "source": "4", "target": "8"}
    ]
};

//  state variable for current link set
var firstLinks = true;

//  svg and sizing
var svg = d3.select("svg"),
    width = +svg.attr("width"),
    height = +svg.attr("height");

//  d3 color scheme
var color = d3.scaleOrdinal(d3.schemeCategory10);

// elements for data join
var link = svg.append("g").selectAll(".link"),
    node = svg.append("g").selectAll(".node");

//  simulation initialization
var simulation = d3.forceSimulation()
    .force("link", d3.forceLink()
        .id(function(d) { return d.id; }))
    .force("charge", d3.forceManyBody()
        .strength(function(d) { return -500;}))
    .force("center", d3.forceCenter(width / 2, height / 2));

//  button event handling
d3.select("#switch-btn").on("click", function() {
    firstLinks = !firstLinks;
    update();
});

//  follow v4 general update pattern
function update_local() {
    // Update link set based on current state
    // DATA JOIN
    link = link.data(firstLinks ? graph.links1 : graph.links2);
    
    // EXIT
    // Remove old links
    link.exit().remove();

    // ENTER
    // Create new links as needed.  
    link = link.enter().append("line")
        .attr("class", "link")
        .merge(link);

    // DATA JOIN
    node = node.data(graph.nodes);

    // EXIT
    node.exit().remove();

    // ENTER
    node = node.enter().append("circle")
        .attr("class", "node")
        .attr("r", 10)
        .attr("fill", function(d) {return color(d.group);})
        .call(d3.drag()
          .on("start", dragstarted)
          .on("drag", dragged)
          .on("end", dragended)
        )
        .merge(node);

    //  Set nodes, links, and alpha target for simulation
    simulation
        .nodes(graph.nodes)
        .on("tick", ticked);

    simulation.force("link")
        .links(firstLinks ? graph.links1 : graph.links2);

    simulation.alphaTarget(0.3).restart();
}

//  follow v4 general update pattern
function update_http() {
    var new_graph_http_data = fetchNewData();
    // Update link set based on current state
    // DATA JOIN
    link = link.data(firstLinks ? new_graph_http_data.links1 : new_graph_http_data.links2);
    
    // EXIT
    // Remove old links
    link.exit().remove();

    // ENTER
    // Create new links as needed.  
    link = link.enter().append("line")
        .attr("class", "link")
        .merge(link);

    // DATA JOIN
    node = node.data(new_graph_http_data.nodes);

    // EXIT
    node.exit().remove();

    // ENTER
    node = node.enter().append("circle")
        .attr("class", "node")
        .attr("r", 10)
        .attr("fill", function(d) {return color(d.group);})
        .call(d3.drag()
          .on("start", dragstarted)
          .on("drag", dragged)
          .on("end", dragended)
        )
        .merge(node);

    //  Set nodes, links, and alpha target for simulation
    simulation
        .nodes(new_graph_http_data.nodes)
        .on("tick", ticked);

    simulation.force("link")
        .links(firstLinks ? new_graph_http_data.links1 : new_graph_http_data.links2);

    simulation.alphaTarget(0.3).restart();
}

//  drag event handlers
function dragstarted(d) {
    if (!d3.event.active) simulation.alphaTarget(0.3).restart();
    d.fx = d.x;
    d.fy = d.y;
}

function dragged(d) {
    d.fx = d3.event.x;
    d.fy = d3.event.y;
}

function dragended(d) {
    if (!d3.event.active) simulation.alphaTarget(0);
    d.fx = null;
    d.fy = null;
}

//  tick event handler (nodes bound to container)
function ticked() {
    link
        .attr("x1", function(d) { return d.source.x; })
        .attr("y1", function(d) { return d.source.y; })
        .attr("x2", function(d) { return d.target.x; })
        .attr("y2", function(d) { return d.target.y; });

    node
        .attr("cx", function(d) { return d.x; })
        .attr("cy", function(d) { return d.y; });
}

// Value copying function
function fetchNewData() {
  var idx, tempStr;
    var obj = {
        "nodes": [],
        "links1": [],
        "links2": [],
    };

    for (idx in graph.nodes) {
        tempStr = JSON.stringify(graph.nodes[idx]);
        tempStr = JSON.parse(tempStr);
        obj.nodes.push(tempStr);
    }

    for (idx in graph.links1) {
        tempStr = JSON.stringify(graph.links1[idx]);
        tempStr = JSON.parse(tempStr);
        obj.links1.push(tempStr);
    }

    for (idx in graph.links2) {
        tempStr = JSON.stringify(graph.links2[idx]);
        tempStr = JSON.parse(tempStr);
        obj.links2.push(tempStr);
    }
  
  if(simulation.nodes().length) {
  var old = simulation.nodes()
  obj.nodes.forEach(function(node,i) {
       node.x = old[i].x;
       node.y = old[i].y;
       node.vx = old[i].vx;
       node.vy = old[i].vy;
  })
  }
  

    return obj;
}

</script>
</html>
like image 61
Andrew Reid Avatar answered Nov 12 '22 12:11

Andrew Reid