Oliver Evans
Oliver Evans

Reputation: 989

Reloading (updating) d3.js force-directed graph holds onto old JSON data

So I have a d3.js force-directed graph that displays data from a JSON feed. When I click a node I requested an updated JSON feed based off the node that was clicked.

The JSON that is returned is correct. But what displays in the graph does not reflect what data is held in the JSON. I have a feeling the graph is holding onto previous graph data.

Heres a quick gif that should help visualise the issue. enter image description here

Here is a JSFiddle to give you an idea of how to graph currently works. And the Javascript on its own is at the bottom of this question.

In a bit more detail. When you click a node, it passes the word that node is associated with into a query string of a URL. I then run the d3.json using this new 'clicked' url and run an update function to recreate the graph.

So an example of how this is wrong. So if you go onto the JSFiddle and click on the node called 'piercingly' you will find that the next graph that is loaded doesn't even have the word 'piercingly' in it, and still has labels associated to bitter (the original search). Yet it if you change the variable at the top of the JS to 'piercingly' a different but correct graph is loaded.

The number of nodes is correct. But the label and other attributes in the full version (not the version on JSFiddle) are incorrect.

Any help would be much appreciated.

$wordToSearch = "bitter";

var w = 960,
    h = 960,
    node,
    link,
    root,
    title;

var jsonURL = 'http://desolate-taiga-6759.herokuapp.com/word/basic/' + $wordToSearch;

d3.json(jsonURL, function(json) {
    root = json.words[0]; //set root node
    root.fixed = true;
    root.x = w / 2;
    root.y = h / 2 - 80;
    update();
});

var force = d3.layout.force()
    .on("tick", tick)
    .charge(-700)
    .gravity(0.1)
    .friction(0.9)
    .linkDistance(50)
    .size([w, h]);

var svg = d3.select(".graph").append("svg")
    .attr("width", w)
    .attr("height", h);



//Update the graph
function update() {
    var nodes = flatten(root),
    links = d3.layout.tree().links(nodes);

    // Restart the force layout.
    force
        .nodes(nodes)
        .links(links)
        .start();

    // Update the links…
    link = svg.selectAll("line.link")
        .data(links, function(d) { return d.target.id; });

    // Enter any new links.
    link.enter().insert("svg:line", ".node")
        .attr("class", "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; });

    // Exit any old links.
    link.exit().remove();

    // Update the nodes…
    node = svg.selectAll(".node")
        .data(nodes);

    var nodeE = node
        .enter();

    var nodeG = nodeE.append("g")
        .attr("class", "node")
        .call(force.drag);

    nodeG.append("circle")  
        .attr("r", 10)
        .on("click", click)
        .style("fill", "red");

    nodeG.append("text")
        .attr("dy", 10 + 15)
        .attr("text-anchor", "middle")
        .text(function(d) { return d.word });

    node.exit().remove();

}

function tick() {
    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("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
}




/***********************
*** CUSTOM FUNCTIONS ***
***********************/

//Request extended JSON objects when clicking a clickable node
function click(d) {
    $wordClicked = d.word;

    var jsonURL = 'http://desolate-taiga-6759.herokuapp.com/word/basic/' + $wordClicked;
    console.log(jsonURL);

    updateGraph(jsonURL);
}

// Returns a list of all nodes under the root.
function flatten(root) {
    var nodes = [], i = 0;

    function recurse(node) {
        if (node.children) node.size = node.children.reduce(function(p, v) { return p + recurse(v); }, 0);
        if (!node.id) node.id = ++i;
        nodes.push(node);
        return node.size;
    }

    root.size = recurse(root);
    return nodes;

}

//Update graph with new extended JSON objects
function updateGraph(newURL) {
    d3.json(newURL, function(json) {
        root = json.words[0]; //set root node
        root.fixed = true;
        root.x = w / 2;
        root.y = h / 2 - 80;

        update();
    });
}

function getUrlParameter(sParam)
{
    var sPageURL = window.location.search.substring(1);
    var sURLVariables = sPageURL.split('&');
    for (var i = 0; i < sURLVariables.length; i++) 
    {
        var sParameterName = sURLVariables[i].split('=');
        if (sParameterName[0] == sParam)  {
            return sParameterName[1];
        }
    }
} 

EDIT: So I tried logging out the word when it is added to the text element. On first load all the words get logged as the get assigned to their respected text element. But when you click on the node, they don't. (Please see gif below). This is strange as I called the update function on click. So the word (in theory) should be fetched again for that node. But it doesnt.

enter image description here

Upvotes: 2

Views: 8645

Answers (1)

Ian
Ian

Reputation: 34549

It's quite hard to grasp on a phone but I think the reason is probably because it's getting confused about the new data. By default the data() function uses the index of the item to join to the DOM.

What you need to do instead is pass another function to your calls to data() which is described as a key function. Here you can probably just return the word.

.data(nodes, function(d) { return d.word; })

Take a look at the data function in the API docs https://github.com/mbostock/d3/wiki/Selections . I've had complex cases catch me out a couple of times where I missed a key function.

Upvotes: 2

Related Questions